pygame

用Python和Pygame写游戏-从入门到精通(1)

 

Pygame的历史css

 

Pygame是一个利用SDL库的写就的游戏库,SDL呢,全名Simple DirectMedia Layer,是一位叫作Sam Lantinga的大牛写的,听说他为了让Loki(致力于向Linux上移植Windows的游戏的一家大好人公司,惋惜已经倒闭,唉好人不长命啊……)更有效的工做,创造了这个东东。html

SDL是用C写的,不过它也能够使用C++进行开发,固然还有不少其它的语言,Pygame就是Python中使用它的一个库。Pygame已经存在不少时间了,许多优秀的程序员加入其中,把Pygame作得愈来愈好。python

安装Pygame程序员

你能够从www.pygame.org下载Pygame,选择合适你的操做系统和合适的版本,而后安装就能够了(什么,你连Python都没有?您多是不适合看这个系列了,不过若是执意要学,很好!快去www.python.org下载吧!)。 一旦你安装好,你能够用下面的方法确认下有没有安装成功:算法

Python编程

小程序

 

使用Pygamewindows

Pygame有不少的模块,下面是一张一览表:后端

模块名数组

功能

pygame.cdrom

访问光驱

pygame.cursors

加载光标

pygame.display

访问显示设备

pygame.draw

绘制形状、线和点

pygame.event

管理事件

pygame.font

使用字体

pygame.image

加载和存储图片

pygame.joystick

使用游戏手柄或者 相似的东西

pygame.key

读取键盘按键

pygame.mixer

声音

pygame.mouse

鼠标

pygame.movie

播放视频

pygame.music

播放音频

pygame.overlay

访问高级视频叠加

pygame

就是咱们在学的这个东西了……

pygame.rect

管理矩形区域

pygame.sndarray

操做声音数据

pygame.sprite

操做移动图像

pygame.surface

管理图像和屏幕

pygame.surfarray

管理点阵图像数据

pygame.time

管理时间和帧信息

pygame.transform

缩放和移动图像

有些模块可能在某些平台上不存在,你能够用None来测试一下。

Python

新的Hello World

学程序一开始咱们总会写一个Hello world程序,但那只是在屏幕上写了两个字,如今咱们来点更帅的!写好之后会是这样的效果:

 

 

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

#!/usr/bin/env python

 

background_image_filename = 'sushiplate.jpg'

mouse_image_filename = 'fugu.png'

#指定图像文件名称

 

import pygame

#导入pygame库

from pygame.locals import *

#导入一些经常使用的函数和常量

from sys import exit

#向sys模块借一个exit函数用来退出程序

 

pygame.init()

#初始化pygame,为使用硬件作准备

 

screen = pygame.display.set_mode((640, 480), 0, 32)

#建立了一个窗口

pygame.display.set_caption("Hello, World!")

#设置窗口标题

 

background = pygame.image.load(background_image_filename).convert()

mouse_cursor = pygame.image.load(mouse_image_filename).convert_alpha()

#加载并转换图像

 

while True:

#游戏主循环

 

    for event in pygame.event.get():

        if event.type == QUIT:

            #接收到退出事件后退出程序

            exit()

 

    screen.blit(background, (0,0))

    #将背景图画上去

 

    x, y = pygame.mouse.get_pos()

    #得到鼠标位置

    x-= mouse_cursor.get_width() / 2

    y-= mouse_cursor.get_height() / 2

    #计算光标的左上角位置

    screen.blit(mouse_cursor, (x, y))

    #把光标画上去

 

    pygame.display.update()

    #刷新一下画面

这个程序须要两张图片,你能够在这篇文章最后的地方找到下载地址,虽然你也能够随便找两张。为了达到最佳效果,背景的 sushiplate.jpg应要有640×480的分辨率,而光标的fugu.png大约应为80×80,并且要有Alpha通道(若是你不知道这是什么,仍是下载吧……)。
注意:代码中的注释我使用的是中文,若是执行报错,能够直接删除。

游戏中我已经为每一行写了注释,另外若是打算学习,强烈建议本身动手输入一遍而不是复制粘贴!

稍微讲解一下比较重要的几个部分:

set_mode会返回一个Surface对象,表明了在桌面上出现的那个窗口,三个参数第一个为元祖,表明分 辨率(必须);第二个是一个标志位,具体意思见下表,若是不用什么特性,就指定0;第三个为色深。

标志位

功能

FULLSCREEN

建立一个全屏窗口

DOUBLEBUF

建立一个“双缓冲”窗口,建议在HWSURFACE或者OPENGL时使用

HWSURFACE

建立一个硬件加速的窗口,必须和FULLSCREEN同时使用

OPENGL

建立一个OPENGL渲染的窗口

RESIZABLE

建立一个能够改变大小的窗口

NOFRAME

建立一个没有边框的窗口

convert函数是将图像数据都转化为Surface对象,每次加载完图像之后就应该作这件事件(事实上由于 它太经常使用了,若是你不写pygame也会帮你作);convert_alpha相比convert,保留了Alpha 通道信息(能够简单理解为透明的部分),这样咱们的光标才能够是不规则的形状。

游戏的主循环是一个无限循环,直到用户跳出。在这个主循环里作的事情就是不停地画背景和更新光标位置,虽然背景是不动的,咱们仍是须要每次都画它, 不然鼠标覆盖过的位置就不能恢复正常了。

blit是个重要函数,第一个参数为一个Surface对象,第二个为左上角位置。画完之后必定记得用update更新一下,不然画面一片漆黑。

 

 

理解事件

事件是什么,其实从名称来看咱们就能想到些什么,并且你所想到的基本就是事件的真正意思了。咱们上一个程序,会一直运行下去,直到你关闭窗口而产生了一个QUIT事件,Pygame会接受用户的各类操做(好比按键盘,移动鼠标等)产生事件。事件随时可能发生,并且量也可能会很大,Pygame的作法是把一系列的事件存放一个队列里,逐个的处理。

事件检索

上个程序中,使用了pygame.event.get()来处理全部的事件,这好像打开大门让全部的人进入。若是咱们使用pygame.event.wait(),Pygame就会等到发生一个事件才继续下去,就好像你在门的猫眼上盯着外面同样,来一个放一个……通常游戏中不太实用,由于游戏每每是须要动态运做的;而另一个方法pygame.event.poll()就好一些,一旦调用,它会根据如今的情形返回一个真实的事件,或者一个“什么都没有”。下表是一个经常使用事件集:

事件

产生途径

参数

QUIT

用户按下关闭按钮

none

ATIVEEVENT

Pygame被激活或者隐藏

gain, state

KEYDOWN

键盘被按下

unicode, key, mod

KEYUP

键盘被放开

key, mod

MOUSEMOTION

鼠标移动

pos, rel, buttons

MOUSEBUTTONDOWN

鼠标按下

pos, button

MOUSEBUTTONUP

鼠标放开

pos, button

JOYAXISMOTION

游戏手柄(Joystick or pad)移动

joy, axis, value

JOYBALLMOTION

游戏球(Joy ball)?移动

joy, axis, value

JOYHATMOTION

游戏手柄(Joystick)?移动

joy, axis, value

JOYBUTTONDOWN

游戏手柄按下

joy, button

JOYBUTTONUP

游戏手柄放开

joy, button

VIDEORESIZE

Pygame窗口缩放

size, w, h

VIDEOEXPOSE

Pygame窗口部分公开(expose)?

none

USEREVENT

触发了一个用户事件

code

若是你想把这个表如今就背下来,固然我不会阻止你,但实在不是个好主意,在实际的使用中,天然而然的就会记住。咱们先来写一个能够把全部方法输出的程序,它的结果是这样的。

咱们这里使用了wait(),由于这个程序在有事件发生的时候动弹就能够了。还用了font模块来显示文字(后面会讲的),下面是源代码:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

import pygame

from pygame.locals import *

from sys import exit

 

pygame.init()

SCREEN_SIZE = (640, 480)

screen = pygame.display.set_mode(SCREEN_SIZE, 0, 32)

 

font = pygame.font.SysFont("arial", 16);

font_height = font.get_linesize()

event_text = []

 

while True:

 

    event = pygame.event.wait()

    event_text.append(str(event))

    #得到时间的名称

    event_text = event_text[-SCREEN_SIZE[1]/font_height:]

    #这个切片操做保证了event_text里面只保留一个屏幕的文字

 

    if event.type == QUIT:

        exit()

 

    screen.fill((255, 255, 255))

 

    y = SCREEN_SIZE[1]-font_height

    #找一个合适的起笔位置,最下面开始可是要留一行的空

    for text in reversed(event_text):

        screen.blit( font.render(text, True, (0, 0, 0)), (0, y) )

        #之后会讲

        y-=font_height

        #把笔提一行

 

    pygame.display.update()


书上说,若是你把填充色的(0, 0, 0)改成(0, 255, 0),效果会想黑客帝国的字幕雨同样,我得说,实际试一下并不太像……不过之后你彻底能够写一个以假乱真甚至更酷的!

这个程序在你移动鼠标的时候产生了海量的信息,让咱们知道了Pygame是多么的繁忙……咱们第一个程序那样是调用pygame.mouse.get_pos()来获得当前鼠标的位置,而如今利用事件能够直接得到!

处理鼠标事件

MOUSEMOTION事件会在鼠标动做的时候发生,它有三个参数:

  • buttons – 一个含有三个数字的元组,三个值分别表明左键、中键和右键,1就是按下了。
  • pos – 就是位置了……
  • rel – 表明了如今距离上次产生鼠标事件时的距离

和MOUSEMOTION相似的,咱们还有MOUSEBUTTONDOWNMOUSEBUTTONUP两个事件,看名字就明白是什么意思了。不少时候,你只须要知道鼠标点下就能够了,那就能够不用上面那个比较强大(也比较复杂)的事件了。它们的参数为:

  • button – 看清楚少了个s,这个值表明了哪一个按键被操做
  • pos – 和上面同样

处理键盘事件

键盘和游戏手柄的事件比较相似,为KEYDOWNKEYUP,下面有一个例子来演示使用方向键移动一些东西。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

background_image_filename = 'sushiplate.jpg'

 

import pygame

from pygame.locals import *

from sys import exit

 

pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)

background = pygame.image.load(background_image_filename).convert()

 

x, y = 0, 0

move_x, move_y = 0, 0

 

while True:

    for event in pygame.event.get():

        if event.type == QUIT:

           exit()

        if event.type == KEYDOWN:

            #键盘有按下?

            if event.key == K_LEFT:

                #按下的是左方向键的话,把x坐标减一

                move_x = -1

            elif event.key == K_RIGHT:

                #右方向键则加一

                move_x = 1

            elif event.key == K_UP:

                #相似了

                move_y = -1

            elif event.key == K_DOWN:

                move_y = 1

        elif event.type == KEYUP:

            #若是用户放开了键盘,图就不要动了

            move_x = 0

            move_y = 0

 

        #计算出新的坐标

        x+= move_x

        y+= move_y

 

        screen.fill((0,0,0))

        screen.blit(background, (x,y))

        #在新的位置上画图

        pygame.display.update()

当咱们运行这个程序的时候,按下方向键就能够把背景图移动,可是等等!为何我只能按一下动一下啊……太很差试了吧?!用脚掌考虑下就应该按着就一直动下去才是啊!?Pygame这么垃圾么……

哦,真是抱歉上面的代码有点小bug,可是真的很小,你都不须要更改代码自己,只要改一下缩进就能够了,你能够发现么?Python自己是缩进编排来表现层次,有些时候可能会出现一点小麻烦,要咱们本身注意才能够。

KEYDOWN和KEYUP的参数描述以下:

  • key – 按下或者放开的键值,是一个数字,估计地球上不多有人能够记住,因此Pygame中你能够使用K_xxx来表示,好比字母a就是K_a,还有K_SPACEK_RETURN等。
  • mod – 包含了组合键信息,若是mod & KMOD_CTRL是真的话,表示用户同时按下了Ctrl键。相似的还有KMOD_SHIFTKMOD_ALT
  • unicode – 表明了按下键的Unicode值,这个有点很差理解,真正说清楚又太麻烦,游戏中也不太经常使用,说明暂时省略,何时须要再讲吧。

事件过滤

并非全部的事件都须要处理的,就好像不是全部登门造访的人都是咱们欢迎的同样。好比,俄罗斯方块就无视你的鼠标,而在游戏场景切换的时候,你按什么都是徒劳的。咱们应该有一个方法来过滤掉一些咱们不感兴趣的事件(固然咱们能够不处理这些没兴趣的事件,但最好的方法仍是让它们根本不进入咱们的事件队列,就好像在门上贴着“XXX免进”同样),咱们使用pygame.event.set_blocked(事件名)来完成。若是有好多事件须要过滤,能够传递一个列表,好比pygame.event.set_blocked([KEYDOWN, KEYUP]),若是你设置参数None,那么全部的事件有被打开了。与之相对的,咱们使用pygame.event.set_allowed()来设定容许的事件。

产生事件

一般玩家作什么,Pygame就产生对应的事件就能够了,不过有的时候咱们须要模拟出一些事件来,好比录像回放的时候,咱们就要把用户的操做再现一遍。

为了产生事件,必须先造一个出来,而后再传递它:

Python

 

1

2

3

4

my_event = pygame.event.Event(KEYDOWN, key=K_SPACE, mod=0, unicode=u' ')

#你也能够像下面这样写,看起来比较清晰(但字变多了……)

my_event = pygame.event.Event(KEYDOWN, {"key":K_SPACE, "mod":0, "unicode":u' '})

pygame.event.post(my_event)

你甚至能够产生一个彻底自定义的全新事件,有些高级的话题,暂时不详细说,仅用代码演示一下:

Python

 

1

2

3

4

5

6

7

8

CATONKEYBOARD = USEREVENT+1

my_event = pygame.event.Event(CATONKEYBOARD, message="Bad cat!")

pgame.event.post(my_event)

 

#而后得到它

for event in pygame.event.get():

    if event.type == CATONKEYBOARD:

        print event.message

此次的内容不少,又很重要,一遍看下来云里雾里或者看的时候明白看完了全忘了什么的估计不少,慢慢学习吧~~多看看动手写写,其实都很简单。

下次讲解显示的部分。

用Python和Pygame写游戏-从入门到精通(3)

 

全屏显示

咱们在第一个程序里使用了以下的语句

Python

 

1

screen = pygame.display.set_mode((640, 480), 0, 32)

也讲述了各个参数的意思,当咱们把第二个参数设置为FULLSCREEN时,就能获得一个全屏窗口了

Python

 

1

screen = pygame.display.set_mode((640, 480), FULLSCREEN, 32)

注意:若是你的程序有什么问题,极可能进入了全屏模式就不太容易退出来了,因此最好先用窗口模式调试好,再改成全屏模式。

在全屏模式下,显卡可能就切换了一种模式,你能够用以下代码得到您的机器支持的显示模式:

Python

 

1

2

3

>>> import pygame

>>> pygame.init()

>>> pygame.display.list_modes()

看一下一个实例:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

background_image_filename = 'sushiplate.jpg'

 

import pygame

from pygame.locals import *

from sys import exit

 

pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)

background = pygame.image.load(background_image_filename).convert()

 

Fullscreen = False

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

    if event.type == KEYDOWN:

        if event.key == K_f:

            Fullscreen = not Fullscreen

            if Fullscreen:

                screen = pygame.display.set_mode((640, 480), FULLSCREEN, 32)

            else:

                screen = pygame.display.set_mode((640, 480), 0, 32)

 

    screen.blit(background, (0,0))

    pygame.display.update()

运行这个程序,默认仍是窗口的,按“f ”,显示模式会在窗口和全屏之间切换。程序也没有什么难度,应该都能看明白。

可变尺寸的显示

虽然通常的程序窗口都能拖边框来改变大小,pygame的默认显示窗口是不行的,而事实上,不少游戏确实也不能改变显示窗口的大小,咱们能够使用一个参数来改变这个默认行为。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

background_image_filename = 'sushiplate.jpg'

 

import pygame

from pygame.locals import *

from sys import exit

 

SCREEN_SIZE = (640, 480)

 

pygame.init()

screen = pygame.display.set_mode(SCREEN_SIZE, RESIZABLE, 32)

 

background = pygame.image.load(background_image_filename).convert()

 

while True:

 

    event = pygame.event.wait()

    if event.type == QUIT:

        exit()

    if event.type == VIDEORESIZE:

        SCREEN_SIZE = event.size

        screen = pygame.display.set_mode(SCREEN_SIZE, RESIZABLE, 32)

        pygame.display.set_caption("Window resized to "+str(event.size))

 

    screen_width, screen_height = SCREEN_SIZE

    # 这里须要从新填满窗口

    for y in range(0, screen_height, background.get_height()):

        for x in range(0, screen_width, background.get_width()):

            screen.blit(background, (x, y))

 

    pygame.display.update()

当你更改大小的时候,后端控制台会显示出新的尺寸,这里咱们学习到一个新的事件VIDEORESIZE,它包含以下内容:

  • size  —  一个二维元组,值为更改后的窗口尺寸,size[0]为宽,size[1]为高
  • w  —  宽
  • h  —  一目了然,高;之因此多出这两个,无非是为了方便

 

其余、复合模式

咱们还有一些其余的显示模式,但未必全部的操做系统都支持(放心windows、各类比较流行的Linux发行版都是没问题的),通常来讲窗口就用0全屏就用FULLSCREEN,这两个老是OK的。

若是你想建立一个硬件显示(surface会存放在显存里,从而有着更高的速度),你必须和全屏一块儿使用:

Python

 

1

screen = pygame.display.set_mode(SCREEN_SIZE, HWSURFACE | FULLSCREEN, 32)

固然你彻底能够把双缓冲(更快)DOUBLEBUF也加上,这就是一个很棒的游戏显示了,不过记得你要使用pygame.display.flip()来刷新显示。pygame.display.update()是将数据画到前面显示,而这个是交替显示的意思。

稍微说一下双缓冲的意思,能够作一个比喻:个人任务就是出黑板报,若是只有一块黑板,那我得不停的写,所有写完了稍微Show一下就要擦掉重写,这样一来别人看的基本都是我在写黑板报的过程,看到的都是不完整的黑板报;若是我有两块黑板,那么能够挂一块给别人看,我本身在底下写另外一块,写好了把原来的换下来换上新的,这样一来别人基本老是看到完整的内容了。双缓冲就是这样维护两个显示区域,快速的往屏幕上换内容,而不是每次都慢慢地重画。

 

用Python和Pygame写游戏-从入门到精通(4)

 

使用字体模块

就像上一次说的,一个游戏,再怎么寒碜也得有文字,俄罗斯方块还有个记分数的呢;印象中没有文字的电子游戏只有电脑刚刚诞生的那种打乒乓的了。Pygame能够直接调用系统字体,或者也能够使用TTF字体,稍有点电脑知识的都知道这是什么。为了使用字体,你得先建立一个Font对象,对于系统自带的字体:

Python

 

1

my_font = pygame.font.SysFont("arial", 16)

第一个参数是字体名,第二个天然就是大小,通常来讲“Arial”字体在不少系统都是存在的,若是找不到的话,就会使用一个默认的字体,这个默认的字体和每一个操做系统相关,你也能够使用pygame.font.get_fonts()来得到当前系统全部可用字体。还有一个更好的方法的,使用TTF的方法:

Python

 

1

my_font = pygame.font.Font("my_font.ttf", 16)

这个语句使用了一个叫作“my_font.ttf”,这个方法之因此好是由于你能够把字体文件随游戏一块儿分发,避免用户机器上没有须要的字体。。一旦你建立了一个font对象,你就能够使用render方法来写字了,而后就能blit到屏幕上:

Python

 

1

text_surface = my_font.render("Pygame is cool!", True, (0,0,0), (255, 255, 255))

第一个参数是写的文字;第二个参数是个布尔值,觉得这是否开启抗锯齿,就是说True的话字体会比较平滑,不过相应的速度有一点点影响;第三个参数是字体的颜色;第四个是背景色,若是你想没有背景色(也就是透明),那么能够不加这第四个参数。

下面是一个小例子演示下文字的使用,不过并非显示在屏幕上,而是存成一个图片文件

Python

 

1

2

3

4

5

6

my_name = "Will McGugan"

import pygame

pygame.init()

my_font = pygame.font.SysFont("arial", 64)

name_surface = my_font.render(my_name, True, (0, 0, 0), (255, 255, 255))

pygame.image.save(name_surface, "name.png")

追加说明一下如何显示中文,这在原书但是没有的哦:) 简单来讲,首先你得用一个能够使用中文的字体,宋体、黑体什么的,或者你直接用中文TTF文件,而后文字使用unicode,即u”中文的文字”这种,最后不要忘了源文件里加上一句关于文件编码的“魔法注释”,具体的能够查一下Python的编码方面的文章。举一个这样的例子:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

# -*- coding: utf-8 -*-

# 记住上面这行是必须的,并且保存文件的编码要一致!

import pygame

from pygame.locals import *

from sys import exit

 

pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)

 

#font = pygame.font.SysFont("宋体", 40)

#上句在Linux可行,在个人Windows 7 64bit上不行,XP不知道行不行

#font = pygame.font.SysFont("simsunnsimsun", 40)

#用get_fonts()查看后看到了这个字体名,在个人机器上能够正常显示了

font = pygame.font.Font("simsun.ttc", 40)

#这句话老是能够的,因此仍是TTF文件保险啊

text_surface = font.render(u"你好", True, (0, 0, 255))

 

x = 0

y = (480 - text_surface.get_height())/2

 

background = pygame.image.load("sushiplate.jpg").convert()

 

while True:

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

 

    screen.blit(background, (0, 0))

 

    x -= 2  # 文字滚动太快的话,改改这个数字

    if x < -text_surface.get_width():

        x = 640 - text_surface.get_width()

 

    screen.blit(text_surface, (x, y))

 

    pygame.display.update()

Pygame的错误处理

程序总会出错的,好比当内存用尽的时候Pygame就没法再加载图片,或者文件根本就不存在。再好比下例:

Python

 

1

2

3

4

5

6

7

>>> import pygame

>>> screen = pygame.display.set_mode((640, -1))

---------------------------------

Traceback (most recent call last):

  File "<interactive input>", line 1, in ?

pygame.error: Cannot set 0 sized display mode

----------------------------------

对付这种错误一个比较好的方法:

Python

 

1

2

3

4

5

6

try:

    screen = pygame.display.set_mode(SCREEN_SIZE)

except pygame.error, e:

    print "Can't create the display :-("

    print e

    exit()

其实就是Python的标准的错误捕捉方法就是了,实际的游戏(或者程序)中,错误捕捉实在过重要了,若是你写过比较大的应用,应该不用我来讲明这一点,Pygame中也是同样的。

Pygame的基础就到这里,后面咱们会进行一些高级的介绍,下一次的话,就开始讲画东西了~

用Python和Pygame写游戏-从入门到精通(5)

像素的威力

凑近显示器,你能看到图像是由一个一个点构成,这就是像素。至于屏幕分辨率的意义,也就不用多说了吧,一个1280×1024的显示器,有着1310720个像素,通常的32为RGB系统,每一个像素能够显示16.7百万种颜色(能够看个人另外一篇一张白纸能够承载多少重的文章),咱们能够写一个小程序来显示这么多的颜色~

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

import pygame

pygame.init()

 

screen = pygame.display.set_mode((640, 480))

 

all_colors = pygame.Surface((4096,4096), depth=24)

 

