pygame

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

 

Pygame的历史

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

SDL是用C写的,不过它也可以使用C++进行开发,当然还有很多其它的语言,Pygame就是Python中使用它的一个库。Pygame已经存在很多时间了,许多优秀的程序员加入其中,把Pygame做得越来越好。

安装Pygame

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

Python

 

使用Pygame

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,第一、第二、第四的参数都是相同的,只不过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_

            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,而不是整个代码设计,我可不能误人子弟啊)。

下一个游戏会更加的精彩……

 

原文地址:https://www.cnblogs.com/kwkk978113/p/11232612.html