Post

《Python基础教程》笔记 第29章 项目10:自制街机游戏

欢迎来到最后一个项目。在本章中,你将学习如何使用Pygame (https://www.pygame.org/),这个库让你能够使用Python编写功能齐全的街机游戏。Pygame虽然易于使用,功能却非常强大。它由多个组件组成,Pygame文档(https://www.pygame.org/docs/)做了详尽的介绍。

29.1 问题描述

游戏的基本设计过程与其他程序类似,但开发对象模型前,必须先设计游戏本身:游戏的角色、设定和目标等。

这里将创建的游戏是基于巨蟒剧团的著名短剧 “Self-Defense Against Fresh Fruit” 改编的。在这个短剧中,军士长John Cleese指挥士兵使用防守战术抵御入侵者使用新鲜水果(例如石榴、芒果、青梅和香蕉)发起的进攻。防守战术包括使用枪支、放老虎以及在敌人头顶扔下16吨重的秤砣。在这个游戏中,我们将反过来——玩家控制一个香蕉,躲开从天而降的秤砣,尽力在防御战中存活下来。这个游戏叫做Squish(压扁)比较合适。

这个项目的目标是围绕着游戏设计展开的。这款游戏应该像设计的那样:香蕉应该可以移动,秤砣应该从天而降。另外,与往常一样,代码应该是模块化、易于扩展的。一个重要的需求是,设计应该包含游戏状态(例如游戏简介、不同关卡和游戏结束),并且可以很容易地添加新状态。

29.2 有用的工具

这个项目需要的唯一新工具就是Pygame。最简单的方式是使用pip安装Pygame。

1
$ pip install pygame

Pygame包含多个模块,接下来的几小节将描述需要用到的模块(只讨论需要用到的具体函数或类)。

pygame

pygame模块自动导入其他所有的Pygame模块,因此如果在程序开头放置import pygame,就能访问其他模块(例如pygame.displaypygame.font)。

pygame模块包含Surface类。Surface对象就是一个指定尺寸的空图像,用于绘制和传输。传输(调用Surface对象的blit()方法)意味着将一个Surface的内容转移到另一个。(单词 “blit” 是从技术术语块传输(block transfer)的缩写BLT衍生而来的。)

init()函数是Pygame游戏的核心,必须在游戏进入主事件循环前调用。这个函数自动初始化其他所有模块(例如fontimage)。

如果要捕获Pygame特有的错误,就需要使用error类。

pygame.locals

pygame.locals模块包含事件类型、键、视频模式等的名称。可以导入这个模块的所有内容(from pygame.locals import *),但如果知道需要的名称,应该导入更具体的内容(例如from pygame.locals import FULLSCREEN)。

pygame.display

pygame.display模块包含处理Pygame显示的函数。在这个项目中,需要用到以下函数:

  • flip():更新显示。一般来说,修改当前屏幕要经过两步。首先,对get_surface()返回的Surface对象做必要的修改,然后调用flip()来更新显示以反映所做的修改。
  • update():只想更新屏幕的一部分时,使用这个函数而不是flip()
  • set_mode():设置显示的尺寸和类型(普通窗口或全屏)。
  • set_caption():设置Pygame程序的窗口标题。
  • get_surface():返回一个Surface对象(屏幕),可以在其中绘制图形,再调用pygame.display.flip()pygame.display.blit()

pygame.font

pygame.font模块包含Font类。Font对象表示不同的字体,可用于将文本渲染为可在Pygame中作为普通图形使用的图像。

pygame.sprite

pygame.sprite模块包含两个非常重要的类:SpriteGroup

Sprite类是所有可见游戏对象(在这个项目中是香蕉和秤砣)的基类。要实现自定义的游戏对象,需要继承Sprite,覆盖构造函数以设置imagerect属性(决定了外观和位置),再覆盖update()方法(在Sprite需要更新时调用)。

Group类(及其子类)的实例用作Sprite对象的容器。在简单的游戏(例如这个项目)中,只需创建一个组,并将所有的Sprite对象添加到其中。这样,当你调用Group对象的update()方法时,将自动调用所有Sprite对象的update()方法。另外,Group对象的clear()方法用于清除它包含的所有Sprite对象,而draw()方法可用于绘制所有的Sprite对象。

在这个项目中,将使用Group的子类RenderUpdates,其draw()方法返回受影响的矩形列表。可以将这个列表传递给pygame.display.update(),以只更新需要更新的部分。这可能会极大地改善游戏的性能。

pygame.mouse

在这个项目中,只使用pygame.mouse模块来做两件事:使用pygame.mouse.set_visible(False)隐藏鼠标,以及使用pygame.mouse.get_pos()获取鼠标位置。

pygame.event

pygame.event模块跟踪各种事件,例如鼠标单击、鼠标移动、按下或松开键盘等。要获取最近的事件列表,可以使用pygame.event.get()函数。

pygame.image

pygame.image模块用于处理图像,例如GIF、PNG、JPEG等其他文件格式。在这个项目中,只需要使用load()函数,它读取图像文件并创建一个包含该图像的Surface

注:官方教程提供了一个弹跳球的动画示例: https://www.pygame.org/docs/tut/PygameIntro.html ,代码如下。

弹跳球动画

29.3 准备工作

在编写游戏的第一个原型之前需要做些准备工作。首先,确保安装了Pygame。

还需要准备几幅图像(可以从 https://openclipart.org/ 或Google上搜索)。本章的游戏需要两幅图像,分别表示16吨秤砣和香蕉,如下图所示。图像的尺寸最好在100×100到200×200之间,应该使用常见的图像文件格式,例如GIF、PNG或JPEG。

16吨秤砣 香蕉

29.4 初次实现

使用Pygame这样的新工具时,应该让第一个原型尽可能简单,并专注于学习新工具的基本知识,而不是程序本身的细节,这样做通常大有裨益。因此,游戏Squish的第一个版本只是秤砣从天而降的动画。需要的步骤如下:

  1. 使用pygame.init()pygame.display.set_mode()pygame.mouse.set_visible()初始化Pygame。
  2. 加载秤砣图像。
  3. 使用这个图像创建自定义的Weight类(Sprite的子类)的实例,将这个对象添加到名为spritesRenderUpdates组中。
  4. 使用pygame.display.get_surface()获取屏幕表面,使用fill()方法将屏幕填充为白色,并调用pygame.display.flip()显示所做的修改。
  5. 使用pygame.event.get()获取所有的最近事件,并依次检查这些事件。如果发现QUIT类型的事件,或者按下Escape键(K_ESCAPE)触发的KEYDOWN类型的事件,就退出程序。(事件类型和键分别存储在事件对象的typekey属性中。QUIT等常量可以从pygame.locals模块导入。)
  6. 调用sprites组的clear()update()方法。clear()方法使用回调函数来清除所有的Sprite对象(这里是秤砣),而update()方法调用Weight实例的update()方法(后者必须自己实现,在其中更新秤砣的位置)。
  7. 调用sprites.draw(),以屏幕表面作为参数,在当前位置绘制秤砣。(每次调用update()时位置都会变化。)
  8. 调用pygame.display.update(),以sprites.draw()返回的矩形列表作为参数,只在需要的位置更新显示。(如果不在乎性能,可以使用pygame.display.flip()更新整个显示。)
  9. 重复第5~8步。

实现这些步骤的代码见代码清单29-1。

代码清单29-1 简单的“掉落秤砣”动画

注:秤砣的实际掉落速度不仅取决于speed,还与帧率有关。直接运行书中代码可能会看到秤砣飞快地掉落,因为帧率过高。可以使用pygame.time.Clock限制帧率。

可以使用下面的命令运行这个程序:

1
$ python weights.py

执行这个命令时,需要确保weights.py和weight.png都在当前目录中。下图展示了程序的屏幕截图。

简单的掉落秤砣动画

这些代码大都是不言自明的,但有几点需要解释一下:

  • 所有的Sprite对象都有imagerect属性,前者是一个Surface对象(图像),后者是一个矩形对象(使用self.image.get_rect()初始化)。绘制Sprite对象时将用到这两个属性。通过修改self.rect可以移动Sprite对象。
  • Surface对象有一个convert()方法,可用于创建使用不同颜色模型的副本。
  • 颜色是使用RGB三元组((red, green, blue),每个值的范围都是0~255)指定的,因此元组(255, 255, 255)表示白色。
  • 要修改矩形,可以给矩形的属性(topbottomleftright等)赋值,或者调用inflate()move()等方法(详见文档 https://www.pygame.org/docs/ref/rect.html )。

29.5 再次实现

在本节中,不再演示如何逐步设计和实现游戏,而在源代码中添加了大量的注释和文档字符串,如代码清单29-2~29-4所示。这里简要地解释其中的要点(以及一些不那么直观的细节):

  • 游戏由5个文件组成:config.py包含各种配置变量;objects.py包含游戏对象的实现;squish.py包含主类Game和各种游戏状态类;weight.png和banana.png是游戏使用的两个图像。
  • 矩形方法clamp()确保一个矩形位于另一个矩形内,必要时移动矩形。这用于避免香蕉移到屏幕外。
  • 矩形方法inflate()调整矩形的尺寸(水平和垂直方向的像素数量)。这用于收缩香蕉的边界,从而在判定碰撞(“压扁”)前允许香蕉和秤砣有一定的重叠。
  • 游戏本身由一个游戏对象和各种游戏状态组成。游戏对象在一个时刻只有一种状态,而状态负责处理事件并将自己显示在屏幕上。状态还能让游戏切换到另一种状态(例如,Level状态可以让游戏切换到GameOver状态)。

代码清单29-2 Squash游戏配置文件

代码清单29-3 Squash游戏对象

代码清单29-4 游戏主模块

执行squish.py文件运行游戏:

1
$ python squish.py

下面是一些游戏截图。

Squish游戏开始画面

即将被压扁的香蕉

过关画面

游戏结束画面

29.6 进一步探索

下面是一些改进这个游戏的点子:

  • 添加声音。
  • 记录得分。例如,每躲开一个秤砣得16分。可以使用文件或在线服务器保存最高分(分别使用第24章和第27章讨论的asyncore和XML-RPC)。
  • 让更多的物体同时掉落。
  • 将逻辑反过来:要求玩家尽可能接住而不是躲避掉落的物体。
  • 让玩家拥有多条命。
  • 创建游戏的独立可执行版(参见第18章)。

有关更精致(且娱乐性极高)的Pygame编程示例,参阅Pygame维护者Pete Shinners开发的游戏SolarWolf (https://www.pygame.org/shredwheat/solarwolf/index.shtml)。在Pygame网站(https://www.pygame.org/tags/all)上还能找到很多其他游戏。如果Pygame让你迷上了游戏开发,可以参阅网站 https://www.gamedev.net/https://gamedev.stackexchange.com/

This post is licensed under CC BY 4.0 by the author.