for r in xrange(256):

    print r+1, "out of 256"

    x = (r&15)*256

    y = (r>>4)*256

    for g in xrange(256):

        for b in xrange(256):

            all_colors.set_at((x+g, y+b), (r, g, b))

 

pygame.image.save(all_colors, "allcolors.bmp")

运行可能有些慢,你应该等生成bmp图像文件,打开看看效果吧

色彩是一个颇有趣的话题,好比把蓝色和黄色混合产生绿色,事实上你能够用红黄蓝混合出全部的颜色(光学三原色),电脑屏幕上的三原色是红绿蓝(RGB),要想更深入的理解这个东西,你得学习一下(就看看李涛的PhotoShop讲座吧,VeryCD上有下的,讲的仍是很清楚的)~

稍有点经验的图像设计者应该看到RGB的数值就能想象出大概的颜色,咱们来用一个Python脚本增强这个认识。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

#!/usr/bin/env python

import pygame

from pygame.locals import *

from sys import exit

 

pygame.init()

 

screen = pygame.display.set_mode((640, 480), 0, 32)

 

def create_scales(height):

    red_scale_surface = pygame.surface.Surface((640, height))

    green_scale_surface = pygame.surface.Surface((640, height))

    blue_scale_surface = pygame.surface.Surface((640, height))

    for x in range(640):

        c = int((x/640.)*255.)

        red = (c, 0, 0)

        green = (0, c, 0)

        blue = (0, 0, c)

        line_rect = Rect(x, 0, 1, height)

        pygame.draw.rect(red_scale_surface, red, line_rect)

        pygame.draw.rect(green_scale_surface, green, line_rect)

        pygame.draw.rect(blue_scale_surface, blue, line_rect)

    return red_scale_surface, green_scale_surface, blue_scale_surface

 

red_scale, green_scale, blue_scale = create_scales(80)

 

color = [127, 127, 127]

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

 

    screen.fill((0, 0, 0))

 

    screen.blit(red_scale, (0, 00))

    screen.blit(green_scale, (0, 80))

    screen.blit(blue_scale, (0, 160))

 

    x, y = pygame.mouse.get_pos()

 

    if pygame.mouse.get_pressed()[0]:

        for component in range(3):

            if y > component*80 and y < (component+1)*80:

                color[component] = int((x/639.)*255.)

        pygame.display.set_caption("PyGame Color Test - "+str(tuple(color)))

 

    for component in range(3):

        pos = ( int((color[component]/255.)*639), component*80+40 )

        pygame.draw.circle(screen, (255, 255, 255), pos, 20)

 

    pygame.draw.rect(screen, tuple(color), (0, 240, 640, 240))

 

    pygame.display.update()

 

颜色的缩放

“缩放颜色”并非一种合适的说法,它的准确意义就是上面所说的把颜色变亮或者变暗。通常来讲,把颜色的RGB每个数值乘以一个小于1的正小数,颜色看起来就会变暗了(记住RGB都是整数因此可能须要取整一下)。咱们很容易能够写一个缩放颜色的函数出来,我就不赘述了。

很天然的能够想到,若是乘以一个大于1的数,颜色就会变亮,不过一样要记住每一个数值最多255,因此一旦超过,你得把它归为255!使用Python的内置函数min,你能够方便的作到这事情,也很少说了。若是你乘的数字偏大,颜色很容易就为变成纯白色,就失去了原来的色调。并且RGB也不多是负数,因此谨慎选择你的缩放系数!

颜色的混合

不少时候咱们还须要混合颜色,好比一个僵尸在路过一个火山熔岩坑的时候,它会由绿色变成橙红色,再变为正常的绿色,这个过程必须表现的很平滑,这时候咱们就须要混合颜色。

咱们用一种叫作“线性插值(linear interpolation)”的方法来作这件事情。为了找到两种颜色的中间色,咱们将这第二种颜色与第一种颜色的差乘以一个0~1之间的小数,而后再加上第一种颜色就好了。若是这个数为0,结果就彻底是第一种颜色;是1,结果就只剩下第二种颜色;中间的小数则会皆有二者的特点。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

#!/usr/bin/env python

 

import pygame

from pygame.locals import *

from sys import exit

 

pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)

 

color1 = (221, 99, 20)

color2 = (96, 130, 51)

factor = 0.

 

def blend_color(color1, color2, blend_factor):

    r1, g1, b1 = color1

    r2, g2, b2 = color2

    r = r1 + (r2 - r1) * blend_factor

    g = g1 + (g2 - g1) * blend_factor

    b = b1 + (b2 - b1) * blend_factor

    return int(r), int(g), int(b)

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

 

    screen.fill((255,255,255))

 

    tri = [ (0, 120), (639, 100), (639, 140) ]

    pygame.draw.polygon(screen, (0, 255, 0), tri)

    pygame.draw.circle(screen, (0, 0, 0), (int(factor * 639.0), 120), 10)

 

    x, y = pygame.mouse.get_pos()

    if pygame.mouse.get_pressed()[0]:

        factor = x / 639.0

        pygame.display.set_caption("Pygame Color Blend Test - %.3f" % factor)

 

    color = blend_color(color1, color2 , factor)

    pygame.draw.rect(screen, color, (0, 240, 640, 240))

 

    pygame.display.update()

 

用Python和Pygame写游戏-从入门到精通(6)

这个世界上有不少存储图像的方式(也就是有不少图片格式),好比JPEG、PNG等,Pygmae都能很好的支持,具体支持的格式以下:

  • JPEG(Join Photograhpic Exper Group),极为经常使用,通常后缀名为.jpg或者.jpeg。数码相机、网上的图片基本都是这种格式。这是一种有损压缩方式,尽管对图片质量有些损坏,但对于减少文件尺寸很是棒。优势不少只是不支持透明。
  • PNG(Portable Network Graphics)将会大行其道的一种格式,支持透明,无损压缩。对于网页设计,软件界面设计等等都是很是棒的选择!
  • GIF 网上使用的不少,支持透明和动画,只是只能有256种颜色,软件和游戏中使用不多
  • BMP Windows上的标准图像格式,无压缩,质量很高但尺寸很大,通常不使用
  • PCX
  • TGA
  • TIF
  • LBM, PBM
  • XPM

使用Surface对象

对于Pygame而已,加载图片就是pygame.image.load,给它一个文件名而后就还给你一个surface对象。尽管读入的图像格式各不相同,surface对象隐藏了这些不一样。你能够对一个Surface对象进行涂画、变形、复制等各类操做。事实上,屏幕也只是一个surface,pygame.display.set_mode就返回了一个屏幕surface对象。

建立Surfaces对象

一种方法就是刚刚说的pygame.image.load,这个surface有着和图像相同的尺寸和颜色;另一种方法是指定尺寸建立一个空的surface,下面的语句建立一个256×256像素的surface:

Python

 

1

bland_surface = pygame.Surface((256, 256))

若是不指定尺寸,那么就建立一个和屏幕同样大小的。

你还有两个参数可选,第一个是flags:

  • HWSURFACE – 相似于前面讲的,更快!不过最好不设定,Pygmae能够本身优化。
  • SRCALPHA – 有Alpha通道的surface,若是你须要透明,就要这个选项。这个选项的使用须要第二个参数为32~

第二个参数是depth,和pygame.display.set_mode中的同样,你能够不设定,Pygame会自动设的和display一致。不过若是你使用了SRCALPHA,仍是设为32吧:

Python

 

1

bland_alpha_surface = pygame.Surface((256, 256), flags=SRCALPHA, depth=32)

 

转换Surfaces

一般你不用在乎surface里的具体内容,不过也许须要把这些surface转换一下以得到更高的性能,还记得一开始的程序中的两句话吗:

Python

 

1

2

background = pygame.image.load(background_image_filename).convert()

mouse_cursor = pygame.image.load(mouse_image_filename).convert_alpha()

第一句是普通的转换,相同于display;第二句是带alpha通道的转换。若是你给convert或者conver_alpha一个surface对象做为参数,那么这个会被做为目标来转换。

矩形对象(Rectangle Objects)

通常来讲在制定一个区域的时候,矩形是必须的,好比在屏幕的一部分画东西。在pygame中矩形对象极为经常使用,它的指定方法能够用一个四元素的元组,或者两个二元素的元组,前两个数为左上坐标,后两位为右下坐标。

Pygame中有一个Rect类,用来存储和处理矩形对象(包含在pygame.locals中,因此若是你写了from pygame.locals import *就能够直接用这个对象了),好比:

Python

 

1

2

3

4

5

my_rect1 = (100, 100, 200, 150)

my_rect2 = ((100, 100), (200, 150))

#上两种为基础方法,表示的矩形也是同样的

my_rect3 = Rect(100, 100, 200, 150)

my_rect4 = Rect((100, 100), (200, 150))

一旦有了Rect对象,咱们就能够对其作不少操做,好比调整位置和大小,判断一个点是否在其中等等。之后会慢慢接触到,求知欲旺盛的能够在http://www.pygame.org/docs/ref/rect.html中找到Rect的详细信息。

剪裁(Clipping)

一般游戏的时候你只须要绘制屏幕的一部分。好比魔兽上面是菜单,下面是操做面板,中间的小兵和英雄打的不可开交时候,上下的部分也是保持相对不动的。为了实现这一点,surface就有了一种叫裁剪区域(clipping area)的东西,也是一个矩形,定义了哪部分会被绘制,也就是说一旦定义了这个区域,那么只有这个区域内的像素会被修改,其余的位置保持不变,默认状况下,这个区域是全部地方。咱们能够使用set_clip来设定,使用get_clip来得到这个区域。

下面几句话演示了如何使用这个技术来绘制不一样的区域:

Python

 

1

2

3

4

5

6

screen.set_clip(0, 400, 200, 600)

draw_map()

#在左下角画地图

screen.set_clip(0, 0, 800, 60)

draw_panel()

#在上方画菜单面板

 

子表面(Subsurfaces)

Subsurface就是在一个Surface中再提取一个Surface,记住当你往Subsurface上画东西的时候,同时也向父表面上操做。这能够用来绘制图形文字,尽管pygame.font能够用来写很不错的字,但只是单色,游戏可能须要更丰富的表现,这时候你能够把每一个字母(中文的话有些吃力了)各自作成一个图片,不过更好的方法是在一张图片上画满全部的字母。把整张图读入,而后再用Subsurface把字母一个一个“抠”出来,就像下面这样:

Python

 

1

2

3

4

my_font_image = Pygame.load("font.png")

letters = []

letters["a"] = my_font_image.subsurface((0,0), (80,80))

letters["b"] = my_font_image.subsurface((80,0), (80,80))

 

填充Surface

填充有时候能够做为一种清屏的操做,把整个surface填上一种颜色:

Python

 

1

screen.fill((0, 0, 0))

一样能够提供一个矩形来制定填充哪一个部分(这也能够做为一种画矩形的方法)。

设置Surface的像素

咱们能对Surface作的最基本的操做就是设置一个像素的色彩了,虽然咱们基本不会这么作,但仍是要了解。set_at方法能够作到这一点,它的参数是坐标和颜色,下面的小脚本会随机的在屏幕上画点:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

import pygame

from pygame.locals import *

from sys import exit

from random import randint

 

pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)

 

while True:

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

 

    rand_col = (randint(0, 255), randint(0, 255), randint(0, 255))

    #screen.lock()    #很快你就会知道这两句lock和unlock的意思了

    for _ in xrange(100):

        rand_pos = (randint(0, 639), randint(0, 479))

        screen.set_at(rand_pos, rand_col)

    #screen.unlock()

 

    pygame.display.update()

得到Surface上的像素

set_at的兄弟get_at能够帮咱们作这件事,它接受一个坐标返回指定坐标点上的颜色。不过记住get_at在对hardware surface操做的时候很慢,而全屏的时候老是hardware的,因此慎用这个方法!

锁定Surface

当Pygame往surface上画东西的时候,首先会把surface锁住,以保证不会有其它的进程来干扰,画完以后再解锁。锁和解锁时自动发生的,因此有时候可能不那么有效率,好比上面的例子,每次画100个点,那么就得锁解锁100次,如今咱们把两句注释去掉,再执行看看是否是更快了(好吧,其实我没感受出来,由于如今的机器性能都不错,这么点的差别还不太感受的出来。不过请相信我~复杂的状况下会影响效率的)?

当你手动加锁的时候,必定不要忘记解锁,不然pygame有可能会失去响应。虽然上面的例子可能没问题,可是隐含的bug是咱们必定要避免的事情。

Blitting

blit的的中文翻译给人摸不着头脑的感受,能够译为位块传送(bit block transfer),其意义是将一个平面的一部分或所有图象整块从这个平面复制到另外一个平面,下面仍是直接使用英文。

blit是对表面作的最多的操做,咱们在前面的程序中已经屡次用到,很少说了;blit的还有一种用法,每每用在对动画的表现上,好比下例经过对frame_no的值的改变,咱们能够把不一样的帧(同一副图的不一样位置)画到屏幕上:

Python

 

1

screen.blit(ogre, (300, 200), (100 * frame_no, 0, 100, 100))

此次东西真是很多,打完脖子都酸了……

不少之前的程序中已经出现,看完这部分才能算是真正了解。图像是游戏相当重要的一部分,值得多花时间,下一次讲解绘制图形~

用Python和Pygame写游戏-从入门到精通(7)

pygame.draw中函数的第一个参数老是一个surface,而后是颜色,再后会是一系列的坐标等。稍有些计算机绘图经验的人就会知道,计算机里的坐标,(0,0)表明左上角。而返回值是一个Rect对象,包含了绘制的领域,这样你就能够很方便的更新那个部分了。

函数

做用

rect

绘制矩形

polygon

绘制多边形(三个及三个以上的边)

circle

绘制圆

ellipse

绘制椭圆

arc

绘制圆弧

line

绘制线

lines

绘制一系列的线

aaline

绘制一根平滑的线

aalines

绘制一系列平滑的线

咱们下面一个一个详细说明。

pygame.draw.rect

用法:pygame.draw.rect(Surface, color, Rect, width=0)

pygame.draw.rect在surface上画一个矩形,除了surface和color,rect接受一个矩形的坐标和线宽参数,若是线宽是0或省略,则填充。咱们有一个另外的方法来画矩形——fill方法,若是你还记得的话。事实上fill可能还会快一点点,由于fill由显卡来完成。

pygame.draw.polygon

用法:pygame.draw.polygon(Surface, color, pointlist, width=0)

polygon就是多边形,用法相似rect,第1、第2、第四的参数都是相同的,只不过polygon会接受一系列坐标的列表,表明了各个顶点。

pygame.draw.circle

用法:pygame.draw.circle(Surface, color, pos, radius, width=0)

很简单,画一个圆。与其余不一样的是,它接收一个圆心坐标和半径参数。

pygame.draw.ellipse

用法:pygame.draw.ellipse(Surface, color, Rect, width=0)

你能够把一个ellipse想象成一个被压扁的圆,事实上,它是能够被一个矩形装起来的。pygame.draw.ellipse的第三个参数就是这个椭圆的外接矩形。

pygame.draw.arc

用法:pygame.draw.arc(Surface, color, Rect, start_angle, stop_angle, width=1)

arc是椭圆的一部分,因此它的参数也就比椭圆多一点。但它是不封闭的,所以没有fill方法。start_angle和stop_angle为开始和结束的角度。

pygame.draw.line

用法:pygame.draw.line(Surface, color, start_pos, end_pos, width=1)

我相信全部的人都能看明白。

pygame.draw.lines

用法:pygame.draw.lines(Surface, color, closed, pointlist, width=1)

closed是一个布尔变量,指明是否须要多画一条线来使这些线条闭合(感受就和polygone同样了),pointlist是一个点的数组。

上面的表中咱们还有aalineaalines,玩游戏的都知道开出抗锯齿(antialiasing效果会让画面更好看一些,模型的边就不会是锯齿形的了,这两个方法就是在画线的时候作这事情的,参数和上面同样,省略。

咱们用一个混杂的例子来演示一下上面的各个方法:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

#!/usr/bin/env python

 

import pygame

from pygame.locals import *

from sys import exit

 

from random import *

from math import pi

 

pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)

points = []

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

        if event.type == KEYDOWN:

            # 按任意键能够清屏并把点回复到原始状态

            points = []

            screen.fill((255,255,255))

        if event.type == MOUSEBUTTONDOWN:

            screen.fill((255,255,255))

            # 画随机矩形

            rc = (randint(0,255), randint(0,255), randint(0,255))

            rp = (randint(0,639), randint(0,479))

            rs = (639-randint(rp[0], 639), 479-randint(rp[1], 479))

            pygame.draw.rect(screen, rc, Rect(rp, rs))

            # 画随机圆形

            rc = (randint(0,255), randint(0,255), randint(0,255))

            rp = (randint(0,639), randint(0,479))

            rr = randint(1, 200)

            pygame.draw.circle(screen, rc, rp, rr)

            # 得到当前鼠标点击位置

            x, y = pygame.mouse.get_pos()

            points.append((x, y))

            # 根据点击位置画弧线

            angle = (x/639.)*pi*2.

            pygame.draw.arc(screen, (0,0,0), (0,0,639,479), 0, angle, 3)

            # 根据点击位置画椭圆

            pygame.draw.ellipse(screen, (0, 255, 0), (0, 0, x, y))

            # 从左上和右下画两根线链接到点击位置

            pygame.draw.line(screen, (0, 0, 255), (0, 0), (x, y))

            pygame.draw.line(screen, (255, 0, 0), (640, 480), (x, y))

            # 画点击轨迹图

            if len(points) > 1:

                pygame.draw.lines(screen, (155, 155, 0), False, points, 2)

            # 和轨迹图基本同样,只不过是闭合的,由于会覆盖,因此这里注释了

            #if len(points) >= 3:

            #    pygame.draw.polygon(screen, (0, 155, 155), points, 2)

            # 把每一个点画明显一点

            for p in points:

                pygame.draw.circle(screen, (155, 155, 155), p, 3)

 

    pygame.display.update()

运行这个程序,在上面点鼠标就会有图形出来了;按任意键能够从新开始。另外这个程序只是各个命令的堆砌,并不见得是一个好的程序代码。

到此次为止,文字、颜色、图像、图形都讲好了,静态显示的部分都差很少了。然而多彩的游戏中只有静态的东西实在太让人寒心了(GalGame大多如此),下次开始咱们学习游戏中的动画制做。

用Python和Pygame写游戏-从入门到精通(8)

 

直线运动

咱们先来看一下初中一开始就学习的直线运动,咱们让一开始的程序中出现的那条鱼本身动起来~

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

background_image_filename = 'sushiplate.jpg'

sprite_image_filename = 'fugu.png'

 

import pygame

from pygame.locals import *

from sys import exit

 

pygame.init()

 

screen = pygame.display.set_mode((640, 480), 0, 32)

 

background = pygame.image.load(background_image_filename).convert()

sprite = pygame.image.load(sprite_image_filename)

 

# sprite的起始x坐标

x = 0.

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

 

    screen.blit(background, (0,0))

    screen.blit(sprite, (x, 100))

    x+= 10.     #若是你的机器性能太好以致于看不清,能够把这个数字改小一些

 

    # 若是移动出屏幕了,就搬到开始位置继续

    if x > 640.:

        x = 0.    

 

    pygame.display.update()

我想你应该须要调节一下“x += 10.”来让这条鱼游的天然一点,不过,这个动画的帧率是多少的?在这个情形下,动画很简单,因此应该会很快;而有些时候动画元素不少,速度就会慢下来。这可不是咱们想看到的!

关于时间

有一个解决上述问题的方法,就是让咱们的动画基于时间运做,咱们须要知道上一个画面到如今通过了多少时间,而后咱们才能决定是否开始绘制下一幅。pygame.time模块给咱们提供了一个Clock的对象,使咱们能够轻易作到这一些:

Python

 

1

2

3

clock = pygame.time.Clock()

time_passed = clock.tick()

time_passed = clock.tick(30)

第一行初始化了一个Clock对象;第二行的意识是返回一个上次调用的时间(以毫秒计);第三行很是有用,在每个循环中加上它,那么给tick方法加上的参数就成为了游戏绘制的最大帧率,这样的话,游戏就不会用掉你全部的CPU资源了!可是这仅仅是“最大帧率”,并不能表明用户看到的就是这个数字,有些时候机器性能不足,或者动画太复杂,实际的帧率达不到这个值,咱们须要一种更有效的手段来控制咱们的动画效果。

为了使得在不一样机器上有着一致的效果,咱们实际上是须要给定物体(咱们把这个物体叫作精灵,Sprite)恒定的速度。这样的话,从起点到终点的时间点是同样的,最终的效果也就相同了,所差异的,只是流畅度。看下面的图试着理解一下~

 

咱们把上面的结论实际试用一下,假设让咱们的小鱼儿每秒游动250像素,这样游动一个屏幕差很少须要2.56秒。咱们就须要知道,从上一帧开始到如今,小鱼应该游动了多少像素,这个算法很简单,速度*时间就好了,也就是250 * time_passed_second。不过咱们刚刚获得的time_passed是毫秒,不要忘了除以1000.0,固然咱们也能假设小鱼每毫秒游动0.25像素,这样就能够直接乘了,不过这样的速度单位有些怪怪的……

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

background_image_filename = 'sushiplate.jpg'

sprite_image_filename = 'fugu.png'

 

import pygame

from pygame.locals import *

from sys import exit

 

pygame.init()

 

screen = pygame.display.set_mode((640, 480), 0, 32)

 

background = pygame.image.load(background_image_filename).convert()

sprite = pygame.image.load(sprite_image_filename)

 

# Clock对象

clock = pygame.time.Clock()

 

x = 0.

# 速度(像素/秒)

speed = 250.

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

 

    screen.blit(background, (0,0))

    screen.blit(sprite, (x, 100))    

 

    time_passed = clock.tick()

    time_passed_seconds = time_passed / 1000.0

 

    distance_moved = time_passed_seconds * speed

    x += distance_moved

 

    # 想一下,这里减去640和直接归零有何不一样?

    if x > 640.:

        x -= 640.    

 

    pygame.display.update()

好了,这样无论你的机器是更深的蓝仍是打开个记事本都要吼半天的淘汰机,人眼看起来,不一样屏幕上的鱼的速度都是一致的了。请紧紧记住这个方法,在不少状况下,经过时间控制要比直接调节帧率好用的多。

斜线运动

下面有一个更有趣一些的程序,再也不是单纯的直线运动,而是有点像屏保同样,碰到了壁会反弹。不过也并无新的东西在里面,原理上来讲,反弹只不过是把速度取反了而已~ 能够先试着本身写一个,而后与这个对照一下。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

background_image_filename = 'sushiplate.jpg'

sprite_image_filename = 'fugu.png'

 

import pygame

from pygame.locals import *

from sys import exit

 

pygame.init()

 

screen = pygame.display.set_mode((640, 480), 0, 32)

 

background = pygame.image.load(background_image_filename).convert()

sprite = pygame.image.load(sprite_image_filename).convert_alpha()

 

clock = pygame.time.Clock()

 

x, y = 100., 100.

speed_x, speed_y = 133., 170.

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

 

    screen.blit(background, (0,0))

    screen.blit(sprite, (x, y))

 

    time_passed = clock.tick(30)

    time_passed_seconds = time_passed / 1000.0

 

    x += speed_x * time_passed_seconds

    y += speed_y * time_passed_seconds    

 

    # 到达边界则把速度反向

    if x > 640 - sprite.get_width():

        speed_x = -speed_x

        x = 640 - sprite.get_width()

    elif x < 0:

        speed_x = -speed_x

        x = 0.

 

    if y > 480 - sprite.get_height():

        speed_y = -speed_y

        y = 480 - sprite.get_height()

    elif y < 0:

        speed_y = -speed_y

        y = 0

 

    pygame.display.update()

