带薪玩游戏,是多么开心的事情,我就找到了。
前段时间,公司接到一个模拟业务场景的项目,需要在图形界面上模拟业务场景,比如人跑动,拖拽物体等,从而获取不太业务场景的模拟数据。
想了一下,不就是编个游戏吗?之前写过 骰筛子,和 模拟疫情扩散 的程序,于是果断请缨……
经过一个星期的尝试,终于用 Pygame 实现了业务场景的可视化模拟,坐等收钱。
由于商务限制,无法展示模拟程序,所以今天制作一个 打猴子 游戏,来介绍 Pygame 的一些用法,这个游戏也是我完成模拟程序的主要学习对象。
重识 Pygame
之前对 Pygame 的认识很肤浅,停留在画图工具的层面上。
通过学习和探索之后,才发现 Pygame 的强度功能,而且有很多框架和组件,帮助开发者提高效率,比如对象类 Sprite 等。
Pygame 中 Surface 是个很重要的概念,相当于 Ps(Photo shop) 的图层,图层之间可以叠加,可以相对所在图层定位,比如将背景图层相对屏幕定位后,背景图层上的其他图层可以相对于背景定位,这样就不用每次都计算一个图层想对于屏幕的具体位置了。
打猴子游戏
Chimp 是 Pygame 文档中的一个示例,帮助开发者学习和理解,我们就以这个游戏为,并且在原版基础上,做了些改动,比如加入了游戏统计信息的显示等。
最终效果是这样的:
思路
打猴子游戏,就是一只猴子在不断地跑,然后用鼠标控制一个拳头去击打它,玩的过程中会记录击中次数和击中率。
根据游戏思路,游戏中有猴子和拳头两个对象,对了提高可玩性,需要添加一些音效和动作。
实现
实现过程将分为 游戏加载、对象设置、游戏主循环三个方面做说明。
游戏资源
游戏有两种资源,对象图像和声效。
图像资源可以是图片,格式可以是比如 bmp,jpg,png 等。
我们编写一个图像加载方法:
1 | def load_image(name, colorkey=None): |
load_image
方法用来加载一个图像,参数name
为图像文件名,colorkey
为需要去除的关键颜色- 利用
Pygame.image.load
用图片文件创建为image
对象 image.convert()
的作用是对图像做适应环境的优化,以便绘制更高效- 如果
colorkey
参数没有提供的话,会将图像上第一个像素颜色作为colorkey
image.set_colorkey(colorkey, RLEACCEL)
用于设置图像的colorkey
,其中RLEACCEL
作用是对colorkey
做除去操作- 方法返回图像对象和图像的矩形区域,矩形区域在对象绘制时很重要
声效资源的加载也类似:
1 | def load_sound(name): |
pygame.mixer
是 Pygame 的声音合成器,如果电脑设备上没有声卡(声音处理组件),将无法播放声音- 定义了一个
NoneSound
类,实现了空的 play 方法,以便在没有声卡的设备上正常运行 - 最好返回声音资源
通过 load_image
和 load_sound
两个方法,就完成了游戏中资源的加载。
对象设置
现在可以定义游戏对象了,需要处理样式,形态,状态等,为了对象定义动作行为等。
如果光靠手工绘制,是很麻烦的,Pygame 提供了对象类 Sprite,只需要初始化数据,实现状态更新就可以了。
游戏需要两种对象:拳头和猴子。
拳头,需要在屏幕上显示一个拳头,并且需要跟着鼠标移动,当做打击动作时,需要变化形态,具体的代码如下:
1 | class Fist(pygame.sprite.Sprite): |
- 在初始化方法
__init__
里,用load_image
加载了拳头图像,获得了image
和rect
,这两个属性是Sprite
对象必须的 update
是必须实现的Sprite
方法,用来更新对象的状态:首先获得鼠标的位置坐标,并将位置赋予对象区域,即将对象区域的中上位置移动到鼠标位置,从而实现对象位置的变化- 然后判读对象是否处于击打状态,如果是则将对象的相对位置,向右移动 5 个像素,向下移动 10 个像素,表示在向下挥舞拳头
punch
是自定义方法,作为外边调用的接口。调用时会设置对象状态,并返回是否击中。具体是:如果击打的话,且当前不是在击打状态时,设置状态为击打中,并将对象的区域缩小 5 个像素,即打击下去的话拳头远去,区域会缩小。colliderect
方法用于做碰撞检测,即与目标的区域做判断,是否碰撞到一起unpunch
也是自定方法,结束击打之后,将击打状态设置为否
仔细阅读代码就会发现,Sprite
让一个业务复杂的对象很简单地实现了,而且不用关注如何绘制。
同样的方式定义猴子对象,猴子对象需要自动移动,如果被打击到,会做旋转动作,具体代码如下:
1 | class Chimp(pygame.sprite.Sprite): |
__init__
中,area
获取了屏幕的区域,以便判断猴子移动范围;move 为移动速度,即每帧移动多数像素,值越大速度越快update
方法中,根据对象状态调用对于状态的行为_walk
方法为走动行为,首先将对象区域移动到新位置,获得新位置范围newpos
,然后判断新位置是否在屏幕区域area
中,没有的话,将移动距离设置为相反数,向相反的方向移动;pygame.transform.flip
的作用时让图片水平翻转,好像猴子掉了个头一样_spin
方法为转圈行为,当被打中之后,做360度旋转,dizzy
为旋转角度,每帧旋转 12 度,rotate
方法用于图像旋转,当旋转角度超出 360 度后,回复图像样式。punched
方法在被击中时调用,只做状态设置,其中original
是将旋转前的图像暂存起来,用于旋转结束后的图像恢复
请注意 图像旋转方法
rotate
,虽然能产生旋转效果,但是却不能指定旋转中心点,其旋转中心点为将图像刚好放在一个正方形的左侧时,正方形的中心点作为旋转中心点。
如果要实现围绕特定中心点旋转,就需要在旋转之后,将图像移动到这个点。
这就是在_spin
方法中,self.rect = self.image.get_rect(center=center)
这句代码的作用——让图像围绕图像中心点旋转。
到这里,我们的游戏对象就设置完成了。
理解主循环
Pygame 中有个重要的概念就是主循环。
一般来说,会认为对象会自动发生移动和操作。
其实各种动作的游戏场景是由每帧图像合并而成的,就好比拍一个 pose,拍一张照,如此反复,最后将图片快速的切换,可以看到动画效果一样。
理解了这个,就能理解为什么电脑游戏的性能常常用更多的帧数来衡量的,更多的帧,意味着需要每秒绘制更多的图像。
Pygame 就是这么做的,通过一个主循环,不断地绘制对象的行为,从而产生动画效果。
主循环实际上就是个死循环,通过 pygame.time.Clock
控制游戏帧数。
来看看具体代码:
1 | def main(): |
main
方法为游戏的主方法,会在游戏启动时调用- 在
while
之前的代码主要做的是初始化和定义,需要注意字体的设置,如果需要显示汉字的话,需要加载中文字体,可以参考这篇文章 while
里面是个游戏的主循环,首先设置游戏帧数为 60 帧;然后进入事件循环,游戏过程中的事件都会存储在 Pygame 的event
中,通过get
方获取,对我们关心的事件进行处理,比如游戏退出事件,鼠标点击事件等- 在鼠标点击中,判断是否打中猴子,如果打中,播放打中音效,并且调用猴子被击中的方法
punched
;如果没有打中,播放挥舞空拳的音效。如果捕获到鼠标键抬起事件,则调用停止打击方法。 - 事件循环之后,进行画面绘制,首先是拷贝一个起始背景,因为在每次绘制都会污染背景。然后绘制游戏信息,将信息绘制在背景上。
allsprites.update()
会调用对象组里的每个对象的update
方法,完成对象的状态更新allsprites.draw(screen)
将对象图像绘制到窗口中pygame.display.flip()
刷新屏幕,我们就会看到新的游戏画面了
至此游戏的主要编码工作就完成了,不到 200 行代码,做了一个可玩的小游戏,赶紧运行起来试试吧。
总结
这个简单的游戏制作过程,让我更深入地理解了 Pygame 的特点和使用方法,建立起来游戏开发的基本认知。
总体来看,要学会一个新东西,需要正确的理解它的核心概念,与自己原来的知识体系相融合,就能很快习得,而能灵活应用的前提是多玩多练。
通过一个星期的探索实践,为客户解决了模拟业务场景的问题,坐等加薪。
期望通过这个练习也能帮助你扩展一项技能,开拓一条道路,比心。
示例代码:https://github.com/JustDoPython/python-examples/tree/master/taiyangxue/pygame