概述
1. 介绍
pygame是主要构筑在SDL库基础上的一组Python模块的集合,它使我们能够用Python语言来创建功能完整的游戏和多媒体程序。pygame是高度括平台可移植的,在任何SDL支持的平台上都可以运行(几乎可以在任何平台和操作系统上运行)。
注:SDL(Simple DirectMedia Layer)是一个跨平台的多媒体库,可以用来访问底层的音频、键盘、鼠标、摇杆、3D硬件(通过OpenGL)以及2D视频缓冲。它被用在MPEG播放软件、模拟器和很多流行的游戏中,包括获得大奖的Linux版的"Civilization: Call To Power"。
pygame是自由、免费的,以GNU LGPL 2.1协议发布,完整的协议可以在 http://www.gnu.org/copyleft/lesser.html 找到。这协议基本上就让你可以在你的任何项目中使用pygame。你可以用它来创建开源、自由、免费、共享或者商业游戏。但是如果你要增加和改变pygame本身的功能,你必须以一种LGPL兼容的协议来发布这种修改。
而examples子目录中的程序属于公共域软件。
2. 安装
pygame安装的简单方法是下载对应于你的系统的二进制pygame安装包。二进制安装包通常包括了依赖信息。从 http://www.pygame.org/download.shtml 可以找到你的系统和python版本对应的pygame安装包。
从源代码安装pygame也是相当自动的过程。主要的工作就是要安装pygame所依赖的软件包。一旦它所依赖的东西就绪,那么只要简单的运行setup.py就可以完成pygame的自动配置、编译和安装。
pygame非常强的依赖于SDL和Pygame。同时它也连接或者嵌入了几个其他的小程序库。font模块依赖于SDL_tff(它又依赖于freetype),mixer和mixer.music模块依赖于SDL_mixer,imag模块依赖于SDL_image(它又依赖于libjpeg和libpng),transform模块为了rotozoom函数而内嵌了SDL_rotozoom,surfarray模块需要python的Numeric包来提供多维数组。
在 http://www.pygame.org/install.html 有更多关于安装的信息。
3. 文档
作为起步,pygame有很多tutorial和介绍文章(在 http://www.pygame.org/wiki/tutorials 可以找到)。然后在examples目录中,有很多可以运行的小程序,可以从这些程序的代码开始动手。
而完整的pygame参考手册在 http://www.pygame.org/docs/index.html 可以找到。
如果需要帮助,可以在IRC的pygame频道找到可以提供帮助的人:irc.freenode.net 6667 #pygame
pygame维护了一个活跃的邮件列表。你可以发信到[email protected]来发信到邮件列表。发送特定内容的信到[email protected]可以实现一些特别功能,比如订阅、取消订阅和获取帮助可以分别用如下内容(写在信的内容而不是标题中)的信件实现:
- subscribe pygame-users
- unsubscribe pygame-users
- help
4. 参考资料
pygame下载 http://www.pygame.org/download.shtml
pygame入门 http://www.pygame.org/wiki/tutorials
pygame文档 http://www.pygame.org/docs/
获取pygame帮助 http://www.pygame.org/wiki/info
pygame游戏下载 http://www.pygame.org/projects/6
起步
1. 初始化
pygame的import和初始化是非常简单的。首先我们必须先导入(import)pygame这个包。
第一行是必须的,它导入了pygame包中所有可用的模块。第二行是可选的,它把一些常量和函数放在global名字空间中。
必需注意的是,有些pygame模块是可选的,比如font模块。当你import pygame时,pygame会检查font模块是否存在。如果font模块存在,那么它会自动被导入为pygame.font。如果它不存在,pygame.font会被设置成None。
在用pygame前,必须先初始化。一般只要一句话就可以完成:
1 pygame.init()
执行这个语句,它会试着去初始化所有的pygame模块。并不是所有的模块都是需要初始化的,这个语句会自动去初始化那些需要初始化的模块。你也可以手动的去初始化各个模块,比如说要单独初始化font模块可以这样做:
1 pygame.font.init()
注意,如果执行pygame.init()初始化有错误,它不会给出异常。如果手动初始化单个模块,错误会导致抛出异常。任何必须初始化的模块都包含一个get_init()函数,来判断这个模块是否已经被初始化。
可以对任何模块多次调用init()函数而不会出错。
已经初始化的模块通常会有一个quit()函数来完成清理工作。没有必要显式的调用这个函数,因为pygame会在python退出的时候自动清理所有已经初始化的模块。
2. surface和屏幕
pygame最重要的部分就是surface。我们可以把surface看作是一张白纸。你可以对surface作很多操作,比如在surface上画线、用某种颜色填充surface上的部分区域、把图片拷贝到surface上去,把图片从surface上复制下来、设置或者读取surface上某个点的颜色。一个surface可以是任何大小,一个游戏可以有任意多surface。其中有一个surface是特别的,就是用pygame.display.set_mode()创建的display surface。它代表了屏幕,对它的任何操作会出现在用户的屏幕上。一个游戏只能有一个这样的surface,这是SDL的限制。
怎样创建surface?刚才提到,用pygame.display.set_mode()可以创建特殊的display surface。此外,还可以用image.load()创建一个包含图片的surface,还可以用font.render()创建一个包含文字的surface。你甚至可以用Surface()创建一个不包含任何东西的surface。
surface的大部分方法都不重要,只要学习其中的blit(), fill(), set_at()和get_at()就够用了。
display surface的初始化操作是这样的:
1 screen = pygame.display.set_mode((1024, 768))
或者
screen = pygame.display.set_mode((1024, 768), pygame.FULLSCREEN)
你可以用set_mode把原来窗口的游戏变成全屏。其它的俄显式模式(可以用|连接)有
- DOUBLEBUF: 对于平滑的动画所必须
- OPENGL: 让你可以用PyOpenGL,但是不能用pygame的绘图函数
还有一个可选的depth参数,用来控制颜色显示的深度。一般情况下不用指定这个参数,只要用默认值就可以了。
如果使用DOUBLEBUF,你需要用flip函数来把绘制的内容显示到屏幕上。
>>> pygame.display.flip()
3. 画图
接下来,我们在屏幕上画一幅图像。我们通过最重要的画图原语BLIT(BLock Image Transfer)来实现,它可以把图像从一个地方(比如源图像)拷贝到另一个地方(比如屏幕上的某个位置)。
>>> car = pygame.image.load('car.png') >>> screen.blit(car, (50, 100)) >>> pygame.display.flip()
这时,图片car.png中的内容会显示在屏幕上,图片的左上角在屏幕上的坐标是(50, 100)。屏幕坐标的X轴从作往右的,Y轴是从上往下的。然后我们可以用如下语句来旋转图片:
>>> rotated = pygame.transform.rotate(car, 90) >>> screen.blit(car, (50, 100)) >>> pygame.display.flip()
要让屏幕上的任何东西动起来,需要一个这样的过程:画一个场景,然后把它擦掉,然后再画一幅稍微不同的场景,然后再擦掉……如此反复。比如:
注意:这里我们把整个屏幕清楚然后重新绘制不是很好的方法。最好是只把屏幕上变化的部分擦掉重画,其它部分不变。Sprite模块可以帮助我们做这个事情。
4. 定时事件
pygame有多种方法获得用户的输入事件,最常见的方法有:
wait函数会阻塞游戏的执行直到有用户事件发生。这对于游戏不是很有用,因为我们希望在等待输入的同时还要产生动画。poll函数会查看是否有事件等待处理,并返回等待处理的一个事件,如果没有事件它就返回NOEVENT。get函数和poll类似,只是它会返回所有在等待处理的事件,没有事件它会返回空列表。
这里还要提一下非常重要的时间控制,如果没有它你的游戏会在计算机上能跑多快就跑多快,这往往不是我们希望的。时间控制很容易:
tick函数会暂停一定的时间,使得上次调用tick的时间到此次调用tick的时间满1/30秒。这样有效的限制了调用tick的次数只有每秒30次。两次tick调用之间的实际时间差由tick函数返回(以毫秒为单位),在比较慢的计算机上,可能不能达到每秒30次。
非常重要的是每秒30帧的速度也决定了游戏响应用户输入的速度,因为它只在每次画图的时候检查用户的输入。以慢于每秒30次的速度检测输入会使用户觉得明显的延时。以每秒30次的帧速显示是因为30帧以上的速度对于人眼是没有用的。如果你的游戏是动作型的,那么你可能要把帧速提高一倍,这样使用户觉得他们的输入能够被更快地得到响应。
5. 结合在一起
把前面所讲的各个部分结合在一起,我们可以构成一个完整pygame程序:
1 # INTIALISATION
2 import pygame, math, sys
3 from pygame.locals import *
4 screen = pygame.display.set_mode((640, 480))
5 car = pygame.image.load('car.png')
6 clock = pygame.time.Clock()
7 k_up = k_down = k_left = k_right = 0
8 speed = direction = 0
9 position = (100, 100)
10 TURN_SPEED = 5
11 ACCELERATION = 2
12 MAX_FORWARD_SPEED = 10
13 MAX_REVERSE_SPEED = 5
14 BLACK = (0,0,0)
15 while 1:
16 # USER INPUT
17 clock.tick(30)
18 for event in pygame.event.get():
19 if not hasattr(event, 'key'): continue
20 down = event.type == KEYDOWN # key down or up?
21 if event.key == K_RIGHT: k_right = down * TURN_SPEED
22 elif event.key == K_LEFT: k_left = down * TURN_SPEED
23 elif event.key == K_UP: k_up = down * ACCELERATION
24 elif event.key == K_DOWN: k_down = down * ACCELERATION
25 elif event.key == K_ESCAPE: sys.exit(0) # quit the game
26 screen.fill(BLACK)
27 # SIMULATION
28 # .. new speed and direction based on acceleration and turn
29 speed += (k_up - k_down)
30 if speed > MAX_FORWARD_SPEED: speed = MAX_FORWARD_SPEED
31 if speed < MAX_REVERSE_SPEED: speed = MAX_REVERSE_SPEED
32 direction += (k_left - k_right)
33 # .. new position based on current position, speed and direction
34 x, y = position
35 rad = math.radians(direction)
36 x += speed*math.sin(rad)
37 y += speed*math.cos(rad)
38 position = (x, y)
39 # RENDERING
40 # .. rotate the car image for direction
41 rotated = pygame.transform.rotate(car, direction)
42 # .. position the car on screen
43 rect = rotated.get_rect()
44 rect.center = position
45 # .. render the car to screen
46 screen.blit(rotated, rect)
47 pygame.display.flip()
6. sprite
上面的代码有点混乱,我们需要更好的组织一下,以更好的适应更大的游戏开发。为此我们需要使用sprite。一个sprite就是一个图像(比如说一辆车)和有关这个图像被显示在屏幕上什么地方的信息(即位置)。这些信息分别储存在sprite的image属性和rect属性里。
sprite总是被一组一组的处理而不是单个处理,甚至一组只有一个sprite也是这样。sprite组有一个叫做draw的方法,可以把这组里面的sprite画在给定的surface上。sprite组还有一个clear方法可以把sprite从surface上清除。上面的代码可以用sprite重写为:
1 # INTIALISATION
2 import pygame, math, sys
3 from pygame.locals import *
4 screen = pygame.display.set_mode((640, 480))
5 clock = pygame.time.Clock()
6 class CarSprite(pygame.sprite.Sprite):
7 MAX_FORWARD_SPEED = 10
8 MAX_REVERSE_SPEED = 5
9 ACCELERATION = 2
10 TURN_SPEED = 5
11 def __init__(self, image, position):
12 pygame.sprite.Sprite.__init__(self)
13 self.src_image = pygame.image.load(image)
14 self.position = position
15 self.speed = self.direction = 0
16 self.k_left = self.k_right = self.k_down = self.k_up = 0
17 def update(self, deltat):
18 # SIMULATION
19 self.speed += (self.k_up - self.k_down)
20 if self.speed > self.MAX_FORWARD_SPEED:
21 self.speed = self.MAX_FORWARD_SPEED
22 if self.speed < self.MAX_REVERSE_SPEED:
23 self.speed = self.MAX_REVERSE_SPEED
24 self.direction += (self.k_left - self.k_right)
25 x, y = self.position
26 rad = math.radians(self.direction)
27 x += self.speed*math.sin(rad)
28 y += self.speed*math.cos(rad)
29 self.position = (x, y)
30 self.image = pygame.transform.rotate(self.src_image, self.direction)
31 self.rect = self.image.get_rect()
32 self.rect.center = self.position
33
34 # CREATE A CAR AND RUN
35 rect = screen.get_rect()
36 car = CarSprite('car.png', rect.center)
37 car_group = pygame.sprite.RenderPlain(car)
38 while 1:
39 # USER INPUT
40 deltat = clock.tick(30)
41 for event in pygame.event.get():
42 if not hasattr(event, 'key'): continue
43 down = event.type == KEYDOWN
44 if event.key == K_RIGHT: car.k_right = down * car.TURN_SPEED
45 elif event.key == K_LEFT: car.k_left = down * car.TURN_SPEED
46 elif event.key == K_UP: car.k_up = down * car.ACCELERATION
47 elif event.key == K_DOWN: car.k_down = down * car.ACCELERATION
48 elif event.key == K_ESCAPE: sys.exit(0)
49 # RENDERING
50 screen.fill((0,0,0))
51 car_group.update(deltat)
52 car_group.draw(screen)
53 pygame.display.flip()
这里我们并没有发现sprite带来的多大好处,sprite的真正好处是在屏幕上有很多很多图像要画的时候。sprite有一些功能可以帮助我们来检测碰撞,使得碰撞检测变得容易。Let's put some pads to drive over into the simulation:
1 class PadSprite(pygame.sprite.Sprite):
2 normal = pygame.image.load('pad_normal.png')
3 hit = pygame.image.load('pad_hit.png')
4 def __init__(self, position):
5 pygame.sprite.Sprite.__init__(self)
6 self.rect = pygame.Rect(self.normal.get_rect())
7 self.rect.center = position
8 def update(self, hit_list):
9 if self in hit_list: self.image = self.hit
10 else: self.image = self.normal
11 pads = [
12 PadSprite((100, 100)),
13 PadSprite((400, 100)),
14 PadSprite((100, 300)),
15 PadSprite((400, 300)),
16 ]
17 pad_group = pygame.sprite.RenderPlain(*pads)
现在,当进行动画的时候,在画车之前,我们检查车的sprite是否和任意一个pad的sprite相撞了,并把这个信息传给pad.update(),使得每个pad知道自己是否被撞到(hit)了:
现在,我们有一个可以由玩家控制在屏幕上移动的汽车,并且能够检测汽车是否碰到了屏幕上的其他东西。
7. 背景
接下来我们给汽车构造一个赛道。 It'd be nice if we could determine whether the car has made a "lap" of the "circuit" we've constructed. We'll keep information indicating which order the pads must be visited:
1 class PadSprite(pygame.sprite.Sprite):
2 normal = pygame.image.load('pad_normal.png')
3 hit = pygame.image.load('pad_hit.png')
4 def __init__(self, number, position):
5 pygame.sprite.Sprite.__init__(self)
6 self.number = number
7 self.rect = pygame.Rect(self.normal.get_rect())
8 self.rect.center = position
9 self.image = self.normal
10 pads = [
11 PadSprite(1, (100, 100)),
12 PadSprite(2, (400, 100)),
13 PadSprite(3, (400, 300)),
14 PadSprite(4, (100, 300)),
15 ]
16 current_pad_number = 0
17 pad_group = pygame.sprite.RenderPlain(*pads)
我们把检测碰撞代码改成这样(用来保证汽车与pad碰撞的正确的顺序):
1 pads = pygame.sprite.spritecollide(car, pad_group, False)
2 if pads:
3 pad = pads[0]
4 if pad.number == current_pad_number + 1:
5 pad.image = pad.hit
6 current_pad_number += 1
7 elif current_pad_number == 4:
8 for pad in pad_group.sprites():
9 pad.image = pad.normal
10 current_pad_number = 0
11 pad_group.draw(screen)
现在我们在画每一帧之前都把整个屏幕清空screen.fill((0,0,0))。这是相当慢的,不过这也是容易改进的。首先我们载入一张背景图片,把它画在屏幕上。
然后在循环中,在update之前,我们把汽车sprite从屏幕上清除掉,也把pad清除掉。
现在我们每画一帧只用更新屏幕的一小部分。还可以更进一步的优化改进,因为pad很少情况下才需要更新,我们不需要每一帧都清除然后重画它们,除非他们的状态确实改变了。这个优化暂时不需要,一个简单的规则就是除非真的有必要,否则不需要优化,没有必要的优化只会增加代码的复杂度。