OK,此次的运动就说到这里。仔细一看的话,就会明白游戏中的所谓运动(尤为是2D游戏),不过是把一个物体的坐标改一下而已。不过老是不停的计算和修改x和y,有些麻烦不是么,下次咱们引入向量,看看使用数学怎样能够帮咱们减轻负担。

用Python和Pygame写游戏-从入门到精通(9)

引入向量

咱们先考虑二维的向量,三维也差很少了,而游戏中的运动最多只用获得三维,更高的留给之后的游戏吧~

向量的表示和坐标很像,(10,20)对坐标而言,就是一个固定的点,然而在向量中,它意味着x方向行进10,y方向行进20,因此坐标(0,0)加上向量(10,20)后,就到达了点(10,20)。

向量能够经过两个点来计算出来,以下图,A通过向量AB到达了B,则向量AB就是(30, 35) – (10, 20) = (20, 15)。咱们也能猜到向量BA会是(-20, -15),注意向量AB和向量BA,虽然长度同样,可是方向不一样。

 

在Python中,咱们能够建立一个类来存储和得到向量(虽然向量的写法很像一个元组,但由于向量有不少种计算,必须使用类来完成):

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

class Vector2(object):

    def __init__(self, x=0.0, y=0.0):

        self.x = x

        self.y = y

    def __str__(self):

        return "(%s, %s)"%(self.x, self.y)

 

    @classmethod

    def from_points(cls, P1, P2):

        return cls( P2[0] – P1[0], P2[1] – P1[1] )

#咱们能够使用下面的方法来计算两个点之间的向量

A = (10.0, 20.0)

B = (30.0, 35.0)

AB = Vector2.from_points(A, B)

print AB

原理上很简单,函数修饰符@不用我说明了吧?若是不明白的话,能够参考Python的编程指南。

向量的大小

向量的大小能够简单的理解为那根箭头的长度,勾股定理熟稔的各位马上知道怎么计算了:

Python

 

1

2

    def get_magnitude(self):

        return math.sqrt( self.x**2 + self.y**2 )

把这几句加入到刚刚的Vector2里,咱们的向量类就多了计算长度的能力。嗯,别忘了一开始要引入math库。

单位向量

一开头说过,向量有着大小和方向两个要素,经过刚刚的例子,咱们能够理解这两个意思了。在向量的你们族里,有一种比较特殊的向量叫“单位向量”,意思是大小为1的向量,咱们还能把任意向量方向不变的缩放(体如今数字上就是x和y等比例的缩放)到一个单位向量,这叫向量的规格(正规)化,代码体现的话:

Python

 

1

2

3

4

    def normalize(self):

        magnitude = self.get_magnitude()

        self.x /= magnitude

        self.y /= magnitude

使用过normalize方法之后,向量就成了一个单位向量。单位向量有什么用?咱们之后会看到。

向量运算

咱们观察下图,点B由A出发,经过向量AB到达,C则有B到达,经过BC到达;C直接由A出发的话,就得经由向量AC。

 

由此咱们获得一个显而易见的结论向量AC = 向量AB + 向量BC。向量的加法计算方法呼之欲出:

(20, 15) + (-15, 10) = (20-15, 15+10) = (5, 25)

把各个方向分别相加,咱们就获得了向量的加法运算法则。很相似的,减法也是一样,把各个方向分别想减,能够本身简单验证一下。代码表示的话:

Python

 

1

2

3

4

    def __add__(self, rhs):

        return Vector2(self.x + rhs.x, self.y + rhs.y)

    def __sub__(self, rhs):

        return Vector2(self.x - rhs.x, self.y - rhs.y)

两个下划线“__”为首尾的函数,在Python中通常就是重载的意思,若是不知道的话还须要稍微努力努力:)固然,功力稍深厚一点的,就会知道这里super来代替Vector2可能会更好一些,确实如此。不过这里只是示例代码,讲述一下原理而已。

有加减法,那乘除法呢?固然有!不过向量的乘除并非发生在两个向量直接,而是用一个向量来乘/除一个数,其实际意义就是,向量的方向不变,而大小放大/缩小多少倍。以下图:

Python

 

1

2

3

4

    def __mul__(self, scalar):

        return Vector2(self.x * scalar, self.y * scalar)

    def __div__(self, scalar):

        return Vector2(self.x / scalar, self.y / scalar)

向量的运算被普遍的用来计算到达某个位置时的中间状态,好比咱们知道一辆坦克从A到B,中间有10帧,那么很显然的,把步进向量经过(B-A)/10计算出来,每次在当前位置加上就能够了。很简单吧?

更好的向量类

咱们创造的向量类已经不错了,不过毕竟只能作一些简单的运算,别人帮咱们已经写好了更帅的库(早点不拿出来?写了半天…… 原理始终是咱们掌握的,本身动手,印象更深),是发挥拿来主义的时候了(能够尝试使用easy_install gameobjects简单的安装起来)。若是您没法打开这个地址,文章最后能够下载。下面是一个使用的例子:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

from gameobjects.vector2 import *

A = (10.0, 20.0)

B = (30.0, 35.0)

AB = Vector2.from_points(A, B)

print "Vector AB is", AB

print "AB * 2 is", AB * 2

print "AB / 2 is", AB / 2

print "AB + (–10, 5) is", AB + (–10, 5)

print "Magnitude of AB is", AB.get_magnitude()

print "AB normalized is", AB.get_normalized()

 

# 结果是下面

Vector AB is ( 20, 15 )

AB * 2 is ( 40, 30 )

AB / 2 is ( 10, 7.5 )

AB + (-10, 5) is ( 10, 20 )

Magnitude of AB is 25.0

AB normalized is ( 0.8, 0.6 )

 

使用向量的游戏动画

终于能够实干一番了!这个例子比咱们之前写的都要帅的多,小鱼不停的在咱们的鼠标周围游动,若即若离:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

background_image_filename = 'sushiplate.jpg'

sprite_image_filename = 'fugu.png'

 

import pygame

from pygame.locals import *

from sys import exit

from gameobjects.vector2 import Vector2

 

pygame.init()

 

screen = pygame.display.set_mode((640, 480), 0, 32)

 

background = pygame.image.load(background_image_filename).convert()

sprite = pygame.image.load(sprite_image_filename).convert_alpha()

 

clock = pygame.time.Clock()

 

position = Vector2(100.0, 100.0)

heading = Vector2()

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

 

    screen.blit(background, (0,0))

    screen.blit(sprite, position)

 

    time_passed = clock.tick()

    time_passed_seconds = time_passed / 1000.0

 

    # 参数前面加*意味着把列表或元组展开

    destination = Vector2( *pygame.mouse.get_pos() ) - Vector2( *sprite.get_size() )/2

    # 计算鱼儿当前位置到鼠标位置的向量

    vector_to_mouse = Vector2.from_points(position, destination)

    # 向量规格化

    vector_to_mouse.normalize()

 

    # 这个heading能够看作是鱼的速度,可是因为这样的运算,鱼的速度就不断改变了

    # 在没有到达鼠标时,加速运动,超过之后则减速。于是鱼会在鼠标附近晃动。

    heading = heading + (vector_to_mouse * .6)    

 

    position += heading * time_passed_seconds

    pygame.display.update()

虽然这个例子里的计算有些让人看不明白,可是很明显heading的计算是关键,如此复杂的运动,使用向量竟然两句话就搞定了~看来没有白学。

动画总结

  • 正如上一章所说,所谓动画,不过是在每一帧上,相对前一帧把精灵的坐标在加减一些而已;
  • 使用时间来计算加减的量以在不一样性能的计算机上得到一致的动画效果;
  • 使用向量来计算运动的过程来减轻咱们的劳动,在3D的状况下,简单的使用Vector3即可以了。

现在咱们已经学习到了游戏动画制做的精髓,一旦能够动起来,就能创造无数让人叹为观止的效果,是否是应该写个程序在朋友们面前炫耀炫耀了?
在下面,咱们要学习接受输入和游戏里的物体互动起来。

gameobjects-0.0.3.win32.exe可运行的安装文件
gameobjects-0.0.3源码

 

用Python和Pygame写游戏-从入门到精通(10)

 

游戏设备

玩过游戏的都知道鼠标和键盘是游戏的不可或缺的输入设备。键盘能够控制有限的方向和诸多的命令操做,而鼠标更是提供了全方位的方向和位置操做。不过这两个设备并非为游戏而生,专业的游戏手柄给玩家提供了更好的操做感,加上力反馈等技术,应该说游戏设备愈来愈丰富,玩家们也是愈来愈幸福。

键盘设备

咱们先从最普遍的键盘开始讲起。

如今使用的键盘,基本都是QWERTY键盘(看看字幕键盘排布的左上就知道了),尽管这个世界上还有其余种类的键盘,好比AZERTY啥的,反正我是没见过,若是你能在写游戏的时候考虑到这些特殊用户天然是最好,我的感受是问题不大吧。

之前第二部分也稍微使用了一下键盘,那时候是用了pygame.event.get()获取全部的事件,当event.type == KEYDOWN的时候,在判断event.key的种类,而各个种类也使用K_aK_b……等判断。这里再介绍一个pygame.key.get_pressed()来得到全部按下的键值,它会返回一个元组。这个元组的索引就是键值,对应的就是是否按下,好比说:

Python

 

1

2

3

4

    pressed_keys = pygame.key.get_pressed()

    if pressed_keys[K_SPACE]:

        # Space key has been pressed

        fire()pressed_keys = pygame.key.get_pressed()

固然key模块下还有不少函数:

  • key.get_focused —— 返回当前的pygame窗口是否激活
  • key.get_pressed —— 刚刚解释过了
  • key.get_mods —— 按下的组合键(Alt, Ctrl, Shift)
  • key.set_mods —— 你也能够模拟按下组合键的效果(KMOD_ALT, KMOD_CTRL, KMOD_SHIFT)
  • key.set_repeat —— 无参数调用设置pygame不产生重复按键事件,二参数(delay, interval)调用设置重复事件发生的时间
  • key.name —— 接受键值返回键名

注:感谢xumaomao朋友的倾情指正!

使用键盘控制方向

有了上一章向量的基础,只需一幅图就能明白键盘如何控制方向:

 

不少游戏也使用ASDW当作方向键来移动,咱们来看一个实际的例子:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

background_image_filename = 'sushiplate.jpg'

sprite_image_filename = 'fugu.png'

 

import pygame

from pygame.locals import *

from sys import exit

from gameobjects.vector2 import Vector2

 

pygame.init()

 

screen = pygame.display.set_mode((640, 480), 0, 32)

 

background = pygame.image.load(background_image_filename).convert()

sprite = pygame.image.load(sprite_image_filename).convert_alpha()

 

clock = pygame.time.Clock()

 

sprite_pos = Vector2(200, 150)

sprite_speed = 300.

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

 

    pressed_keys = pygame.key.get_pressed()

 

    key_direction = Vector2(0, 0)

    if pressed_keys[K_LEFT]:

        key_direction.x = -1

    elif pressed_keys[K_RIGHT]:

        key_direction.x = +1

    if pressed_keys[K_UP]:

        key_direction.y = -1

    elif pressed_keys[K_DOWN]:

        key_direction.y = +1

 

    key_direction.normalize()

 

    screen.blit(background, (0,0))

    screen.blit(sprite, sprite_pos)

 

    time_passed = clock.tick(30)

    time_passed_seconds = time_passed / 1000.0

 

    sprite_pos+= key_direction * sprite_speed * time_passed_seconds

 

    pygame.display.update()

这个例子很简单,就是使用方向键移动小鱼。使用的知识也都讲过了,相信你们均可以理解。不过这里并非单纯的判断按下的键来得到方向,而是经过对方向的加减来得到最终的效果,这样可能会更简短一些,也须要一些技术;若是把方向写入代码,效率更高,不过明显通用性就要低一些。记得把力气花在刀刃上!固然这个例子也不是那么完美,看代码、实践一下都能看到,左方向键的优先级大于右方向键,而上则优于下,咱们是否有更好的方法?……有兴趣的本身考虑~

这个例子咱们能够看到,小鱼只能在八个方向移动,如何作到全方向?若是你游戏经验足一点或许能够想到,是的,先转向,再移动,尽管不是那么快捷,但毕竟达到了目标。咱们看一下这样的代码怎么写:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

background_image_filename = 'sushiplate.jpg'

sprite_image_filename = 'fugu.png'

 

import pygame

from pygame.locals import *

from sys import exit

from gameobjects.vector2 import Vector2

from math import *

 

pygame.init()

 

screen = pygame.display.set_mode((640, 480), 0, 32)

 

background = pygame.image.load(background_image_filename).convert()

sprite = pygame.image.load(sprite_image_filename).convert_alpha()

 

clock = pygame.time.Clock()

 

sprite_pos = Vector2(200, 150)   # 初始位置

sprite_speed = 300.     # 每秒前进的像素数(速度)

sprite_rotation = 0.      # 初始角度

sprite_rotation_speed = 360. # 每秒转动的角度数(转速)

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

 

    pressed_keys = pygame.key.get_pressed()

 

    rotation_direction = 0.

    movement_direction = 0.

 

    # 更改角度

    if pressed_keys[K_LEFT]:

        rotation_direction = +1.

    if pressed_keys[K_RIGHT]:

        rotation_direction = -1.

    # 前进、后退

    if pressed_keys[K_UP]:

        movement_direction = +1.

    if pressed_keys[K_DOWN]:

        movement_direction = -1.

 

    screen.blit(background, (0,0))

 

    # 得到一条转向后的鱼

    rotated_sprite = pygame.transform.rotate(sprite, sprite_rotation)

    # 转向后,图片的长宽会变化,由于图片永远是矩形,为了放得下一个转向后的矩形,外接的矩形势必会比较大

    w, h = rotated_sprite.get_size()

    # 得到绘制图片的左上角(感谢pltc325网友的指正)

    sprite_draw_pos = Vector2(sprite_pos.x-w/2, sprite_pos.y-h/2)

    screen.blit(rotated_sprite, sprite_draw_pos)

 

    time_passed = clock.tick()

    time_passed_seconds = time_passed / 1000.0

 

    # 图片的转向速度也须要和行进速度同样,经过时间来控制

    sprite_rotation += rotation_direction * sprite_rotation_speed * time_passed_seconds

 

    # 得到前进(x方向和y方向),这两个须要一点点三角的知识

    heading_x = sin(sprite_rotation*pi/180.)

    heading_y = cos(sprite_rotation*pi/180.)

    # 转换为单位速度向量

    heading = Vector2(heading_x, heading_y)

    # 转换为速度

    heading *= movement_direction

 

    sprite_pos+= heading * sprite_speed * time_passed_seconds

 

    pygame.display.update()

咱们经过上下控制前进/后退,而左右控制转向。咱们经过pygame.transform.rotate()来得到了转向后的图片,具体参数能够参考代码。各条语句的做用也能够参考注释。

下次讲解使用鼠标控制游戏。

用Python和Pygame写游戏-从入门到精通(11)

咱们已经看到如何画一个光标了,只是简单的在鼠标坐标上画一个图像而已,咱们能够从MOUSEMOTION或者pygame.mouse.get_pos方法来得到坐标。但咱们还能够使用这个坐标来控制方向,好比在3D游戏中,能够使用鼠标来控制视角。这种时候,咱们不使用鼠标的位置,由于鼠标可能会跑到窗口外面,咱们使用鼠标如今与上一帧的相对偏移量。在下一个例子中,咱们演示使用鼠标的左右移动来转动咱们熟悉的小鱼儿:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

background_image_filename = 'sushiplate.jpg'

sprite_image_filename = 'fugu.png'

 

import pygame

from pygame.locals import *

from sys import exit

from gameobjects.vector2 import Vector2

from math import *

 

pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)

 

background = pygame.image.load(background_image_filename).convert()

sprite = pygame.image.load(sprite_image_filename).convert_alpha()

 

clock = pygame.time.Clock()

 

# 让pygame彻底控制鼠标

pygame.mouse.set_visible(False)

pygame.event.set_grab(True)

 

sprite_pos = Vector2(200, 150)

sprite_speed = 300.

sprite_rotation = 0.

sprite_rotation_speed = 360.

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

        # 按Esc则退出游戏

        if event.type == KEYDOWN:

            if event.key == K_ESCAPE:

                exit()

 

    pressed_keys = pygame.key.get_pressed()

    # 这里获取鼠标的按键状况

    pressed_mouse = pygame.mouse.get_pressed()

 

    rotation_direction = 0.

    movement_direction = 0.

 

    # 经过移动偏移量计算转动

    rotation_direction = pygame.mouse.get_rel()[0]/5.0

 

    if pressed_keys[K_LEFT]:

        rotation_direction = +1.

    if pressed_keys[K_RIGHT]:

        rotation_direction = -1.

    # 多了一个鼠标左键按下的判断

    if pressed_keys[K_UP] or pressed_mouse[0]:

        movement_direction = +1.

    # 多了一个鼠标右键按下的判断

    if pressed_keys[K_DOWN] or pressed_mouse[2]:

        movement_direction = -1.

 

    screen.blit(background, (0,0))

 

    rotated_sprite = pygame.transform.rotate(sprite, sprite_rotation)

    w, h = rotated_sprite.get_size()

    sprite_draw_pos = Vector2(sprite_pos.x-w/2, sprite_pos.y-h/2)

    screen.blit(rotated_sprite, sprite_draw_pos)

 

    time_passed = clock.tick()

    time_passed_seconds = time_passed / 1000.0

 

    sprite_rotation += rotation_direction * sprite_rotation_speed * time_passed_seconds

 

    heading_x = sin(sprite_rotation*pi/180.)

    heading_y = cos(sprite_rotation*pi/180.)

    heading = Vector2(heading_x, heading_y)

    heading *= movement_direction

 

    sprite_pos+= heading * sprite_speed * time_passed_seconds

 

    pygame.display.update()

一旦打开这个例子,鼠标就看不到了,咱们得使用Esc键来退出程序,除了上一次的方向键,当鼠标左右移动的时候,小鱼转动,按下鼠标左右键的时候,小鱼前进/后退。看代码,基本也是同样的,就多了几句带注释的。

这里使用了

 

1

2

pygame.mouse.set_visible(False)

pygame.event.set_grab(True)

来彻底控制鼠标,这样鼠标的光标看不见,也不会跑到pygame窗口外面去,一个反作用就是没法使用鼠标关闭窗口了,因此你得准备一句代码来退出程序。

而后咱们使用

 

1

rotation_direction = pygame.mouse.get_rel()[0] / 5.

来得到x方向上的偏移量,除以5是把动做放慢一点……

还有

 

1

lmb, mmb, rmb = pygame.mouse.get_pressed()

得到了鼠标按键的状况,若是有一个按键按下,那么对应的值就会为True。

总结一下pygame.mouse的函数:

  • pygame.mouse.get_pressed —— 返回按键按下状况,返回的是一元组,分别为(左键, 中键, 右键),如按下则为True
  • pygame.mouse.get_rel —— 返回相对偏移量,(x方向, y方向)的一元组
  • pygame.mouse.get_pos —— 返回当前鼠标位置(x, y)
  • pygame.mouse.set_pos —— 显而易见,设置鼠标位置
  • pygame.mouse.set_visible —— 设置鼠标光标是否可见
  • pygame.mouse.get_focused —— 若是鼠标在pygame窗口内有效,返回True
  • pygame.mouse.set_cursor —— 设置鼠标的默认光标式样,是否是感受咱们之前作的事情白费了?哦不会,咱们使用的方法有着更好的效果。
  • pyGame.mouse.get_cursor —— 再也不解释。

关于使用鼠标

在游戏中活用鼠标是一门学问,像在FPS中,鼠标用来瞄准,ARPG或RTS中,鼠标用来指定位置和目标。而在不少策略型的小游戏中,鼠标的威力更是被发挥的 淋漓尽致,也许是能够放置一些道具,也许是用来操控蓄力。咱们如今使用的屏幕是二维的,而鼠标也能在2维方向到达任何的位置,因此鼠标相对键盘,更适合现代的复杂操做,只有想不到没有作不到啊。

绝大多数时候,鼠标和键盘是合做使用的,好比使用键盘转换视角,使用键盘移动,或者键盘对应不少快捷键,而键盘则用来指定位置。开动大脑,创造将来!

 

用Python和Pygame写游戏-从入门到精通(13)

 

咱们要学习游戏的另一个支撑物,智能,或者帅气一点称为AI(Artificial Intelligence,人工智能,由于游戏里的智能确定是人赋予的)。玩家操做咱们本身的角色,那么NPC(nonplayer characters)呢?交由AI去操做,因此若是游戏中有何你相同地位的角色存在的话,你就是在和AI对垒。智能意味着对抗,“与人斗其乐无穷”,就是由于人足够聪明,要想“玩游戏其乐无穷”,咱们都得赋予游戏足够的AI。

为游戏建立人工智能

也许你但愿能在Pygame中发现一个pygame.ai模块,不过每一个游戏中的智能都是不一样的,很难准备一个通用的模块。一个简单的游戏中并不须要多少AI编程的代码,好比俄罗斯方块,你只须要随机的落下一个方块组合,而后每次降低完毕扫描一下落下的方块就行了,这甚至不能称为AI。但好比魔兽争霸,这里面的AI就很是的复杂,通常人都要学习一段时间才能战胜电脑,可想而知其高度了。

尽管通常游戏中的人工智能都是用来对付人类的,不过随着游戏发展,AI也多是咱们朋友,甚至AI互相影响从而改变整个游戏世界,这样的游戏就有了更多的深度和未知,没法预知的东西老是吸引人的不是么?

游戏的AI编程不是一件简单的事情,幸运的是AI代码每每能够重用,这个咱们之后再讲。

咱们接下来要讲述游戏AI的技术,赋予游戏角色以生命,应该说人工智能是很高端的技术,花费几十年都未必能到达怎么的一个高度,因此这里的几章仍是以讲解重要概念为主。做为参考,我的推荐Mat Buckland的《AI Techniques for Game Programming》,中文版《游戏编程中的人工智能技术》由清华大学出版社出版,很不错的一本入门书籍。

什么是人工智能

出于严谨性,咱们应该给人工智能下一个定义。每一个人都会对智能有一个概念,但又很难给它下一个确切的定义。著名的美国斯坦福大学人工智能研究中心尼尔逊教授对人工智能下了这样一个定义:“人工智能是关于知识的学科――怎样表示知识以及怎样得到知识并使用知识的科学。”而另外一个美国麻省理工学院的温斯顿教授认为:“人工智能就是研究如何使计算机去作过去只有人才能作的智能工做。”这些说法反映了人工智能学科的基本思想和基本内容。即人工智能是研究人类智能活动的规律,构造具备必定智能的人工系统,研究如何让计算机去完成以往须要人的智力才能胜任的工做,也就是研究如何应用计算机的软硬件来模拟人类某些智能行为的基本理论、方法和技术。但这些说辞太麻烦了,我以为,人工智能就是自我感知和反应的人造系统,足矣。

智能是一件玄妙的事情,在游戏中的人工智能更是如此,咱们用程序中的一些数据结构和算法就构筑了NPC的大脑,听起来太酷了!更酷的是,Python很是适合用来编写人工智能。

