使用Pygame扩展,编写功能齐备的全屏街机游戏。参考《python基础教程》
问题描述
如何编写计算机游戏呢?游戏的基本设计过程与其他程序类似,但开发对象模型前, 必须先设计游戏本身,如游戏包含的角色、所处的环境以及要实现的目标。
这里将创建的游戏是从巨蟒剧团推出的著名短剧“Self-Defense Against Fresh Fruit”改编而 来的。在这个短剧中,军士长John Cleese指挥士兵使用防守战术抵御入侵者使用新鲜水果(如石 榴、糖水芒果、青梅和香蕉)发起的进攻。防守战术包括使用枪支、放老虎以及在敌人头顶扔下 重达16吨的铅锤。在这个游戏中,我们将反过来,让玩家控制一支香蕉。这支香蕉要躲开从天而 降的16吨铅锤,尽力在防御战中活下来。我想将这个游戏命名为Squish比较合适。
这个项目的目标是围绕着游戏设计展开的。这款游戏必须像设计的那样:香蕉能够移动,16 吨的铅锤从天而降。另外,与往常一样,代码必须是模块化的,且易于扩展。一个重要的需求是, 设计应包含一些游戏状态(如游戏简介、关卡和“游戏结束”状态),同时可轻松地添加新状态。
有用的工具
pygame
模块pygame自动导入其他所有的Pygame模块。如pygame.display和pygame.font。
模块pygame包含函数Surface,它返回一个新的Surface对象。Surface对象其实就是一个指定尺寸的空图像,可用来绘画和传送。传送(调用Surface对象的方法blit)意味着在Surface之间 传输内容。[传送的英文单词blit是从技术术语块传输(block transfer)的简写BLT衍生而来的。]
函数init是Pygame游戏的核心,必须在游戏进入主事件循环前调用。这个函数自动初始化其他所有模块(如font和image)。
如果要捕获Pygame特有的错误,就需要使用error类。
1 | import pygame, sys |
pygame.locals
模块pygame.locals包含你可能在自定义模块的作用域内使用的名称(变量),如事件类型、 键、视频模式等的名称。可导入这个模块的所有内容(from pygame.locals import *),但如果知道需要哪些名称,应该做更具体的导入,如from pygame.locals import FULLSCREEN。
pygame.display
模块pygame.display包含处理内容显示的函数,这些内容可显示在普通窗口中,也可占据整个屏幕。在这个项目中,需要用到如下函数。
- flip:更新显示。一般而言,分两步来修改当前屏幕。首先,对函数get_surface返回 的Surface对象做必要的修改,然后调用pygame.display.flip来更新显示,反映出所做 的修改。
- update:只想更新屏幕的一部分时,使用这个函数,而不是flip。调用这个函数时,可只 提供一个参数,即RenderUpdates类的方法draw返回的矩形列表。
- set_mode:设置显示的尺寸和类型。显示模式有多种,但这里只使用全屏模式和默认模式 “在窗口中显示”。
- set_caption:设置Pygame程序的标题。函数set_caption主要用于游戏在窗口中运行(而不是以全屏模式运行)时,因为标题将用作窗口的标题。
- get_surface:返回一个Surface对象,你可在其中绘制图形,再调用pygame.display.flip 或pygame.display.blit。这个项目只使用了Surface对象的一个方法来绘画,这就是blit, 它将一个Surface对象中的图形传输到另一个Surface对象的指定位置。另外,还将使用 Group对象的方法draw在Surface上绘制Sprite对象。
pygame.font
模块pygame.font包含函数Font。字体对象用于表示不同的字体,可用于将文本渲染为可在 Pygame中作为普通图形使用的图像。
pygame.sprite (小精灵)
模块pygame.sprite包含两个非常重要的类:Sprite和Group。
Sprite类是所有可见游戏对象(在这个项目中,是香蕉和重16吨的铅锤)的基类。要实现自 定义的游戏对象,可从Sprite派生出子类,并重写构造函数以设置其属性image和rect(这些属性 决定了Sprite的外观和位置),同时重写在Sprite可能需要更新时调用的方法update。
Group及其子类的实例用作Sprite对象的容器。一般而言,使用Group是个不错的主意。在简 单的游戏(如本章的项目)中,只需创建一个名为sprites或allsprites之类的Group,并将所有 Sprite都添加到其中。这样,当你调用Group对象的方法update时,将自动调用所有Sprite对象的方法update。另外,Group对象的方法clear用于清除它包含的所有Sprite对象(实际的清理工作是使用一个回调函数完成的),而方法draw可用于绘制所有的Sprite对象。
在这个项目中,将使用Group的子类RenderUpdates,其方法draw返回列表,其中包含所有受 到影响的矩形。可将这个列表传递给pygame.display.update,以只更新需要更新的部分。通过这 样做,有可能极大地改善游戏的性能。
pygame.mouse
在即将开发的游戏Squish中,只使用模块pygame.mouse
来做两件事情:隐藏鼠标以及获取鼠标的位置。这两件事分别是使用pygame.mouse.set_visible(False)
和pygame.mouse.get_pos()
来完成的。
pygame.event
模块pygame.event
跟踪各种事件,如鼠标单击、鼠标移动、按下或松开键等。要获取最近发生的事件列表,可使用函数pygame.event.get
。
如果只需要状态信息,如pygame.mouse.get_pos
返回的鼠标位置,就无需使用pygame.event.get
。 然而,你需要确保Pygame
同步地更新,为此可定期调用函数pygame.event.pump
。
pygame.image
模块pygame.image用于处理图像,如以GIF、PNG、JPEG和其他几种文件格式存储的图像。 在这个项目中,只需要这个模块中的函数load,它读取图像文件并创建一个包含该图像的Surface 对象。
pygame.Rect
Rect是用于存储矩形坐标的pygame对象。
rect对象有一些虚拟属性,比如top.left,bottom.right这些是用来固定矩形的位置的,
还有size,width,height,这些是描述矩形大小,宽高分别是多大。
center为矩形的中心点,其实就是关于横纵坐标的二元组,因此又有centerx,centery两个属性。此外,还有x,y。
初次实现(铅锤下降)
使用诸如Pygame等新工具开发程序时,应让第一个原型尽可能简单,并将重点放在学习新 工具的基本知识,而不是程序本身的细节上。这样做通常大有裨益。因此,在游戏Squish的第一 个版本中,我们只创建重16吨的铅锤从天而降的动画。制作这个动画需要的步骤如下。
步骤如下:
(1) 使用pygame.init
、pygame.display.set_mode
和pygame.mouse.set_visible
初始化Pygame
。 使用pygame.display.get_surface
获取屏幕表面,使用方法fill以白色填充屏幕表面,再调用 pygame.display.flip
显示所做的修改。
1 | white = 255,255,255 |
(2) 加载铅锤图像。
1 | plumb = pygame.image.load('plumb.png') |
(3) 使用这幅图像创建自定义类Weight(Sprite的子类)的一个实例。将这个对象添加到Render Updates编组sprites中。(处理多个Sprite对象时,这样做很有帮助。)
1 | sprites = pygame.sprite.RenderUpdates() |
(4) 使用pygame.event.get获取最近发生的所有事件,并依次检查这些事件。如果发现事件QUIT 或因按下Escape键(K_ESCAPE)而触发的KEYDOWN事件,就退出程序。事件类型和键分别存储在事件对象的属性type和key中。诸如QUIT、KEYDOWN和K_ESCAPE等常量可从模块pygame.locals导入。)
1 | while True: |
(5) 调用编组sprites的方法clear和update。方法clear使用回调函数来清除所有的Sprite对象 (这里是铅锤),而方法update调用Weight实例的方法update(你必须在Weight类中实现方法update)
1 | def update(self, *args): |
(6) 调用sprites.draw并将屏幕表面作为参数,以便在当前位置绘制铅锤(每次调用Weight实例 的update方法后,位置都将发生变化)。
1 | updates = sprites.draw(screen) |
(7) 调用pygame.display.update,并将sprites.draw返回的矩形列表作为参数,只更新需要更新的部分。(如果你不在乎性能,可使用pygame.display.flip来更新整个屏幕。)
1 | pygame.display.update(updates) |
(8) 重复4-7步
完整代码如下:
1 | import pygame |
补充:
- 所有的Sprite对象都有属性image和rect,其中前者应是一个Surface对象(图像),而后者应是一个矩形对象(只需使用self.image.get_rect()初始化它即可)。绘制Sprite对象时,将用到这两个属性。通过修改self.rect,可移动Sprite对象。
- Surface对象包含方法convert,可用于创建使用不同颜色模式的副本。你无需关心细节, 只需在调用convert时不提供任何参数即可。这将根据当前显示量身定制一个Surface对 象,从而最大限度地提高其显示速度。
- 颜色是使用RGB元组(红绿蓝,每个值的取值范围都是0~255)指定的,因此元素(255, 255, 255)表示白色。
- 要修改矩形(如这里的self.rect),可设置其属性(top、bottom、left、right、topleft、 topright、bottomleft、bottomright、size、width、height、center、centerx、centery、midleft、 midright、midtop和midbottom),也可调用诸如inflate、move等方法。
游戏实现
结果预览
这是游戏界面
分析
下图是整个游戏的uml用例图。
状态类:(State)
暂停状态(Paused)
StartUp(显示启动图像和欢迎消息的暂停状态,后面是Info状态);Info(显示游戏信息的暂停状态,后面紧跟第一关) ;LevelCleard(已过关的暂停状态);GameOver(游戏已结束的状态,后面是第一关的Level状态)
进行状态:
Level(游戏关卡)
Game(主事件循环)
下图是objects.py的图。
总共四个类,Sprite是pygame.sprite模块下的类,我们自定义三个类,其中,SquishSprite是Banana(香蕉)和Weight(铅锤)的父类,它加载香蕉和铅锤的图像,定义了它们的外接矩阵和活动范围。
config.py
1 | # 游戏Squish的配置文件 |
objects.py
1 | import pygame,config,os |
squish.py
1 | import os,sys,pygame |
出现的问题
只显示铅锤,香蕉并没有显示
是因为,精灵的活动范围设置的超过了屏幕的尺寸,而且,香蕉是在底部,就造成它没有显示的现象。
1 | self.area = screen.get_rect().inflate(shrink, shrink) # 缩小rect对象的属性。 |
pygame中文乱码问题解决
1 | # 打印系统支持的字体 |