一点声明
原文链接:http://eli.thegreenplace.net/2008/12/13/writing-a-game-in-python-with-pygame-part-i/
作者主页:http://eli.thegreenplace.net/
本文已征得原文作者同意,由Arthur1989翻译为中文,你可以任意转载本文,请注明出处,尤其是原英文出处,谢谢
这是"使用Python语言和Pygame模块编写游戏"教程的第1部分.
概述
游戏是编程中应用性最强的领域之一.即使是编写最为简单的游戏,你也不得不考虑图形,数学,物理甚至于人工智能等知识.它是一项很有趣的练习编程的方法.
如果你是Python的一名粉丝(就算你不是),而且你又对游戏的设计很感兴趣,那么对你而言Pygame将是一个很伟大的模块,它专为电子游戏设计,你绝对不容错过.它能在大部分平台上运行,并且提供了许多简洁的工具,以管理复杂图形世界中的移动和声音.
因特网上有大量的Pygame的教程,但绝大部分是相对基础的.即便是Pygame book这本书也限于介绍的层次.为了达到精通的境界,我决定自己写一个教程,希望它能为那些想要使用Pygame的人提供进一步的台阶.
本教程强烈建议你自己动手修改代码,不妨做做课后练习.这对于理解你所学到的知识很有帮助.
前言
正如我前面提到过的,本教程并不适合毫无经验的新手.如果你刚刚开始学习Pygame,你可以暂时移步到这个页面.这份教程简单介绍了Pyagme,它也是值得参考的.
这里,我假设一了解下面的知识:
>>Python(你可以不是高级开发者,但绝不能是完全的初学者)
>>基本的数学和物理知识(容器,矩形,移动法则,概率,等等).我会介绍并不常见的技巧,但是我不会教你如何向容器添加元素等.
>>熟悉Pygame.也就是,你至少看过上面提过的一个教程.
哦,还有一件事...本教程主要集中于2D游戏.3D游戏完全是一个新的层次,比起粗糙的3D模型,我更喜欢相对简单但是完整的游戏.
让我们开始吧.
在这一部分中,我们最终会实现这么一个模型--一个creeps(译注: 野生生物,下文中此词不作翻译.后面有对该词的介绍)的完全模拟器,圆形的生物在屏幕上四处爬行,时不时地碰到墙而改变方向.
尽管这还不是一个游戏,但它确实一个有用的起点,我们可以从它出发实现各色各样的想法.目前为止,我很享受于慢慢决定最终它会变成什么游戏.
代码
本教程第1部分的完整代码可以在这里下载,它包含所有需要的图片.我建议你下载它,并且运行里面的演示程序.手头有程序的代码是很有帮助的.我的测试环境是Python 2.5.2和Pygame 1.8.1,但它应该能够在其他版本上运行.
Pygame的文档
Pygame的API帮助文档写的很不错.它列出了Pyagme中的所有模块,类,常量以及函数,对于每一个你并不熟悉的类/模块,我建议你多多参考这个资源.
Creeps
好吧,首先让我们看看本教程第1部分的目标.
>>我们希望creeps可以在屏幕上四处爬行.
>>creeps的数量和形状是可以很容易的配置的.
>>creeps碰到墙之后将会真实的反弹.
>>为了让程序更加有趣,creeps的移动将是随机的.
那么,究竟creep是什么呢?
一个creep就是一张可以旋转(通过Pygame)并且移动位置的小图片.让旋转了的图片足够好看是一件充满艺术的事,这超出了我的能力,因此我限定图片的旋转角度必须是45的倍数.(意味着creep可以往东南西北和东北,西北,东南,西南这8个方向移动)
在下载链接中,creep的图片是.png格式的文件[1]
注意,所有的creep图片都有相同的方向.后面我们将会知道,这是很重要的一点.
creep是怎么移动的呢?
正如你已经毫无疑问的读过的Pygame的简单教程中提到的一样(你一定读过了吧?),物体的移动仅仅是一种视觉的幻象.在显示屏上,其实没有什么东西真的是在移动.相反的,为了让人产生物体移动的错觉,游戏仅仅是飞快地显示了一系列的图片,这些图片之间相差无几.对大众来说,每秒更新频率超过30的一组图片看起来已经足够流畅了.
为了实现屏幕的周期刷新,在游戏代码中我们使用"game loop"(循环).
游戏的循环
就像所有的GUI程序一样,每个游戏都有它自己的"主循环".在Pygame中,你可以用一个Python循环来实现它,这是很简单的.下面是我们的主循环:
# The main game loop # while True: # Limit frame speed to 50 FPS # time_passed = clock.tick(50) for event in pygame.event.get(): if event.type == pygame.QUIT: exit_game() # Redraw the background screen.fill(BG_COLOR) # Update and redraw all creeps for creep in creeps: creep.update(time_passed) creep.blitme() pygame.display.flip()
让我们看看程序,怎么样?好吧,让我们看看程序里到底发生了什么.正如我说过的,它是你Python循环的基础--不会终止,除非用户打算退出.你可以看得出来,pygame.QUIT是唯一的事件处理函数,当用户点击程序的关闭框时,它将被激活.
这个循环多久运行一次呢?这是由对clock的调用决定的.clock是pygame.time.Clock的一个对象,它在上面代码之前已被创建.对clock的调用大概是这样子的:程序休眠直到下一个1/50秒(上界).在实际中,设定游戏的刷新率为50FPS(帧每秒)是很不错的,因为一方面,我们希望游戏看起来流畅;另一方面,我们并不希望游戏消耗太多的CPU资源.你可以尝试设置不同的刷新率来看看效果.比如说,将它设置为10PFS时,演示程序看起来怎么样?而且,你可以看看练习题1和练习题3.
顺便说一下,现在,你自己在文档中查阅tick的介绍的机会来了.
真正有趣的事情接下来就要发生了.在每一次循环中,屏幕都将被背景色重绘,并且所有的creep状态都将被更新,然后显示在屏幕上.最后,显示(译注:原文是display,这是Pygame的一个重量级的概念,为清晰起见,下文中display将被翻译为"显示(display)")的重绘是通过flip来完成的(是的,现在你应该查看它的文档了).
在主循环之前发生了什么
现在,让我们看看主循环之前发生了什么吧:
# Game parameters SCREEN_WIDTH, SCREEN_HEIGHT = 400, 400 BG_COLOR = 150, 150, 80 CREEP_FILENAMES = [ 'bluecreep.png', 'pinkcreep.png', 'graycreep.png'] N_CREEPS = 20 pygame.init() screen = pygame.display.set_mode( (SCREEN_WIDTH, SCREEN_HEIGHT), 0, 32) clock = pygame.time.Clock() # Create N_CREEPS random creeps. creeps = [] for i in range(N_CREEPS): creeps.append(Creep(screen, choice(CREEP_FILENAMES), ( randint(0, SCREEN_WIDTH), randint(0, SCREEN_HEIGHT)), ( choice([-1, 1]), choice([-1, 1])), 0.1))
好吧,并没有什么魔法.前面几行代码的意思是自明的.假设你已经懂得如何初始化Pygame,如何创建一个显示(display)对象.那么,creep的创建是怎样的呢?
creeps是一串Creep对象的链表--这正是creep图片的核心和灵魂.下面是Creep类的声明,它包含了类Creep的构造函数:
class Creep(Sprite): """ A creep sprite that bounces off walls and changes its direction from time to time. """ def __init__( self, screen, img_filename, init_position, init_direction, speed): """ Create a new Creep. screen: The screen on which the creep lives (must be a pygame Surface object, such as pygame.display) img_filaneme: Image file for the creep. init_position: A vec2d or a pair specifying the initial position of the creep on the screen. init_direction: A vec2d or a pair specifying the initial direction of the creep. Must have an angle that is a multiple of 45 degres. speed: Creep speed, in pixels/millisecond (px/ms) """
构造函数的参数是组织得很好的,你可以看到,当一个Creep的对象creep被创建时,实参是如何被传递给形参的:
creeps.append(Creep(screen, choice(CREEP_FILENAMES), ( randint(0, SCREEN_WIDTH), randint(0, SCREEN_HEIGHT)), ( choice([-1, 1]), choice([-1, 1])), 0.1))
首先,我们把屏幕的图层(译注:原文是surface,为另一Pygame的概念,下文译为"图层(surface)")传递给Creep.Creep用它来计算碰墙后如何反弹(译注:即,通过图层(surface)的边界知道墙的坐标),还有,在哪里显示自己.
接下来,Creep随机地从图片列表中选出一张作为自己的图片(choice是Python的标准random库中的一个函数),而且,它被随机地设置了它在屏幕上的初始位置(randint也是random中的函数)),初始方向(后文再作讨论).((译注,这句话中的"屏幕",根据作者的意思是,客户区).速度的单位是0.1 px/ms(像素每毫秒),或者说,每秒100像素.
向量和方向
在creeps演示程序中,这一部分也许最为简单了.在游戏编程中,对向量的融会贯通很重要,因为在涉及到屏幕上图像的运动时,向量是主要的数学工具.
我们使用向量来完成两件事.一件是描述位置和速度(位移).你肯定知道的,在XY坐标轴上的一个点的位置可以用一个2维向量表示.位置和速度这两个向量的区别在于,位移是有矢量的.换句话说,在原来的位置向量上加上一个位移向量,得到的是另一个位置的向量:
这是很不错的,但还是有一点点的别扭.在数学里,正如上图所示,XY坐标系是这样子的,X轴正方向向右,Y轴正方向向上.但是,在屏幕上绘图时,事情有点不一样.在大部分的图形绘制接口中,客户区的左上角逻辑坐标为(0,0),X轴正方向向右,Y轴正方向向下.也就是说,图形绘制中,XY坐标系是这样子的:
上图很重要!它表示了我们在creep演示程序中将要使用到的8个基本的单位向量.这些也就是creep能够前进的方向了(都是45度的倍数).在阅读下文之前,请确定你已经理解了这一点.
还记得Creep构造函数中的方向参数吗?它用来设置creep的初始方向.实际上,构造函数允许接受一个pair(译注,一种二维的数据结构),它将被转为向量并且规范化.(比如说,传进(-1,-1)将得到期望的西北方向)
当creep决定改变方向或者撞到墙壁时,creep的方向将发生改变.
实现向量
很意外地,在Pygame发布的版本中,并没有带上一个"标准的"向量的实现.因此游戏开发者要么自己写一个,要么上网找一个向量的模块.
在下载链接的包中,含有一个vec2d.py文件,它是一个2维向量是实现,我是在这个Pygame wiki的页面上找到它的.该文件很漂亮地实现了2维向量的许多有用的功能.你不必读懂它的所有代码,但你可以看一看练习题4.
更新creep的信息
这个演示程序中最有趣的部分是Creep类中的Update函数.
def update(self, time_passed):
这个函数由主循环调用,它接受的参数是自从上次调用之后过去的时间(以毫秒计).通过这一点,我们就能计算creep的下一个位置.
让我们一步步的剖析update函数的代码:
# Maybe it's time to change the direction ? # self._change_direction(time_passed) # Make the creep point in the correct direction. # Since our direction vector is in screen coordinates # (i.e. right bottom is 1, 1), and rotate() rotates # counter-clockwise, the angle must be inverted to # work correctly. # self.image = pygame.transform.rotate( self.base_image, -self.direction.angle) (Arthur1989注:未完,待续.)
[Update 2009/11/11]
首先,当creep随机的改变方向时,内联函数_change_direction将被调用.在读懂update函数之后,回过头来你会觉_change_direction其实非常简单,所以我把它留在了练习题5.
update的下一操作是将creep的图像往正确的方向旋转一定的角度.还记得每个creep图片是怎么指向正确方向的吗?这对于不断正确更新creep方向而言来说相当重要.transform.rotate(读它的文档!)根据所得到的角度将给定的图层进行逆时针旋转.
现在解释一下,为什么我们传递给transform.rotate函数的角度是负角度呢?其实这正是因为上文中我介绍的Y轴翻转了的"XY坐标系".想想一下creep的基图像:(顺便提一下,它在Creep的构造函数中被加载.)
假设我们想得到的是,在我们视觉中(即,物理坐标系),creep朝东南方向的图像.如果我们将45度传递给rotate,那么我们得到的将是creep朝东北方向的图像(因为rotate函数做的是逆时针旋转).因此,为了正确的旋转creep,我们必须将旋转角度取反.
接下来,在update函数中,我们看到:
# Compute and apply the displacement to the position # vector. The displacement is a vector, having the angle # of self.direction (which is normalized to not affect # the magnitude of the displacement) # displacement = vec2d( self.direction.x * self.speed * time_passed, self.direction.y * self.speed * time_passed) self.pos += displacement
正如我说过的,self.direction是一个规范化的向量,它代表creep运行的方向.规划化是很重要的,因为我们不希望它影响位移的正确计算复杂度.位移的计算是简单的在x轴和y轴两个方向上的将速度乘以时间.
Update函数中其余的代码处理creep碰墙反弹的情形.为了让它更为智能,我打算首先介绍creep是如何绘制到屏幕上的.
位块传送
Blitting是游戏编程者间通用的传递图片(或模式)到可绘图层的行话.在Pygame中,这是通过blit函数实现的:
def blitme(self): """ Blit the creep onto the screen that was provided in the constructor. """ # The creep image is placed at self.pos. # To allow for smooth movement even when the creep rotates # and the image size changes, its placement is always # centered. # draw_pos = self.image.get_rect().move( self.pos.x - self.image_w / 2, self.pos.y - self.image_h / 2) self.screen.blit(self.image, draw_pos)
位块传送,就像Pygame中其他很多功能一样,使用功能多面的pygame.Rect类.blit函数接收的参数为:一张图片(实际上是一个图层),一个矩形,它用来指定blit调用的图层,图片将绘制到该图层上.
当然,我们必须提供creep当前所在的坐标,在该坐标上绘制creep.但是,我们还需要对坐标做一些小小的调整,为什么呢?
这是因为,当图片在Pygame中旋转时,它的面积会增大,下面是解释:
因为图片是矩形的,因此在旋转之后的图片中,Pygame必须保持原有图片的所有信息,因此,旋转后的图片面积可能会增大.这种情况只发生在旋转角度不是90度的倍数的时候,你可以做做练习题6.
所以,不管什么时候creep改变了方向,它的面积都会变大(译注:相对于上一时刻方向未变时的面积).如果不做一定的调整,在creep每次改变方向的那一刻,它的位置会发生位移,这将使得演示程序看起来不够流畅顺滑.
其实所谓的调整是很简单的:每次要绘制creep时,我们将其绘制在它的中点(重心)位置.请看代码:
draw_pos = self.image.get_rect().move( self.pos.x - self.image_w / 2, self.pos.y - self.image_h / 2) self.screen.blit(self.image, draw_pos)
上面计算所得到的正是creep图像的重心位置.即便是creep旋转时,面积变大了,它的重心还是不会改变的.
碰墙反弹
首先,请确定上面的"重心"的技巧你已经掌握了(参考练习题7).如果你确实已经掌握了,那么creep碰墙反弹是很容易的,下面是这部分功能的代码:
# When the image is rotated, its size is changed. # We must take the size into account for detecting # collisions with the walls. # self.image_w, self.image_h = self.image.get_size() bounds_rect = self.screen.get_rect().inflate( -self.image_w, -self.image_h) if self.pos.x < bounds_rect.left: self.pos.x = bounds_rect.left self.direction.x *= -1 elif self.pos.x > bounds_rect.right: self.pos.x = bounds_rect.right self.direction.x *= -1 elif self.pos.y < bounds_rect.top: self.pos.y = bounds_rect.top self.direction.y *= -1 elif self.pos.y > bounds_rect.bottom: self.pos.y = bounds_rect.bottom self.direction.y *= -1
首先,通过将与屏幕重叠的矩形转为图层计算屏幕的边界(长和宽).(为了使用重心代表creep所在位置,这是必须的.)
然后,对于四面墙,我们计算creep是不是撞上它们了,如果是的话,在理想的情况下,creep将与墙成镜面反射弹出.让我们对其中一扇墙做分析:
if self.pos.x < bounds_rect.left: self.pos.x = bounds_rect.left self.direction.x *= -1
这部分代码是计算creep与左边的墙碰撞后的方向的.creep总是从右方撞上左墙,因此,只要将creep的方向向量中X取反,Y保持不变,creep就会以正确的方向反弹了.
总结
到这里,我们已经看过creeps.py中大部分最有趣的代码了.如果你有什么不懂的地方,不妨对照着本教程中的图片再一次仔细地查看完整的代码.如果还是有什么不明白的话,请告诉我,我会很乐于帮助你的.
相同的教程/幻灯片,不同的人理解的层次不同.最基础的是读懂它,高级一点的是实践它.为了真正地掌握知识,你必须勇于挑战教程中没有解释过的那一部分.因此,我再一次建议你看看下面的练习题,想想到底该怎么做.最好你能写出答案,并且在代码上实现出来.
接下来是什么?
这个creep的演示程序对于一些游戏来说,是个不错的模板.我还没有决定接下来我要写些什么,我也还没决定究竟要把教程写到什么地步.因此,我接下来的打算将取决于读者的反馈.请随意留言或者发我邮件.
练习
1.通过修改N_CREEPS常量的值来增加creep的数量.在我的桌面电脑上,在有上百哥creep时,演示程序还是运行得很流畅.
2.修改生成creep的代码块,使得所有creep中有60%是灰色的,20%是蓝色和粉色的.
3.调用主循环中的clock.tick函数.尝试打印出相邻的两次tick间经过了多长时间,然后修改tick的参数.如果你不提供任何参数,tick函数将尽可能快地运行.计算一下,当creep数目增加时,相邻的两次tick间时间是多少.
4.打开vec2d.py文件,读懂它的所有函数和属性.
5.修改_change_direction函数,来改变creep的行为.
(1)让它们更频繁地改变方向.
(2)你能让creep每隔一段时间停止不动1秒钟吗?(提示:修改速度函数)
6.你能用基本的三角几何知识计算出在Pygame中,图片旋转45度,它的面积增大多少吗?
7重写绘制函数,删去图像的重心位置作为绘制位置的代码(直接使用未调整的图像的位置).演示程序运行起来怎么样?
我一直也想自己弄个网站弄个技术博客,然后的然后,一直在做别的事情,也实在是懒得写技术日志/(ㄒoㄒ)/~~正好现在在学pygame,就在网上看到你写的东西,让我甚是羞愧。总而言之,很感谢博主的教程,然后的然后,等我用pygame写完我的那个游戏,一定也要把那游戏的制作教程写出来,O(∩_∩)O谢谢,现在,就去看教程了,↖(^ω^)↗