人工智能初探

举超级玛丽为一个例子,那些走来走去的老乌龟,咱们控制英雄踩到它们头上就能杀死它们,而平时,它们就在两根管子之间走来走去(这样的人生真可悲……),若是咱们打开它们的脑壳看一下,可能会看到这样的代码:

Python

 

1

2

3

self.move_forward()

if self.hit_wall():

    self.change_direction()

无比简单,向前走,一撞墙就回头,而后重复。它只能理解一种状态,就是撞墙,而一旦到达这个状态,它的反应就是回头。

在考虑一个会发子弹的小妖怪,它的脑壳多是这么长的:

Python

 

1

2

3

4

5

6

7

8

9

10

if self.state == "exploring":

    self.random_heading()

    if self.can_see(player):

        self.state = "seeking"

elif self.state == "seeking":

    self.head_towards("player")

    if self.in_range_of(player):

        self.fire_at(player)

    if not self.can_see(player):

        self.state = "exploring"

它就有了两个状态,搜寻锁定。若是正在搜寻,就随处走动,若是发现目标,就锁定他,而后靠近并试着向其开火,而一旦丢失目标(目标走出了视线范围或者被消灭),从新进入搜寻状态。这个AI也是很简单的,可是它对玩家来讲就有了必定的危险性。若是咱们给它加入更多的状态,它就会变得更厉害,游戏的趣味性也就可能直线上扬了。

OK,这就是咱们下一次要讲的主题,状态机

用Python和Pygame写游戏-从入门到精通(14)

状态定义了两个内容:

  • 当前正在作什么
  • 转化到下一件事时候的条件

状态同时还可能包含进入(entry退出(exit)两种动做,进入时间是指进入某个状态时要作的一次性的事情,好比上面的怪,一旦进入攻击状态,就得开始计算与玩家的距离,或许还得大吼一声“我要杀了你”等等;而退出动做则是与之相反的,离开这个状态要作的事情。

咱们来建立一个更为复杂的场景来阐述这个概念——一个蚁巢世界。咱们经常使用昆虫来研究AI,由于昆虫的行为很简单容易建模。在咱们此次的环境里,有三个实体(entity)登场:叶子、蜘蛛、蚂蚁。叶子会随机的出如今屏幕的任意地方,并由蚂蚁回收至蚁穴,而蜘蛛在屏幕上随便爬,平时蚂蚁不会在乎它,而一旦进入蚁穴,就会遭到蚂蚁的极力驱赶,直至蜘蛛挂了或远离蚁穴。

尽管咱们是对昆虫建模的,这段代码对不少场景都是合适的。把它们替换为巨大的机器人守卫(蜘蛛)、坦克(蚂蚁)、能源(叶子),这段代码依然可以很好的工做。

游戏实体类

这里出现了三个实体,咱们试着写一个通用的实体基类,省得写三遍了,同时若是加入了其余实体,也能很方便的扩展出来。

一个实体须要存储它的名字,如今的位置,目标,速度,以及一个图形。有些实体可能只有一部分属性(好比叶子不该该在地图上瞎走,咱们把它的速度设为0),同时咱们还须要准备进入和退出的函数供调用。下面是一个完整的GameEntity类:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

class GameEntity(object):

    def __init__(self, world, name, image):

        self.world = world

        self.name = name

        self.image = image

        self.location = Vector2(0, 0)

        self.destination = Vector2(0, 0)

        self.speed = 0.

        self.brain = StateMachine()

        self.id = 0

    def render(self, surface):

        x, y = self.location

        w, h = self.image.get_size()

        surface.blit(self.image, (x-w/2, y-h/2))

    def process(self, time_passed):

        self.brain.think()

        if self.speed > 0 and self.location != self.destination:

            vec_to_destination = self.destination - self.location

            distance_to_destination = vec_to_destination.get_length()

            heading = vec_to_destination.get_normalized()

            travel_distance = min(distance_to_destination, time_passed * self.speed)

            self.location += travel_distance * heading

观察这个类,会发现它还保存一个world,这是对外界描述的一个类的引用,不然实体没法知道外界的信息。这里类还有一个id,用来标示本身,甚至还有一个brain,就是咱们后面会定义的一个状态机类。

render函数是用来绘制本身的。

process函数首先调用self.brain.think这个状态机的方法来作一些事情(好比转身等)。接下来的代码用来让实体走近目标。

世界类

咱们写了一个GameObject的实体类,这里再有一个世界类World用来描述外界。这里的世界不须要多复杂,仅仅须要准备一个蚁穴,和存储若干的实体位置就足够了:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

class World(object):

    def __init__(self):

        self.entities = {} # Store all the entities

        self.entity_id = 0 # Last entity id assigned

        # 画一个圈做为蚁穴

        self.background = pygame.surface.Surface(SCREEN_SIZE).convert()

        self.background.fill((255, 255, 255))

        pygame.draw.circle(self.background, (200, 255, 200), NEST_POSITION, int(NEST_SIZE))

    def add_entity(self, entity):

        # 增长一个新的实体

        self.entities[self.entity_id] = entity

        entity.id = self.entity_id

        self.entity_id += 1

    def remove_entity(self, entity):

        del self.entities[entity.id]

    def get(self, entity_id):

        # 经过id给出实体,没有的话返回None

        if entity_id in self.entities:

            return self.entities[entity_id]

        else:

            return None

    def process(self, time_passed):

        # 处理世界中的每个实体

        time_passed_seconds = time_passed / 1000.0

        for entity in self.entities.itervalues():

            entity.process(time_passed_seconds)

    def render(self, surface):

        # 绘制背景和每个实体

        surface.blit(self.background, (0, 0))

        for entity in self.entities.values():

            entity.render(surface)

    def get_close_entity(self, name, location, range=100.):

        # 经过一个范围寻找以内的全部实体

        location = Vector2(*location)

        for entity in self.entities.values():

            if entity.name == name:

                distance = location.get_distance_to(entity.location)

                if distance < range:

                    return entity

        return None

由于咱们有着一系列的GameObject,使用一个列表来存储就是很天然的事情。不过若是实体增长,搜索列表就会变得缓慢,因此咱们使用了字典来存储。咱们就使用GameObjectid做为字典的key,实例做为内容来存放,实际的样子会是这样:

 

大多数的方法都用来管理实体,好比add_entityremove_entityprocess方法是用来调用全部试题的process,让它们更新本身的状态;而render则用来绘制这个世界;最后get_close_entity用来寻找某个范围内的实体,这个方法会在实际模拟中用到。

这两个类还不足以构筑咱们的昆虫世界,可是倒是整个模拟的基础,下一次咱们就要讲述实际的蚂蚁类和大脑(状态机类)。

用Python和Pygame写游戏-从入门到精通(15)

蚂蚁实例类

在咱们正式建造大脑以前,咱们得先作一个蚂蚁类出来,就是下面的这个,从GameEntity继承而来:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

class Ant(GameEntity):

    def __init__(self, world, image):

        # 执行基类构造方法

        GameEntity.__init__(self, world, "ant", image)

        # 建立各类状态

        exploring_state = AntStateExploring(self)

        seeking_state = AntStateSeeking(self)

        delivering_state = AntStateDelivering(self)

        hunting_state = AntStateHunting(self)

        self.brain.add_state(exploring_state)

        self.brain.add_state(seeking_state)

        self.brain.add_state(delivering_state)

        self.brain.add_state(hunting_state)

        self.carry_image = None

    def carry(self, image):

        self.carry_image = image

    def drop(self, surface):

        # 放下carry图像

        if self.carry_image:

            x, y = self.location

            w, h = self.carry_image.get_size()

            surface.blit(self.carry_image, (x-w, y-h/2))

            self.carry_image = None

    def render(self, surface):

        # 先调用基类的render方法

        GameEntity.render(self, surface)

        # 额外绘制carry_image

        if self.carry_image:

            x, y = self.location

            w, h = self.carry_image.get_size()

            surface.blit(self.carry_image, (x-w, y-h/2))

这个Ant类先调用了父类的__init__,都是Python基础很少说了。下面的代码就是一些状态机代码了,对了还有一个carry_image变量,保持了如今蚂蚁正在搬运物体的图像,或许是一片树叶,或许是一只死蜘蛛。这里咱们写了一个增强的render函数,由于咱们可能还须要画一下搬的东西。

建造大脑

咱们给每一只蚂蚁赋予四个状态,这样才能足够建造咱们的蚂蚁的状态机。在建造状态机以前,咱们得先把这些状态的详细信息列出来。

状态

动做

探索(Exploring)

随机的走向一个点

搜集(Seeking)

向一篇树叶前进

搬运(Dellivering)

搬运一个什么回去

狩猎(Hunting)

攻击一只蜘蛛

咱们也须要定义一下各个状态之间的连接,或者能够叫转移条件。这里举两个例子(实际上不止):

条件

转移状态

发现树叶

搜集

有蜘蛛攻击

狩猎

咱们仍是最终画一张图来表示整个状态机:


高水平的你也许能够看着上面的图写状态机了,不过为了方便先创建一个State类,来保存一个状态。很简单,只是一个框子,实际上什么都不作:

Python

 

1

2

3

4

5

6

7

8

9

10

11

class State():

    def __init__(self, name):

        self.name = name

    def do_actions(self):

        pass

    def check_conditions(self):

        pass

    def entry_actions(self):

        pass

    def exit_actions(self):

        pass

而后能够创建一个状态机类来管理这些状态,这个状态机但是整个代码的核心类。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

class StateMachine():

    def __init__(self):

        self.states = {}    # 存储状态

        self.active_state = None    # 当前有效状态

    def add_state(self, state):

        # 增长状态

        self.states[state.name] = state

    def think(self):

        if self.active_state is None:

            return

        # 执行有效状态的动做,并作转移检查

        self.active_state.do_actions()

        new_state_name = self.active_state.check_conditions()

        if new_state_name is not None:

            self.set_state(new_state_name)

    def set_state(self, new_state_name):

        # 更改状态,执行进入/退出动做

        if self.active_state is not None:

            self.active_state.exit_actions()

        self.active_state = self.states[new_state_name]

        self.active_state.entry_actions()

而后就能够经过继承State建立一系列的实际状态了,这些状态传递给StateMachine保留并运行。StateMachine类的think方法是检查当前有效状态并执行其动做的,最后还可能会调用set_state来进入下一个状态。

 

用Python和Pygame写游戏-从入门到精通(16) 

下面给出完整代码(注意须要gameobjects库才能够运行,参考以前的向量篇):

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

SCREEN_SIZE = (640, 480)

NEST_POSITION = (320, 240)

ANT_COUNT = 20

NEST_SIZE = 100.

 

import pygame

from pygame.locals import *

 

from random import randint, choice

from gameobjects.vector2 import Vector2

 

class State(object):

    def __init__(self, name):

        self.name = name

    def do_actions(self):

        pass

    def check_conditions(self):

        pass

    def entry_actions(self):

        pass

    def exit_actions(self):

        pass        

 

class StateMachine(object):

    def __init__(self):

        self.states = {}

        self.active_state = None

 

    def add_state(self, state):

        self.states[state.name] = state

 

    def think(self):

        if self.active_state is None:

            return

        self.active_state.do_actions()

        new_state_name = self.active_state.check_conditions()

        if new_state_name is not None:

            self.set_state(new_state_name)

 

    def set_state(self, new_state_name):

        if self.active_state is not None:

            self.active_state.exit_actions()

        self.active_state = self.states[new_state_name]

        self.active_state.entry_actions()

 

class World(object):

    def __init__(self):

        self.entities = {}

        self.entity_id = 0

        self.background = pygame.surface.Surface(SCREEN_SIZE).convert()

        self.background.fill((255, 255, 255))

        pygame.draw.circle(self.background, (200, 255, 200), NEST_POSITION, int(NEST_SIZE))

 

    def add_entity(self, entity):

        self.entities[self.entity_id] = entity

        entity.id = self.entity_id

        self.entity_id += 1

 

    def remove_entity(self, entity):

        del self.entities[entity.id]

 

    def get(self, entity_id):

        if entity_id in self.entities:

            return self.entities[entity_id]

        else:

            return None

 

    def process(self, time_passed):

        time_passed_seconds = time_passed / 1000.0

        for entity in self.entities.values():

            entity.process(time_passed_seconds)

 

    def render(self, surface):

        surface.blit(self.background, (0, 0))

        for entity in self.entities.itervalues():

            entity.render(surface)

 

    def get_close_entity(self, name, location, range=100.):

        location = Vector2(*location)

        for entity in self.entities.itervalues():

            if entity.name == name:

                distance = location.get_distance_to(entity.location)

                if distance < range:

                    return entity

        return None

 

class GameEntity(object):

 

    def __init__(self, world, name, image):

 

        self.world = world

        self.name = name

        self.image = image

        self.location = Vector2(0, 0)

        self.destination = Vector2(0, 0)

        self.speed = 0.

        self.brain = StateMachine()

        self.id = 0

 

    def render(self, surface):

        x, y = self.location

        w, h = self.image.get_size()

        surface.blit(self.image, (x-w/2, y-h/2))  

 

    def process(self, time_passed):

        self.brain.think()

        if self.speed > 0. and self.location != self.destination:

            vec_to_destination = self.destination - self.location

            distance_to_destination = vec_to_destination.get_length()

            heading = vec_to_destination.get_normalized()

            travel_distance = min(distance_to_destination, time_passed * self.speed)

            self.location += travel_distance * heading

 

class Leaf(GameEntity):

    def __init__(self, world, image):

        GameEntity.__init__(self, world, "leaf", image)

 

class Spider(GameEntity):

    def __init__(self, world, image):

        GameEntity.__init__(self, world, "spider", image)

        self.dead_image = pygame.transform.flip(image, 0, 1)

        self.health = 25

        self.speed = 50. + randint(-20, 20)

 

    def bitten(self):

        self.health -= 1

        if self.health <= 0:

            self.speed = 0.

            self.image = self.dead_image

        self.speed = 140.

 

    def render(self, surface):

        GameEntity.render(self, surface)

        x, y = self.location

        w, h = self.image.get_size()

        bar_x = x - 12

        bar_y = y + h/2

        surface.fill( (255, 0, 0), (bar_x, bar_y, 25, 4))

        surface.fill( (0, 255, 0), (bar_x, bar_y, self.health, 4))

 

    def process(self, time_passed):

        x, y = self.location

        if x > SCREEN_SIZE[0] + 2:

            self.world.remove_entity(self)

            return

        GameEntity.process(self, time_passed)

 

class Ant(GameEntity):

    def __init__(self, world, image):

        GameEntity.__init__(self, world, "ant", image)

        exploring_state = AntStateExploring(self)

        seeking_state = AntStateSeeking(self)

        delivering_state = AntStateDelivering(self)

        hunting_state = AntStateHunting(self)

        self.brain.add_state(exploring_state)

        self.brain.add_state(seeking_state)

        self.brain.add_state(delivering_state)

        self.brain.add_state(hunting_state)

        self.carry_image = None

 

    def carry(self, image):

        self.carry_image = image

 

    def drop(self, surface):

        if self.carry_image:

            x, y = self.location

            w, h = self.carry_image.get_size()

            surface.blit(self.carry_image, (x-w, y-h/2))

            self.carry_image = None

 

    def render(self, surface):

        GameEntity.render(self, surface)

        if self.carry_image:

            x, y = self.location

            w, h = self.carry_image.get_size()

            surface.blit(self.carry_image, (x-w, y-h/2))

 

class AntStateExploring(State):

    def __init__(self, ant):

        State.__init__(self, "exploring")

        self.ant = ant

 

    def random_destination(self):

        w, h = SCREEN_SIZE

        self.ant.destination = Vector2(randint(0, w), randint(0, h))    

 

    def do_actions(self):

        if randint(1, 20) == 1:

            self.random_destination()

 

    def check_conditions(self):

        leaf = self.ant.world.get_close_entity("leaf", self.ant.location)

        if leaf is not None:

            self.ant.leaf_id = leaf.id

            return "seeking"

        spider = self.ant.world.get_close_entity("spider", NEST_POSITION, NEST_SIZE)

        if spider is not None:

            if self.ant.location.get_distance_to(spider.location) < 100.:

                self.ant.spider_id = spider.id

                return "hunting"

        return None

 

    def entry_actions(self):

        self.ant.speed = 120. + randint(-30, 30)

        self.random_destination()

 

class AntStateSeeking(State):

    def __init__(self, ant):

        State.__init__(self, "seeking")

        self.ant = ant

        self.leaf_id = None

 

    def check_conditions(self):

        leaf = self.ant.world.get(self.ant.leaf_id)

        if leaf is None:

            return "exploring"

        if self.ant.location.get_distance_to(leaf.location) < 5.0:

            self.ant.carry(leaf.image)

            self.ant.world.remove_entity(leaf)

            return "delivering"

        return None

 

    def entry_actions(self):

        leaf = self.ant.world.get(self.ant.leaf_id)

        if leaf is not None:

            self.ant.destination = leaf.location

            self.ant.speed = 160. + randint(-20, 20)

 

class AntStateDelivering(State):

    def __init__(self, ant):

        State.__init__(self, "delivering")

        self.ant = ant

 

    def check_conditions(self):

        if Vector2(*NEST_POSITION).get_distance_to(self.ant.location) < NEST_SIZE:

            if (randint(1, 10) == 1):

                self.ant.drop(self.ant.world.background)

                return "exploring"

        return None

 

    def entry_actions(self):

        self.ant.speed = 60.

        random_offset = Vector2(randint(-20, 20), randint(-20, 20))

        self.ant.destination = Vector2(*NEST_POSITION) + random_offset      

 

class AntStateHunting(State):

    def __init__(self, ant):

        State.__init__(self, "hunting")

        self.ant = ant

        self.got_kill = False

 

    def do_actions(self):

        spider = self.ant.world.get(self.ant.spider_id)

        if spider is None:

            return

        self.ant.destination = spider.location

        if self.ant.location.get_distance_to(spider.location) < 15.:

            if randint(1, 5) == 1:

                spider.bitten()

                if spider.health <= 0:

                    self.ant.carry(spider.image)

                    self.ant.world.remove_entity(spider)

                    self.got_kill = True

 

    def check_conditions(self):

        if self.got_kill:

            return "delivering"

        spider = self.ant.world.get(self.ant.spider_id)

        if spider is None:

            return "exploring"

        if spider.location.get_distance_to(NEST_POSITION) > NEST_SIZE * 3:

            return "exploring"

        return None

 

    def entry_actions(self):

        self.speed = 160. + randint(0, 50)

 

    def exit_actions(self):

        self.got_kill = False

 

def run():

    pygame.init()

    screen = pygame.display.set_mode(SCREEN_SIZE, 0, 32)

    world = World()

    w, h = SCREEN_SIZE

    clock = pygame.time.Clock()

    ant_image = pygame.image.load("ant.png").convert_alpha()

    leaf_image = pygame.image.load("leaf.png").convert_alpha()

    spider_image = pygame.image.load("spider.png").convert_alpha()

 

    for ant_no in xrange(ANT_COUNT):

        ant = Ant(world, ant_image)

        ant.location = Vector2(randint(0, w), randint(0, h))

        ant.brain.set_state("exploring")

        world.add_entity(ant)

 

    while True:

        for event in pygame.event.get():

            if event.type == QUIT:

                return

        time_passed = clock.tick(30)

 

        if randint(1, 10) == 1:

            leaf = Leaf(world, leaf_image)

            leaf.location = Vector2(randint(0, w), randint(0, h))

            world.add_entity(leaf)

 

        if randint(1, 100) == 1:

            spider = Spider(world, spider_image)

            spider.location = Vector2(-50, randint(0, h))

            spider.destination = Vector2(w+50, randint(0, h))

            world.add_entity(spider)

 

        world.process(time_passed)

        world.render(screen)

 

        pygame.display.update()

 

if __name__ == "__main__":

    run()

这个程序的长度超过了以往任何一个,甚至可能比咱们写的加起来都要长一些。然而它能够展示给咱们的也史无前例的惊喜。无数勤劳的小蚂蚁在整个地图上处处觅食,随机出现的叶子一旦被蚂蚁发现,就会搬回巢穴,而蜘蛛一旦出如今巢穴范围以内,就会被蚂蚁们群起而攻之,直到被驱逐出地图范围或者挂了,蜘蛛的尸体也会被带入巢穴。

这个代码写的不够漂亮,没有用过高级的语法,甚至都没有注释天哪……基本代码都在前面出现了,只是新引入了四个新的状态,AntStateExploringAntStateSeekingAntStateDeliveringAntStateHunting,意义的话前面已经说明。好比说AntStateExploring,继承了基本的Stat,这个状态的动做平时就是让蚂蚁以一个随机的速度走向屏幕随机一个点,在此过程当中,check_conditions会不断检查周围的环境,发现了树叶或蜘蛛都会采起相应的措施(进入另一个状态)。

用Python和Pygame写游戏-从入门到精通(17)

 

距离的魔法

咱们看现实中的东西,和咱们看画面上的东西,最大差异在于能感觉现实物体的距离。而距离的产生,则是由于咱们双眼看到的东西是不一样的,两眼交替闭合,你会发现眼前的东西左右移动。一只眼睛则很难正确的判断距离,虽然比上眼睛仍是能感受到远近,但更精细一点,好比很难把线穿过针眼。

咱们在3D画面上绘图的时候,就要遵循这个规律,看看下面的代码。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

import pygame

from pygame.locals import *

from random import randint

 

class Star(object):

 

    def __init__(self, x, y, speed):

 

        self.x = x

        self.y = y

        self.speed = speed

 

def run():

 

    pygame.init()

    screen = pygame.display.set_mode((640, 480)) #, FULLSCREEN)

 

    stars = []

 

    # 在第一帧,画上一些星星

    for n in xrange(200):

 

        x = float(randint(0, 639))

        y = float(randint(0, 479))

        speed = float(randint(10, 300))

        stars.append( Star(x, y, speed) )

 

    clock = pygame.time.Clock()

 

    white = (255, 255, 255)

 

    while True:

 

        for event in pygame.event.get():

            if event.type == QUIT:

                return

            if event.type == KEYDOWN:

                return

 

        # 增长一颗新的星星

        y = float(randint(0, 479))

        speed = float(randint(10, 300))

        star = Star(640., y, speed)

        stars.append(star)

 

        time_passed = clock.tick()

        time_passed_seconds = time_passed / 1000.

 

        screen.fill((0, 0, 0))

 

        # 绘制全部的星

        for star in stars:

 

            new_x = star.x - time_passed_seconds * star.speed

            pygame.draw.aaline(screen, white, (new_x, star.y), (star.x+1., star.y))

            star.x = new_x

 

        def on_screen(star):

            return star.x > 0

 

        # 星星跑出了画面,就删了它

        stars = filter(on_screen, stars)

 

        pygame.display.update()

 

if __name__ == "__main__":

    run()

这里你还能够把FULLSCREEN加上,更有感受。

这个程序给个人画面,发挥一下你的想象,不是一片宇宙么,无数的星云穿梭,近的速度更快,远的则很慢。而实际上看代码,咱们只是画了一些长短不一样的线而已!虽然很简单,仍是用了很多很多python的技术,特别是函数式编程的(小)技巧。不过强大的你必定没问题:)可是pygame的代码,没有任何没讲过的,为何这样就能有3D的效果了?感谢你的大脑,由于它知道远的看起来更慢,因此这样的错觉就产生了。

理解3D空间

