Pygame起步

概述

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]可以实现一些特别功能,比如订阅、取消订阅和获取帮助可以分别用如下内容(写在信的内容而不是标题中)的信件实现:

4. 参考资料

起步

1. 初始化

pygame的import和初始化是非常简单的。首先我们必须先导入(import)pygame这个包。

   1 import pygame
   2 from pygame.locals import *

第一行是必须的,它导入了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把原来窗口的游戏变成全屏。其它的俄显式模式(可以用|连接)有

还有一个可选的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()

要让屏幕上的任何东西动起来,需要一个这样的过程:画一个场景,然后把它擦掉,然后再画一幅稍微不同的场景,然后再擦掉……如此反复。比如:

   1 for i in range(100):
   2     screen.fill((0, 0, 0))
   3     screen.blit(rotated, (i*10, 0))
   4     pygame.display.flip()

注意:这里我们把整个屏幕清楚然后重新绘制不是很好的方法。最好是只把屏幕上变化的部分擦掉重画,其它部分不变。Sprite模块可以帮助我们做这个事情。

4. 定时事件

pygame有多种方法获得用户的输入事件,最常见的方法有:

   1 pygame.event.wait()
   2 pygame.event.poll()
   3 pygame.event.get()

wait函数会阻塞游戏的执行直到有用户事件发生。这对于游戏不是很有用,因为我们希望在等待输入的同时还要产生动画。poll函数会查看是否有事件等待处理,并返回等待处理的一个事件,如果没有事件它就返回NOEVENT。get函数和poll类似,只是它会返回所有在等待处理的事件,没有事件它会返回空列表。

这里还要提一下非常重要的时间控制,如果没有它你的游戏会在计算机上能跑多快就跑多快,这往往不是我们希望的。时间控制很容易:

   1 clock = pygame.time.Clock()
   2 FRAMES_PER_SECOND = 30
   3 deltat = clock.tick(FRAMES_PER_SECOND)

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)了:

   1 collisions = pygame.sprite.spritecollide(car, pad_group, False)
   2 pad_group.update(collisions)
   3 pad_group.draw(screen)

现在,我们有一个可以由玩家控制在屏幕上移动的汽车,并且能够检测汽车是否碰到了屏幕上的其他东西。

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))。这是相当慢的,不过这也是容易改进的。首先我们载入一张背景图片,把它画在屏幕上。

   1 background = pygame.image.load('track.png')
   2 screen.blit(background, (0,0))

然后在循环中,在update之前,我们把汽车sprite从屏幕上清除掉,也把pad清除掉。

   1 pad_group.clear(screen, background)
   2 car_group.clear(screen, background)

现在我们每画一帧只用更新屏幕的一小部分。还可以更进一步的优化改进,因为pad很少情况下才需要更新,我们不需要每一帧都清除然后重画它们,除非他们的状态确实改变了。这个优化暂时不需要,一个简单的规则就是除非真的有必要,否则不需要优化,没有必要的优化只会增加代码的复杂度。

End

Pygame起步 (2010-01-29 16:48:28由s235-200编辑)