3D空间的事情,基本就是立体几何的问题,高中学一半应该就差很少理解了,这里很少讲了。你能明白下图的小球在(7, 5, 10)的位置,换句话说,若是你站在原点,面朝Z轴方向。那么小球就在你左边7,上面5,前面10的位置。这就够了~

 

使用3D向量

咱们已经学习了二维向量来表达运动,在三维空间内,固然要使用三维的向量。其实和二维的概念都同样,加减缩放啥的,这里就不用三个元素的元组列表先演练一番了,直接祭出咱们的gameobjects神器吧!

Python

 

1

2

3

4

5

6

7

8

9

10

from gameobjects.vector3 import *

A = Vector3(6, 8, 12)

B = Vector3(10, 16, 12)

print "A is", A

print "B is", B

print "Magnitude of A is", A.get_magnitude()

print "A+B is", A+B

print "A-B is", A–B

print "A normalized is", A.get_normalized()

print "A*2 is", A * 2

运行一下看看结果吧,有些无趣?确实,光数字无法展示3D的美啊,下一次,让咱们把物体在立体空间内运动起来。

用Python和Pygame写游戏-从入门到精通(18)

 

基于时间的三维移动

咱们使用Vector3类来进行3D上的移动,与2D很是相似,看下面一个例子:

 

直升机A在(-6, 2, 2)的位置上,目标是直升机B(7, 5, 10),A想摧毁B,因此发射了一枚火箭AB,如今咱们得把火箭的运动轨迹过程给画出来,不然一点发射敌机就炸了,多没意思啊~~ 经过计算出二者之间的向量为(13, 3, 8),而后单位化这个向量,这样就能够在运动中用到了,下面的代码作了这些事情。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

from gameobjects.vector3 import *

A = (–6, 2, 2)

B = (7, 5, 10)

plasma_speed = 100. # meters per second

AB = Vector3.from_points(A, B)

print "Vector to droid is", AB

distance_to_target = AB.get_magnitude()

print "Distance to droid is", distance_to_target, "meters"

plasma_heading = AB.get_normalized()

print "Heading is", plasma_heading

#######输出结果#########

Vector to droid is (13, 3, 8)

Distance to droid is 15.5563491861 meters

Heading is (0.835672, 0.192847, 0.514259)

而后不停的重绘火箭的位置,用这个语句:
rocket_location += heading * time_passed_seconds * speed

不过咱们还不能直接在pygame中绘制3D物体,得先学习一下下面讲的,“如何把3D转换为2D”。

3D透视

若是您初中美术认真学了的话,应该会知道这里要讲什么,还记得当初咱们是如何在纸上画立方体的?

忘了?OK,从头开始提及吧,存储、计算3D坐标是很是容易的,可是要把它展示到屏幕上就不那么简单了,由于pygame中全部的绘图函数都只接受2D坐标,所以,咱们必须把这些3D的坐标投影到2D的图面上。

平行投影

最简单的投影方法是——把第三个坐标z坐标给丢弃,用这样的一个简单的函数就能够作到:

Python

 

1

2

def parallel_project(vector3):

    return (vector3.x, vector3.y)

尽管这样的转换简单又快速,咱们却不能用它。为何?效果太糟糕了,咱们甚至没法在最终的画面上感觉到一点立体的影子,这个世界看起来仍是平的,没有那个物体看起来比其余物体更远或更近。就好像我右边这幅图同样。

立体投影

在3D游戏中使用的更为普遍且合理的技术是立体投影,由于它的结果更为真实。立体投影把远处的物体缩小了,也就是使用透视法(foreshortening),如左图所示,而后下面是咱们的转换函数,看起来也很简单:

Python

 

1

2

3

def perspective_project(vector3, d):

    x, y, z = vector3

    return (x * d/z, –y * d/z)

与上一个转换函数不一样的是,这个转换函数还接受一个d参数(后面讨论),而后全部的x、y坐标都会接受这个d的洗礼,同时z也会插一脚,把本来的坐标进行缩放。

d的意思是视距(viewing distance),也就是摄像头到3D世界物体在屏幕上的像素体现之间的距离。好比说,一个在(10, 5, 100)的物体移动到了(11, 5, 100),视距是100的时候,它在屏幕上就恰好移动了1个像素,但若是它的z不是100,或者视距不是100,那么可能移动距离就再也不是1个像素的距离。有些抽象,不过玩过3D游戏的话(这里指国外的3D大做),都有一种滚轮调节远近的功能,这就是视距(固然调的时候视野也会变化,这个下面说)。

在咱们玩游戏的时候,视距就为咱们的眼睛到屏幕的直线距离(以像素为单位)。

视野

那么咱们怎么选取一个好的d呢?咱们固然能够不断调整实验来获得一个,不过咱们还能够经过视野(field of view)来计算一个出来。视野也就是在一个时刻能看到的角度。看一下左图的视野和视距的关系,能够看到二者是有制约关系,当视野角度(fov)增大的时候,d就会减少;而d增长的话,视野角度就会减少,能看到的东西也就变少了。

视野是决定在3D画面上展示多少东西的绝好武器,而后咱们还须要一个d来决定透视深度,使用一点点三角只是,咱们就能够从fov计算出d,写一下下面的代码学习学习:
Internet上,你老是能找到99%以上的须要的别人写好的代码。不过偶尔仍是要本身写一下的,不用担忧本身的数学是不及格的,这个很简单~ 不少时候实际动手试一下,你就能明白更多。

Python

 

1

2

3

4

from math import tan

def calculate_viewing_distance(fov, screen_width):

    d = (screen_width/2.0) / tan(fov/2.0)

    return d

fov角度可能取45~60°比较合适,这样看起来很天然。固然每一个人每一个游戏都有特别的地方,好比FPS的话,fov可能比较大;而策略性游戏的fov角度会比较小,这样能够看到更多的东西。不少时候还须要不停的变化fov,最明显的CS中的狙击枪(从没玩过,不过听过),开出来和关掉是彻底不一样的效果,改的就是视野角度。

今天又是补充了一大堆知识,等不及了吧~咱们下一章就能看到一个用pygame画就的3D世界了!

用Python和Pygame写游戏-从入门到精通(19)

 

3D世界

让咱们如今开始写一个3D的程序,巩固一下这几回学习的东西。由于咱们尚未好好深刻如何画3D物体,暂时就先用最简单的投影(上次讨论过的第二种)方法来画吧。这个程序画一个空间里的立方体,只不过各个部分并不会随着距离而产生大小上的变化。

您能够看到,不少的小球构成了立方体的各个边,经过按住方向键,能够水平或垂直方向的更改“摄像头”的位置,Q和A键会把摄像头拉近或拉远,而W和S会改变视距,绿色的三角是视距和视角的示意图。fov角大的话,立方体就显得比较短,反之就显得比较长。

 

代码稍微有点长,下面有解释,静下心来慢慢阅读。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

import pygame

from pygame.locals import *

from gameobjects.vector3 import Vector3

 

from math import *

from random import randint

 

SCREEN_SIZE =  (640, 480)

CUBE_SIZE = 300

 

def calculate_viewing_distance(fov, screen_width):

    d = (screen_width/2.0) / tan(fov/2.0)

    return d

 

def run():

    pygame.init()

    screen = pygame.display.set_mode(SCREEN_SIZE, 0)

 

    default_font = pygame.font.get_default_font()

    font = pygame.font.SysFont(default_font, 24)

 

    ball = pygame.image.load("ball.png").convert_alpha()

 

    # 3D points

    points = []

 

    fov = 90. # Field of view

    viewing_distance = calculate_viewing_distance(radians(fov), SCREEN_SIZE[0])

 

    # 边沿的一系列点

    for x in xrange(0, CUBE_SIZE+1, 20):

        edge_x = x == 0 or x == CUBE_SIZE

 

        for y in xrange(0, CUBE_SIZE+1, 20):

            edge_y = y == 0 or y == CUBE_SIZE

 

            for z in xrange(0, CUBE_SIZE+1, 20):

                edge_z = z == 0 or z == CUBE_SIZE

 

                if sum((edge_x, edge_y, edge_z)) >= 2:

 

                    point_x = float(x) - CUBE_SIZE/2

                    point_y = float(y) - CUBE_SIZE/2

                    point_z = float(z) - CUBE_SIZE/2

 

                    points.append(Vector3(point_x, point_y, point_z))

 

    # 以z序存储,相似于css中的z-index

    def point_z(point):

        return point.z

    points.sort(key=point_z, reverse=True)

 

    center_x, center_y = SCREEN_SIZE

    center_x /= 2

    center_y /= 2

 

    ball_w, ball_h = ball.get_size()

    ball_center_x = ball_w / 2

    ball_center_y = ball_h / 2

 

    camera_position = Vector3(0.0, 0.0, -700.)

    camera_speed = Vector3(300.0, 300.0, 300.0)

 

    clock = pygame.time.Clock()

 

    while True:        

 

        for event in pygame.event.get():

            if event.type == QUIT:

                return            

 

        screen.fill((0, 0, 0))

 

        pressed_keys = pygame.key.get_pressed()

 

        time_passed = clock.tick()

        time_passed_seconds = time_passed / 1000.

 

        direction = Vector3()

        if pressed_keys[K_LEFT]:

            direction.x = -1.0

        elif pressed_keys[K_RIGHT]:

            direction.x = +1.0

 

        if pressed_keys[K_UP]:

            direction.y = +1.0

        elif pressed_keys[K_DOWN]:

            direction.y = -1.0

 

        if pressed_keys[K_q]:

            direction.z = +1.0

        elif pressed_keys[K_a]:

            direction.z = -1.0

 

        if pressed_keys[K_w]:

            fov = min(179., fov+1.)

            w = SCREEN_SIZE[0]

            viewing_distance = calculate_viewing_distance(radians(fov), w)

        elif pressed_keys[K_s]:

            fov = max(1., fov-1.)

            w = SCREEN_SIZE[0]

            viewing_distance = calculate_viewing_distance(radians(fov), w)

 

        camera_position += direction * camera_speed * time_passed_seconds      

 

        # 绘制点

        for point in points:

 

            x, y, z = point - camera_position

 

            if z > 0:

                x =  x * viewing_distance / z

                y = -y * viewing_distance / z

                x += center_x

                y += center_y

                screen.blit(ball, (x-ball_center_x, y-ball_center_y))

 

        # 绘制表

        diagram_width = SCREEN_SIZE[0] / 4

        col = (50, 255, 50)

        diagram_points = []

        diagram_points.append( (diagram_width/2, 100+viewing_distance/4) )

        diagram_points.append( (0, 100) )

        diagram_points.append( (diagram_width, 100) )

        diagram_points.append( (diagram_width/2, 100+viewing_distance/4) )

        diagram_points.append( (diagram_width/2, 100) )

        pygame.draw.lines(screen, col, False, diagram_points, 2)        

 

        # 绘制文字

        white = (255, 255, 255)

        cam_text = font.render("camera = "+str(camera_position), True, white)

        screen.blit(cam_text, (5, 5))

        fov_text = font.render("field of view = %i"%int(fov), True, white)

        screen.blit(fov_text, (5, 35))

        txt = "viewing distance = %.3f"%viewing_distance

        d_text = font.render(txt, True, white)

        screen.blit(d_text, (5, 65))

 

        pygame.display.update()

 

if __name__ == "__main__":

    run()

上面的例子使用Vector3来管理向量数据,点的存储是按照z坐标来的,这样在blit的时候,离摄像机近的后画,就能够覆盖远的,不然看起来就太奇怪了……

在主循环的代码中,会根据按键摄像头会更改位置——固然这是用户的感受,实际上代码作的是更改了点的位置。而3D的移动和2D是很是像的,只不过多了一个z来判断覆盖远近(也就是绘制顺序),同样的基于时间移动,同样的向量运算,只是由Vector2变为了Vector3。

而后代码须要绘制所有的点。首先,点的位置须要根据camera_position变量校订,若是结果的z0还大,说明点在摄像头以前,须要画的,不然就是不须要画。y须要校准一下方向,最后把xy定位在中间(小球仍是有一点点尺寸的)。

 

 

3D第一部分总结

3D是迄今为止游戏发展中最大的里程碑(下一个会是什么呢?虚拟体验?),咱们这几回学习的,是3D的基础,你能够看到,仅有2D绘图经验也能很方便的过渡过来。仅仅是Vector2→Vector3,担任3D向量仍是有一些特有的操做的,须要慢慢学习,可是基本的思想不会变。

可是,请继续思考~ 难道全部的3D游戏就是一系列的3D坐标再手动转换为2D画上去就行了?很惋惜或者说很幸运不是的,咱们有3D引擎来作这些事情,对Pygame来讲,原生的3D处理时不存在的,那么如何真正绘制3D画面?有一个很是普遍的选择——OpenGL,不了解的请自行Wiki,不过OpenGL并非Pygame的一部分,并且3D实际作下去实在很繁杂,这个系列就暂时不深刻讲解了。

尽管有3D引擎帮咱们作投影等事情,咱们这几回学的东西绝对不是白费,对基础的必定了解能够更好的写入3D游戏,对画面的掌握也会更好。若是有须要,这个系列的主线完成后,会根据你们的要求追加讲解OpenGL的内容。

 

用Python和Pygame写游戏-从入门到精通(20)

 

什么是声音?

又要开始讲原理了啊,作游戏真是什么都要懂,物理数学美术心理学和编程等等等等,你们都不容易呀~~

声音的本质是振动,经过各类介质传播到咱们的耳朵里。基本任何物质均可以振动,好比说一旦咱们敲打桌子,桌子表面会快速振动,推进附近的空气一块儿振动,而这种振动会传播(宛如水中扔一颗石子,水波会慢慢传播同样),这种振动最终进入咱们的耳道,使得鼓膜振动,引发咱们的听觉。

振动的幅度(响度)越大,听到的声音也就越大,这个很好理解,咱们越用力拍桌子,声音也就越大(同时手也越疼——)。同时,振动的快慢(音调)也会直接影响咱们对声音高低的判断,也就是平时说的高音和低音的差异,决定着个音调的要素每秒振动的次数,也就是频率,单位是赫兹(Hz)。好比100Hz意味着这个振动在1秒内进行了100次。音色也是一个重要指标,敲打木头和金属听到的声音彻底不一样,是音色的做用,这个的要素是有振动波形的形状来决定。

现实中不少声音都是许多不一样的声音组合而来的。同时声音在传播的时候也会发生变化,最直接的就是随着距离增大,响度会减少;而在不一样的环境中,由于反射和混合,声音的效果也彻底不同。这些都要好好考虑,好比脚步声,空旷的山谷中应该是空谷足音的效果,楼梯上则是比较短可是渐渐靠近的效果。甚至发声物体的速度也会影响咱们听到的声音,谓之多普勒效应”……好麻烦!不过最后游戏里可能不是那么符合现实的,好比说太空中发射导弹什么,按说是听不到声音的,由于没有介质传播,不过为了效果好,咱也不在乎了……

声音的存储

声音彻底是一种模拟的信号,而咱们的计算机只能存储数字(二进制)信号,咋办?数字化咯~

(一下说明摘录修改自轩辕天数-丝竹的文章,表示感谢)

以最多见的WAV文件为例,要把声音记录成WAV格式,电脑要先把声音的波形画在一张坐标纸上。而后呢,电脑要看了横坐标第一格处,波形图的纵坐标是多少啊?哦,差很少是500啊(仅仅是打比方,并且这个差很少很关键),那么横坐标第二格呢?…”最后,电脑就得出来一大堆坐标值。而后再通过一些其余后续工做,电脑就把这些坐标值保存下来了。

 

当要放音的时候,电脑要根据这些坐标值在坐标纸上面画点,最后用线把点连起来,差很少就把原先的波形还原出来了。其实数字化录音基本上就是这样的原理。

电脑记录波形时,用的坐标纸格子越密,天然记录下来的点就越多、越精确,未来还原出来的波形就越接近原始波形?上边例子的横坐标轴格子密度就至关于采样频率(好比,44.1KHz),纵坐标格子密度就至关于量化精度(好比,16BIT)。这就是“KHZ”“BIT”的值越高,音乐的音质越好的缘由。

这个世界上天然不只仅有WAV这一种存储声音的文件格式,宛若图像文件格式中的BMP同样,WAV是一种无压缩的格式,体积最大;而OGG则好像PNG,是无损的压缩,能够彻底保持图像的本真,可是大小又比较小;经常使用的MP3,则是相似于JPG的有损压缩格式。

声音处理

想要得到声音,最简单的天然是录制,不过有的时候比较困难,好比录制心跳要很高昂的仪器,而录制火山爆发的声音实在过于……

这时候咱们能够手动合成声音,而录制得到的声音还须要通过处理,好比净化等,有不少软件能够选择,开源的Audacity就是一个很不错的选择。具体的这里就不说了,一门大学问啊。

网上也有不少声音素材可供下载,好的专业的素材都是卖钱的,哎这个世界什么都是钱啊~~

Pygame中声音的初始化

此次来不及举一个实际例子放声音了,先说一下初始化。

pygame中,使用mixer模块来播放声音,不过在实际播放以前,咱们须要使用pygame.mixer.init函数来初始化一些参数,不过在有的平台上,pygame.mixer.init会随着pygame.init一块儿被初始化,pygame干脆提供了一个pygame.mixer.pre_init()来进行最早的初始化工做,参数说明以下:

  • frequency – 声音文件的采样率,尽管高采样率可能会下降性能,可是再次的声卡均可以轻松对应44.1KHz的声音回放,因此就设这个值吧;
  • size – 量化精度
  • stereo – 立体声效果,1mono2stereo,具体请google,通常设2好了
  • buffer – 缓冲大小,2的倍数,设4096就差很少了吧

你能够像这样初始化声音:

Python

 

1

2

pygame.mixer.pre_init(44100, 16, 2, 4096)

pygame.init()

这里先用pre_init来设定了参数,而后在pygame.init中初始化全部的东西。

若是你须要从新设定声音的参数,那么你须要先执行pygame.mixer.quit而后再执行pygame.mixer.init,不过通常用不到吧……

用Python和Pygame写游戏-从入门到精通(21)

 

 

Sound对象

在初始化声音系统以后,咱们就能够读取一个音乐文件到一个Sound对象中了。pygame.mixer.Sound()接受一个文件名,或者也能够使一个文件对象,不过这个文件必须是WAV或者OGG,切记!

Python

 

1

hello_sound = Pygame.mixer.Sound("hello.ogg")

一旦这个Sound对象出来了,你能够使用play方法来播放它。play(loop, maxtime)能够接受两个参数,loop天然就是重复的次数,-1意味着无限循环,1呢?是两次,记住是重复的次数而不是播放的次数;maxtime是指多少毫秒后结束,这个很简单。当你不使用任何参数调用的时候,意味着把这个声音播放一次。一旦play方法调用成功,就会返回一个Channel对象,不然返回一个None。

Channel对象

Channel,也就是声道,能够被声卡混合(共同)播放的数据流。游戏中能够同时播放的声音应该是有限的,pygame中默认是8个,你能够经过pygame.mixer.get_num_channels()来得知当前系统能够同时播放的声道数,而一旦超过,调用sound对象的play方法就会返回一个None,若是你肯定本身要同时播放不少声音,能够用set_num_channels()来设定一下,最好一开始就设,由于从新设定会中止当前播放的声音。

那么Channel对象有什么用呢?若是你只是想简单的播放一下声音,那么根本不用管这个东西,不过若是你想创造出一点有意境的声音,好比说一辆火车从左到右呼啸而过,那么应该是一开始左声道比较响,而后至关,最后右声道比较响,直至慢慢消失。这就是Channel能作到的事情。Channel的set_volume(left, right)方法接受两个参数,分别是表明左声道和右声道的音量的小数,从0.0~1.0。

竖起咱们的耳朵

OK,来个例子试试吧~

这个例子里咱们经过释放金属小球并让它们自由落体和弹跳,听碰撞的声音。这里面不只仅有此次学习的声音,还有几个一块儿没有接触到的技术,最重要的一个就是重力的模拟,咱们能够设置一个重力因子来影响小球下落的速度,还有一个弹力系数,来决定每次弹跳损失的能量,虽然不难,可是游戏中引入这个东西能让咱们的游戏仿真度大大提升。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

SCREEN_SIZE = (640, 480)

 

# 重力因子,其实是单位 像素/平方秒

GRAVITY = 250.0

# 弹力系数,不要超过1!

BOUNCINESS = 0.7

 

import pygame

from pygame.locals import *

from random import randint

from gameobjects.vector2 import Vector2

 

def stero_pan(x_coord, screen_width):

    """这个函数根据位置决定要播放声音左右声道的音量"""

    right_volume = float(x_coord) / screen_width

    left_volume = 1.0 - right_volume

    return (left_volume, right_volume)

 

class Ball():

    """小球类,实际上咱们能够使用Sprite类来简化"""

    def __init__(self, position, speed, image, bounce_sound):

        self.position = Vector2(position)

        self.speed = Vector2(speed)

        self.image = image

        self.bounce_sound = bounce_sound

        self.age = 0.0

 

    def update(self, time_passed):

        w, h = self.image.get_size()

        screen_width, screen_height = SCREEN_SIZE

 

        x, y = self.position

        x -= w/2

        y -= h/2

        # 是否是要反弹了

        bounce = False

 

        # 小球碰壁了么?

        if y + h >= screen_height:

            self.speed.y = -self.speed.y * BOUNCINESS

            self.position.y = screen_height - h / 2.0 - 1.0

            bounce = True

        if x <= 0:

            self.speed.x = -self.speed.x * BOUNCINESS

            self.position.x = w / 2.0 + 1

            bounce = True

        elif x + w >= screen_width:

            self.speed.x = -self.speed.x * BOUNCINESS

            self.position.x = screen_width - w / 2.0 - 1

            bounce = True

 

        # 根据时间计算如今的位置,物理好的马上发现这其实不标准,

        # 正规的应该是“s = 1/2*g*t*t”,不过这样省事省时一点,咱只是模拟~

        self.position += self.speed * time_passed

        # 根据重力计算速度

        self.speed.y += time_passed * GRAVITY

 

        if bounce:

            self.play_bounce_sound()

 

        self.age += time_passed

 

    def play_bounce_sound(self):

        """这个就是播放声音的函数"""

        channel = self.bounce_sound.play()

 

        if channel is not None:

            # 设置左右声道的音量

            left, right = stero_pan(self.position.x, SCREEN_SIZE[0])

            channel.set_volume(left, right)

 

    def render(self, surface):

        # 真有点麻烦了,有爱的,本身用Sprite改写下吧……

        w, h = self.image.get_size()

        x, y = self.position

        x -= w/2

        y -= h/2

        surface.blit(self.image, (x, y))

 

def run():

    # 上一次的内容

    pygame.mixer.pre_init(44100, 16, 2, 1024*4)

    pygame.init()

    pygame.mixer.set_num_channels(8)

    screen = pygame.display.set_mode(SCREEN_SIZE, 0)

 

    pygame.mouse.set_visible(False)

    clock = pygame.time.Clock()

 

    ball_image = pygame.image.load("ball.png").convert_alpha()

    mouse_image = pygame.image.load("mousecursor.png").convert_alpha()

 

    # 加载声音文件

    bounce_sound = pygame.mixer.Sound("bounce.ogg")

    balls = []

 

    while True:

        for event in pygame.event.get():

            if event.type == QUIT:

                return

            if event.type == MOUSEBUTTONDOWN:

                # 刚刚出来的小球,给一个随机的速度

                random_speed = ( randint(-400, 400), randint(-300, 0) )

                new_ball = Ball( event.pos,

                                 random_speed,

                                 ball_image,

                                 bounce_sound )

                balls.append(new_ball)

 

        time_passed_seconds = clock.tick() / 1000.

        screen.fill((255, 255, 255))

        # 为防止小球太多,把超过寿命的小球加入这个“死亡名单”

        dead_balls = []

        for ball in balls:

            ball.update(time_passed_seconds)

            ball.render(screen)

            # 每一个小球的的寿命是10秒

            if ball.age > 10.0:

                dead_balls.append(ball)

        for ball in dead_balls:

            balls.remove(ball)

 

        mouse_pos = pygame.mouse.get_pos()

        screen.blit(mouse_image, mouse_pos)

        pygame.display.update()

 

if __name__ == "__main__":

    run()

 

 

用Python和Pygame写游戏-从入门到精通(22)

 

Sound对象

方法名

做用

fadeout

淡出声音,可接受一个数字(毫秒)做为淡出时间

get_length

得到声音文件长度,以秒计

get_num_channels

声音要播放多少次

get_volume

获取音量(0.0 ~ 1.0)

play

开始播放,返回一个Channel对象,失败则返回None

set_volume

设置音量

stop

马上中止播放

Channels对象

方法名

做用

fadeout

相似

get_busy

若是正在播放,返回true

get_endevent

获取播放完毕时要作的event,没有则为None

get_queue

获取队列中的声音,没有则为None

get_volume

相似

pause

暂停播放

play

相似

queue

将一个Sound对象加入队列,在当前声音播放完毕后播放

set_endevent

设置播放完毕时要作的event

set_volume

相似

stop

马上中止播放

unpause

继续播放

Music对象:

方法名

做用

fadeout

相似

get_endevent

相似

get_volume

相似

load

加载一个音乐文件

pause

相似

play

相似

rewind

从头开始从新播放

set_endevent

相似

set_volume

相似

stop

马上中止播放

unpause

继续播放

get_pos

得到当前播放的位置,毫秒计

 

界面如上,运行的时候,脚本读取./MUSIC下全部的OGGMP3文件(若是你不是Windows,可能要去掉MP3的判断),显示的也很简单,几个控制按钮,下面显示当前歌名(显示中文老是不那么方便的,若是你运行失败,请具体参考代码内的注释本身修改):

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

# -*- coding: utf-8 -*-

# 注意文件编码也必须是utf-8

SCREEN_SIZE = (800, 600)

# 存放音乐文件的位置

MUSIC_PATH = "./MUSIC"

 

import pygame

from pygame.locals import *

from math import sqrt

import os

import os.path

 

def get_music(path):

 

    # 从文件夹来读取全部的音乐文件

    raw_filenames = os.listdir(path)

 

    music_files = []

    for filename in raw_filenames:

        # 不是Windows的话,仍是去掉mp3吧

        if filename.lower().endswith('.ogg') or filename.lower().endswith('.mp3'):

            music_files.append(os.path.join(MUSIC_PATH, filename))

 

    return sorted(music_files)

 

class Button(object):

    """这个类是一个按钮,具备自我渲染和判断是否被按上的功能"""

    def __init__(self, image_filename, position):

 

        self.position = position

        self.image = pygame.image.load(image_filename)

 

    def render(self, surface):

        # 屡见不鲜的代码了

        x, y = self.position

        w, h = self.image.get_size()

        x -= w / 2

        y -= h / 2

        surface.blit(self.image, (x, y))

 

    def is_over(self, point):

        # 若是point在自身范围内,返回True

        point_x, point_y = point

        x, y = self.position

        w, h = self.image.get_size()

        x -= w /2

        y -= h / 2

 

        in_x = point_x >= x and point_x < x + w

        in_y = point_y >= y and point_y < y + h

        return in_x and in_y

 

def run():

 

    pygame.mixer.pre_init(44100, 16, 2, 1024*4)

    pygame.init()

    screen = pygame.display.set_mode(SCREEN_SIZE, 0)    

 

    #font = pygame.font.SysFont("default_font", 50, False)

    # 为了显示中文,我这里使用了这个字体,具体本身机器上的中文字体请本身查询

    # 详见本系列第四部分://eyehere.net/2011/python-pygame-novice-professional-4/

    font = pygame.font.SysFont("simsunnsimsun", 50, False)    

 

    x = 100

    y = 240

    button_width = 150

    buttons = {}

    buttons["prev"] = Button("prev.png", (x, y))

    buttons["pause"] = Button("pause.png", (x+button_width*1, y))

    buttons["stop"] = Button("stop.png", (x+button_width*2, y))

    buttons["play"] = Button("play.png", (x+button_width*3, y))

    buttons["next"] = Button("next.png", (x+button_width*4, y))

 

    music_filenames = get_music(MUSIC_PATH)

    if len(music_filenames) == 0:

        print "No music files found in ", MUSIC_PATH

        return

 

    white = (255, 255, 255)

    label_surfaces = []

    # 一系列的文件名render

    for filename in music_filenames:

        txt = os.path.split(filename)[-1]

        print "Track:", txt

        # 这是简体中文Windows下的文件编码,根据本身系统状况请酌情更改

        txt = txt.split('.')[0].decode('gb2312')

        surface = font.render(txt, True, (100, 0, 100))

        label_surfaces.append(surface)

 

    current_track = 0

    max_tracks = len(music_filenames)

    pygame.mixer.music.load( music_filenames[current_track] )  

 

    clock = pygame.time.Clock()

    playing = False

    paused = False

 

    # USEREVENT是什么?请参考本系列第二部分:

    # //eyehere.net/2011/python-pygame-novice-professional-2/

    TRACK_END = USEREVENT + 1

    pygame.mixer.music.set_endevent(TRACK_END)

 

    while True:

 

        button_pressed = None

 

        for event in pygame.event.get():

 

            if event.type == QUIT:

                return

 

            if event.type == MOUSEBUTTONDOWN:

 

                # 判断哪一个按钮被按下

                for button_name, button in buttons.iteritems():

                    if button.is_over(event.pos):

                        print button_name, "pressed"

                        button_pressed = button_name

                        break

 

            if event.type == TRACK_END:

                # 若是一曲播放结束,就“模拟”按下"next"

                button_pressed = "next"

 

        if button_pressed is not None:

 

            if button_pressed == "next":

                current_track = (current_track + 1) % max_tracks

                pygame.mixer.music.load( music_filenames[current_track] )

                if playing:

                    pygame.mixer.music.play()

 

            elif button_pressed == "prev":

 

                # prev的处理方法:

                # 已经播放超过3秒,从头开始,不然就播放上一曲

                if pygame.mixer.music.get_pos() > 3000:

                    pygame.mixer.music.stop()

                    pygame.mixer.music.play()

                else:

                    current_track = (current_track - 1) % max_tracks

                    pygame.mixer.music.load( music_filenames[current_track] )

                    if playing:

                        pygame.mixer.music.play()

 

            elif button_pressed == "pause":

                if paused:

                    pygame.mixer.music.unpause()

                    paused = False

                else:

                    pygame.mixer.music.pause()

                    paused = True

 

            elif button_pressed == "stop":

                pygame.mixer.music.stop()

                playing = False

 

            elif button_pressed == "play":

                if paused:

                    pygame.mixer.music.unpause()

                    paused = False

                else:

                    if not playing:

                        pygame.mixer.music.play()

                        playing = True

 

        screen.fill(white)

 

        # 写一下当前歌名

        label = label_surfaces[current_track]

        w, h = label.get_size()

        screen_w = SCREEN_SIZE[0]

        screen.blit(label, ((screen_w - w)/2, 450))

 

        # 画全部按钮

        for button in buttons.values():

            button.render(screen)

 

        # 由于基本是不动的,这里帧率设的很低

        clock.tick(5)

        pygame.display.update()

 

if __name__ == "__main__":

 

    run()

这个程序虽然能够运行,仍是很简陋,有兴趣的能够改改,好比显示播放时间/总长度,甚至更厉害一点,鼠标移动到按钮上班,按钮会产生一点变化等等,咱们如今已经什么都学过了,惟一欠缺的就是实践而已!

 

用Python和Pygame写游戏-从入门到精通(py2exe篇)

 

perl有perlcc(免费高效但配置极其复杂),perlapp(简单效果也不错可是收费)等工具;而对python来讲,py2exe是不二之选,首先是免费的,并且压出来的文件,虽然不能和编译软件相比,仍是不错的了。

到py2exe的官方网站下载安装包,注意要对应本身的python版本。

py2exe是须要写一个脚本进行打包的操做,使用下面这个专为pygame写就的脚本(参考py2exe官方),能够极大的方便打包操做,注意在使用前修改BuildExe里的各个参数。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

#!python

# -*- coding: gb2312 -*-

 

# 这个脚本专为pygame优化,使用py2exe打包代码和资源至dist目录

#

# 使用中如有问题,能够留言至:

#  //eyehere.net/2011/python-pygame-novice-professional-py2exe/

#

# 安装需求:

#         python, pygame, py2exe 都应该装上

 

# 使用方法:

#         1: 修改此文件,指定须要打包的.py和对应数据

#         2: python pygame2exe.py

#         3: 在dist文件夹中,enjoy it~

 

try:

    from distutils.core import setup

    import py2exe, pygame

    from modulefinder import Module

    import glob, fnmatch

    import sys, os, shutil

except ImportError, message:

    raise SystemExit,  "Sorry, you must install py2exe, pygame. %s" % message

 

# 这个函数是用来判断DLL是不是系统提供的(是的话就不用打包)

origIsSystemDLL = py2exe.build_exe.isSystemDLL

def isSystemDLL(pathname):

    # 须要hack一下,freetype和ogg的dll并非系统DLL

    if os.path.basename(pathname).lower() in ("libfreetype-6.dll", "libogg-0.dll", "sdl_ttf.dll"):

        return 0

    return origIsSystemDLL(pathname)

# 把Hack过的函数从新写回去

py2exe.build_exe.isSystemDLL = isSystemDLL

 

# 这个新的类也是一个Hack,使得pygame的默认字体会被拷贝

class pygame2exe(py2exe.build_exe.py2exe):

    def copy_extensions(self, extensions):

        # 得到pygame默认字体

        pygamedir = os.path.split(pygame.base.__file__)[0]

        pygame_default_font = os.path.join(pygamedir, pygame.font.get_default_font())

        # 加入拷贝文件列表

        extensions.append(Module("pygame.font", pygame_default_font))

        py2exe.build_exe.py2exe.copy_extensions(self, extensions)

 

# 这个类是咱们真正作事情的部分

class BuildExe:

    def __init__(self):

        #------------------------------------------------------#

        ##### 对于一个新的游戏程序,须要修改这里的各个参数 #####

        #------------------------------------------------------#

 

        # 起始py文件

        self.script = "MyGames.py"

        # 游戏名

        self.project_name = "MyGames"

        # 游戏site

        self.project_url = "about:none"

        # 游戏版本

        self.project_version = "0.0"

        # 游戏许可

        self.license = "MyGames License"

        # 游戏做者

        self.author_name = "xishui"

        # 联系电邮

        self.author_email = "blog@eyehere.net"

        # 游戏版权

        self.copyright = "Copyright (c) 3000 xishui."

        # 游戏描述

        self.project_description = "MyGames Description"

        # 游戏图标(None的话使用pygame的默认图标)

        self.icon_file = None

        # 额外须要拷贝的文件、文件夹(图片,音频等)

        self.extra_datas = []

        # 额外须要的python库名

        self.extra_modules = []

        # 须要排除的python库

        self.exclude_modules = []

        # 额外须要排除的dll

        self.exclude_dll = ['']

        # 须要加入的py文件

        self.extra_scripts = []

        # 打包Zip文件名(None的话,打包到exe文件中)

        self.zipfile_name = None

        # 生成文件夹

        self.dist_dir ='dist'

 

    def opj(self, *args):

        path = os.path.join(*args)

        return os.path.normpath(path)

 

    def find_data_files(self, srcdir, *wildcards, **kw):

        # 从源文件夹内获取文件

        def walk_helper(arg, dirname, files):

            # 固然你使用其余的版本控制工具什么的,也能够加进来

            if '.svn' in dirname:

                return

            names = []

            lst, wildcards = arg

            for wc in wildcards:

                wc_name = self.opj(dirname, wc)

                for f in files:

                    filename = self.opj(dirname, f)

 

                    if fnmatch.fnmatch(filename, wc_name) and not os.path.isdir(filename):

                        names.append(filename)

            if names:

                lst.append( (dirname, names ) )

 

        file_list = []

        recursive = kw.get('recursive', True)

        if recursive:

            os.path.walk(srcdir, walk_helper, (file_list, wildcards))

        else:

            walk_helper((file_list, wildcards),

                        srcdir,

                        [os.path.basename(f) for f in glob.glob(self.opj(srcdir, '*'))])

        return file_list

 

    def run(self):

        if os.path.isdir(self.dist_dir): # 删除上次的生成结果

            shutil.rmtree(self.dist_dir)

 

        # 得到默认图标

        if self.icon_file == None:

            path = os.path.split(pygame.__file__)[0]

            self.icon_file = os.path.join(path, 'pygame.ico')

 

        # 得到须要打包的数据文件

        extra_datas = []

        for data in self.extra_datas:

            if os.path.isdir(data):

                extra_datas.extend(self.find_data_files(data, '*'))

            else:

                extra_datas.append(('.', [data]))

 

        # 开始打包exe

        setup(

            cmdclass = {'py2exe': pygame2exe},

            version = self.project_version,

            description = self.project_description,

            name = self.project_name,

            url = self.project_url,

            author = self.author_name,

            author_email = self.author_email,

            license = self.license,

 

            # 默认生成窗口程序,若是须要生成终端程序(debug阶段),使用:

            # console = [{

            windows = [{

                'script': self.script,

                'icon_resources': [(0, self.icon_file)],

                'copyright': self.copyright

            }],

            options = {'py2exe': {'optimize': 2, 'bundle_files': 1,

                                  'compressed': True,

                                  'excludes': self.exclude_modules,

                                  'packages': self.extra_modules,

                                  'dist_dir': self.dist_dir,

                                  'dll_excludes': self.exclude_dll,

                                  'includes': self.extra_scripts} },

            zipfile = self.zipfile_name,

            data_files = extra_datas,

            )

 

        if os.path.isdir('build'): # 清除build文件夹

            shutil.rmtree('build')

 

if __name__ == '__main__':

    if len(sys.argv) < 2:

        sys.argv.append('py2exe')

    BuildExe().run()

    raw_input("Finished! Press any key to exit.")

能够先从简单的程序开始,有了一点经验再尝试打包复杂的游戏。
一些Tips:

  • 若是执行出错,会生成一个xxx.exe.log,参考这里的log信息看是否是少打包了东西。
  • 一开始能够使用console来打包,这样能够在命令行里看到更多的信息。
  • 对于每个游戏,基本都须要拷贝上面的原始代码修改成独一无二的打包执行文件。
  • 即便一个很小的py文件,最终生成的exe文件也很大(看安装的库而定,我这里最小4.7M左右),事实上py2exe在打包的时候会把无数的不须要的库都打进来致使最终文件臃肿,若是你安装了很繁杂的库(wxPython等)更是如此。使用zip打包之后查看里面的库文件,把不须要的逐一加入到self.exclude_modules中,最后能够把文件尺寸控制在一个能够接受的范围内。

“dist_dir”应该是属于py2exe的特有options而不是setup的。用Python和Pygame写游戏-从入门到精通(Sprite篇)

 

pygame.sprite.Spritepygame精灵的基类,通常来讲,你老是须要写一个本身的精灵类继承一下它而后加入本身的代码。举个例子:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

import cStringIO, base64

import pygame

from pygame.locals import *

 

class Ball(pygame.sprite.Sprite):

    def __init__(self, color, initial_position):

        pygame.sprite.Sprite.__init__(self)

        ball_file = cStringIO.StringIO(base64.decodestring(

"""iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ

bWFnZVJlYWR5ccllPAAABBJJREFUeNqsVj2PG1UUvfPp8XictXfHa+9mlyJCNEQRWiToqACJAgGC

LqJNlQZR0IFEj8RPSJkGGooUpEWJkGhR0tAAElI2tsfjjxnPjIdz7oyDF2wSUK72yN43793z7rkf

Y8N2HFmbbVliGIYiyzIpy1Isy3oHeMswzLOyXJ2tVit9VhTFAxz5Cfge+A7IZIcZmySObQudwIE0

veanraB1w/O8l5x6D9eXy6UkSaJYLBa6BvsNuAV8uY3sCQlvX4LANM0Xw/Dgdhj2Xm02m+K6LqPR

PXmeS5qmMp/PZTabyXQ6lclkosS1/QJcB+5vkthrAkoAuc4uHx//0B8MvCAIxG/5jEg0kpIkmcwX

icTxBIhlHWEURXoedgW4B3wIfHuBJM9yMQ3j5PTk5N7g6MjtdrrS3e9Ku90GUUvc2hkdMYJx5Ivn

NRC19UReRlRLR/sGeB34UUkMJBcJlcHg6K4SdDvS7/el1+tJp7MnQdCWRqMhDGWZLmWCCFog9rBm

GBYc50rOKON4uqkSC+IQSC3moeX7N09PX/i4AwLkAoQDxeFhHziU8CCUzt6e+EFLc2QaJi4mFQHy

kQLZMpME+WJF1sabdYA7Nq4jQbv9OZPs+75cgkSMYH9/X6PhJ9dpTLjruFLkBRyjACBd1BoLzzY8

T3O0IRntJvCZDXsTTnq262CzrzmgRHu4+QEIQhAxNzRWU1mTxfjOwvBIAOlIYNnWtja5bqM33mN/

sBEdx9bNPOQ1PWlqZJdAFKoMrEI6R+9gj6t7cUl1zjKnjFvsfaybr1Uqlv94ypXSKCud+aefpezs

7O3LL9s4c5U65gCrhGDDpUkqyWIuU1STweNlJRe7nAlmA+ZaVbnmiD4KFNEWC+3VqjB5YImDdMA+

YKONx2OVgxefojRL8CzmCxkOhxLhWYy+mGIvz6RKmv096X91PErP4Byazapbs3vZB45bVQqTzBzQ

kjQBQSTnjx7JcDTCRSLkKNY9SbKACsttHKZdrIqHILnGCNhoDU0qG83U5mNUVTOKShRPYo3m8fAc

nT/S/3mWFy2KrXKNOFbuI+Rr1FvLsB731Ho2m2pU7I1Sx8pSHTLaESIZjob6nfso2w77mSR3IMsN

zh4mmLOIBAkO6fjAgESdV1MYiV4kiUZHRDjD3E0Qza580D+rjsUdAQEj4fRl8wUkqBttPeo5RlJI

uB71jIASc8D+i4W8IoX8CviC5cuI+JlgpLsgcF1ng6RQyaoX1oWX1i67DTxe9w+9/EHW9VOrngCW

ZfNFpmvVWOfUzZ/mfG0HwHBz4ZV1kz8nvLuL+YPnRPDJ00J8A/j9fzrnW+sjeUbjbP8amDyj86z+

tXL5PwzOC4njj4K3gavA8cazczYacLd+p/+6y8mfAgwAsRuLfp/zVLMAAAAASUVORK5CYII="""))

        self.image = pygame.image.load(ball_file, 'file').convert_alpha()

        self.rect = self.image.fill(color, None, BLEND_ADD)

        self.rect.topleft = initial_position

 

pygame.init()

screen = pygame.display.set_mode([350, 350])

 

ball = Ball((255, 0, 0), (100, 100))

screen.blit(ball.image, ball.rect)

pygame.display.update()

while pygame.event.poll().type != KEYDOWN:

    pygame.time.delay(10)

那一大堆的字符串,相信懂Python的人会明白的,不明白的请去查阅一下base64编码和Python对应的StringIObase64库。我这里使用这种方法而不是直接读取文件,只是想告诉你们pygame.image.load方法不只仅能够读取文件,也能够读取文件对象。是否是感受一会儿思路开阔了?Python那么多方便的文件对象,之后游戏的资源文件就能够不用一一独立放出来了,使用zipfile,咱们很容易就能够把资源文件打包起来,这样看起来咱的游戏可就专业多了~这是后话,之后有机会再讲。

而本例没有直接画一个圆,而是使用用了颜色混合的方法,这样能够画出有立体感的球体,效果如左图。而上面一大堆的字符串,其实就是那个球体的图像文件编码之后的东西。这个和本教程没啥大联系,请自行学习光与色的知识……

可是可是,看了上面的代码你们必定会有意见了,这样感受比直接用Surface写的代码还多啊!一点好处都没有的样子。确实会有这样的错觉,可是一个球看不出好处来,多个球呢?咱们就能够这么写了:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

balls = []

for color, location in [([255, 0, 0], [50, 50]),

                        ([0, 255, 0], [100, 100]),

                        ([0, 0, 255], [150, 150])]:

    boxes.append(Box(color, location))

 

...

for b in balls: screen.blit(b.image, b.rect)

pygame.display.update()

 

# 咱们还能用一种更牛的重绘方式

# rectlist = [screen.blit(b.image, b.rect) for b in balls]

# pygame.display.update(rectlist)

# 这样的好处是,pygame只会重绘有更改的部分

我就不给出完整代码和效果图了,请你们本身试验。

不过光这样还不足以体现sprite的好处,sprite最大的优点在于动画,这里就须要用一下update方法,举一个例子,把第一个程序,从33行开始换成下面的代码:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

class MoveBall(Ball):

    def __init__(self, color, initial_position, speed, border):

        super(MoveBall, self).__init__(color, initial_position)

        self.speed = speed

        self.border = border

        self.update_time = 0

 

    def update(self, current_time):

        if self.update_time < current_time:

            if self.rect.left < 0 or self.rect.left > self.border[0] - self.rect.w:

                self.speed[0] *= -1

            if self.rect.top < 0 or self.rect.top > self.border[1] - self.rect.h:

                self.speed[1] *= -1

 

            self.rect.x, self.rect.y = self.rect.x + self.speed[0], self.rect.y + self.speed[1]

            self.update_time = current_time + 10

 

pygame.init()

screen = pygame.display.set_mode([350, 350])

 

balls = []

for color, location, speed in [([255, 0, 0], [50, 50], [2,3]),

                        ([0, 255, 0], [100, 100], [3,2]),

                        ([0, 0, 255], [150, 150], [4,3])]:

    balls.append(MoveBall(color, location, speed, (350, 350)))

 

while True:

    if pygame.event.poll().type == QUIT: break

 

    screen.fill((0,0,0,))

    current_time = pygame.time.get_ticks()

    for b in balls:

        b.update(current_time)

        screen.blit(b.image, b.rect)

    pygame.display.update()

咱们能够看到小球欢快的运动起来,碰到边界就会弹回来,这才是sprite类的真正用处。每个Sprite类都会有各自的速度属性,每次调用update都会各自更新本身的位置,主循环只须要update+blit就能够了,至于各个小球到底在一个怎样的状态,彻底能够不在乎。不过精灵的魅力仍是不只在此,上面的代码中咱们把每一个精灵加入一个列表,而后分别调用每一个精灵的update方法,太麻烦了!使用pygame.sprite.Group类吧,创建它的一个实例balls,而后用add方法把精灵加入,而后只须要调用balls.update(args..)就能够了,连循环的不用写。一样的使用balls.draw()方法,你可让pygame只重绘有变化的部分。请尝试使用(记住还有一个balls.clear()方法,实际写一下就知道这个方法用来干吗了)。

尽管咱们已经说了不少,也着实领略到了精灵的好处,但故事尚未结束,pygame.sprite有着层与碰撞的概念。层的引入是由于Group默认是没有次序的,因此哪一个精灵覆盖哪一个精灵彻底就不知道了,解决方法嘛,使用多个Group、使用OrderedUpdates,或者使用LayeredUpdates,至于具体使用方法,相信若是您须要用到的时候,已经到至关的高度了,随便看看文档就明白了,我就很少说了;而碰撞,又是一个无底洞啊,下次有机会再讲吧~

用Python和Pygame写游戏-从入门到精通(实战一:涂鸦画板1)

功能样式

作以前总要有个数,咱们的程序作出来会是个什么样子。所谓从顶到底或者从底到顶啥的,咱就不研究了,这个小程序随你怎么弄了,并且咱们主要是来熟悉pygame,高级的软件设计方法一律不谈~

由于是抄袭画图板,也就是鼠标按住了能在上面涂涂画画就是了,选区、放大镜、滴管功能啥的就通通不要了。画笔的话,基本的铅笔画笔老是要的,也能够考虑加一个刷子画笔,这样有一点变化;而后颜色应该是要的,不然太过单调了,不过调色板啥的就暂时免了,提供几个候选色就行了;而后橡皮……橡皮不就是白色的画笔么?免了免了!还有啥?彷佛够了。。。 OK,开始吧!

框架

pygame程序的框架都是差很少的,考虑到咱们这个程序的实际做用,大概创建这样的一个代码架子就能够了。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

import pygame

from pygame.locals import *

 

class Brush():

    def __init__(self):

        pass

 

class Painter():

    def __init__(self):

        self.screen = pygame.display.set_mode((800, 600))

        pygame.display.set_caption("Painter")

        self.clock = pygame.time.Clock()

 

    def run(self):

        self.screen.fill((255, 255, 255))

        while True:

            # max fps limit

            self.clock.tick(30)

            for event in pygame.event.get():

                if event.type == QUIT:

                    return

                elif event.type == KEYDOWN:

                    pass

                elif event.type == MOUSEBUTTONDOWN:

                    pass

                elif event.type == MOUSEMOTION:

                    pass

                elif event.type == MOUSEBUTTONUP:

                    pass

 

            pygame.display.update()

 

if __name__ == '__main__':

    app = Painter()

    app.run()

这个很是简单,准备好画板类,画笔类,暂时还都是空的,其实也就是作了一些pygame的初始化工做。若是这样还不能读懂的话,您须要把前面22篇从头再看看,有几句话不懂就看几遍:)

这里只有一点要注意一下,咱们把帧率控制在了30,没有人但愿在画画的时候,CPU风扇狂转的。并且只是画板,没有自动运动的物体,纯粹的交互驱动,咱们也不须要很高的刷新率。

第一次的绘图代码

按住鼠标而后在上面移动就画东西,咱们很容易能够想到这个流程:

 

1

2

3

按下左键  →  绘制flag开

移动鼠标  →  flag开的时候,在移动坐标上留下痕迹

放开左键  →  绘制flag关

马上试一试吧:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

class Brush():

    def __init__(self, screen):

        self.screen = screen

        self.color = (0, 0, 0)

        self.size  = 1

        self.drawing = False

 

    def start_draw(self):

        self.drawing = True

    def end_draw(self):

        self.drawing = False

 

    def draw(self, pos):

        if self.drawing:

            pygame.draw.circle(self.screen, self.color, pos, self.size)

 

class Painter():

    def __init__(self):

        #*#*#*#*#

        self.brush = Brush(self.screen)

 

    def run(self):

         #*#*#*#*#

                elif event.type == KEYDOWN:

                    # press esc to clear screen

                    if event.key == K_ESCAPE:

                        self.screen.fill((255, 255, 255))

                elif event.type == MOUSEBUTTONDOWN:

                    self.brush.start_draw()

                elif event.type == MOUSEMOTION:

                    self.brush.draw(event.pos)

                elif event.type == MOUSEBUTTONUP:

                    self.brush.end_draw()

框架中有的代码我就不贴了,用#*#*#*#*#代替,最后会给出完整代码的。

这里主要是给Brush类增长了一些功能,也就是上面咱们提到的流程想对应的功能。留下痕迹,咱们是使用了在坐标上画圆的方法,这也是最容易想到的方法。这样的效果好很差呢?咱们试一试:

哦,太糟糕了,再劣质的铅笔也不会留下这样断断续续的笔迹。上面是当咱们鼠标移动的快一些的时候,点之间的间距很大;下面是移动慢一些的时候,勉勉强强显得比较连续。从这里咱们也能够看到pygame事件响应的频度(这个距离和上面设置的最大帧率有关)。

怎么办?要修改帧率让pygame平滑的反应么?不,那样作得不偿失,换一个角度思考,若是有间隙,咱们让pygame把这个间隙链接起来很差么?

第二次的绘图代码

思路仍是很简单,当移动的时候,Brush在上一次和这一次的点之间连一条线就行了:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

class Brush():

    def __init__(self, screen):

        self.screen = screen

        self.color = (0, 0, 0)

        self.size  = 1

        self.drawing = False

        self.last_pos = None     # <--

 

    def start_draw(self, pos):

        self.drawing = True

        self.last_pos = pos    # <--

    def end_draw(self):

        self.drawing = False

 

    def draw(self, pos):

        if self.drawing:

            pygame.draw.line(self.screen, self.color,

                    self.last_pos, pos, self.size * 2)

            self.last_pos = pos

在__init__和start_draw中各加了一句,用来存储上一个点的位置,而后draw也由刚刚的话圆变成画线,效果如何?咱们来试试。嗯,好多了,若是你动做能温柔一些的话,线条已经很圆润了,至少没有断断续续的存在了。

知足了么?我但愿你的回答是“NO”,为何,若是你划线很快的话,你就能明显看出棱角来,就好像左图上半部分,仍是能看出是由几个线段组合的。只有永不知足,咱们才能不停进步。

不过对咱们这个例程而言,差很少了,通常人在真正画东西的时候,也不会动那么快的:)

那么这个就是咱们最终的绘图机制了么?回头看看咱们的样式,好用还须要加一个笔刷……所谓笔刷,不只仅是很粗,并且是由不少细小的毛组成,画出来的线是给人一种一缕一缕的感受,用这个方法能够实现么?好像很是很是的困难。。。孜孜不倦的咱们再次进入了沉思……

这个时候,若是没有头绪,就得借鉴一下前辈的经验了。看看人家是如何实现的?

 

Photoshop的笔刷尺寸改大,你会发现它会画成这样

若是你的Photoshop不错,应该知道它里面复杂的笔刷设定,而Photoshop画出来的笔画,并非真正一直线的,而是由无数细小的点组成的,这些点之间的间距是如此的密,以致于咱们误会它是一直线……因此说,咱们还得回到第一种方法上,把它发扬光大一下~ 这没有什么很差意思的,放弃第二种方法并不意味着咱们是多么的愚蠢,而是说明咱们从本身身上又学到了不少!

(公元前1800年)医生:来,试试吃点儿这种草根,感谢伟大的部落守护神赐与咱们神药!
(公元900年)医生:别再吃那种草根,简直是野蛮不开化不尊重上帝,这是一篇祈祷词,天天虔诚地向上帝祈祷一次,不久就会治愈你的疾病。
(公元1650年)医生:祈祷?!封建迷信!!!来,只要喝下这种药水,什么病都能治好!
(公元1960年)医生:什么药水?早就不用了!别喝那骗人的”万灵药”,仍是这种药片的疗效快!
(公元1995年)医生:哪一个庸医给你开的处方?那种药片吃半瓶也抵不上这一粒,来来来,试试科技新成果—抗生素
(公元2003年)医生:据最新科学研究,抗生素反作用太强,毕竟是人造的东西呀……来,试试吃点儿这种草根!早在公元前1800年,文献就有记载了。

返璞归真,大抵如此了。

第三次的绘图代码

此次咱们考虑的更多,但愿在点与点之间充满咱们的笔画,很天然的咱们就须要一个循环来作这样的事情。咱们的笔画有两种,普通的实心和刷子,实心的话,用circle来画也不失为一个好主意;刷子的话,咱们可能须要一个刷子的图案来填充了。

下面是咱们新的Brush类:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

class Brush():

    def __init__(self, screen):

        self.screen = screen

        self.color = (0, 0, 0)

        self.size  = 1

        self.drawing = False

        self.last_pos = None

        self.space = 1

        # if style is True, normal solid brush

        # if style is False, png brush

        self.style = False

        # load brush style png

        self.brush = pygame.image.load("brush.png").convert_alpha()

        # set the current brush depends on size

        self.brush_now = self.brush.subsurface((0,0), (1, 1))

 

    def start_draw(self, pos):

        self.drawing = True

        self.last_pos = pos

    def end_draw(self):

        self.drawing = False

 

    def set_brush_style(self, style):

        print "* set brush style to", style

        self.style = style

    def get_brush_style(self):

        return self.style

 

    def set_size(self, size):

        if size < 0.5: size = 0.5

        elif size > 50: size = 50

        print "* set brush size to", size

        self.size = size

        self.brush_now = self.brush.subsurface((0,0), (size*2, size*2))

    def get_size(self):

        return self.size

 

    def draw(self, pos):

        if self.drawing:

            for p in self._get_points(pos):

                # draw eveypoint between them

                if self.style == False:

                    pygame.draw.circle(self.screen,

                            self.color, p, self.size)

                else:

                    self.screen.blit(self.brush_now, p)

 

            self.last_pos = pos

 

    def _get_points(self, pos):

        """ Get all points between last_point ~ now_point. """

        points = [ (self.last_pos[0], self.last_pos[1]) ]

        len_x = pos[0] - self.last_pos[0]

        len_y = pos[1] - self.last_pos[1]

        length = math.sqrt(len_x ** 2 + len_y ** 2)

        step_x = len_x / length

        step_y = len_y / length

        for i in xrange(int(length)):

            points.append(

                    (points[-1][0] + step_x, points[-1][1] + step_y))

        points = map(lambda x:(int(0.5+x[0]), int(0.5+x[1])), points)

        # return light-weight, uniq list

        return list(set(points))

咱们增长了几个方法,_get_points()返回上一个点到如今点之间全部的点(这话听着真别扭),draw根据这些点填充。
同时咱们把get_size()、set_size()也加上了,用来设定当前笔刷的大小。
而变化最大的,则是set_style()和get_style(),咱们如今载入一个PNG图片做为笔刷的样式,当style==True的时候,draw再也不使用circle填充,而是使用这个PNG样式,固然,这个样式大小也是应该可调的,全部咱们在set_size()中,会根据size大小实时的调整PNG笔刷。

固然,咱们得在主循环中调用set方法,才能让这些东西工做起来~ 过一下子再讲。再回顾下咱们的样式,还有什么?颜色……咱们立刻把颜色设置代码也加进去吧,太简单了!我这里就先偷偷懒了~

控制代码

到如今,咱们已经完成了绘图部分的全部功能了。如今已经能够在屏幕上自由发挥了,可是笔刷的颜色和大小好像不能改啊……咱们有这样的接口你却不调用,浪费了。

…… 真抱歉,我原想一次就把涂鸦画板讲完了,没想到笔刷就讲了这么多,再把GUI说一下就文章就太长了,很差消化。因此仍是分红两部分吧,下一次咱们把GUI部分加上,就能完成对笔刷的控制了,咱们的第一个实战也就宣告成功了!

用Python和Pygame写游戏-从入门到精通(实战一:涂鸦画板2)

By xishui | 2011/08/27

趁热打铁赶快把咱们这个画板完成吧~

 

……鼠绘无能,不许笑!全部评论中噗嗤画的好搓啊画的好棒啊等,都会被我无情扑杀掉!可是能告诉我怎样画能够更漂亮的话,绝对欢迎。

上次讲Brush的时候,由于以为太简单把color设置跳过了,如今实际写的时候才发现,由于咱们设置了颜色须要对刷子也有效,因此实际上set_color方法还有一点点收尾工做须要作:

Python

 

1

2

3

4

5

6

    def set_color(self, color):

        self.color = color

        for i in xrange(self.brush.get_width()):

            for j in xrange(self.brush.get_height()):

                self.brush.set_at((i, j),

                        color + (self.brush.get_at((i, j)).a,))

也就是在设定color的时候,顺便把笔刷的颜色也改了,可是要保留原来的alpha值,其实也很简单就是了……

按钮菜单部分

上图能够看到,按钮部分分别为铅笔、毛笔、尺寸大小、(当前样式)、颜色选择者几个组成。咱们只以笔刷选择为例讲解一下,其余的都是相似的。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

# 初始化部分

        self.sizes = [

                pygame.image.load("big.png").convert_alpha(),

                pygame.image.load("small.png").convert_alpha()

            ]

        self.sizes_rect = []

        for (i, img) in enumerate(self.sizes):

            rect = pygame.Rect(10 + i * 32, 138, 32, 32)

            self.sizes_rect.append(rect)

 

# 绘制部分

        for (i, img) in enumerate(self.pens):

            self.screen.blit(img, self.pens_rect[i].topleft)

 

# 点击判断部分

        for (i, rect) in enumerate(self.pens_rect):

            if rect.collidepoint(pos):

                self.brush.set_brush_style(bool(i))

                return True

这些代码其实是我这个例子最想给你们说明的地方,按钮式咱们从未接触过的东西,然而游戏中按钮的应用我都没必要说。

不过这代码也都不困难,基本都是咱们学过的东西,只不过变换了一下组合而已,我稍微说明一下:

初始化部分:读入图标,并给每一个图标一个Rect
绘制部分: 根据图表的Rect绘制图表
点击判断部分:根据点击的位置,依靠“碰撞”来判断这个按钮是否被点击,若点击了,则作相应的操做(这里是设置样式)后返回True。这里的collidepoint()是新内容,也就是Rect的“碰撞”函数,它接收一个坐标,若是在Rect内部,就返回True,不然False。

好像也就如此,有了必定的知识积累后,新东西的学习也变得易如反掌了。

在这个代码中,为了明晰,我把各个按钮按照功能都分红了好几组,在实际应用中按钮数量不少的时候可能并不合适,请本身斟酌。

完整代码

OK,这就结束了~ 下面把整个代码贴出来。不过,我是一边写代码一遍写文章,思路不是很连贯,并且python也很久不用了……若是有哪里写的有问题(没有就怪了),还请不吝指出!

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

import pygame

from pygame.locals import *

import math

 

# 2011/08/27 Version 1, first imported

 

class Brush():

    def __init__(self, screen):

        self.screen = screen

        self.color = (0, 0, 0)

        self.size  = 1

        self.drawing = False

        self.last_pos = None

        self.space = 1

        # if style is True, normal solid brush

        # if style is False, png brush

        self.style = False

        # load brush style png

        self.brush = pygame.image.load("brush.png").convert_alpha()

        # set the current brush depends on size

        self.brush_now = self.brush.subsurface((0,0), (1, 1))

 

    def start_draw(self, pos):

        self.drawing = True

        self.last_pos = pos

    def end_draw(self):

        self.drawing = False

 

    def set_brush_style(self, style):

        print "* set brush style to", style

        self.style = style

    def get_brush_style(self):

        return self.style

 

    def get_current_brush(self):

        return self.brush_now

 

    def set_size(self, size):

        if size < 0.5: size = 0.5

        elif size > 32: size = 32

        print "* set brush size to", size

        self.size = size

        self.brush_now = self.brush.subsurface((0,0), (size*2, size*2))

    def get_size(self):

        return self.size

 

    def set_color(self, color):

        self.color = color

        for i in xrange(self.brush.get_width()):

            for j in xrange(self.brush.get_height()):

                self.brush.set_at((i, j),

                        color + (self.brush.get_at((i, j)).a,))

    def get_color(self):

        return self.color

 

    def draw(self, pos):

        if self.drawing:

            for p in self._get_points(pos):

                # draw eveypoint between them

                if self.style == False:

                    pygame.draw.circle(self.screen, self.color, p, self.size)

                else:

                    self.screen.blit(self.brush_now, p)

 

            self.last_pos = pos

 

    def _get_points(self, pos):

        """ Get all points between last_point ~ now_point. """

        points = [ (self.last_pos[0], self.last_pos[1]) ]

        len_x = pos[0] - self.last_pos[0]

        len_y = pos[1] - self.last_pos[1]

        length = math.sqrt(len_x ** 2 + len_y ** 2)

        step_x = len_x / length

        step_y = len_y / length

        for i in xrange(int(length)):

            points.append(

                    (points[-1][0] + step_x, points[-1][1] + step_y))

        points = map(lambda x:(int(0.5+x[0]), int(0.5+x[1])), points)

        # return light-weight, uniq integer point list

        return list(set(points))

 

class Menu():

    def __init__(self, screen):

        self.screen = screen

        self.brush  = None

        self.colors = [

                (0xff, 0x00, 0xff), (0x80, 0x00, 0x80),

                (0x00, 0x00, 0xff), (0x00, 0x00, 0x80),

                (0x00, 0xff, 0xff), (0x00, 0x80, 0x80),

                (0x00, 0xff, 0x00), (0x00, 0x80, 0x00),

                (0xff, 0xff, 0x00), (0x80, 0x80, 0x00),

                (0xff, 0x00, 0x00), (0x80, 0x00, 0x00),

                (0xc0, 0xc0, 0xc0), (0xff, 0xff, 0xff),

                (0x00, 0x00, 0x00), (0x80, 0x80, 0x80),

            ]

        self.colors_rect = []

        for (i, rgb) in enumerate(self.colors):

            rect = pygame.Rect(10 + i % 2 * 32, 254 + i / 2 * 32, 32, 32)

            self.colors_rect.append(rect)

 

        self.pens = [

                pygame.image.load("pen1.png").convert_alpha(),

                pygame.image.load("pen2.png").convert_alpha()

            ]

        self.pens_rect = []

        for (i, img) in enumerate(self.pens):

            rect = pygame.Rect(10, 10 + i * 64, 64, 64)

            self.pens_rect.append(rect)

 

        self.sizes = [

                pygame.image.load("big.png").convert_alpha(),

                pygame.image.load("small.png").convert_alpha()

            ]

        self.sizes_rect = []

        for (i, img) in enumerate(self.sizes):

            rect = pygame.Rect(10 + i * 32, 138, 32, 32)

            self.sizes_rect.append(rect)

 

    def set_brush(self, brush):

        self.brush = brush

 

    def draw(self):

        # draw pen style button

        for (i, img) in enumerate(self.pens):

            self.screen.blit(img, self.pens_rect[i].topleft)

        # draw < > buttons

        for (i, img) in enumerate(self.sizes):

            self.screen.blit(img, self.sizes_rect[i].topleft)

        # draw current pen / color

        self.screen.fill((255, 255, 255), (10, 180, 64, 64))

        pygame.draw.rect(self.screen, (0, 0, 0), (10, 180, 64, 64), 1)

        size = self.brush.get_size()

        x = 10 + 32

        y = 180 + 32

        if self.brush.get_brush_style():

            x = x - size

            y = y - size

            self.screen.blit(self.brush.get_current_brush(), (x, y))

        else:

            pygame.draw.circle(self.screen,

                    self.brush.get_color(), (x, y), size)

        # draw colors panel

        for (i, rgb) in enumerate(self.colors):

            pygame.draw.rect(self.screen, rgb, self.colors_rect[i])

 

    def click_button(self, pos):

        # pen buttons

        for (i, rect) in enumerate(self.pens_rect):

            if rect.collidepoint(pos):

                self.brush.set_brush_style(bool(i))

                return True

        # size buttons

        for (i, rect) in enumerate(self.sizes_rect):

            if rect.collidepoint(pos):

                if i:   # i == 1, size down

                    self.brush.set_size(self.brush.get_size() - 0.5)

                else:

                    self.brush.set_size(self.brush.get_size() + 0.5)

                return True

        # color buttons

        for (i, rect) in enumerate(self.colors_rect):

            if rect.collidepoint(pos):

                self.brush.set_color(self.colors[i])

                return True

        return False

 

class Painter():

    def __init__(self):

        self.screen = pygame.display.set_mode((800, 600))

        pygame.display.set_caption("Painter")

        self.clock = pygame.time.Clock()

        self.brush = Brush(self.screen)

        self.menu  = Menu(self.screen)

        self.menu.set_brush(self.brush)

 

    def run(self):

        self.screen.fill((255, 255, 255))

        while True:

            # max fps limit

            self.clock.tick(30)

            for event in pygame.event.get():

                if event.type == QUIT:

                    return

                elif event.type == KEYDOWN:

                    # press esc to clear screen

                    if event.key == K_ESCAPE:

                        self.screen.fill((255, 255, 255))

                elif event.type == MOUSEBUTTONDOWN:

                    # <= 74, coarse judge here can save much time

                    if ((event.pos)[0] <= 74 and

                            self.menu.click_button(event.pos)):

                        # if not click on a functional button, do drawing

                        pass

                    else:

                        self.brush.start_draw(event.pos)

                elif event.type == MOUSEMOTION:

                    self.brush.draw(event.pos)

                elif event.type == MOUSEBUTTONUP:

                    self.brush.end_draw()

 

            self.menu.draw()

            pygame.display.update()

 

if __name__ == '__main__':

    app = Painter()

    app.run()

200行左右,注释也不是不少,由于在这两篇文章里都讲了,有哪里不明白的请留言,我会根据实际状况再改改。

用Python和Pygame写游戏-从入门到精通(实战二:恶搞俄罗斯方块1)

By xishui | 2011/09/02

游戏是为了什么而存在的?Bingo,是为了娱乐~ 在这个最高主题以前,技术啥的什么都无所谓!

前一段时间,有位姓刘的网友用Pygame写了个俄罗斯方块,在用py2exe打包的时候遇到一些问题,和我交流了一下。有兴趣的能够在这里下载,除了代码,打包后的exe文件也一并提供了。

受他启发,此次咱们就以俄罗斯方块为主题作一个游戏吧,可是,咱不能走寻常路啊,得把它整的很是有趣才行。记得曾经在网上看到一个搞笑俄罗斯方块,当时看了笑到肚子疼啊,时隔好久如今翻出来,同样笑到脱力:

咱们就来作一个这样的俄罗斯方块吧:)作好了之后,给朋友玩玩,好好看看他(她,它?)的囧表情!

构架原理

构架这个词太大了,其实就是草稿了~ 看过这个视频,咱们能够看到这个蛋疼的游戏有几种模式,把几个可能用到咱们游戏中的模式分别整理一下:

  1. 落下马上消失
  2. 一屏幕长的长条
  3. 掉各类房间的方块,挂了之后算房钱
  4. 长条的宽度稍稍宽于通常尺寸
  5. 落下奇怪的东西(豆荚,气泡等)
  6. 长条会从底部消失
  7. 方块很是很是小
  8. 同时快速落下三个方块
  9. 落下超级玛丽,碰到蘑菇长大挂掉
  10. 固然咱们至少得有一个正常的模式

很是的多,不过这个界面仍是都同样的,不一样的是咱们掉下的东西和消除判断。咱们先把UI考虑一下,就用普通的俄罗斯方块的界面就能够了,像这样:

 

虽然说至关不酷,不过各个部分一目了然,应该也能够了。分别是游戏显示区,右边则是下一个方块的显示,得分显示区域,和一些功能按钮。

接下来咱们考虑这个程序的运行机理,咱们先从最基本的状况开始。在经典俄罗斯方块运行的时候,不停的有随机的方块落下,用户控制它们的转向和位置落下,落下之后,若是稳固的方块堆有哪一行是彻底填充的,就消除得分。

俄罗斯方块的思想其实很是的简单,人们热衷于它,不得不说简单又有有足够变化的规则是主因,还有就是用户受众很大的关系……

右半部分的都很简单,分别是下一个方块,分数和一些功能按钮,左半部分是和谐,这里得不停的刷新,由于方块无论有没有操做都会缓慢落下直至彻底落地。而一旦落地,就须要看是否消除并刷新分数,同时落下接着的方块,并显示下一个方块。

原理的进一步思考

俄罗斯方块诞生于1985年,那时候尚未什么成熟的面向对象的编程方法,因此俄罗斯方块从一开始,界面就是以古朴的数组的方式运行的。

若是你有用其余语言编写俄罗斯方块的经验的话,就会知道大多数的实现方法都是维护一个二维数组(长度对应区域中的格子数,好比20×10。固然也能够是一维的,由于知道宽度,因此转换很容易),当数组某一位是1的时候,说明对应的位置有方块,这个数组不停的更新,程序就把这个数组实时的画到屏幕上,如此而已。

咱们再考虑的仔细一点,由于是使用pygame编写,有没有什么更好的方法呢?若是咱们把每个方块(这里指四个小方块组成的总体)当作一个Sprite,那么就能够很方便的绘制,可是Sprite老是方形的,作碰撞判断就无效了,因此这样不行。那若是把每个小方块做为一个Sprie,再把四个小方块组成的放开作一个Group呢?听起来不错,可是再想一想也很是麻烦,咱们就得判断碰撞的方向,要多作不少事情……考虑良久,感受仍是使用数组最靠谱啊,真惋惜!因此咱们也仍是用数组来作这事情吧。

实现初期的一些优化思考

尽管咱们仍然使用数组这种古老的方式,咱们仍是应该要利用一下pygame的绘图优点,不然就太失败了。举个例子,通常的俄罗斯方块,在重绘的时候把数组从头至尾扫描一遍,碰到一个1就画一个方块,俄罗斯方块的刷新率就算很低,一秒钟也要画了好屡次吧(不然后期速度快的时候画面就了)。算它是15次,这样一来,每秒钟要扫描15次,须要画几百甚至上千次方块,调用这么屡次绘图函数,仍是至关浪费资源的。

咱们能够这么考虑,除了数组以外,咱们还维护一个已经落下的方块的图形的Surface,这样每次只须要把这个Surface贴到屏幕上就能够了,很是的轻量级。而这个Surface的更新也只须要在新的方块着地之后进行,彻底能够在判断消除的代码里一块儿作,平均几秒钟才会执行一次,大大减小了计算机的工做了。固然,这个简单的程序里,咱们这么作也许并不能看到性能的提高,不过一直有这么的思想,当咱们把工程越作越大的时候,和别人的差距也许就会体现出来了。

同时,出于人性化的思考,咱们是否是能够提供用户点击其余窗口的时候就把游戏暂停了?实现起来并不困难,可是好感度的提高但是至关的大。

实现中的一些细节

按左向左,按右向右,这一点事毫无疑问的,不过当咱们按着向左向右不放的时候,就会持续移动,这一点也是要注意的,上面那位朋友实现的俄罗斯方块就没有考虑这一点,固然可能仍是中途的版本的关系,咱们这里要考虑到。

由于咱们要实现几种不一样模式的俄罗斯方块,那么比较通常的考虑方法就是先实现一个通用的、标准的而定制能力又很强的游戏类。当咱们之后去作其余模式的时候,能够很方便的从这个类扩展出来,因此一开始设计的时候,就要尽量多的考虑各类变种。

另外,考虑此次就彻底使用键盘来控制吧,鼠标就不要了,上面的概念图,旁边几个按钮请无视……

下一次开始,咱们就从基本的框架开始,慢慢地搭一个不一样寻常的俄罗斯方块出

用Python和Pygame写游戏-从入门到精通(实战二:恶搞俄罗斯方块2)

By xishui | 2011/09/10

咱们接着来作这个整死人不偿命的俄罗斯方块。

代码组织和名词约定

上一次咱们稍微整理了一下游戏运行的框架,这里须要整理一下python代码的框架,一个典型的pygame脚本结构以下:

其中,lib为pygame的脚本,游戏中声音、图像、控制模块等都放在这里;而data就是游戏的资源文件,图像、声音等文件放在这里。固然这东西并非硬性规定的,你能够用你本身喜欢的结构来组织本身的pygame游戏,事实上,除了付你工钱的那家伙之外,没有人能够强迫你这样作或那样作~ 此次我仍是用这种典型的方法来存放各个文件,便于你们理解。

由于我是抽空在LinuxWindows上交叉编写的,代码中没有中文注释,游戏里也没有中文的输出,因此但愿看到清楚解释的话,仍是应该好好的看这几篇文章。固然最后我会放出全部的代码,也会用py2exe编译一份exe出来,方便传给不会用python的人娱乐。

由于主要是讲解pygame,不少python相关的知识点就一代而过了,只稍微解释一下可能有些疑问的,若是有看不懂的,请留言,我会酌情追加到文章中。

咱们约定,那个掉落方块的区域叫board,落下的方块叫shape,组成方块的最小单位叫tile

咱们的lib中可能会有这几个文件(未必是所有):

 

1

2

3

4

5

6

game.py    ---- 主循环,该文件会调用main,menu来展现不一样界面

main.py    ---- 游戏界面,但为了实现不一样模式,这里需再次调用tetris

menu.py    ---- 菜单界面,开始的选择等

shape.py   ---- 方块的类

tetris.py  ---- 游戏核心代码,各类不一样种类的俄罗斯方块实现

util.py    ---- 各类工具函数,如读取图片等

引导文件

run_game.py很简单,并且基本全部的pygame工程里都长得同样:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

#!/usr/bin/env python

 

import sys, os

 

try:

    libdir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lib')

    sys.path.insert(0, libdir)

except:

    # in py2exe, __file__ is gone...

    pass

 

import game

game.run()

lib目录加入搜索路径就完事了,没有什么值得特别说明的。

主循环文件

到如今为止,咱们全部的pygame都只有一个界面,打开是什么,到关闭也就那个样子。但实际上的游戏,通常进去就会有一个开始界面,那里咱们能够选开始继续选项”……等等内容。pygame中如何实现这个呢?

由于pygame一开始运行,就是一根筋的等事件并响应,因此咱们就须要在事件的循环中加入界面的判断,而后针对的显示界面。例如:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

    def loop(self):

        clock = pygame.time.Clock()

        while self.stat != 'quit':

            elapse = clock.tick(25)

            if self.stat == 'menu':

                self.stat = self.menu.run(elapse)

            elif self.stat == 'game':

                self.stat = self.main.run(elapse)

 

            if self.stat.startswith('level'):

                level = int(self.stat.split()[1])

                print "Start game at level", level

                self.main.start(level)

                self.stat = "game"

 

            pygame.display.update()

        pygame.quit()

由于有不少朋友说以前使用的while True的方法不能正常退出,这里我就听取你们的意见干脆把退出也做为一种状态,兼容性可能会好一些。不过在个人机器上(Windows 7 64bit + Python 2.6 32bit + Pygame 1.9.1 32bit)是正常的,怀疑是否是没法正常退出的朋友使用了64位的PythonPygame(尽管64Pygame也有,但并非官方推出的,不保证效果)。

这里定义了几个游戏状态,最主要的就是menugame,意义也是一目了然的。咱们有游戏对象和菜单对象,当游戏处于某种状态的时候,就调用对应对象的run方法,这个run接受一个时间参数,具体意义相信你们也明白了,基于时间的控制。

同时,咱们还有一个level X的状态,这个主要是控制菜单到游戏之间的转换,不过虽然写的level,实际的意义是模式,由于咱们但愿有几种不一样的游戏模式,因此在从菜单到游戏过渡的时候,须要这个信息。

这个程序中,全部的状态都是经过字符串来实现的,说实话未必很好。虽然容易理解可是效率等可能不高,也许使用标志变量会更好一些。不过既然是例子,首先天然是但愿你们可以看的容易一些。因此最终仍是决定使用这个方法。

Menu

菜单显示了一些选项,而且在用户调节的时候能够显示当前的选项(通常来讲就是高亮出来),最后肯定时,改变状态。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

class Menu:

    OPTS = ['LEVEL 1', 'LEVEL 2', 'LEVEL 3', 'QUIT']

    def __init__(self, screen):

        self.screen = screen

        self.current = 0

 

    def run(self, elapse):

        self.draw()

        for e in pygame.event.get():

            if e.type == QUIT:

                return 'quit'

            elif e.type == KEYDOWN:

                if e.key == K_UP:

                    self.current = (self.current - 1) % len(self.OPTS)

                elif e.key == K_DOWN:

                    self.current = (self.current + 1) % len(self.OPTS)

                elif e.key == K_RETURN:

                    return self.OPTS[self.current].lower()

        return 'menu'

菜单的话,大概就是长这个样子,都是咱们已经熟练掌握的东西,按上下键的时候会修改当前的选项,而后draw的时候也就判断一下颜色有些不一样的标识一下就OK了。这里的draw就是把几个项目写出来的函数。绘图部分和控制部分尽可能分开,比较清晰,也容易修改。

这里的run其实并无用到elapse参数,不过咱们仍是把它准备好了,首先能够与main一致,其次若是咱们想在开始菜单里加一些小动画什么的,也比较便于扩展。

工具函数

工具库util.py里其实没有什么特别的,都是一些便于使用的小东西,好比说在加载资源文件是,咱们但愿只给出一个文件名就能正确加载,那就须要一个返回路径的函数,就像这样:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

_ME_PATH = os.path.abspath(os.path.dirname(__file__))

DATA_PATH = os.path.normpath(os.path.join(_ME_PATH, '..', 'data'))

 

def file_path(filename=None):

    """ give a file(img, sound, font...) name, return full path name. """

    if filename is None:

        raise ValueError, 'must supply a filename'

 

    fileext = os.path.splitext(filename)[1]

    if fileext in ('.png', '.bmp', '.tga', '.jpg'):

        sub = 'image'

    elif fileext in ('.ogg', '.mp3', '.wav'):

        sub = 'sound'

    elif fileext in ('.ttf',):

        sub = 'font'

 

    file_path = os.path.join(DATA_PATH, sub, filename)

    print 'Will read', file_path

 

    if os.path.abspath(file_path):

        return file_path

    else:

        raise ValueError, "Cant open file `%s'." % file_path

这个函数能够根据给定的文件名,本身搜索相应的路径,最后返回全路径以供加载。

此次把一些周边的代码说明了一下,固然仅有这些没法构成一个能够用的俄罗斯方块,下一次咱们就要开始搭建俄罗斯方块的游戏代码了

用Python和Pygame写游戏-从入门到精通(实战二:恶搞俄罗斯方块3)

By xishui | 2011/09/18

咱们讲解了俄罗斯方块的各个宏观的部分,此次就是更细致的编程了,不过代码量实在不小,若是彻底贴出来估计会吓退不少人,因此我打算这里只贴出数据和方法名,至于方法里的代码就省略了,一切有兴趣的朋友,请参考最后放出来的源文件。

这个是main调用的Tetris类,这个类实现了咱们所看到的游戏画面,是整个俄罗斯方块游戏的核心代码。为了明晰,它还会调用shape类来实现当前的shape,下面会讲:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

class Tetris(object):

    W = 12          # board区域横向多少个格子

    H = 20          # 纵向多少个格子

    TILEW = 20      # 每一个格子的高/宽的像素数

    START = (100, 20) # board在屏幕上的位置

    SPACE = 1000    # 方块在多少毫秒内会落下(如今是level 1)

 

    def __init__(self, screen):

        pass

 

    def update(self, elapse):

        # 在游戏阶段,每次都会调用这个,用来接受输入,更新画面

        pass

 

    def move(self, u, d, l, r):

        # 控制当前方块的状态

        pass

 

    def check_line(self):

        # 判断已经落下方块的状态,而后调用kill_line

        pass

 

    def kill_line(self, filled=[]):

        # 删除填满的行,须要播放个消除动画

        pass

 

    def get_score(self, num):

        # 计算得分

       pass

 

    def add_to_board(self):

        # 将触底的方块加入到board数组中

       pass

 

    def create_board_image(self):

        # 创造出一个稳定方块的图像

        pass

 

    def next(self):

        # 产生下一个方块

        pass

 

    def draw(self):

        # 把当前状态画出来

        pass

 

    def display_info(self):

        # 显示各类信息(分数,等级等),调用下面的_display***

        pass

 

    def _display_score(self):

        pass

 

    def _display_next(self):

        pass

 

    def game_over(self):

        # 游戏结束

        pass

这里的东西基本都是和python语言自己相关的,pygame的内容并很少,因此就很少讲了。看一下__init__的内容,了解告终构和数据,整个运做也就能明白了:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

    def __init__(self, screen)

        self.stat = "game"

        self.WIDTH = self.TILEW * self.W

        self.HEIGHT = self.TILEW * self.H

        self.screen = screen

        # board数组,空则为None

        self.board = []

        for i in xrange(self.H):

            line = [ None ] * self.W

            self.board.append(line)

        # 一些须要显示的信息

        self.level = 1

        self.killed = 0

        self.score = 0

        # 多少毫秒后会落下,固然在init里确定是不变的(level老是一)

        self.time = self.SPACE * 0.8 ** (self.level - 1)

        # 这个保存自从上一次落下后经历的时间

        self.elapsed = 0

        # used for judge pressed firstly or for a  long time

        self.pressing = 0

        # 当前的shape

        self.shape = Shape(self.START,

                (self.WIDTH, self.HEIGHT), (self.W, self.H))

        # shape须要知道周围世界的事情

        self.shape.set_board(self.board)

        # 这个是“世界”的“快照”

        self.board_image = pygame.Surface((self.WIDTH, self.HEIGHT))

        # 作一些初始化的绘制

        self.screen.blit(pygame.image.load(

            util.file_path("background.jpg")).convert(), (0, 0))

        self.display_info()

注意咱们这里update方法的实现有些不一样,并非等待一个事件就马上相应。记得一开是说的左右移动的对应么?按下去天然马上移动,但若是按下了没有释放,那么方块就会持续移动,为了实现这一点,咱们须要把event.get和get_pressed混合使用,代码以下:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

    def update(self, elapse):

        for e in pygame.event.get():    # 这里是普通的

            if e.type == KEYDOWN:

                self.pressing = 1           # 一按下,记录“我按下了”,而后就移动

                self.move(e.key == K_UP, e.key == K_DOWN,

                        e.key == K_LEFT, e.key == K_RIGHT)

                if e.key == K_ESCAPE:

                    self.stat = 'menu'

            elif e.type == KEYUP and self.pressing:

                self.pressing = 0        # 若是释放,就撤销“我按下了”的状态

            elif e.type == QUIT:

                self.stat = 'quit'

        if self.pressing:         # 即便没有得到新的事件,也要根据“我是否按下”来查看

            pressed = pygame.key.get_pressed()    # 把按键状态交给move

            self.move(pressed[K_UP], pressed[K_DOWN],

                    pressed[K_LEFT], pressed[K_RIGHT])

        self.elapsed += elapse    # 这里是在指定时间后让方块自动落下

        if self.elapsed >= self.time:

            self.next()

            self.elapsed = self.elapsed - self.time

            self.draw()

        return self.stat

稍微看一下消除动画的实现,效果就是若是哪一行填满了,就在把那行删除前闪两下:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

    def kill_line(self, filled=[]):

        if len(filled) == 0:

            return

        # 动画的遮罩

        mask = pygame.Surface((self.WIDTH, self.TILEW), SRCALPHA, 32)

        for i in xrange(5):

            if i % 2 == 0:

                # 比较透明

                mask.fill((255, 255, 255, 100))

            else:

                # 比较不透明

                mask.fill((255, 255, 255, 200))

            self.screen.blit(self.board_image, self.START)

            # 覆盖在满的行上面

            for line in filled:

                self.screen.blit(mask, (

                        self.START[0],

                        self.START[1] + line * self.TILEW))

                pygame.display.update()

            pygame.time.wait(80)

        # 这里是使用删除填满的行再在顶部填空行的方式,比较简单

        # 若是清空再让方块下落填充,就有些麻烦了

        [self.board.pop(l) for l in sorted(filled, reverse=True)]

        [self.board.insert(0, [None] * self.W) for l in filled]

        self.get_score(len(filled))

这个类自己没有操纵shape的能力,第一块代码中move的部分,实际上是简单的调用了self.shape的方法。而shape则响应当前的按键,作各类动做。同时,shape还有绘制自身和下一个图像的能力。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

class Shape(object):

    # shape是画在一个矩阵上面的

    # 由于咱们有不一样的模式,因此矩阵的信息也要详细给出

    SHAPEW = 4    # 这个是矩阵的宽度

    SHAPEH = 4    # 这个是高度

    SHAPES = (

        (   ((0,0,0,0),     #

             (0,1,1,0),     #   [][]

             (0,1,1,0),     #   [][]

             (0,0,0,0),),   #

        ),

        # 还有不少图形,省略,具体请查看代码

        ),

    )

    COLORS = ((0xcc, 0x66, 0x66), # 各个shape的颜色

        )

 

    def __init__(self, board_start, (board_width, board_height), (w, h)):

        self.start = board_start

        self.W, self.H = w, h           # board的横、纵的tile数

        self.length = board_width / w   # 一个tille的长宽(正方形)

        self.x, self.y = 0, 0     # shape的起始位置

        self.index = 0          # 当前shape在SHAPES内的索引

        self.indexN = 0         # 下一个shape在SHAPES内的索引

        self.subindex = 0       # shape是在怎样的一个朝向

        self.shapes = []        # 记录当前shape可能的朝向

        self.color = ()

        self.shape = None

        # 这两个Surface用来存放当前、下一个shape的图像

        self.image = pygame.Surface(

                (self.length * self.SHAPEW, self.length * self.SHAPEH),

                SRCALPHA, 32)

        self.image_next = pygame.Surface(

                (self.length * self.SHAPEW, self.length * self.SHAPEH),

                SRCALPHA, 32)

        self.board = []         # 外界信息

        self.new()            # let's dance!

 

    def set_board(self, board):

        # 接受外界情况的数组

        pass

 

    def new(self):

        # 新产生一个方块

        # 注意这里实际上是新产生“下一个”方块,而立刻要落下的方块则

        # 从上一个“下一个”方块那里得到

        pass

 

    def rotate(self):

        # 翻转

        pass

 

    def move(self, r, c):

        # 左右下方向的移动

 

    def check_legal(self, r=0, c=0):

        # 用在上面的move判断中,“这样的移动”是否合法(如是否越界)

        # 合法才会实际的动做

        pass

 

    def at_bottom(self):

        # 是否已经不能再降低了

        pass

 

    def draw_current_shape(self):

        # 绘制当前shhape的图像

        pass

    def draw_next_shape(self):

        # 绘制下一个shape的图像

        pass

    def _draw_shape(self, surface, shape, color):

        # 上两个方法的支援方法

        # 注意这里的绘制是绘制到一个surface中方便下面的draw方法blit

        # 并非画到屏幕上

        pass

 

    def draw(self, screen):

        # 更新shape到屏幕上

        pass

框架如上所示,一个Shape类主要是有移动旋转和标识本身的能力,当用户按下按键时,Tetris会把这些按键信息传递给Shape,而后它相应以后在返回到屏幕之上。

这样的Tetris和Shape看起来有些复杂,不过想清楚了仍是能够接受的,主要是由于咱们得提供多种模式,因此分割的细一些容易继承和发展。好比说,咱们实现一种方块一落下就消失的模式,之须要这样作就能够了:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

class Tetris1(Tetris):

    """ 任何方块一落下即消失的模式,只须要覆盖check_line方法,

    不是返回一个填满的行,而是返回全部有东西的行 """

    def check_line(self):

        self.add_to_board()

        filled = []

        for i in xrange(self.H-1, -1, -1):

            line = self.board[i]

            sum = 0

            for t in line:

                sum += 1 if t else 0

            if sum != 0:    # 这里与通常的不一样

                filled.append(i)

            else:

                break

        if i == 0 and sum !=0:

            self.game_over()

        self.create_board_image() # used for killing animation

        self.kill_line(filled)

        self.create_board_image() # used for update

此次全是代码,不由让人感到索然。

游戏玩起来很开心,开发嘛,说白了就是艰难而持久的战斗(我的开发还要加上“孤独”这个因素),代码是绝对不可能缺乏的。因此你们也就宽容一点,网上找个游戏玩了几下感受不行就不要骂街了,多多鼓励:)谁来作都不容易啊!

咱们此次基本就把代码都实现了,下一次就有个完整的能够动做的东西了

用Python和Pygame写游戏-从入门到精通(实战二:恶搞俄罗斯方块4)

By xishui | 2011/09/27

恶搞俄罗斯方块的制造之旅也能够结束了,经过上三次的说明,基本就整遍了整个代码,虽然说都说了一些类名和方法名而没有涉及到具体的实现,不过实现就是排列几句代码,你们必定没问题吧:)

揉成一团

老是能够把这几回说的东西放在一块儿运行了,界面的美化啥的我彻底没有作,因此很难看,我们主要学习的东西是pygame,就不在这上面多花功夫了。

运行界面:

 

这个是第4种模式的截图,会落下莫名其妙的东西的版本…… 落下个猫先生记念“夏目友人账3”的完结。。。

各个模式说明:

  • 1: 落下的直接消失
  • 2: 落下长条
  • 3: 很是小的方块
  • 4: 上图所示,落下乱糟糟的东西(固然能够随便改)
  • 5: 暂时和6同样,发挥交给大家了:)
  • 6: 正常模式

完成度说明:

直接进去菜单是没有背景的,你很容易本身加一个……游戏过程当中空格暂停,Esc返回菜单,返回菜单时直接覆写在当前的游戏画面上。暂时懒得弄了,你们先凑合凑合。

资源说明:

图片和音效是网上随便找的,许可什么的,我什么不知道……
背景音乐史俄罗斯方块之经典“永恒俄罗斯”的音乐中的Hawker’s song,理论上应该是有版权的,不过都已经20多年了,并且我们是学习,学习~ 不要太在乎了(笑)

 

总结

若是您作过游戏,稍微看看这里面的代码必定会嗤之以鼻,看似有条不紊实际上可实在有些乱,各类界面的跳转也是很让人崩溃。无论图像仍是运动,都是用最原始的东西组织起来的,维护起来简直要命啊。

咱们须要“游戏引擎”来让咱们的游戏书写更加的漂亮。

为何一开始不提供一个引擎?若是一开始就引入引擎的概念,会让咱们对引擎的认识不深入。只有真正的用原始的代码写过一个游戏后,咱们才能意识到“引擎”的做用和必要性。对咱们面对的东西有足够深入的认识,才能让咱们更卓越!

当时光看这几篇文章的内容,是不够理解的,只要把代码通读,而后完成一个新的模式(就是那个空着的模式5),才能有足够的认识(这个代码写的不够漂亮,着重理解的是pygame,而不是整个代码设计,我可不能误人子弟啊)。

下一个游戏会更加的精彩……

相关文章
相关标签/搜索