Python-任务指南-全-
Python 任务指南(全)
原文:
zh.annas-archive.org/md5/776fffc55af3496b6b163cc05128df68译者:飞龙
序言

空气正在耗尽。太空站出现了泄漏,所以你必须迅速行动。你能找到安全的出路吗?你需要在太空站中导航,找到门的通行卡,并修理你受损的太空服。冒险已经开始!
一切从这里开始:地球,任务指挥中心,也就是你的计算机。本书将向你展示如何使用 Python 构建一个火星太空站,探索该太空站,并在冒险游戏中逃脱危险,游戏包括图形内容。你能像宇航员一样思考,成功找到安全的出路吗?
如何使用本书
通过本书中的指引,你可以构建一个名为逃脱的游戏,游戏中有地图可以探索和谜题可以解答。它使用了 Python 语言,这是一种易于阅读的流行编程语言。它还使用了 Pygame Zero,它提供了一些管理图像和声音等的指令。一步步,我将向你展示如何制作游戏以及代码的主要部分如何工作,这样你就可以根据我的游戏代码定制或创建自己的游戏。你还可以下载所需的所有代码。如果你卡住了,或者只是想直接开始玩游戏并查看它是如何工作的,你也可以这样做。所有你需要的软件都是免费的,我还提供了 Windows PC 和 Raspberry Pi 的安装指南。我建议你使用 Raspberry Pi 3 或 Raspberry Pi 2。Pi Zero、原始的 Model B+ 和旧型号可能运行游戏太慢,无法享受游戏体验。
你可以通过几种不同的方式使用本书和游戏:
-
下载游戏,先玩它,然后通过本书了解它是如何工作的。 这样,你可以避免在玩游戏之前看到书中的任何剧透!虽然我尽量将剧透最小化,但你可能会在阅读代码时注意到一些线索。如果你在游戏中遇到困难,可以尝试阅读代码来找到解决方案。无论如何,我建议你至少运行一次游戏,看看你将要构建的内容,并学习如何运行你的程序。
-
构建游戏,然后玩它。 本书将引导你从头到尾地创建游戏。当你逐章进行时,你会为游戏添加新部分并看到它们的运行效果。如果在任何阶段你无法让代码正常工作,你可以使用我提供的代码版本并从那里继续构建。如果选择这种方法,直到你完成构建、玩游戏并完成游戏前,不要对游戏做任何自定义修改。否则,你可能会不小心让游戏变得无法完成。(你可以按照练习中的建议进行修改。)
-
自定义游戏。 当你理解了程序的工作原理后,你可以通过使用自己的地图、图形、物品和谜题来进行修改。逃脱 游戏设定在太空站中,但你的游戏可以设定在丛林中、海底,甚至几乎任何地方。你可以先使用这本书来构建自己的 逃脱 版本,或者使用我提供的最终版本进行自定义。我很期待看到你使用这个程序作为起点所制作的内容!你可以通过 Twitter 联系我,用户名是 @musicandwords,或者访问我的网站 www.sean.co.uk。
这本书有什么内容?
这是你开始任务之前的简要介绍。
-
第一章 向你展示了如何进行太空行走。你将学习如何使用 Pygame Zero 在 Python 程序中使用图形,并发现一些制作 Python 程序的基础知识。
-
第二章 介绍了 列表,它们存储了 逃脱 游戏中的大量信息。你将学习如何使用列表来制作地图。
-
第三章 向你展示了如何让程序的某些部分重复执行,以及如何利用这些知识显示地图。你还将设计太空站的房间布局,使用墙柱和地板砖。
-
在 第四章 中,你将开始构建 逃脱 游戏,打下太空站的蓝图。你将看到程序如何理解太空站的布局,并利用它来创建房间的框架,放置墙壁和地板。
-
在 第五章 中,你将学习如何在 Python 中使用 字典,这是一种存储信息的重要方式。你将为游戏使用的所有对象添加信息,并且你将看到如何创建自己房间设计的预览。当你在 第六章 中扩展程序时,你将看到所有场景都已就位,并能够查看所有房间。
-
在建造完太空站后,你可以搬进去。在 第七章 中,你将添加你的宇航员角色,并学习如何在房间中移动并进行动画效果。
-
第八章 向你展示了如何通过阴影、渐变墙壁和一个新功能来修复剩余的图形故障,从而完善游戏的图形。
-
当太空站投入使用时,你可以拆开个人物品。在 第九章 中,你将放置玩家可以检查、捡起和丢弃的物品。在 第十章 中,你将看到如何使用和组合物品,从而在游戏中解决谜题。
-
太空站快要建成了。第十一章 增加了安全门,用于限制对某些区域的访问。当你放松身心,庆祝工作圆满完成时,危险也悄然逼近,因为你将在 第十二章 中添加动态危险。
在你完成本书的学习时,你将完成一些训练任务,借此机会测试你的程序和编码技能。如果你需要答案,它们会在每章的末尾提供。
本书末尾的附录也会对你有所帮助。附录 A 包含了整个游戏的代码清单。如果你不确定该将一段新代码放在哪里,可以在此查找。附录 B 包含了最重要的变量、列表和字典的表格,如果你记不住某些内容存储在哪里,可以查阅它;附录 C 提供了一些调试技巧,帮助你解决程序无法运行的问题。
欲获取更多信息和本书的支持资源,请访问本书网站 www.sean.co.uk/books/mission-python/。你也可以在 nostarch.com/missionpython/ 上找到相关信息和资源。
安装软件
该游戏使用 Python 编程语言和 Pygame Zero,这是一个简化图形和声音处理的软件。在开始之前,你需要先安装这两个软件。
注意
欲查看更新的安装说明,请访问本书的网页 nostarch.com/missionpython/。
在 Raspberry Pi 上安装软件
如果你使用的是 Raspberry Pi,Python 和 Pygame Zero 已经预安装好了。你可以直接跳到第 7 页中的“下载游戏文件”部分。
在 Windows 上安装 Python
在 Windows PC 上安装软件,请按照以下步骤操作:
-
打开浏览器,访问
www.python.org/downloads/。 -
在撰写本文时,Python 3.7 是最新版本,但 Pygame 目前尚无法在此版本上轻松安装。我建议你改用 Python 3.6 的最新版本(撰写时为 3.6.6)。你可以在下载页面下方找到旧版本的 Python(参见图 1)。将文件保存到桌面或其他便于查找的位置。(Pygame Zero 仅支持 Python 3,因此如果你通常使用 Python 2,你需要切换到 Python 3 来进行本书的学习。)
![image]()
图 1:Python 下载页面
-
文件下载完成后,双击运行它。
-
在弹出的窗口中,选择勾选框以将 Python 3.6 添加到 PATH 中(参见图 2)。
-
点击 立即安装。
![image]()
图 2:Python 安装程序
-
如果系统提示你是否允许该应用程序对设备进行更改,点击 是。
-
Python 安装需要几分钟时间。安装完成后,点击 关闭 以完成安装。
在 Windows 上安装 Pygame Zero
现在你已经在电脑上安装了 Python,可以继续安装 Pygame Zero。请按照以下步骤操作:
-
按住Windows 开始键并按R。运行窗口应会打开(见图 3)。
-
输入 cmd(见图 3)。按 ENTER 或点击确定。
![image]()
图 3:Windows 运行对话框
-
命令行窗口应会打开,如图 4 所示。在这里,你可以输入用于管理文件或启动程序的指令。输入 pip install pgzero 并在行尾按下 ENTER。
![image]()
图 4:命令行窗口
-
Pygame Zero 应该开始安装。它会花费一些时间,你会在
>提示符再次出现时知道它已经完成。 -
如果你收到错误消息说 pip 无法识别,请尝试重新安装 Python。你可以通过重新运行安装程序或使用 Windows 控制面板卸载 Python。确保在安装 Python 时选中 PATH 的框(见图 2)。重新安装 Python 后,尝试再次安装 Pygame Zero。
-
当 Pygame Zero 下载完成并且你可以再次输入时,输入以下内容:
echo print("Hello!") > test.py -
这行代码创建了一个名为test.py的新文件,文件中包含指令
print("Hello!")。我将在第一章中解释print()指令,但目前这只是一个快速创建测试文件的方式。输入括号(圆括号)和引号时要小心:如果漏掉一个,文件将无法正常工作。 -
输入以下内容以打开测试文件:
pgzrun test.py -
稍等片刻,一个空白窗口应该会打开,标题为Pygame Zero Game。再次点击命令行窗口将其带到前面:你应该能看到文本
Hello!。按 CTRL-C 停止命令行窗口中的程序。 -
如果你想删除你的测试程序,输入 del test.py。
在其他机器上安装软件
Python 和 Pygame Zero 也可以用于其他计算机系统。Pygame Zero 部分是为了使游戏能够跨不同计算机运行,因此Escape代码应该在任何运行 Pygame Zero 的地方都能运行。本书仅为 Windows 和 Raspberry Pi 计算机用户提供指导。但如果你使用其他计算机,可以在www.python.org/downloads/下载 Python,并在pygame-zero.readthedocs.io/en/latest/installation.html找到安装 Pygame Zero 的建议。
下载游戏文件
我已经提供了所有你需要的程序文件、声音和图像,用于Escape游戏。你也可以下载本书中的所有列表,所以如果你无法让其中某个工作,你可以使用我的文件。所有本书的内容会作为一个名为escape.zip的 ZIP 文件下载。
在树莓派上下载并解压文件
在 Raspberry Pi 上下载游戏文件,请按照以下步骤操作,并参考图 5。图 5 中的数字会告诉你每个步骤应该在哪里进行。
➊ 打开网页浏览器,访问nostarch.com/missionpython/。点击链接下载文件。
➋ 从桌面上,点击屏幕顶部任务栏中的文件管理器图标。
➌ 双击打开“下载”文件夹。
➍ 双击escape.zip文件。
➎ 点击提取文件按钮,打开提取文件对话框。
➏ 更改将要提取的文件夹路径,使其显示/home/pi/escape。
➐ 确保选中了“提取带完整路径的文件”选项。
➑ 点击提取。

图 5:你应该采取的解压文件步骤
在 Windows PC 上解压文件
在 Windows PC 上解压文件,请按照以下步骤操作。
-
打开你的网页浏览器,访问
nostarch.com/missionpython/。点击链接下载文件。将 ZIP 文件保存在桌面、Documents文件夹中,或其他你能轻松找到的地方。 -
根据你使用的浏览器,ZIP 文件可能会自动打开,或者可能在屏幕底部有一个选项可以打开它。如果没有,按住Windows 开始键并按E键,Windows 资源管理器窗口应该会打开。前往你保存 ZIP 文件的文件夹。双击该 ZIP 文件。
-
点击窗口顶部的提取全部。
-
我建议你在Documents文件夹中创建一个名为escape的文件夹,并将文件解压到那里。我的文档文件夹是C:\Users\Sean\Documents,所以我只需在文件夹名称的末尾输入\escape,就可以在该文件夹中创建一个新文件夹(参见图 6)。如果需要,你可以使用浏览按钮先进入Documents文件夹。
-
点击提取。
![image]()
图 6:设置解压游戏文件的文件夹
ZIP 文件中包含的内容
你刚刚下载的 ZIP 文件包含三个文件夹和一个 Python 程序,escape.py(参见图 7)。这个 Python 程序是Escape游戏的最终版本,所以你可以立即开始玩。images文件夹包含了游戏以及本书其他项目所需的所有图片。sounds文件夹包含了音效。
在listings文件夹中,你将找到本书中的所有编号列表。如果你无法使某个程序正常运行,可以尝试从这个文件夹复制我的版本。你需要先从listings文件夹中复制它,然后粘贴到现在escape.py程序所在的escape文件夹中。这样做的原因是程序需要与images和sounds文件夹一起放在同一个位置才能正常工作。

图 7:ZIP 文件的内容,在 Windows 中可能呈现的样子
运行游戏
当你下载 Python 时,另一个名为 IDLE 的程序也会与其一起下载。IDLE 是一个集成开发环境(IDE),是你用来编写 Python 程序的软件。你可以按照提供的说明,通过 IDLE Python 编辑器运行本书中的某些清单。然而,大多数程序使用 Pygame Zero,你必须从命令行运行这些程序。按照这里的说明来运行 Escape 游戏和其他任何 Pygame Zero 程序。
在树莓派上运行 Pygame Zero 程序
如果你使用的是树莓派,请按照以下步骤运行 Escape 游戏:
-
使用文件管理器,进入 pi 文件夹中的 escape 文件夹。
-
点击菜单中的 工具,选择 在终端中打开当前文件夹,或者按 F4 键。命令行窗口(也称为 shell)应该会打开,如图 8 所示。你可以在这里输入指令来管理文件或启动程序。
![image]()
图 8:树莓派中的命令行窗口
-
输入以下命令并按下回车键。游戏开始!
pgzrun escape.py
这是在树莓派上运行 Pygame Zero 程序的方式。要再次运行相同的程序,请重复上一步。要运行保存在同一文件夹中的不同程序,重复上一步,但在 pgzrun 后更改文件名。要在不同的文件夹中运行 Pygame Zero 程序,请从第 1 步开始,打开包含你要运行程序的文件夹的命令行。
在 Windows 上运行 Pygame Zero 程序
如果你使用的是 Windows,请按照以下步骤运行程序:
-
进入你的 escape 文件夹。(按住 Windows 启动键,然后按 E 键重新打开文件资源管理器。)
-
点击文件上方的长条,如图 9 所示。输入 cmd 并按回车键。
![image]()
图 9:查找 Pygame 文件的路径
-
命令行窗口将打开。你的名为 escape 的文件夹将出现在最后一行的
>之前,如图 10 所示。![image]()
图 10:Windows 中的命令行窗口
-
在命令行窗口中输入 pgzrun escape.py,按下回车键,Escape 游戏开始。
这是在 Windows 计算机上运行 Pygame Zero 程序的方式。你可以通过重复上一步来再次运行该程序。要运行保存在同一文件夹中的不同程序,重复上一步,但在 pgzrun 后更改文件名。要在不同的文件夹中运行 Pygame Zero 程序,请从第 1 步开始,打开包含你要运行程序的文件夹的命令行。
玩游戏
你正在火星的空间站上独自工作,离家数百万公里远。其他队员正在执行一项长途任务,探索峡谷中的生命迹象,他们几天内不会回来。生命支持系统的低沉嗡嗡声环绕在你四周。
当警报声响起时,你被吓了一跳!空间站的墙壁出现了裂缝,你的空气正在慢慢泄漏到火星的大气层中。你迅速但小心地穿上宇航服,但电脑提示宇航服损坏。你的生命处于危险之中。
你的首要任务是修复你的宇航服,确保可靠的供氧系统。第二任务是通过无线电求救,但空间站的无线电系统出现故障。昨晚,由地球发射的“贵宾犬”号登陆器在火星尘土中迫降。如果你能找到它,或许可以使用它的无线电发出求救信号。
使用箭头键在空间站内移动。要检查一个物体,站在上面并按下空格键。或者,如果物体是你不能走上去的,按下空格键并走向它。
要捡起物品,走到物品上并按下 G 键(代表获取)。
要选择你的物品栏中的物品,物品栏显示在屏幕的顶部(见图 11),按 TAB 键在物品间切换。要丢弃选中的物品,按 D 键。

图 11:你的冒险开始了!
要使用物品,可以在物品栏中选择它,或者走到物品上或进入物品,并按下 U 键。当你携带一个物品并站在另一个物品上,或携带一个物品并走进另一个物品时,你可以将物品组合或一起使用。
你需要创造性地利用有限的资源,解决障碍并找到安全的地方。祝你好运!
第一章:你的第一次太空行走

欢迎加入太空军团。你的任务是建立人类在火星上的第一个前哨站。多年来,世界上最伟大的科学家们一直在派遣机器人进行近距离研究。很快,你也将踏上这片尘土飞扬的表面。
前往火星的旅行通常需要六到八个月,具体取决于地球和火星的对齐方式。在旅途中,飞船有可能会撞上流星体和其他太空垃圾。如果发生任何损坏,你需要穿上宇航服,走到气闸处,然后进入太空修复,类似于图 1-1 中的宇航员。
在本章中,你将通过使用 Python 让角色在屏幕上移动,来进行一次太空行走。你将启动第一个 Python 程序,并学习一些你在本书后面创建太空站时所需要的基础 Python 指令。你还将学习如何通过重叠图像来创造深度感,这在我们稍后创建 3D 版本的Escape游戏时(从第三章开始的第一个房间模型)将非常有用。

图 1-1:2010 年,NASA 宇航员 Rick Mastracchio 进行 26 分钟的太空行走,由宇航员 Clayton Anderson 拍摄。这次太空行走是在国际空间站外进行的,目的是更换冷却液罐。
如果你还没有安装 Python 和 Pygame Zero(Windows 用户),请参阅“安装软件”第 3 页的内容。你还需要本章的Escape游戏文件。“下载游戏文件”在第 7 页说明了如何下载和解压这些文件。
启动 Python 编辑器
正如我在引言中提到的,在本书中我们将使用 Python 编程语言。编程语言提供了一种为计算机编写指令的方法。我们的指令将告诉计算机如何执行某些操作,例如响应按键或显示图像。我们还将使用 Pygame Zero,它为 Python 提供了一些处理声音和图像的额外指令。
Python 附带了 IDLE 编辑器,我们将使用该编辑器来创建 Python 程序。由于你已经安装了 Python,IDLE 现在应该也已安装在你的计算机上。接下来的部分将根据你使用的计算机类型解释如何启动 IDLE。
在 Windows 10 中启动 IDLE
在 Windows 10 中启动 IDLE,请按照以下步骤操作:
-
点击屏幕底部的 Cortana 搜索框,并在框中输入 Python。
-
点击IDLE以打开它。
-
当 IDLE 运行时,右键点击屏幕底部任务栏中的图标并固定它。以后你可以通过单击从任务栏直接运行它。
在 Windows 8 中启动 IDLE
在 Windows 8 中启动 IDLE,请按照以下步骤操作:
-
将鼠标移到屏幕右上角,显示出魅力条。
-
点击搜索图标,在框中输入 Python。
-
点击IDLE以打开它。
-
启动 IDLE 后,右击屏幕底部任务栏中的其图标并将其固定。以后你可以通过单击它来运行 IDLE。
在 Raspberry Pi 上启动 IDLE
在 Raspberry Pi 上启动 IDLE,请按照以下步骤操作:
-
点击屏幕左上角的“程序”菜单。
-
找到“编程”类别。
-
点击 Python 3(IDLE)图标。Raspberry Pi 上安装了 Python 2 和 Python 3,但本书中的大多数程序只在 Python 3 上运行。
介绍 Python Shell
启动 IDLE 时,你应该看到 Python 的Shell,如图 1-2 所示。这个窗口是你可以给 Python 输入指令并立即看到计算机回应的地方。三个箭头(>>>)被称为提示符,它们告诉你 Python 已经准备好接受指令了。

图 1-2:Python Shell
那么,让我们给 Python 一些任务吧!
显示文本
对于我们的第一个指令,让我们告诉 Python 在屏幕上显示文本。输入以下内容并按回车:
>>> print("Prepare for launch!")
当你输入时,文本的颜色会发生变化。一开始是黑色的,但一旦 Python 识别出一个命令,比如 print,文本就会变色。
图 1-3 显示了你刚刚输入的指令的不同部分。紫色的单词 print 是一个内建函数,它是 Python 中始终可用的众多指令之一。print() 函数会在屏幕上显示你放在括号(圆括号)之间的信息。函数括号之间的信息就是该函数的参数。

图 1-3:你第一个指令的不同部分
在我们的第一个指令中,print() 函数的参数是一个字符串,程序员通常称之为一段文本。(字符串可以包含数字,但它们被当作字母处理,所以你不能用字符串中的数字做运算。)双引号(" ")表示字符串的开始和结束。你在双引号之间输入的任何内容都会是绿色,双引号本身也会是绿色的。
颜色不仅仅是让屏幕更加亮眼:它们还突出显示了指令的不同部分,帮助你找出错误。例如,如果你的最后一个括号是绿色的,这意味着你忘记了字符串的闭合双引号。
如果你正确输入了指令,计算机将显示以下文本:
Prepare for launch!
显示为绿色的字符串现在在屏幕上显示为蓝色。所有的输出(计算机提供给你的信息)都以蓝色显示。如果你的命令没有成功,检查你是否做了以下操作:
-
正确拼写了
print。如果拼写正确,它将显示为紫色(见图 1-3)。 -
使用了两个括号。其他类型的括号将无法工作。
-
使用了两个双引号。不要使用两个撇号(
'')代替双引号(")。尽管双引号包含两个符号,它在键盘上其实是一个符号。在美国键盘上,双引号位于字母中间行的右侧,必须与 SHIFT 键一起使用。在英国键盘上,双引号位于 2 键上。
如果你在双引号之间输入的文本有错误,指令仍然会运行,但计算机会显示你输入的内容。例如,试试这个:
>>> print("Prepare for lunch!")
如果现在你打错了字符串也没关系,但以后在书中输入字符串或指令时要小心。错误往往会导致程序无法正常工作,而且在更长的程序中,即使有颜色编码,也很难追踪到错误。
训练任务 #1
你能输入一个新的指令来输出你的名字吗?(你可以在每章结尾的“任务总结”部分找到训练任务的答案。)
输出和使用数字
到目前为止,你已经使用print()函数输出了一个字符串,但它也可以进行计算并输出一个数字。输入以下代码:
>>> print(4 + 1)
计算机应输出数字5,即4 + 1的结果。与字符串不同,数字和计算不需要加引号。但是,你仍然需要使用括号来标记你想要提供给print()函数的信息的开始和结束。
如果你在4 + 1周围加上引号会发生什么?试试看!结果是计算机输出"4 + 1",因为它不会把 4 和 1 当作数字来处理。相反,它把参数当作字符串处理。你要求它输出"4 + 1",它就会准确地做出这个输出!
>>> print(4 + 1)
5
>>> print("4 + 1")
4 + 1
只有在你不加引号时,Python 才会进行计算。在你的程序中,你将经常使用print()函数。
介绍脚本模式
Shell 非常适合快速计算和简短的指令。但对于更长的指令集,比如游戏,创建程序要容易得多。程序是可重复执行的一组指令,我们保存它们,以便随时运行并在需要时修改,而无需重新输入。我们将使用 IDLE 的脚本模式来编写程序。当你在脚本模式下输入指令时,它们不会像在 shell 中那样立即运行。
使用顶部菜单中的菜单,选择文件,然后选择新建文件,以打开一个空白的新窗口,如图 1-4 所示。窗口顶部的标题栏会显示未命名,直到你保存并命名文件。保存文件后,标题栏将显示文件的名称。从现在开始,我们在编写 Python 代码时几乎总是使用脚本模式。

图 1-4:Python 脚本模式
当你在脚本模式中输入指令时,可以使用鼠标或箭头键来更改、添加和删除指令,这样更容易修正错误并构建程序。从 第四章 开始,我们将通过逐步添加并测试每个新部分来构建 Escape 游戏。
提示
如果你不确定自己是在 shell 模式还是脚本模式窗口中,看看顶部的标题栏。Shell 显示的是 Python Shell。脚本模式窗口显示的是 Untitled 或你程序的名称。
创建星空背景
我们将编写的第一个程序将显示星空背景图像,这是我们将用作 太空行走 程序的太空背景。该图像位于 escape 文件夹中的 images 文件夹中。首先在 IDLE 中的新空白窗口中输入 Listing 1-1。
注意
在本书中,我将使用圆圈中的数字(例如: ➊)来引用解释中的不同代码部分,这样你可以更容易地跟随。在程序中不要输入这些数字。当你在文本中看到圆圈中的数字时,回到程序列表中查看我所提到的程序部分。
Listing 1-1 是一个简短的程序,但在你输入时有几个细节需要注意:def语句 ➍ 需要在其行末加上冒号,接下来的行 ➎ 需要以四个空格开始。当你在 def 行末添加冒号并按下 ENTER 时,IDLE 会自动在下一行的开头添加四个空格。
listing1-1.py
➊ # Spacewalk
# by Sean McManus
# www.sean.co.uk / www.nostarch.com
➋ WIDTH = 800
HEIGHT = 600
➌ player_x = 600
player_y = 350
➍ def draw():
➎ screen.blit(images.backdrop, (0, 0))
Listing 1-1:在 Pygame Zero 中查看星空背景。
选择屏幕顶部的 文件 菜单,然后选择 保存(从现在开始,我们将使用类似于 文件 ▸ 保存 的菜单选择简写)。在保存对话框中,将你的程序命名为 listing1-1.py。你需要将文件保存在你在介绍部分设置的 escape 文件夹中。这样,它就与本书的 images 文件夹位于同一文件夹,Pygame Zero 在运行程序时就能找到图像。保存文件后,你的 escape 文件夹现在应该包含 listing1-1.py 文件和 images 文件夹,如 图 1-5 所示(以及 listings 和 sounds 文件夹)。

图 1-5:你的新 Python 程序和 images 文件夹应该存储在同一位置。
我将简要解释listing1-1.py程序是如何工作的,但首先让我们运行程序,以便我们可以欣赏星空背景。程序需要一些来自 Pygame Zero 的指令来管理图像,因此为了使用这些指令,我们需要使用pgzrun指令运行程序。每当我们在 Python 程序中使用 Pygame Zero 的指令时,都需要使用pgzrun来运行。
我们将在计算机的命令行中输入这些内容,就像我们在介绍中运行Escape游戏时所做的那样。首先,回顾一下“运行游戏”,并按照第 9 页上的说明,从你的escape文件夹中打开计算机的命令行终端。然后在命令行中运行以下指令:
pgzrun listing1-1.py
紧急警报
不要在 IDLE 中输入这个指令:一定要在你的 Windows 或 Raspberry Pi 命令行中输入。介绍中展示了如何操作。
如果一切按计划进行,你应该能看到太空的壮丽景象,如图 1-6 所示。

图 1-6:星空。星空图像由 NASA/JPL-Caltech/UCLA 提供,显示的是星团 NGC 2259。
使用我的示例列表
如果你无法使本书中的程序工作,你可以使用我的示例程序。例如,你可以使用我的listing1-1.py示例,并修改它以便快速制作自己的listing1-2.py,以便继续跟随下去。
你可以在escape文件夹中的listings文件夹找到我的程序。只需在 Windows 或 Raspberry Pi 桌面上打开listings文件夹,找到你需要的列表,复制它,然后将其粘贴到escape文件夹中。然后在 IDLE 中打开复制的列表,并按照书中的下一步继续操作。当你查看文件夹时,你应该能够看到 Python 文件和images文件夹在同一个位置(参见图 1-5)。
到目前为止理解程序
本书中你将看到的大部分指令在任何 Python 程序中都能使用。例如,print()函数始终可用。为了制作本书中的程序,我们还使用了 Pygame Zero。它为 Python 添加了一些新函数和功能,用于创建游戏,特别是在屏幕显示和声音方面。列表 1-1 介绍了我们从 Pygame Zero 中获得的第一组指令,用于设置游戏窗口并绘制星空。
让我们更仔细地看一下listing1-1.py程序是如何工作的。
前几行程序是注释 ➊。当你使用#符号时,Python 会忽略该行中#符号后的所有内容,该行会显示为红色。注释帮助你和其他阅读程序的人理解程序的功能及其工作原理。
接下来,程序需要存储一些信息。程序几乎总是需要存储程序使用的信息或稍后需要引用的信息。例如,在许多游戏中,计算机需要跟踪分数和玩家在屏幕上的位置。由于这些细节会随着程序的运行而变化(或波动),它们存储在一个叫做变量的东西中。变量是你给某个信息(无论是数字还是文本)起的名字。
要创建一个变量,你可以使用如下指令:
variable_name = value
注意
以斜体显示的代码术语是占位符,将被填充。你应该用你自己的变量名代替 variable_name。
例如,以下指令将数字 500 存入变量 score 中:
score = 500
你可以几乎随意为你的变量命名。然而,为了让你的程序更容易编写和理解,你应该选择能够描述每个变量内部信息的变量名。请注意,你不能使用 Python 本身用于语言的名称作为变量名,例如 print。
紧急警告
Python 区分大小写,这意味着它对变量是否使用大写或小写字母非常严格。事实上,它将 score、 SCORE* 和 Score 视为三个完全不同的变量。确保你完全照搬我的示例程序,否则它们可能无法正常工作。*
清单 1-1 开始时创建了一些变量。Pygame Zero 使用 WIDTH 和 HEIGHT 变量 ➋ 来设置游戏窗口在屏幕上的大小。我们的窗口比它高,因此 WIDTH 的值(800)比 HEIGHT 的值(600)大。
注意,我们将这些变量拼写为大写字母。变量名中的大写字母告诉我们它们是 常量。常量是一种特殊类型的变量,其值在设定后不应该改变。大写字母有助于其他查看程序的程序员理解,不应该让程序中的其他部分修改这些变量。
player_x 和 player_y 变量 ➌ 会存储你在执行太空行走时在屏幕上的位置。在本章后面,我们将使用这些变量来绘制你的屏幕位置。
然后,我们使用 def() 语句 ➍ 来定义一个函数。一个 函数 是一组指令,你可以在程序中需要它们时运行它们。你已经见过一个内置函数 print()。我们将在这个程序中创建一个名为 draw() 的函数,Pygame Zero 会在屏幕变化时使用它来绘制屏幕显示。
我们使用关键字 def ➍ 来定义一个函数,后跟我们选择的函数名称、空括号和冒号。有时,你会使用函数的括号来传递该函数所需的信息,正如你将在本书后面看到的那样。
然后,我们需要为函数提供指令,告诉它应该做什么。为了告诉 Python 哪些指令属于这个函数,我们通过缩进四个空格来标明它们。Pygame Zero 中的 screen.blit() 指令 ➎ 会在屏幕上绘制一张图片。在括号中,我们告诉它绘制哪张图片以及绘制的位置,像这样:
screen.blit(images.image_name, (x, y) )
从 images 文件夹中,我们将使用 backdrop.jpg 文件,它是星空背景。在我们的 listing1-1.py 程序中,我们将它称为 images.backdrop。我们不需要使用文件的 .jpg 扩展名,因为我们使用 Pygame Zero 来处理图像,而 Pygame Zero 不要求使用扩展名。此外,程序知道图像所在的位置,因为所有图像必须保存在 images 文件夹中,以便 Pygame Zero 可以找到它们。
我们将图像放置在屏幕的 (0, 0) 位置➌,这里是屏幕的左上角。第一个数字,称为 x 位置,告诉 screen.blit() 指令我们希望图像距离左边缘多远;第二个数字,称为 y 位置,描述我们希望图像离上边缘多远。x 位置从窗口的左边缘 0 开始,到右边缘的 799(因为我们的窗口宽度是 800 像素)。类似地,y 位置从窗口的顶部 0 开始,到底部的 599(参见图 1-6)。
对于屏幕上的位置,我们使用 元组,它只是一些数字或字符串放在括号内,例如 (0, 0)。在元组中,数字之间用逗号分隔,另外为了可读性,还可以加上空格。
关于元组,你需要知道的最重要的事情是要小心标点符号。因为元组使用括号,而我们将这个元组放在 screen.blit() 的括号内,所以这里有两组括号。因此,你需要在元组的值周围加上括号,但也需要在元组之后关闭 screen.blit() 的括号。
停止你的 Pygame Zero 程序
类似于太空,你的 Pygame Zero 程序将一直运行下去。要停止它,可以点击游戏窗口右上角的关闭按钮(参见图 1-6)。你也可以通过按 CTRL-C 在输入 pgzrun 指令的命令行窗口中关闭程序。
红色警报
不要关闭命令行窗口。否则,你将不得不重新打开它才能运行另一个 Pygame Zero 程序。如果不小心关闭了它,请参考 “运行游戏” 章节,在 第 9 页 中重新打开它。
添加行星和宇宙飞船
让我们把火星和宇宙飞船显示出来。在 IDLE 中,将清单 1-2 中的最后两行添加到现有的 listing 1-1.py 程序中。
注意
我会在代码清单中使用 --snip-- 来表示我省略了一些代码,通常是因为这些代码在之前已经重复出现过。我还会将重复的代码以灰色显示,这样你就能更清楚地看到你需要添加的新代码。不要再次添加重复的代码!
在以下代码中,我省略了注释和变量设置,以节省空间并使你更容易看到新增的代码。但确保你将这些指令保留在程序中。只需在最后添加两行新代码。
listing1-2.py
--snip--
def draw():
screen.blit(images.backdrop, (0, 0))
screen.blit(images.mars, (50, 50))
screen.blit(images.ship, (130, 150))
清单 1-2:添加火星和飞船
将你的更新程序保存为listing1-2.py,方法是选择文件 ▸ 另存为。通过切换回命令行窗口并输入命令pgzrun listing1-2.py来运行程序。图 1-7 展示了现在屏幕应该呈现的效果,红色的行星和位于其上方的宇宙飞船。

图 1-7:火星和宇宙飞船。火星的图像是 1991 年由哈勃太空望远镜拍摄的。
注意
如果你的程序没有按预期运行,请检查所有的 screen.blit() 指令前是否有四个空格,并且它们是否对齐。
新的第一条指令将图片mars.jpg放置在位置(50, 50),也就是屏幕的左上角附近。第二条新指令将宇宙飞船放置在(130, 150)位置。在这两种情况下,使用的坐标都是图像的左上角位置。
改变视角:飞到行星后面
现在让我们看看如何让宇宙飞船飞到行星后面。按示例 1-3 中所示的方式,在 IDLE 中交换最后两条指令的顺序。为此,选中其中一行,按 CTRL-X 剪切它,点击新的一行,按 CTRL-V 粘贴到那里。你也可以使用屏幕顶部“编辑”菜单中的剪切和粘贴选项。
listing1-3.py
--snip--
def draw():
screen.blit(images.backdrop, (0, 0))
screen.blit(images.ship, (130, 150))
screen.blit(images.mars, (50, 50))
示例 1-3:交换行星和宇宙飞船指令的顺序
如果你之前的程序仍在运行,请现在关闭它。将你的新程序保存为listing1-3.py,并通过命令行输入pgzrun listing1-3.py来运行它。你应该看到宇宙飞船现在位于行星后面,如图 1-8 所示。如果没有,确保你运行了正确的文件(listing1-3.py),然后检查程序中的指令是否正确。
宇宙飞船之所以会在行星后面,是因为图像是按程序中的绘制顺序添加到屏幕上的。在我们更新后的程序中,我们先绘制星空,再绘制宇宙飞船,最后绘制火星。每一张新图像都会显示在前一张图像的上方。如果两张图像重叠,最后绘制的图像会出现在先前绘制的图像前面。

图 1-8:宇宙飞船现在位于行星后面。
训练任务 #2
你能将程序中的一条绘图指令移到其他位置,使行星和宇宙飞船消失吗?如果不确定该做什么,可以通过移动绘图指令来进行实验,看看保存程序并重新运行后会产生什么效果。
确保你在draw()函数内保持绘图指令的对齐,并且每条指令前有四个空格。当你完成实验后,再次将示例 1-3 中的指令恢复,使宇宙飞船和火星重新出现在屏幕上。
太空漫步!
现在是时候从飞船底部爬出来,开始你的太空行走了。编辑你的程序,使其与 Listing 1-4 匹配。但一定要保持之前没有显示在这里的变量指令与之前相同。将更新后的程序保存为listing1-4.py。
listing1-4.py
--snip--
def draw():
screen.blit(images.backdrop, (0, 0))
screen.blit(images.mars, (50, 50))
➊ screen.blit(images.astronaut, (player_x, player_y))
➋ screen.blit(images.ship, (550, 300))
➌ def game_loop():
➍ global player_x, player_y
➎ if keyboard.right:
➏ player_x += 5
➐ elif keyboard.left:
player_x -= 5
➑ elif keyboard.up:
player_y -= 5
elif keyboard.down:
player_y += 5
➒ clock.schedule_interval(game_loop, 0.03)
Listing 1-4: 添加太空行走指令
在这个代码段中,我们添加了一个新的指令➊,用于在player_x和player_y变量指定的位置绘制宇航员图像,这些变量在 Listing 1-1 中已经设置。正如你所看到的,我们可以用这些变量名代替数字来表示宇航员的位置。程序将在每次绘制宇航员时,使用这些变量中存储的当前数字来确定宇航员的位置。
注意,程序中图像的绘制顺序已经改变,现在是背景、火星、宇航员和飞船。确保你修改screen.blit()指令的顺序,以与此列表匹配。
宇航员开始时与飞船重叠。由于宇航员在飞船之前绘制,因此宇航员看起来是从飞船下方(后面)出现的。我们还将飞船的位置➋更改到了屏幕的右下角。这为宇航员飞向行星提供了空间。
通过输入pgzrun listing1-4.py运行程序。现在你应该能够使用方向键自由移动,身穿航天服,穿越太空,如图 1-9 所示。你会发现自己飞在宇宙飞船后面,但在火星和星空之前。我们绘制图像的顺序创造了一个简单的深度错觉。当我们在第三章开始绘制空间站时,我们将使用这种绘制技巧来创建每个房间的 3D 视角。我们将从后到前绘制房间,以创造深度感。

图 1-9:你从飞船中走出来,开始太空行走。
训练任务 #3
你能编辑代码,将飞船和宇航员移动到屏幕的右上角吗?你需要更改player_x和player_y的起始值,以及飞船绘制的位置。确保在程序开始时,玩家是“在”(实际上是在)飞船内部的。你也可以尝试其他位置。这是熟悉屏幕位置的好方法。如果需要,可以参考图 1-6。
理解太空行走代码
太空行走的代码 Listing 1-4 很有趣,因为它允许你通过键盘控制程序的部分内容,这在逃脱游戏中至关重要。让我们看看最终的太空行走程序是如何工作的。
我们在之前的列表基础上进行扩展,添加了一个名为 game_loop() 的新函数 ➌。这个函数的任务是当你按下箭头键时,改变 player_x 和 player_y 变量的值。改变这些变量的值可以移动宇航员角色,因为这些变量决定了宇航员在屏幕上的位置。
在继续之前,我们需要了解两种不同类型的变量。在函数内部改变的变量通常属于该函数,无法被其他函数使用。它们被称为局部变量,它们能够有效地防止程序的不同部分意外干扰,从而避免错误的发生。
但是在太空漫步的列表中,我们需要 draw() 和 game_loop() 函数都使用相同的 player_x 和 player_y 变量,所以它们需要是全局变量,程序的任何部分都可以使用它们。我们在程序的开始部分设置全局变量,放在任何函数之外。
为了告诉 Python game_loop() 函数需要使用并修改我们在该函数外部设置的全局变量,我们使用 global 命令 ➍。我们把它放在函数的开头,并列出我们想要作为全局变量使用的变量。这样做就像是覆盖了一个安全功能,该功能阻止你修改那些在函数内部没有创建的变量。在 draw() 函数中,我们不需要使用 global,因为 draw() 函数不需要修改那些变量,它只需要查看这些变量的内容。
我们通过 if 命令告诉程序使用键盘控制。通过这一指令,我们告诉 Python 仅在某些条件满足时执行某些操作。我们使用四个空格来缩进属于 if 命令的指令。这意味着这些指令在 Listing 1-4 中总共缩进了八个空格,因为它们也在 game_loop() 函数内部。只有在 if 命令后面的条件为真时,这些指令才会执行。如果条件不成立,那么属于 if 命令的指令将被跳过。
这样使用空格来表示哪些指令属于同一组可能看起来有些奇怪,尤其是如果你曾经使用过其他编程语言的话,但这种方式使得程序更容易阅读。其他语言通常需要用括号来包围这类指令集合,而 Python 则保持简单。
我们使用 if 命令来检查是否按下了右箭头键 ➎。如果按下了,我们通过加 5 来改变 player_x 的值 ➏,将宇航员图像向右移动。符号 += 意味着增加,因此下面的这一行将 player_x 变量中的数字增加了 5:
player_x += 5
同样,-= 意味着减少,因此下面的指令将 player_x 中的数字减少了 5:
player_x -= 5
如果右箭头键没有被按下,我们检查左箭头键是否被按下。如果是,程序将从player_x值中减去 5,向左移动宇航员的位置。为此,我们使用elif命令➐,它是“else if”的缩写。你可以把else理解为这里的否则。用通俗的语言来说,这部分程序意味着:“如果按下右箭头键,向x位置加 5。否则,如果按下左箭头键,向x位置减 5。”然后我们使用elif以同样的方式检查上下箭头键的按下,并改变y位置来上下移动宇航员。draw()函数使用player_x和player_y变量表示宇航员的位置,因此更改这些变量中的数字会使宇航员在屏幕上移动。
提示
如果你将➑处的elif命令改为if命令,程序将允许你在移动左右的同时,也能上下移动,实现对角线行走。虽然在太空行走程序中这样做很有趣,但我们稍后会使用类似的代码来移动太空站,那样看起来不自然。
最后一条指令➒设置game_loop()函数每 0.03 秒运行一次,使用 Pygame Zero 中的时钟,这样程序就会不断检查你的按键并频繁更改你的位置变量。注意,在这里你不需要在game_loop后加上括号。这条指令没有缩进,因为它不属于任何函数。当程序启动时,它会按照列表中从上到下的顺序运行那些不在任何函数中的指令。因此,程序的最后一行是设置完变量后首先运行的指令之一。这一行启动了game_loop()函数的运行。
draw()函数会在每次需要更新屏幕时自动运行。这是 Pygame Zero 的一个特点。
训练任务 #4
让我们为太空服安装一些新的推进器。你能想出如何让宇航员在上下方向上的移动比在左右方向上更快吗?每次按下上下方向键时,太空服应该移动得比按下左右方向键时更多。
在进行太空行走并完成任何必要的修理时,享受令人叹为观止的景色。我们将在第二章重新集合,在那里你将学习一些帮助你在太空中保持安全的程序。
你准备好飞行了吗?
勾选以下框以确认你已经掌握了本章的关键知识。如果你对某个内容不确定,可以翻回本章再看一遍。
你可以使用 IDLE 的脚本模式来创建一个可以保存、编辑并重新运行的程序。通过选择文件 ▸ 新建文件进入脚本模式,或者选择文件 ▸ 打开来编辑现有文件。
字符串是代码中的文本片段。双引号标记字符串的起始和结束。字符串可以包含数字,但它们被视为字母。
变量存储信息,可以是数字或字符串。
print() 函数在屏幕上输出信息。您可以用它来处理字符串、数字、计算结果或变量的值。
程序中的 # 符号表示注释。Python 忽略 # 后同一行的任何内容,注释对您和与您分享代码的其他人都很有用。
使用 WIDTH 和 HEIGHT 变量设置游戏窗口的大小。
要运行 Pygame Zero 程序,请从包含 Python 程序的文件夹中打开命令行,然后在命令行中输入 pgzrun 文件名.py。
函数是一组指令,您可以在需要时运行它们。Pygame Zero 使用 draw() 函数来绘制或更新游戏屏幕。
使用 screen.blit``(images.image_name, (x, y)) 在屏幕上的位置 (x, y) 绘制一幅图像。x 和 y 轴从左上角的 0 开始编号。
元组 是一组用括号括起来的数字或字符串,用逗号分隔。元组的内容一经设置后程序无法更改。
要结束 Pygame Zero 程序,点击窗口的关闭按钮或在命令行窗口中按下 CTRL-C。
如果图像重叠,程序中最后绘制的图像将显示在最前面。
elif 命令是“else if”的缩写。使用它来组合 if 条件,以便只能运行一个指令集。在我们的程序中,我们用它来阻止玩家同时在两个方向上移动。
如果我们想在一个函数中更改变量并在另一个函数中使用它,需要使用 全局变量。我们在函数外设置它,并在函数内使用 global 关键字来更改该变量。
我们可以使用 Pygame Zero 中的时钟功能定期运行函数。

第二章:列表能救你的命**

宇航员的生活离不开列表。他们使用的安全检查单有助于确保所有系统在他们将生命托付给这些系统之前正常运作。例如,紧急检查单告诉宇航员在紧急情况下该怎么做,以防止他们慌乱。程序性检查单确认他们正确使用设备,以防设备出现故障,阻碍他们回到家园。这些列表有可能在某天拯救他们的生命。
在本章中,你将学习如何在 Python 中管理列表,以及如何将它们用于检查单、地图和几乎宇宙中的任何事物。当你制作 Escape 游戏时,你将使用列表来存储空间站的布局信息。
创建你的第一个列表:起飞检查单
起飞是太空旅行中最危险的环节之一。当你被绑在火箭上时,你会想在发射前对一切进行仔细检查。一个简单的起飞检查单可能包含以下步骤:
穿上宇航服
密封舱口
检查舱内压力
系好安全带
Python 有一种完美的方式来存储这些信息:Python 列表 就像一个变量,存储多个项目。如你所见,你可以将其用于数字和文本,甚至是数字与文本的组合。
让我们在 Python 中创建一个名为 take_off_checklist 的列表供宇航员使用。因为我们只是用一个简短的例子进行练习,所以我们将在 Python shell 中输入代码,而不是创建一个程序。(如果你需要复习如何使用 Python shell,请参阅 第 15 页 中的 “介绍 Python Shell”)。在 IDLE shell 中输入以下内容,在每一行结束时按 ENTER 键开始新的一行:
>>> take_off_checklist = ["Put on suit",
"Seal hatch",
"Check cabin pressure",
"Fasten seatbelt"]
红色警报
确保代码中的括号、引号和逗号都准确无误。如果遇到任何错误,请重新输入列表代码,并仔细检查括号、引号和逗号是否放置正确。为了避免重新输入代码,可以使用鼠标高亮选中 shell 中的文本,右键点击选中的文本,选择 复制,然后再次右键点击并选择 粘贴。
让我们更仔细地看看 take_off_checklist 列表是如何创建的。你通过一个开括号标记列表的开始,Python 会知道列表尚未结束,直到它检测到最终的闭括号。这意味着你可以在每一行结束时按 ENTER 键继续输入指令,Python 会知道你还没有完成,直到你输入了最终的括号。
引号告诉 Python 你给它的是文本,并且标明每段文本的开始和结束。每个条目需要自己的开引号和闭引号。你还需要用逗号分隔不同的文本项。最后一个条目后面不需要逗号,因为它后面没有其他列表项。
查看你的列表
要查看你的检查清单,你可以像我们在第一章中做的那样使用print()函数。将你的列表名称添加到print()函数中,像这样:
>>> print(take_off_checklist)
['Put on suit', 'Seal hatch', 'Check cabin pressure', 'Fasten seatbelt']
你不需要在take_off_checklist周围加上引号,因为它是一个列表的名称,而不是一段文本。如果你加上引号,Python 会直接在屏幕上输出文本take_off_checklist,而不是返回你的列表。试试看会发生什么。
添加和删除项目
即使在你创建了一个列表之后,你仍然可以使用append()命令向其中添加一个项目。append这个词的意思是将某样东西添加到末尾(想象一下书籍的附录,在书的最后)。你可以像这样使用append()命令:
>>> take_off_checklist.append("Tell Mission Control checks are complete")
你输入列表的名称(不加引号),然后加上一个点,再加上append()命令,然后将要添加的项目放在圆括号中。该项目将被添加到列表的末尾,如你再次打印列表时所看到的:
>>> print(take_off_checklist)
['Put on suit', 'Seal hatch', 'Check cabin pressure', 'Fasten seatbelt', 'Tell
Mission Control checks are complete']
你还可以使用remove()命令从列表中删除项目。让我们删除Seal hatch项目:
>>> take_off_checklist.remove("Seal hatch")
>>> print(take_off_checklist)
['Put on suit', 'Check cabin pressure', 'Fasten seatbelt', 'Tell Mission
Control checks are complete']
再次,你输入列表的名称,后面跟一个点,再加上remove()命令,然后在圆括号中指定你要删除的项目。
红色警报
当你从列表中删除一个项目时,确保你输入的内容与该项目完全匹配,包括大写字母和标点符号。否则,Python 将无法识别它并给你一个错误。
使用索引号
嗯,我们可能应该把Seal hatch检查项重新放回列表中,以免任务控制中心的任何人注意到。你可以通过使用该项目的索引号在列表中的特定位置插入一个项目。索引是项目在列表中的位置。Python 从 0 开始计数,而不是从 1,所以列表中的第一个项目总是索引 0,第二个项目的索引是 1,以此类推。
插入一个项目
使用位置索引,我们将把Seal hatch放回它该在的位置:
>>> take_off_checklist.insert(1, "Seal hatch")
>>> print(take_off_checklist)
['Put on suit', 'Seal hatch', 'Check cabin pressure', 'Fasten seatbelt', 'Tell
Mission Control checks are complete']
呼!我想我们挺过来了。因为索引从 0 开始,当我们插入Seal hatch时,我们把它放在了位置 1,即列表中的第二个项目。列表中的其他项目向下移动,以腾出空间,它们的索引号也随之增加,如图 2-1 所示。

图 2-1:在索引 1 处插入一个项目。上排:插入前。下排:插入后。
访问单个项目
你还可以通过使用列表名称和你想访问的项目的索引号,在方括号内访问列表中的特定项目。例如,要打印列表中的特定项目,你可以输入以下内容:
>>> print(take_off_checklist[0])
Put on suit
>>> print(take_off_checklist[1])
Seal hatch
>>> print(take_off_checklist[2])
Check cabin pressure
现在你可以看到列表中的各个项目了!
红色警报
不要混淆你的括号。大致来说:当你告诉 Python 使用哪个列表项目时,使用方括号。当你对列表或其中的项目执行某些操作时,比如打印列表或将项目附加到列表时,使用圆括号。每一个开括号都需要有一个相同类型的闭括号。
替换项目
你也可以通过索引号替换项。只需输入列表名称,后跟你要替换的项的索引,然后使用等号 (=) 告诉 Python 你希望在该索引位置插入什么,例如:
>>> take_off_checklist[3] = "Take a selfie"
>>> print(take_off_checklist)
['Put on suit', 'Seal hatch', 'Check cabin pressure', 'Take a selfie', 'Tell
Mission Control checks are complete']
索引为 3 的旧项被删除,并被新项替换。请注意,当你替换一项时,Python 会忘记原来的项。回想你的训练,把它放回去,如下所示:
>>> take_off_checklist[3] = "Fasten seatbelt"
>>> print(take_off_checklist)
['Put on suit', 'Seal hatch', 'Check cabin pressure', 'Fasten seatbelt', 'Tell
Mission Control checks are complete']
删除项目
如果你知道列表中某项的位置,也可以通过它的索引号删除该项,例如:
>>> del take_off_checklist[2]
>>> print(take_off_checklist)
['Put on suit', 'Seal hatch', 'Fasten seatbelt', 'Tell Mission Control checks
are complete']
"检查舱内压力" 这一项从列表中消失了。
训练任务 #1
是时候练习你的技能了!我们刚刚删除了列表中的第 2 项。你能把它重新插入列表并放到正确的位置吗?打印列表以检查它是否成功。
创建太空行走检查清单
如你从第一章中所知,对于宇航员来说,另一个危险的活动是仅凭宇航服保护自己,进入漆黑的太空真空中,宇航服为你提供氧气保护。以下是一个检查清单,帮助你在太空行走时保持安全:
穿上宇航服
检查氧气
密封头盔
测试无线电
打开气闸
让我们把这个检查清单转换成 Python 列表。我们将其命名为 spacewalk_checklist,如以下代码所示:
>>> spacewalk_checklist = ["Put on suit",
"Check oxygen",
"Seal helmet",
"Test radio",
"Open airlock"]
记得小心处理逗号和括号。
训练任务 #2
测试你的代码总是一个好主意,这样你就能确保它按预期工作。你能尝试打印所有列表项,以检查它们是否在正确的位置吗?
列表中的列表:飞行手册
现在我们有两个检查清单:一个是起飞清单,一个是太空行走清单。我们可以通过将它们放入另一个列表中来组织它们,从而创建我们的“飞行手册”。可以把飞行手册想象成一个文件夹,里面有两张纸,每张纸上有一个清单。
创建列表中的列表
这是我们创建飞行手册列表的方式:
>>> flight_manual = [take_off_checklist, spacewalk_checklist]
我们给 IDLE 提供 flight_manual 列表名称,使用等号 (=),然后将我们希望放入 flight_manual 列表的两个列表放入方括号中。就像我们之前创建列表时一样,用逗号分隔两个项。新的 flight_manual 列表包含两个项:take_off_checklist 和 spacewalk_checklist。当你打印 flight_manual 时,它会显示如下:
>>> print(flight_manual)
[['Put on suit', 'Seal hatch', 'Check cabin pressure', 'Fasten seatbelt',
'Tell Mission Control checks are complete'], ['Put on suit', 'Check oxygen',
'Seal helmet', 'Test radio', 'Open airlock']]
提示
记住,列表名称不需要加引号;只有在你向列表中输入文本时才需要使用引号。
红色警报
如果你在列表中没有看到 '检查舱内压力' ,那是因为你跳过了训练任务 #1。为了更容易跟上进度,我建议你回去完成那个任务。如果需要,你可以在章节末尾查看训练任务的答案。
输出看起来很乱!要弄清楚发生了什么,仔细看看括号。方括号标记了每个列表的开始和结束。如果你去掉列表项,输出看起来是这样的:
[ [ first list is here ], [ second list is here ] ]
在中间部分,你可以看到第一个列表以一个闭括号结束,然后是一个逗号,接着下一个列表以开括号开始。那么,当你尝试打印 flight_manual 列表中的第一个项目时会发生什么呢?
>>> print(flight_manual[0])
第一个项目是take_off_checklist,因此输出看起来是这样的:
['Put on suit', 'Seal hatch', 'Check cabin pressure', 'Fasten seatbelt', 'Tell
Mission Control checks are complete']
训练任务 #3
尝试将其他清单添加到 flight_manual 中并打印出来。例如,你可以添加一个着陆或与其他飞船对接的清单。
在飞行手册中查找项目
如果你想查看 flight_manual 中某个特定项目,你必须给 Python 提供两项信息:该项目所在的列表以及该项目在列表中的索引,顺序不能错。对于每一项信息,你可以使用索引数字,像这样:
>>> print(flight_manual[0][1])
Seal hatch
将你的结果与上面打印出的清单进行对比。Seal hatch 项目位于第一个列表(索引 0)中,即 take_off_checklist,它是该列表中的第二个项目(索引 1)。这就是我们用来找到它的两个索引号码。接下来我们从第二个列表中选择一个项目:
>>> print(flight_manual[1][3])
Test radio
这次,我们从第二个列表(索引 1)中打印,并且从该列表中打印第四个项目(索引 3)。虽然看起来可能有些困惑,因为 Python 是从 0 开始计数的,但很快你会习惯从你想要的位置减去一。小心,不要在购物时少买了一件东西!
小贴士
要在屏幕上打印一个列表或变量,当你在 shell 中输入时,可以省略 print() 命令,像这样:
>>> flight_manual[0][2]
‘Check cabin pressure’
然而,这种方法只在 shell 中有效,而不适用于程序。通常,你在 Python 中会有多种方式做同一件事。本书聚焦于那些最能帮助你制作 Escape 游戏的技巧。在学习 Python 的过程中,你会找到自己的风格和偏好。
合并列表
你可以使用加号(+)将两个列表合并成一个列表。让我们创建一个包含起飞和太空行走所需技能的列表,并称之为 skills_list:
>>> skills_list = take_off_checklist + spacewalk_checklist
>>> print(skills_list)
['Put on suit', 'Seal hatch', 'Check cabin pressure', 'Fasten seatbelt', 'Tell
Mission Control checks are complete', 'Put on suit', 'Check oxygen', 'Seal
helmet', 'Test radio', 'Open airlock']
你在这里看到的输出是一个包含来自我们已经创建的两个列表的宇航员所需技能的单一列表。我们还可以通过输入合并后的列表名称并使用 += 来将单个项目或其他列表添加到末尾。 (在第一章中,你学会了如何使用 += 来为变量的值添加一个数字。)
很少有人能进入太空,因此宇航员角色的一个重要部分是分享这种经历。让我们添加一个名为 pr_list 的列表,用于宇航员可能需要的公共关系(PR)技能。我想或许“自拍技能”真的有用处!
>>> pr_list = ["Taking a selfie",
"Delivering lectures",
"Doing TV interviews",
"Meeting the public"]
>>> skills_list += pr_list
>>> print(skills_list)
['Put on suit', 'Seal hatch', 'Check cabin pressure', 'Fasten seatbelt',
'Tell Mission Control checks are complete', 'Put on suit', 'Check oxygen',
'Seal helmet', 'Test radio', 'Open airlock', 'Taking a selfie', 'Delivering
lectures', 'Doing TV interviews', 'Meeting the public']
skills_list 现在包含了 pr_list 中的项目。skills_list 仍然是一个包含单个项目的简单列表,不像 flight_manual,它内部有两个独立的列表。
提示
你可能已经注意到这行代码:
skills_list += pr_list
这只是更简洁的写法:
skills_list = skills_list + pr_list
这是一个非常有用的快捷方式!
从列表制作地图:急救室
导航是宇航员的一项基本技能。你必须始终知道自己身处何地,最近的避难所在哪里,甚至空气的来源,以便在紧急情况下随时准备好。逃生游戏会记录玩家所在房间的地图,以便正确绘制房间并使玩家与物品互动。让我们来看一下如何使用列表制作急救物资室的地图。
制作地图
现在你已经知道如何管理列表和嵌套列表,可以开始制作地图了。这一次,我们将创建一个程序,而不是在 shell 中工作。在 Python 窗口的顶部,选择 文件 ▸ 新建文件 打开一个新窗口。
将 Listing 2-1 输入到你的新程序窗口中:
listing2-1.py
room_map = [ [1, 0, 0, 0, 0],
[0, 0, 0, 2, 0],
[0, 0, 0, 0, 0],
[0, 3, 0, 0, 0],
[0, 0, 0, 0, 4]
]
print(room_map)
Listing 2-1:设置急救室
请注意,列表中的最后一行不需要逗号。此程序创建并显示一个名为 room_map 的列表。我们的新急救室是五米乘五米的。room_map 列表包含五个列表。每个列表包含五个数字,表示地图中的一行。我已经在代码中排列好数字,让它看起来像 图 2-2 中显示的网格,这是房间的地图。比较图示和程序,你会发现第一个列表是顶行,第二个列表是第二行,依此类推。0 代表网格中的空地,数字 1 到 4 用于表示房间中的各种急救物品。本章使用的数字代表以下物品:

图 2-2:我们的第一个简单地图
-
化肥
-
备用氧气瓶
-
剪刀
-
牙膏
-
急救毯
-
紧急无线电
红色警报
确保你的括号和逗号放在正确的位置。将 Listing 2-1 放入程序中的原因之一是,如果你犯了错误,可以轻松进行更正,而不是直接在 shell 中输入。
点击 文件 ▸ 保存,并将你的程序保存为 listing2-1.py。此程序不使用 Pygame Zero,因此我们可以从 IDLE 运行它。点击窗口顶部的 运行,然后点击 运行模块。你应该在 Shell 窗口中看到以下输出:
[[1, 0, 0, 0, 0], [0, 0, 0, 2, 0], [0, 0, 0, 0, 0], [0, 3, 0, 0, 0],
[0, 0, 0, 0, 4]]
当列表以这种方式显示时,很难弄清楚你正在查看什么,这就是为什么我在程序清单中将数字排列成网格的原因。但是这个 shell 输出是相同的地图和相同的数据,因此一切都在应该在的位置:它只是以不同的方式呈现。在第三章中,你将学习如何打印这个地图数据,使其看起来更像我们创建的清单。
寻找紧急物品
要找出地图中特定点上的物品,你需要给 Python 一个坐标来检查。坐标是由 y 位置(从上到下)和 x 位置(从左到右)组成的,顺序是这样的。y 位置将是你要检查的 room_map 中的列表(网格中的一行)。x 位置将是你想查看的该列表中的物品(该列)(请参见图 2-3)。和往常一样,记住索引号从 0 开始。

图 2-3:y 坐标表示我们要查看的列表。x 坐标表示该列表中的物品。
红色警报
如果你以前使用过坐标系,你会知道通常将 x 坐标放在 y 坐标之前。我们这里做的是相反的,因为这样可以让代码更简单。如果我们将 x 放在前面,我们必须让每个room_map中的列表代表地图的一列,从上到下,而不是一行,从左到右。那样的话,我们的代码中的地图看起来会错乱:地图会被旋转并成为镜像图像,这会让人非常困惑!只要记住,我们的地图坐标是先 y 然后 x。
让我们通过一个例子来做一下:我们将找出在简单地图图示中标记为 2 的位置上的物品。我们需要知道以下几点:
-
2位于第二行(从上到下),因此它位于room_map中的第二个列表。索引从 0 开始,因此我们从 2 中减去 1 来得到 y 位置的索引号,结果是1。请使用图 2-3 来检查这个索引号:行的索引号显示在网格左侧的红色区域。 -
2位于列表的第四列(从左到右)。同样,我们减去 1 来得到 x 位置的索引号,结果是3。请使用图 2-3 来检查此索引号。列的索引号显示在网格顶部的红色区域。
转到 shell 并输入以下 print() 命令,以查看地图上该位置的数字:
>>> print(room_map[1][3])
2
正如预期的那样,结果是数字 2,它恰好是备用氧气罐。你已经成功地浏览了你的第一个地图!
训练任务 #4
在你将以下命令输入到 shell 之前,尝试预测输出:
>>> print(room_map[3][1])
参考图 2-2 中的地图和你的代码清单来做出预测。如果你需要更多的帮助,可以查看图 2-3。然后通过在 shell 中输入指令来检查你的答案。
交换房间中的物品
你也可以改变房间中的物品。我们来检查一下地图左上角的位置存放的是什么物品,再次使用 shell:
>>> print(room_map[0][0])
1
1 是肥料。我们在急诊室不需要肥料,所以我们将把地图中的该物品改成急救毯,用 5 来表示它们。还记得我们是如何用等号 (=) 来改变列表中项目的值吗?我们可以同样用它来改变地图中的数字,像这样:
>>> room_map[0][0] = 5
我们输入坐标后,输入一个新数字来替换原有的数字。我们可以通过再次打印该坐标的值来检查代码是否有效,刚才的值是 1。我们还可以打印 room_map,确认急救毯是否出现在正确的位置:
>>> print(room_map[0][0])
5
>>> print(room_map)
[[5, 0, 0, 0, 0], [0, 0, 0, 2, 0], [0, 0, 0, 0, 0], [0, 3, 0, 0, 0], [0, 0, 0,
0, 4]]
完美!急救毯存放在房间的左上角。物品 5 是第一个列表中的第一个物品。
训练任务 #5
在急诊室,空间是宝贵的!将牙膏(4)替换为急救无线电(6)。你需要先找到 4 的坐标,然后输入命令来改变它。如果需要更多帮助,参考 图 2-2 和 图 2-3,这些内容涉及索引号。
在 Escape 游戏中,room_map 列表用于记住玩家当前所在房间中的物品。地图存储每个位置上的物品编号,或者如果该位置为空地则存储 0。游戏中的房间将比这个 5 × 5 的网格大,因此 room_map 的大小会根据玩家所处房间的宽度和高度而有所不同。
你适合飞行吗?
勾选以下框以确认你已掌握本章的关键内容。
Python 列表可以存储单词、数字或两者的混合。
要查看列表中的项目,使用其索引号并放在方括号中:例如,print(take_off_checklist[2])。
append() 函数将项目添加到列表的末尾。
remove() 函数可以从列表中删除项目:例如,spacewalk _checklist.remove("Seal helmet")。
你可以使用索引号来删除或插入列表中特定位置的项。
索引号从 0 开始。
你可以使用等号 (=) 来改变列表中的项目:例如,take_off_checklist[3] = "Test comms"。
你可以创建一个包含其他列表的列表来构建一个简单的地图。
你可以使用坐标查看地图中的物品:例如,使用 room_map[y 坐标][x 坐标]。
一定要先使用 y 再使用 x 来表示坐标。在太空中,一切都是颠倒的。
坐标是索引号,因此两者都是从 0 开始的,而不是 1。
你可以使用+=将一个项目添加到列表中,或者将两个列表连接起来。

第三章:REPEAT AFTER ME**

每个人都在谈论太空旅行的英雄主义和光彩,但其中一些其实是日常的、重复的工作。当你在太空站温室里清洁、种植,或者锻炼保持体力时,你是在遵循详细的计划,以确保团队的安全和太空站的正常运行。幸运的是,机器人会处理一些繁重的工作,而且它们从不抱怨需要重复同样的任务。
无论你是在编程机器人还是构建游戏,循环都是你基本的编程构建块之一。循环是程序中的一部分,它会重复执行:有时它会重复一定次数,有时会一直执行,直到发生特定事件。有时,你甚至会设置一个循环让它永远运行。在本章中,你将学习如何使用循环在程序中重复指令一定次数。你将结合对列表的理解,使用循环来显示地图并绘制 3D 房间图像。
使用循环显示地图
在 Escape 游戏中,我们将广泛使用循环。我们经常使用它们从列表中提取信息并执行某些操作。
让我们从使用循环显示文本地图开始。
制作房间地图
我们将为本章示例创建一个新地图,使用 1 表示墙壁,0 表示地面空间。我们的房间四周都有墙壁,中间附近有一根柱子。柱子与墙壁一段相同,因此也用 1 来表示。我选择了它的位置,这样当我们稍后绘制 3D 房间时,看起来会更好。房间中没有其他物体,因此目前我们不会使用其他数字。
在 IDLE 中,打开一个新的 Python 程序,输入列表 3-1 中的代码,并将其保存为 listing3-1.py:
listing3-1.py
room_map = [ [1, 1, 1, 1, 1],
[1, 0, 0, 0, 1],
[1, 0, 1, 0, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1]
]
print(room_map)
列表 3-1:添加房间地图数据
这个程序创建了一个名为 room_map 的列表,包含七个其他列表。每个列表以方括号开始和结束,并且用逗号与下一个列表分隔。正如你在第二章中学到的,最后一个列表后面不需要逗号。每个列表代表地图中的一行。点击 运行 ▸ 运行模块 来运行程序,你应该能在 shell 窗口中看到以下内容:
[[1, 1, 1, 1, 1], [1, 0, 0, 0, 1], [1, 0, 1, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0,
0, 1], [1, 1, 1, 1, 1]]
正如你在第二章中看到的,打印地图列表会将所有行挤在一起,这样查看地图并不方便。我们将使用循环以更易读的方式显示地图。
使用循环显示地图
为了按行列显示地图,删除程序中的最后一行,并添加列表 3-2 中显示的两行新代码。和之前一样,别输入灰色的行——它们只是帮助你定位在程序中的位置。将程序保存为 listing3-2.py。
listing3-2.py
--snip--
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1]
]
➊ for y in range(7):
➋ print(room_map[y])
列表 3-2:使用循环显示房间地图
红色警报
记得在第一行的末尾加上冒号!没有它,程序将无法正常工作。第二行应该缩进四个空格,以告诉 Python 你希望重复执行哪些指令。如果你在for语句行末加上冒号,按下 ENTER 进入下一行时,空格会自动添加。
当你再次运行程序时,应该会在命令行中看到以下内容:
[1, 1, 1, 1, 1]
[1, 0, 0, 0, 1]
[1, 0, 1, 0, 1]
[1, 0, 0, 0, 1]
[1, 0, 0, 0, 1]
[1, 0, 0, 0, 1]
[1, 1, 1, 1, 1]
这是一种更有用的地图查看方式。现在你可以轻松看到围绕地图四周的墙壁(由1表示)。那么代码是如何工作的呢?这里的引擎是for命令➊。它是一个循环命令,告诉 Python 重复执行某段代码指定的次数。Listing 3-2 告诉 Python 为room_map列表中的每个项重复执行print()指令➋。room_map中的每个项都是一个包含地图一行数据的列表,所以逐行打印它们就能逐行显示我们的地图,从而得到这种有序的展示效果。
让我们更详细地分解一下代码。我们使用range()函数来创建一个数字序列。通过range(7),我们告诉 Python 生成一个不包含 7 的数字序列。为什么不包括最后一个数字呢?这就是range()函数的工作方式!如果我们只给range()函数一个数字,Python 会默认从0开始计数。所以range(7)生成的数字序列是 0、1、2、3、4、5 和 6。
每次代码重复时,for命令中的变量会从序列中取出下一个项。在这种情况下,y变量依次取值0、1、2、3、4、5和6。这与room_map中的索引值完美匹配。
我选择y作为变量名,因为我们用它来表示我们想要显示的地图行,地图上的行被称为 y 坐标。
print(room_map[y])命令➋缩进了四个空格,告诉 Python 这是我们希望for循环➊重复执行的代码块。
循环第一次执行时,y的值为0,所以print(room_map[y])打印room_map中的第一个项,即包含地图第一行数据的列表。第二次执行时,y的值为1,所以print(room_map[y])打印第二行。代码会一直重复,直到打印完room_map中的所有七个列表。
训练任务 #1
在太空站的紧急情况下,你可能需要发出求救信号。编写一个简单的程序,使用循环仅打印三次Mayday!。
如果你卡住了,从 Listing 3-2 开始,它用于打印地图。你只需要改变程序打印的内容以及它循环打印代码的次数。
循环循环
我们的地图输出变得更好了,但仍然有几个限制。其中一个是逗号和括号让它看起来有些杂乱。另一个限制是我们无法对房间中单独的墙面板或空间做任何处理。我们需要能够单独处理房间中每个位置的内容,以便能够正确显示其图像。为此,我们需要使用更多的循环。
嵌套循环获取房间坐标
listing3-2.py程序使用循环来提取地图的每一行。现在,我们需要使用另一个循环来检查每行中的每个位置,以便能够单独访问那里的对象。这样做将使我们能够完全控制物品的显示方式。
你刚刚看到我们可以在循环内重复一段代码。我们还可以将一个循环放入另一个循环中,这种结构被称为嵌套循环。为了查看其工作原理,我们首先使用这种技巧来打印房间中每个位置的坐标。编辑你的代码,使其与清单 3-3 匹配:
listing3-3.py
--snip--
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1]
]
➊ for y in range(7):
➋ for x in range(5):
➌ print("y=", y, "x=", x)
➍ print()
清单 3-3:打印坐标
红色警报
正如每个宇航员所知道的那样,太空可能是危险的。空间也是如此。如果循环中的缩进不正确,程序将无法正常工作。将第一个print()命令➌缩进八个空格,使其成为内层* x 循环的一部分。确保最后的print()指令➍与第二个for命令➋(缩进四个空格)对齐,这样它就留在外层循环中。当你开始新的一行时,Python 会像上一行一样自动缩进,但当你不再需要缩进时,你可以删除它。
将你的程序保存为listing3-3.py并通过点击运行 ▸ 运行模块来运行程序。你将看到以下输出:
y= 0 x= 0
y= 0 x= 1
y= 0 x= 2
y= 0 x= 3
y= 0 x= 4
y= 1 x= 0
y= 1 x= 1
y= 1 x= 2
y= 1 x= 3
y= 1 x= 4
y= 2 x= 0
y= 2 x= 1
y= 2 x= 2
--snip--
输出继续并以y= 6 x= 4结束。
我们已经像之前一样设置了y循环,它会重复七次 ➊,每次处理从0到6的数字,并将该值赋给y变量。这就是我们这次程序不同的地方:在y循环内,我们启动了一个新的for循环,使用x变量,并给它一个包含五个值的range,从0到4 ➋。第一次执行y循环时,y是0,此时x依次取值0、1、2、3和4,而y保持为0。第二次执行y循环时,y变为1。我们重新启动一个x循环,它再次依次取值0、1、2、3和4,而y保持为1。这种循环会一直持续,直到y为6且x为4。
通过查看程序的输出,你可以看到循环是如何工作的:在 x 循环内部,我们每次重复 x 循环时都会打印 y 和 x 的值➌。当 x 循环完成时,我们会打印一个空行 ➍,然后进入 y 循环的下一次迭代。我们通过让 print() 函数的括号为空来实现这一点。空行将 y 循环的重复内容分隔开,而输出将展示每次 x 循环中 x 和 y 的值。正如你所看到的,程序输出了房间中每个位置的 y 坐标和 x 坐标。
提示
我们在循环中使用了变量名y和x,但这些变量名并不会影响程序的运行。你可以把它们叫作sausages(香肠)和eggs(鸡蛋),程序仍然可以正常工作。不过,这样做就不太容易理解了。因为我们获取的是 x 和 y 坐标,使用 x 和 y 作为变量名更为合理。
清理地图
我们将使用循环中的坐标打印地图,且不显示括号和逗号。请按照 Listing 3-4 中所示的方式修改程序,改变内部嵌套循环的内容:
listing3-4.py
--snip--
for y in range(7):
for x in range(5):
print(room_map[y][x], end="")
print()
Listing 3-4: 整理地图显示
将程序保存为 listing3-4.py 并通过点击 Run ▸ Run Module 来运行程序。你应该会在终端中看到以下输出:
11111
10001
10101
10001
10001
10001
11111
这个地图看起来更简洁,更容易理解。它的工作方式与 Listing 3-3 中的程序类似。它通过 y 循环逐行遍历坐标,然后通过 x 循环获取每一行中的每个位置。这一次,我们不是打印坐标,而是查看每个位置的 room_map 中的内容,并打印出来。正如你在 第二章 中学到的那样,你可以通过 room_map[y 坐标][x 坐标] 的形式来获取地图中的任何项目。
我们格式化输出的方式使得地图看起来像房间的布局:我们把同一行中的所有数字放在一起,只有当开始新的一行(y 循环的下一次迭代)时,屏幕上才会开始新的一行。
x 循环中的 print() 指令以 end=""(引号之间没有空格)结尾,以防在每个数字后面开始新的一行。否则,默认情况下,print() 函数会在每一项输出后添加一个换行符。但我们告诉它在输出后不加任何内容("")。因此,来自一次完整 x 循环(从 0 到 4)的所有项会显示在同一行。
每行打印后,我们会使用一个空的 print() 命令来开始新的一行。由于我们只用四个空格缩进这个命令,它属于 y 循环,而不是 x 循环中重复的部分。这意味着它每次只会在 y 循环执行时运行一次,并且是在 x 循环打印完一行数字后执行的。
训练任务 #2
最后的print()命令使用了四个空格缩进。试试看如果你将其缩进八个空格,或者不缩进的话会发生什么。每次都记录它运行了多少次,并观察缩进如何影响输出。
显示 3D 房间图像
现在你已经掌握了足够的地图知识,可以显示一个 3D 房间图像了。在第一章中,你学会了如何使用 Pygame Zero 将图像放置到屏幕上。让我们将这一知识与新获得的从room_map中获取数据的技能结合起来,这样我们就可以用图像而不是0和1来显示我们的地图。
点击文件 ▸ 新建文件来开始一个新的 Python 文件,然后输入 Listing 3-5 中的代码。你可以复制本章最新程序中的room_map数据。
listing3-5.py
room_map = [ [1, 1, 1, 1, 1],
[1, 0, 0, 0, 1],
[1, 0, 1, 0, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1]
]
➊ WIDTH = 800 # window size
➋ HEIGHT = 800
top_left_x = 100
top_left_y = 150
➌ DEMO_OBJECTS = [images.floor, images.pillar]
room_height = 7
room_width = 5
➍ def draw():
for y in range(room_height):
for x in range(room_width):
➎ image_to_draw = DEMO_OBJECTS[room_map[y][x]]
➏ screen.blit(image_to_draw,
(top_left_x + (x*30),
top_left_y + (y*30) - image_to_draw.get_height()))
Listing 3-5:显示房间 3D 图像的代码
将程序保存为listing3-5.py。你需要将它保存到escape文件夹中,因为程序会使用存储在该文件夹内的images文件夹中的文件。不要把文件保存在images 文件夹里:文件应该和 images 文件夹并列保存。如果你还没有下载Escape游戏文件,请参见“下载游戏文件”(第 7 页)获取下载说明。
listing3-5.py程序使用了 Pygame Zero,因此你需要进入命令行并输入指令 pgzrun listing3-5.py 来运行程序。关于如何运行使用 Pygame Zero 的程序,包括最终的Escape游戏,请参见“运行游戏”(第 9 页)。
listing3-5.py程序使用了Escape游戏的图像文件来创建房间的图像。图 3-1 显示了房间和它的单个柱子。Escape游戏采用了简化的 3D 透视法,我们可以看到物体的前面和顶部表面。房间前后的物体绘制成相同的大小。
在第一章中创建太空行走模拟器时,你看到了对象绘制顺序如何决定哪些对象在其他对象前面。在Escape游戏和 Listing 3-5 中,物体从房间的后面绘制到前面,这样我们就能创建 3D 效果。离观察者(坐在电脑前的人)更近的物体,看起来会在房间后方的物体前面。

图 3-1:你的第一个 3D 房间(左)和标注部件的同一房间(右)
理解房间是如何绘制的
现在让我们来看一下 listing3-5.py 程序是如何工作的。程序中的大部分内容你会在第一章和第二章中见过。WIDTH ➊ 和 HEIGHT ➋ 变量保存了窗口的大小,我们使用 draw() 函数告诉 Pygame Zero 要在屏幕上绘制什么 ➍。y 和 x 循环来自 Listing 3-4,为我们提供房间中每个位置的坐标。
我们不再像以前那样在 range() 函数中使用数字来告诉 Python 循环多少次,而是使用了新的变量 room_height 和 room_width。这两个变量存储了房间地图的大小,并告诉 Python 循环应当执行多少次。例如,如果我们将 room_height 变量更改为 10,y 循环将执行 10 次,遍历地图中的 10 行。room_width 变量控制了 x 循环重复的次数,类似地,我们就可以显示更宽的房间。
红色警报
如果你使用的房间宽度和高度大于实际的 room_map 数据,将会导致错误。
listing3-5.py 程序使用了来自 images 文件夹中的两张图像:一张地板砖(文件名为 floor.png)和一张墙壁柱子(名为 pillar.png),如图 3-2 所示。PNG(便携式网络图形) 是一种 Pygame Zero 可以使用的图像文件类型。PNG 格式允许图像的某些部分透明,这对于我们的 3D 游戏视角非常重要。否则,我们就无法通过植物的空隙看到背景景物,比如宇航员的周围会出现一个方形的光环。

图 3-2:用于创建你的第一个 3D 房间的图像
在 draw() 函数 ➍ 内,我们使用 y 和 x 循环依次查看房间地图中的每个位置。如你之前所见,我们可以通过访问 room_map``[y][x] 来查找地图中每个位置的数字。在这张地图中,数字为 1 代表墙壁柱子,数字为 0 代表空的地板空间。我们不再像之前那样在屏幕上打印数字,而是使用数字在 DEMO_OBJECTS 列表 ➎ 中查找对应的物品图像。该列表包含了我们的两个图像 ➌:地板砖在索引位置 0,墙壁柱子在索引位置 1。例如,如果 room_map 中我们查看的位置包含 1,我们就会从 DEMO_OBJECTS 列表的索引位置 1 中获取墙壁柱子的图像。我们将该图像存储在变量 image_to_draw ➎ 中。
然后,我们使用 screen.blit() 在屏幕上绘制该图像,并给定它的 x 和 y 坐标,表示我们希望将图像绘制到屏幕上的哪个像素位置 ➏。这条指令跨越了三行,以便更易于阅读。第二行和第三行的缩进量不重要,因为这些行都被 screen.blit() 的括号所包围。
计算每个物品的绘制位置
为了计算每幅构成房间的图像应该绘制的位置,我们需要在 ➏ 处进行一次计算。我们将了解这个计算的过程,但在此之前,我会先解释一下太空站是如何设计的。所有图像的设计都适应网格。我们用来衡量计算机图像的单位叫做 像素,它是你屏幕上能看到的最小点的大小。我们将每个网格的方格称为 砖块。每个砖块在屏幕上是 30 像素宽,30 像素高,大小与一个地砖相同。我们是按砖块来定位物体的,因此一把椅子可能位于 4 个砖块的下方和 4 个砖块的右侧,从左上角开始计算。
图 3-3 显示了我们刚刚创建的房间,并且在上面叠加了一个网格。每个地砖和柱子都是一块砖宽。柱子很高,因此它覆盖了三个砖块位置:墙柱的前表面高两块砖,而柱子的顶部表面覆盖了另外一块砖。

图 3-3:覆盖在第一个房间上的砖块网格
top_left_x 和 top_left_y 变量存储我们希望开始绘制房间的第一幅图像在窗口中的坐标。我们在本章中从不更改这些变量。我选择从 x 为 100 和 y 为 150 开始绘制,这样我们在房间图像周围会有一些边距。
为了计算绘制一块墙壁或地板的位置,我们需要将我们的地图位置(例如,x 方向上范围为 0 到 4)转换为窗口中的像素位置。
每个砖块空间是 30 像素见方,所以我们将 x 循环次数乘以 30,并加上 top_left_x 位置,以获得图像的 x 坐标。在 Python 中,* 符号用于乘法运算。top_left_x 的值为 100,所以第一幅图像绘制在 100 + (0 * 30),即 100 位置。第二幅图像绘制在 100 + (1 * 30),即 130,它位于第一幅图像的右侧一个砖块位置。第三幅图像绘制在 100 + (2 * 30),即 160。这些位置确保图像紧密地并排显示。
y 位置的计算方式类似。我们使用 top_left_y 作为垂直方向的起始位置,然后加上 y * 30,以便让图像精准地连接在一起。不同之处在于,我们需要减去正在绘制图像的高度,这样可以确保图像在底部对齐。因此,高大的物体可以从砖块空间中突出出来,遮挡其后面的风景或地砖,使房间显示出三维效果。如果我们没有在底部对齐图像,它们就会在顶部对齐,这会破坏 3D 效果。例如,第二行和第三行的地砖会遮挡住后墙的前表面。
训练任务 #3
现在你知道如何展示一个 3D 房间了,试着调整地图,改变房间的布局,添加新的柱子或地板空间。你可以编辑room_map数据,向地图中添加新的行或列。记得也要修改room_height和room_width变量。
你或许可以尝试设计一个有更多行的房间,并通过将柱子上的 1 替换为 0 来添加一个门。在最终的Escape游戏中,每个门的宽度将是三格。为了获得最佳效果,设计房间时最好使用奇数的宽度和高度,这样就可以将门居中放置在墙上。
图 3-4 展示了我设计的一个房间,宽度和高度均为 9。如果你愿意,可以尝试复制我的设计。我已添加了一个网格,以便更轻松地计算room_map列表的数据。墙柱从地面上升了两格,所以显示的网格高度为 11 格。请查看墙柱的底部,而不是顶部,以确定它们的位置。查看本章结尾处的代码,了解如何创建这个房间。

图 3-4:一种可能的新的房间设计
在真实的Escape游戏中,高大的墙柱仅会用于房间的边缘。如果它们位于房间中央,特别是当它们接触到后墙时,可能会显得有些奇怪。当我们在本书后续添加阴影时,房间中央的物体看起来不会像是漂浮在空中,这是模拟 3D 透视时的一大风险。
你准备好飞了吗?
勾选以下框框,确认你已经学习了本章的关键内容。
for循环会重复执行一段代码指定次数。
range()函数生成一系列数字。
你可以使用range()来告诉for循环要重复多少次。
for行末的冒号是必不可少的。
为了让 Python 知道在循环中哪些行需要重复,请使用四个空格缩进这些行。
一个循环嵌套在另一个循环中,称为嵌套循环。
图片被对齐到底部,以创造出从地面上升起的高大物体的 3D 错觉。
room_height和room_width变量存储Escape中的房间大小,并用于设置显示房间的循环。

第四章:创建空间站**

在本章中,你将为火星上的空间站绘制地图。通过在本章中添加的简单Explorer代码,你将能够查看每个房间的墙壁并开始找到方向。我们将使用列表、循环以及你在第一章、第二章和第三章中学到的技巧来创建地图数据并以 3D 方式展示房间。
自动化地图生成过程
我们当前的room_map数据的问题是数据量非常大。Escape游戏包括 50 个位置。如果你必须为每个位置输入room_map数据,这将需要大量时间,而且极其低效。举个例子,如果每个房间由 9×9 个方格组成,那么每个房间将有 81 条数据,总共有 4,050 条数据。仅房间数据就会占据本书的 10 页。
其中许多数据是重复的:0 表示地板和出口,1 表示周围的墙壁。从第三章中你已经知道,我们可以使用循环有效地管理重复数据。我们可以利用这一知识编写一个程序,当我们提供特定信息(例如房间大小和出口位置)时,它将自动生成room_map数据。
自动地图生成器工作原理
Escape程序将按以下方式工作:当玩家进入一个房间时,我们的代码将获取该房间的数据(其大小和出口位置),并将其转换为room_map数据。room_map数据将包括代表地板、周围墙壁以及应有出口位置的空隙的列和行。最终,我们将使用room_map数据以正确的位置绘制房间的地板和墙壁。
图 4-1 显示了空间站的地图。我将每个位置称为房间,尽管 1 到 25 号位置是站内地面上的区域,类似于花园。26 到 50 号是空间站内部的房间。
室内布局是一个简单的迷宫,包含许多走廊、死胡同和可探索的房间。当你自己制作地图时,尝试创建蜿蜒的路径和拐角供探索,即使地图并不大。一定要通过在每个走廊的尽头放置一个有用或吸引人的物品来奖励玩家的探索。玩家在探索游戏世界时通常感觉从左到右移动更为舒适,因此玩家的角色将从地图的左侧、第 31 号房间开始。
在外面,玩家可以自由走动,但围栏将阻止他们离开站区(或走出游戏地图)。由于空间站内部有一种幽闭的氛围,玩家在外面会体验到一种自由感,能够在空旷的地方漫游。

图 4-1:空间站地图
当你在玩最终的逃脱游戏时,你可以参考这张地图,但你可能会发现没有地图,或者自己制作一张地图,探索起来会更有趣。这张地图并没有标明门的位置,在最终的游戏中,玩家需要找到正确的钥匙卡才能进入地图的某些部分。
创建地图数据
让我们来创建地图数据。我们空间站中的房间都会相互连接,所以我们只需要存储一侧墙壁上出口的位置。例如,房间 31 右侧的出口和房间 32 左侧的出口实际上是连接这两个房间的同一个门口。我们不需要为这两个房间都指定这个出口。对于地图中的每个房间,我们将存储它是否在顶部或右侧有出口。程序可以自己计算是否存在底部或左侧的出口(稍后我会解释)。这种方法还确保地图的一致性,走过的出口不会消失。如果你能通过某个出口走出去,你也能从另一侧回来。
地图中的每个房间都需要以下数据:
-
房间的简短描述。
-
高度(以瓦片为单位),即房间从顶部到底部在屏幕上的大小。(这与地面到天花板的距离无关。)
-
宽度(以瓦片为单位),即房间从左到右在屏幕上的大小。
-
是否在顶部有出口(
True或False)。 -
是否在右侧有出口(
True或False)。
提示
True 和 False 被称为布尔值。在 Python 中,这些值必须以大写字母开头,并且不需要加引号,因为它们不是字符串。
我们用来测量房间大小的单位称为瓦片,因为它与地砖的大小相同。正如你在第三章中学到的,瓦片将是我们所有物体的基本测量单位。例如,房间中的一个物体,比如椅子或柜子,通常会占据一个瓦片的大小。在第三章(见图 3-1 和清单 3-5)中,我们制作了一个房间地图,该地图有七行,每行五个列表项,这样这个房间就会是七个瓦片高和五个瓦片宽。
不同大小的房间为地图增添了多样性:有些房间可以像走廊一样狭窄,有些则可以像公共休息室一样宽敞。为了适应我们的游戏窗口,房间的最大尺寸为 15 个瓦片高、25 个瓦片宽。然而,大型房间或包含很多物体的房间可能会在旧电脑上运行得较慢。
这是房间 26 的数据示例:它是一个狭窄的房间,高 13 个瓦片,宽 5 个瓦片,顶部有出口,但右侧没有出口(见图 4-1 中的地图)。
["The airlock", 13, 5, True, False]
我们给房间命名(或描述),分别为高度和宽度设置数字,并为顶部和右侧边缘是否有出口设置True和False值。在这个游戏中,每面墙只能有一个出口,并且该出口会自动放置在墙的中间。
当程序为隔壁的 27 号房间生成room_map数据时,它会检查 26 号房间,看看是否有右侧出口。由于 26 号房间没有右侧出口,程序就会知道 27 号房间没有左侧出口。
我们会将每个房间的数据列表存储在一个名为GAME_MAP的列表中。
编写 GAME_MAP 代码
点击文件 ▸ 新建文件,开始创建一个 Python 文件。输入清单 4-1 中的代码,开始构建空间站。将你的清单保存为listing4-1.py。
提示
记得在编写较长程序时定期保存你的工作。像许多应用程序一样,你可以按 CTRL-S 在 IDLE 中保存。
listing4-1.py
# Escape - A Python Adventure
# by Sean McManus / www.sean.co.uk
# Typed in by PUT YOUR NAME HERE
import time, random, math
###############
## VARIABLES ##
###############
WIDTH = 800 # window size
HEIGHT = 800
#PLAYER variables
➊ PLAYER_NAME = "Sean" # change this to your name!
FRIEND1_NAME = "Karen" # change this to a friend's name!
FRIEND2_NAME = "Leo" # change this to another friend's name!
current_room = 31 # start room = 31
➋ top_left_x = 100
top_left_y = 150
➌ DEMO_OBJECTS = [images.floor, images.pillar, images.soil]
###############
## MAP ##
###############
➍ MAP_WIDTH = 5
MAP_HEIGHT = 10
MAP_SIZE = MAP_WIDTH * MAP_HEIGHT
➎ GAME_MAP = [ ["Room 0 - where unused objects are kept", 0, 0, False, False] ]
outdoor_rooms = range(1, 26)
➏ for planetsectors in range(1, 26): #rooms 1 to 25 are generated here
GAME_MAP.append( ["The dusty planet surface", 13, 13, True, True] )
➐ GAME_MAP += [
#["Room name", height, width, Top exit?, Right exit?]
["The airlock", 13, 5, True, False], # room 26
["The engineering lab", 13, 13, False, False], # room 27
["Poodle Mission Control", 9, 13, False, True], # room 28
["The viewing gallery", 9, 15, False, False], # room 29
["The crew's bathroom", 5, 5, False, False], # room 30
["The airlock entry bay", 7, 11, True, True], # room 31
["Left elbow room", 9, 7, True, False], # room 32
["Right elbow room", 7, 13, True, True], # room 33
["The science lab", 13, 13, False, True], # room 34
["The greenhouse", 13, 13, True, False], # room 35
[PLAYER_NAME + "'s sleeping quarters", 9, 11, False, False], # room 36
["West corridor", 15, 5, True, True], # room 37
["The briefing room", 7, 13, False, True], # room 38
["The crew's community room", 11, 13, True, False], # room 39
["Main Mission Control", 14, 14, False, False], # room 40
["The sick bay", 12, 7, True, False], # room 41
["West corridor", 9, 7, True, False], # room 42
["Utilities control room", 9, 9, False, True], # room 43
["Systems engineering bay", 9, 11, False, False], # room 44
["Security portal to Mission Control", 7, 7, True, False], # room 45
➑ [FRIEND1_NAME + "'s sleeping quarters", 9, 11, True, True], # room 46
[FRIEND2_NAME + "'s sleeping quarters", 9, 11, True, True], # room 47
["The pipeworks", 13, 11, True, False], # room 48
["The chief scientist's office", 9, 7, True, True], # room 49
["The robot workshop", 9, 11, True, False] # room 50
]
# simple sanity check on map above to check data entry
➒ assert len(GAME_MAP)-1 == MAP_SIZE, "Map size and GAME_MAP don't match"
清单 4-1: GAME_MAP 数据
让我们仔细看一下这段设置房间地图数据的代码。请记住,在我们构建逃脱游戏时,我们会不断地往程序中添加内容。为了帮助你在程序中找到自己的位置,我会用类似这样的标题来标记不同的部分:
###############
## VARIABLES ##
###############
#符号标记了注释,并告诉 Python 忽略同一行中#之后的内容,因此游戏即使没有这些注释也能正常运行。注释将帮助你更容易地理解自己在代码中的位置,以及在程序变得更大时需要在哪里添加新的指令。我使用注释符号绘制了框架,使得标题在浏览程序代码时更加突出。
三名宇航员驻扎在空间站上,你可以在代码中个性化他们的名字 ➊。将PLAYER_NAME改为你自己的名字,并为FRIEND1_NAME和FRIEND2_NAME变量添加两个朋友的名字。在整个代码中,我们将在需要使用你朋友名字的地方使用这些变量:例如,每个宇航员都有自己的寝室。我们现在需要设置这些变量,因为稍后在程序中设置某些房间描述时会用到它们。你会带谁一起去火星呢?
程序还设置了一些我们在本章后面需要用来绘制房间的变量:top_left_x和top_left_y变量 ➋ 指定了绘制房间的起始位置;而DEMO_OBJECTS列表包含了要使用的图像 ➌。
首先,我们设置了包含地图高度、宽度和总体大小的变量 ➍。我们创建了GAME_MAP列表 ➎ 并为它提供了房间 0 的数据:这个房间用于存储游戏中尚未出现的物品,因为玩家还没有发现或创造它们。它不是玩家可以访问的真实房间。
然后我们使用一个循环➏,为构成复合体的 25 个行星表面房间添加相同的数据。range(1, 26)函数用于重复 25 次。第一个数字是我们希望开始的数字,第二个是我们希望结束的数字再加 1(range()不包括你给出的最后一个数字,记住这一点)。每次循环时,程序都会将相同的数据添加到GAME_MAP的末尾,因为所有的行星表面“房间”大小相同,并且每个方向都有出口。每个表面房间的数据如下:
["The dusty planet surface", 13, 13, True, True]
当这个循环完成后,GAME_MAP将包括房间 0,并且房间 1 到 25 将拥有相同的“尘土行星表面”数据。我们还设置了outdoor_rooms范围,用于存储房间号 1 到 25。当我们需要检查一个房间是位于空间站内还是外时,会用到这个范围。
最后,我们将房间 26 到 50 添加到GAME_MAP中➐。我们通过使用+=将一个新列表添加到GAME_MAP的末尾。这个新列表包含剩余房间的数据。每个房间的数据都不同,所以我们需要分别输入它们的数据。你之前看到过房间 26 的信息:数据包括房间名称、房间的高度和宽度,以及它是否有上方和右方的出口。每个房间的数据都是一个列表,因此它的开始和结束都有方括号。在每个房间数据的末尾(除了最后一个)我们必须用逗号将其与下一个数据分开。我还在每行的末尾加了房间号的注释,帮助跟踪房间号。这些注释在你开发游戏时会很有帮助。像这样注释代码是一个好习惯,这样当你再次查看代码时就能理解它。
房间 46 和 47 将变量FRIEND1_NAME和FRIEND2_NAME添加到房间描述中,因此你将有两个房间,它们的名字可能是“Karen 的睡眠区”,使用你朋友的名字➑。除了使用+符号来加法和合并列表外,你还可以用它来合并字符串。
在listing4-1.py的结尾,我们使用assert()进行简单检查,确保地图数据是合理的➒。我们检查GAME_MAP的长度(地图数据中的房间数量)是否与我们在➍通过将其宽度与高度相乘计算出的地图大小相同。如果不同,说明我们缺少一些数据或数据太多。
我们必须从GAME_MAP的长度中减去 1,因为它还包括房间 0,而在我们计算地图大小时没有包含该房间。这个检查不能捕获所有错误,但它可以告诉你在输入地图数据时是否遗漏了一行数据。只要可能,我会尽量包含像这样的简单测试,帮助你在输入程序代码时检查错误。
测试和调试代码
通过点击运行 ▸ 运行模块或者按 F5(快捷键)来运行listing4-1.py。应该不会发生什么。Shell 窗口只会显示一条消息,显示"RESTART:"以及你的文件名。原因是我们要求程序做的只是设置一些变量和一个列表,所以没有任何可见的东西。但如果你在输入代码时犯了错误,你也可能在 Shell 窗口看到一条红色的错误信息。如果你看到错误,请仔细检查以下细节:
-
引号位置正确吗?在 Python 程序窗口中,字符串是绿色的,所以寻找大面积的绿色,表示你没有关闭字符串。如果房间描述是黑色的,说明你没有打开字符串。这两种情况都表明缺少引号。
-
你在正确的位置使用了正确的括号和圆括号吗?在这个列表中,方括号用来括住列表项,圆括号(括号)用于函数,例如
range()和append()。大括号{…}完全没有使用。 -
你是否漏掉了任何括号或圆括号?检查的简单方法是数一下打开和关闭的括号和圆括号的数量。每个打开的括号或圆括号应该都有一个相同形状的闭合括号或圆括号。
-
你必须按照打开括号和圆括号的顺序来关闭它们。如果你有一个打开的圆括号,然后是一个打开的方括号,你必须先用闭合的方括号关闭它们,然后再用闭合的圆括号。这个格式是正确的:([ … ])。这个格式是错误的:([ … ) ]。
-
你的逗号位置正确吗?记住,
GAME_MAP中每个房间的列表后必须有一个逗号,用来分隔下一个房间的数据(除了最后一个房间)。
提示
为什么不请朋友帮你一起做这个游戏?程序员常常成对工作,互相帮助提出创意,也许最重要的是,两个人的眼睛检查一切。你们还可以轮流打字!
从数据生成房间
现在,空间站地图存储在我们的GAME_MAP列表中。下一步是添加一个功能,从GAME_MAP中获取当前房间的数据,并将其扩展到room_map列表中,逃脱游戏将使用这个列表来查看房间中每个位置的内容。room_map列表始终存储玩家当前所在房间的信息。当玩家进入不同的房间时,我们用新房间的地图替换room_map中的数据。书中的后续部分,我们会往room_map中添加景物和道具,这样玩家也能与物品互动。
room_map数据是由我们将要创建的一个函数generate_map()生成的,见列表 4-2。
将列表 4-2 中的代码添加到列表 4-1 的末尾。灰色部分的代码显示了列表 4-1 的结束位置。确保所有的缩进都是正确的。缩进决定了代码属于get_floor_type()还是generate_map()函数,而某些代码缩进得更深,告诉 Python 它属于哪个if或for命令。
将你的程序保存为listing4-2.py,然后点击运行 ▸ 运行模块,运行它并检查 shell 中是否有任何错误信息。
红色警报
不要仅仅从列表 4-2 中的代码开始新的程序:确保将列表 4-2 添加到列表 4-1 的末尾。在本书中跟随步骤时,你将逐步向现有程序中添加内容,来构建逃脱游戏。
listing4-2.py
--snip--
# simple sanity check on map above to check data entry
assert len(GAME_MAP)-1 == MAP_SIZE, "Map size and GAME_MAP don't match"
###############
## MAKE MAP ##
###############
➊ def get_floor_type():
if current_room in outdoor_rooms:
return 2 # soil
else:
return 0 # tiled floor
def generate_map():
# This function makes the map for the current room,
# using room data, scenery data and prop data.
global room_map, room_width, room_height, room_name, hazard_map
global top_left_x, top_left_y, wall_transparency_frame
➋ room_data = GAME_MAP[current_room]
room_name = room_data[0]
room_height = room_data[1]
room_width = room_data[2]
➌ floor_type = get_floor_type()
if current_room in range(1, 21):
bottom_edge = 2 #soil
side_edge = 2 #soil
if current_room in range(21, 26):
bottom_edge = 1 #wall
side_edge = 2 #soil
if current_room > 25:
bottom_edge = 1 #wall
side_edge = 1 #wall
# Create top line of room map.
➍ room_map=[[side_edge] * room_width]
# Add middle lines of room map (wall, floor to fill width, wall).
➎ for y in range(room_height - 2):
room_map.append([side_edge]
+ [floor_type]*(room_width - 2) + [side_edge])
# Add bottom line of room map.
➏ room_map.append([bottom_edge] * room_width)
# Add doorways.
➐ middle_row = int(room_height / 2)
middle_column = int(room_width / 2)
➑ if room_data[4]: # If exit at right of this room
room_map[middle_row][room_width - 1] = floor_type
room_map[middle_row+1][room_width - 1] = floor_type
room_map[middle_row-1][room_width - 1] = floor_type
➒ if current_room % MAP_WIDTH != 1: # If room is not on left of map
room_to_left = GAME_MAP[current_room - 1]
# If room on the left has a right exit, add left exit in this room
if room_to_left[4]:
room_map[middle_row][0] = floor_type
room_map[middle_row + 1][0] = floor_type
room_map[middle_row - 1][0] = floor_type
➓ if room_data[3]: # If exit at top of this room
room_map[0][middle_column] = floor_type
room_map[0][middle_column + 1] = floor_type
room_map[0][middle_column - 1] = floor_type
if current_room <= MAP_SIZE - MAP_WIDTH: # If room is not on bottom row
room_below = GAME_MAP[current_room+MAP_WIDTH]
# If room below has a top exit, add exit at bottom of this one
if room_below[3]:
room_map[room_height-1][middle_column] = floor_type
room_map[room_height-1][middle_column + 1] = floor_type
room_map[room_height-1][middle_column - 1] = floor_type
列表 4-2:生成 room_map 数据
你可以构建逃脱游戏,甚至制作自己的游戏地图,而不需要理解room_map代码是如何工作的。但如果你有兴趣,继续阅读,我会带你一步步理解。
房间生成代码的工作原理
让我们先回顾一下generate_map()函数需要做的事情。给定房间的高度和宽度,以及出口的位置,我们希望它生成一个房间地图,可能看起来像这样:
[
[1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
]
这是地图上的 31 号房间,玩家开始游戏的房间。它的高度是 7 格,宽度是 11 格,顶部和右侧都有出口。地面空间(以及墙上的出口)用 0 表示。房间周围的墙壁用 1 表示。图 4-2 展示了相同房间的网格布局,列表的索引号显示在顶部和左侧。

图 4-2:一个表示 31 号房间的网格;1 表示墙柱,0 表示空的地面空间。
玩家当前所在的房间号存储在current_room变量中,你在程序的VARIABLES部分设置了这个变量(见列表 4-1)。generate_map()函数首先从GAME_MAP ➋中收集当前房间的数据,并将其放入一个名为room_data的列表中。
如果你回想一下我们设置GAME_MAP时的情况,room_data列表中的信息现在将类似于此:
["The airlock", 13, 5, True, False]
这种列表格式使我们能够通过取该列表中索引为 0 的第一个元素来设置room_name。我们可以通过取接下来的元素,找到房间的高度(索引 1)和宽度(索引 2)。generate_map()函数将高度和宽度信息存储在room_height和room_width变量中。
创建基本房间形状
下一步是设置我们用来构建房间的材料,并使用这些材料创建基本的房间形状。我们稍后会添加出口。每个房间将使用三个元素:
-
地面类型,它存储在
floor_type变量中。在空间站内,我们使用地板砖(在room_map中表示为 0),在外部使用土壤(在room_map中表示为 2)。 -
边缘类型,出现在房间边缘的每个空间中。对于室内房间,这是一个墙柱,表示为 1。对于室外房间,这是土壤。
-
底边类型,这是站内的墙壁,外部通常是土壤。外部复合体的最底行,与空间站接触的地方是一个特殊情况,因为这里可见站墙,所以
bottom_edge类型是墙柱(见图 4-3)。![image]()
图 4-3:根据房间在空间站复合体中的位置,使用不同的材料来构建房间的边缘和底边。(注意,宇航员和额外的景物暂时不会出现在你的游戏中。)
我们使用一个名为get_floor_type()的函数➊来查找房间的正确地面类型。函数可以通过return指令将信息返回到程序的其他部分,就像在这个函数中看到的那样。get_floor_type()函数检查current_room值是否位于outdoor_rooms范围内。如果是,函数返回数字 2,代表火星土壤。否则,它返回数字 0,代表瓷砖地面。这个检查在一个单独的函数中,这样程序的其他部分也可以使用它。generate_map()函数将get_floor_type()返回的数字放入floor_type变量中。通过一条指令➌,generate_map()将floor_type变量设置为get_floor_type()返回的值,并指示get_floor_type()函数现在也要执行。
generate_map()函数还为bottom_edge和side_edge设置了变量。这些变量存储将用于构建房间边缘的材料类型,如图 4-3 所示。侧边材料用于顶部、左侧和右侧,而底边材料用于底边。如果房间号在 1 到 20 之间(含),则是常规的行星表面房间,底边和边缘是土壤。在房间号在 21 到 25 之间时,它是一个与空间站底部接触的行星表面房间。这是一个特殊情况:侧边材料是土壤,但底边由墙柱构成。如果房间号大于 25,则侧边和底边由墙柱构成,因为这是一个室内房间。(你可以在图 4-1 中检查这些房间号是否合理。)
我们开始创建room_map列表,首先构建顶部行,这一行将是外部的土壤或内部的后墙。顶部行由相同的材料构成,所以我们可以使用一个快捷方式。可以在终端中尝试这个:
>>> print([1] * 10)
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
print()指令中的[1]是一个只包含一个元素的列表。当我们将其乘以 10 时,得到的是一个包含该元素 10 次的列表。在我们的程序中,我们将使用的边缘类型乘以房间的宽度 ➍。如果顶部边缘有出口,我们稍后会添加这一部分。
房间的中间行是通过一个循环 ➎生成的,该循环将每一行依次添加到room_map的末尾。房间中的所有中间行都是相同的,构成这些行的内容如下:
-
房间左侧的边缘瓦片(可以是墙或土壤)。
-
中间的地板。我们可以在这里再次使用我们的快捷方式。我们将
floor_type乘以房间中间空间的大小。也就是room_width减去 2,因为有两个边缘空间。 -
右侧的边缘瓦片。
然后添加底部的那一行 ➏,并且它的生成方式与顶部的一行相同。
添加出口
接下来,我们在需要的地方在墙上添加出口。我们将出口放置在墙的中间,所以我们首先通过将房间的高度和宽度除以 2 来计算中间的行和中间的列的位置 ➐。有时这个计算会得到一个带小数的数字。我们需要一个整数作为索引位置,因此我们使用int()函数去掉小数部分 ➐。int()函数将小数转换为整数(整数)。
我们首先检查右侧是否有出口 ➑。记住,room_data包含这个房间的信息,它最初是从GAME_MAP获取的。值room_data[4]告诉我们这个房间的右边是否有出口。这个指令:
if room_data[4]:
是以下指令的简写:
if room_data[4] == True:
我们使用==来检查两个事物是否相同。布尔值之所以常常是处理数据的一个很好的选择,是因为它们让代码更容易阅读和编写,正如这个例子所展示的那样。
当有右侧出口时,我们会将右墙中间的三个位置从边缘类型改为地板类型,从而在墙上留下一个空隙。值room_width-1找到右边缘的x位置:我们减去 1 是因为索引从 0 开始。例如,在图 4-2 中,你可以看到房间的宽度是 11 个瓦片,但右墙的索引位置是 10。在行星表面上,这段代码并不会改变任何东西,因为那里没有墙可以留空隙。但为了简化程序,我们还是让它添加地板瓦片,这样就不需要为特殊情况编写代码了。
在检查左侧墙壁是否需要出口之前,我们确保房间不在地图的左边缘,因为左边缘不能有出口 ➒。% 操作符可以得到两个数相除后的余数。如果我们使用 % 操作符将当前房间号除以地图宽度 5,当房间位于左边缘时,我们会得到 1。左边缘的房间号包括 1、6、11、16、21、26、31、36、41 和 46。所以只有当余数不是 1 时,我们才继续检查左侧出口(!= 表示“不等于”)。
为了查看这个房间是否需要左侧出口,我们通过将当前房间号减去 1 来计算出墙的另一边是哪一个房间。然后我们检查该房间是否有右侧出口。如果有,则说明当前房间需要左侧出口,我们就添加它。
上下出口的添加方式类似 ➓。我们直接检查 room_data 来判断房间顶部是否有出口,如果有,则在该墙壁中添加一个缝隙。我们还可以检查下方的房间,看看是否应该在该房间添加底部出口。
测试程序
当你运行程序时,可以确认在 Python shell 中没有看到任何错误。你还可以通过生成地图并从 shell 中打印出来的方式来检查程序是否工作正常,像这样:
>>> generate_map()
>>> print(room_map)
[[1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1]]
current_room 变量默认设置为房间 31,这是游戏中的起始房间,因此打印出来的是 room_map 数据。从我们的 GAME_MAP 数据(以及 图 4-2)中可以看到,这个房间有 7 行和 11 列,输出结果也证实我们有 7 个列表,每个列表包含 11 个数字:完美。更重要的是,我们可以看到第一行有四个墙柱,三个空格,然后是另外四个墙柱,因此该函数在此处添加了一个出口,正如我们所期望的那样。三个列表的最后一个数字是 0,表示右侧有出口。看起来程序运行正常!
训练任务 #1
你可以通过 shell 更改 current_room 的值来打印不同的房间。尝试输入不同的房间值,重新生成地图并打印出来。检查输出结果与地图以及 GAME_MAP 代码是否匹配,确保结果符合预期。以下是一个示例:
>>> current_room = 45
>>> generate_map()
>>> print(room_map)
[[1, 1, 0, 0, 0, 1, 1], [1, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 1],
[1, 1, 0, 0, 0, 1, 1]]
当你为其中一个行星表面房间输入值时会发生什么?
在 3D 中探索空间站
让我们将房间地图转换为房间!我们将把第三章 中将房间地图转换为 3D 房间的代码与提取游戏地图中的房间地图的代码结合起来。然后我们可以参观空间站并开始定位。
我们程序中的 Explorer 功能将使我们能够查看空间站上的所有房间。我们将为它在程序中创建一个单独的 EXPLORER 部分。这是一个临时措施,方便我们快速查看结果。我们将在第七章 和第八章 中,用更好的代码来替换 Explorer,以便查看房间。
将清单 4-3 中的代码添加到你程序的末尾,放在清单 4-2 中显示为灰色的指令之后。然后将程序保存为listing4-3.py。记得将其与本书的其他程序一起保存在escape文件夹中,这样images文件夹才能正确放置(参见 “下载游戏文件” 第 7 页)。
listing4-3.py
room_map[room_height-1][middle_column] = floor_type
room_map[room_height-1][middle_column + 1] = floor_type
room_map[room_height-1][middle_column - 1] = floor_type
###############
## EXPLORER ##
###############
def draw():
global room_height, room_width, room_map
➊ generate_map()
screen.clear()
➋ for y in range(room_height):
for x in range(room_width):
image_to_draw = DEMO_OBJECTS[room_map[y][x]]
screen.blit(image_to_draw,
(top_left_x + (x*30),
top_left_y + (y*30) - image_to_draw.get_height()))
➌ def movement():
global current_room
old_room = current_room
if keyboard.left:
current_room -= 1
if keyboard.right:
current_room += 1
if keyboard.up:
current_room -= MAP_WIDTH
if keyboard.down:
current_room += MAP_WIDTH
➍ if current_room > 50:
➎ current_room = 50
if current_room < 1:
current_room = 1
➏ if current_room != old_room:
➐ print("Entering room:" + str(current_room))
➑ clock.schedule_interval(movement, 0.1)
清单 4-3:Explorer 代码
清单 4-3 中的新添加内容应该对你来说比较熟悉。我们调用generate_map()函数来为当前房间创建room_map数据➊。然后,我们使用第三章中的清单 3-5 创建的代码来显示它➋。我们使用键盘控制来改变current_room变量➌,类似于我们在第一章中如何改变太空行走宇航员的x和y位置(参见清单 1-4)。要在地图中上下移动一行,我们通过游戏地图的宽度来改变current_room的数字。例如,要从 32 号房间上移一行,我们减去 5 进入 27 号房间(参见图 4-1)。如果房间号发生变化,程序会打印出current_room变量➏。str()函数将房间号转换为字符串➐,以便与"Entering room:"字符串连接。若不使用str()函数,无法将数字与字符串连接。
最后,我们安排movement函数按固定间隔运行➑,就像我们在第一章中所做的那样。这次,我们将函数运行之间的间隔时间加长(0.1 秒),因此按键响应会稍微迟钝一些。
从命令行进入你的escape文件夹,并使用命令行运行程序,命令为pgzrun listing4-3.py。
屏幕应该与图 4-4 相似,显示的是 31 号房间的墙壁和门道。

图 4-4:Explorer 以 3D 展示你的起始房间。
现在,你可以使用箭头键来探索地图。程序将为你绘制房间,并允许你通过按箭头键进入邻近房间。此时,你只能看到房间的外壳:墙壁和地板。稍后我们将在房间中添加更多物体以及你的角色。
目前,你可以朝任何方向走,包括穿过墙壁:程序不会检查任何移动错误。如果你走出地图的左边,你会在右边重新出现,高一个行。如果你走出右边,你会在左边重新出现,低一个行。如果你试图走出地图的顶部或底部,程序会将你带回房间 1(顶部)或房间 50(底部)。例如,如果房间号超过(>)50 ➍,它会重置为 50 ➎。在这段代码中,我降低了键盘的灵敏度,以减少快速穿过房间的风险。如果你发现控制反应迟钝或缓慢,可能需要稍微按住键更长时间。
探索太空站,并将你在屏幕上看到的内容与图 4-1 中的地图进行对比。如果发现任何错误,返回GAME_MAP数据检查数据,然后再检查generate_map()函数,确保其输入正确。为了帮助你跟随地图,当你进入新房间时,其编号会出现在你输入pgzrun命令的命令行窗口中,如图 4-5 所示。

图 4-5:命令行窗口告诉你正在进入哪个房间。
同时,检查出口是否存在于两侧:如果你通过一扇门,而从另一侧看时它不在那里,说明generate_map()输入错误。在开始调试之前,先跟随地图,确保自己没有走出地图的边界并从另一侧返回。此时,花时间确保你的地图数据和函数是正确的非常值得,因为破损的地图数据会使得无法完成Escape游戏!
训练任务 #2
为了更好地享受玩Escape并解决谜题,我建议你使用我为游戏地图提供的数据。在完成游戏并决定重新设计之前,最好不要更改数据。否则,物品可能会出现在你无法到达的位置,导致游戏无法完成。
但是,你可以安全地扩展地图。最简单的方法是,在地图底部再添加一排房间,确保至少有一扇门将新房间与现有的底部行连接。记得修改MAP_HEIGHT变量。你还需要将Explorer代码(listing4-3.py)中的数字 50 更改为你的最高房间编号(见 ➍ 和 ➎)。为什么不现在就加一个走廊呢?
制作你自己的地图
完成构建和游玩Escape后,你可以使用这段代码自定义地图或设计自己的游戏布局。
如果你想为房间 1 到 25 添加自己的地图数据,可以删除自动生成它们数据的代码(见 ➏ 在 Listing 4-1 中)。然后你可以为这些房间添加自己的数据。
或者,如果你不想使用星球表面的位置,可以直接阻止出口通向它们。通往星球表面的出口位于第 26 号房间。修改该房间在GAME_MAP列表中的条目,使其没有顶部出口。你可以从第 26 号房间开始使用房间编号,向下扩展地图,制作一个完全在室内的游戏。这样,你就不需要对星球表面进行任何代码修改。
如果你从逃脱游戏地图中移除一个门(包括第 26 号房间的门),你可能还需要移除一扇门。房间顶部和底部的一些出口会有门将其封闭。(我们将在第十一章中添加门到逃脱游戏中。)
你适合飞行吗?
勾选以下框,以确认你已经学会了本章的关键知识点。
GAME_MAP列表存储逃脱游戏的主要地图数据。
GAME_MAP只需要存储房间顶部和右侧的出口。
当玩家访问一个房间时,generate_map()函数会为当前房间生成room_map列表。room_map列表描述了房间内墙壁和物体的位置。
位置 1 到 25 在星球表面,循环会生成它们的地图数据。位置 26 到 50 是太空站房间,你需要手动输入它们的数据。
我们使用注释帮助我们在逃脱程序列表中找到位置。
当使用脚本模式的程序添加数据时,你可以使用 shell 检查列表和变量的内容,以确保程序正常工作。记得先运行程序来设置数据!
Explorer代码允许你使用箭头键查看游戏地图中的每个房间。
确保游戏地图与图 4-1 相匹配非常重要。否则,玩家可能无法完成逃脱游戏。你可以使用Explorer程序来检查这一点。

第五章:准备空间站设备**

现在,空间站的墙壁已经搭建完成,我们可以开始安装设备了。我们需要有关各种设备的详细信息,包括家具、生存系统和实验机械。在本章中,你将添加空间站上所有物品的信息,包括它们的图片和描述。你还将尝试设计自己的房间,并通过你在第四章中创建的Explorer程序查看它。
创建一个简单的行星字典
为了存储关于空间站设备的信息,我们将使用一个编程概念,称为字典。字典有点像列表,但内置了搜索引擎。让我们更仔细地看看它是如何工作的。
理解列表和字典之间的区别
和纸质字典一样,你可以使用单词或短语在 Python 字典中查找信息。这个单词或短语叫做键,而与键相关的信息叫做值。与纸质字典不同,Python 字典中的条目可以是无序的,它们不必是按字母顺序排列的。Python 可以直接定位到你需要的条目,无论它在哪里。
假设你有一个包含以前太空任务信息的列表。你可以通过以下这行代码获取列表中的第一个项目:
print(mission_info[0])
如果mission_info是一个字典而不是列表,你可以使用任务名称代替索引号来获取该任务的信息,像这样:
print(mission_info["Apollo 11"])
键可以是一个单词或短语,也可以是一个数字。我们将从使用单词开始,因为这样更容易理解列表和字典之间的区别。
制作天文学备忘单字典
所有宇航员都需要对太阳系有良好的理解,因此在构建我们的第一个字典时,让我们了解一下行星。我们将使用行星的名称作为键,并将每个名称与该行星的信息连接起来。
看一下清单 5-1,它创建了一个名为planets的字典。当你创建字典时,使用大括号{}来标记字典的开始和结束,而不是用于列表的方括号。
字典中的每个条目由键、后跟冒号,再然后是该条目的信息组成。和列表一样,我们用逗号分隔条目,并将文本部分用双引号括起来。
在 IDLE 中打开一个新文件(文件 ▸ 新建文件),并输入以下程序。将其保存为listing5-1.py。
listing5-1.py
planets = { "Mercury": "The smallest planet, nearest the Sun",
"Venus": "Venus takes 243 days to rotate",
"Earth": "The only planet known to have native life",
"Mars": "The Red Planet is the second smallest planet",
"Jupiter": "The largest planet, Jupiter is a gas giant",
"Saturn": "The second largest planet is a gas giant",
"Uranus": "An ice giant with a ring system",
"Neptune": "An ice giant and farthest from the Sun"
}
➊ while True:
➋ query = input("Which planet would you like information on? ")
➌ print(planets[query])
清单 5-1:你的第一个字典程序
这个程序没有使用 Pygame Zero,因此你可以通过点击 IDLE 窗口顶部的Run ▸ Run Module来运行它。(即使你通过pgzrun运行,它仍然能正常工作,但使用菜单更方便。)当你运行程序时,它会使用input()内置函数 ➋询问你想要查询哪个行星的信息。试着输入“Earth”或“Jupiter”作为行星名称。
Which planet would you like information on? Earth
The only planet known to have native life
Which planet would you like information on? Jupiter
The largest planet, Jupiter is a gas giant
你输入的每个行星名称都会存储在变量query中。然后,变量会用来在planets字典 ➌ 中查找该行星的信息。与我们在列表中使用索引号不同,这里我们使用输入的单词来获取信息,这个单词被存储在query变量中。
在 Python 中,我们可以使用while ➊循环来重复一组指令。与我们用来重复指定次数的for循环不同,while循环通常会一直重复,直到某些条件发生变化。在游戏中,while命令通常会检查某个变量,决定是否继续重复指令。例如,while lives > 0指令可以让游戏在玩家生命值大于 0 时继续进行。当lives变量变为 0 时,循环中的指令会停止重复。
我们在listing5-1.py中使用的while True命令会一直重复,因为它意味着“只要True为True”,这总是成立的。为了使这个while True命令正常工作,确保你将True的 T 大写,并且在行末加上冒号。
在while命令下,我们使用四个空格来缩进应该重复的指令。这里,我们已经缩进了要求你输入行星名称并给出行星信息的行,因此它们是需要重复的指令。在你输入行星名称并获取信息后,程序会继续要求你输入另一个行星名称,一直重复下去,直到你按下 CTRL-C 停止程序为止。
尽管这个程序可以正常运行,但它还不完整。如果你输入一个不在字典中的行星名称,可能会得到一个没有帮助的错误信息。让我们修复代码,让它返回一个有用的提示信息。
字典错误防护
当你输入一个字典中不存在的键时,你会看到一个错误信息。Python 会查找完全匹配的项。因此,如果你试图查找字典中不存在的项或拼写错误,程序就不会返回你想要的信息。
字典的键名像变量名一样区分大小写,所以如果你输入earth而不是Earth,程序就会崩溃。如果你输入一个不存在的行星,结果会是这样:
Which planet would you like information on? Pluto
Traceback (most recent call last):
File "C:\Users\Sean\Documents\Escape\listing5-1.py", line 13, in <module>
print(planets[query])
KeyError: 'Pluto'
>>>
可怜的冥王星!经过 76 年的服务,它在 2006 年被取消了行星资格,因此它不在我们的planets字典中。
训练任务 #1
你能为冥王星在字典中添加一项吗?请特别注意引号、冒号和逗号的位置。你可以在字典中的任何位置添加它。
当程序查找字典中不存在的项时,它会停止运行并返回到 Python shell 提示符。为了避免这种情况,我们需要程序在尝试使用某个单词之前,检查它是否是字典中的键之一。
你可以通过输入字典名称后跟一个点和 keys() 来查看字典中有哪些键。这个技术术语叫做 方法。简单来说,方法是一组指令,你可以通过句点将它附加到数据上。请在 Python shell 中运行以下代码:
>>> print(planets.keys())
dict_keys(['Mars', 'Pluto', 'Jupiter', 'Earth', 'Uranus', 'Saturn', 'Mercury',
'Neptune', 'Venus'])
你可能会注意到一个奇怪的地方。当我完成训练任务 #1 时,我把冥王星作为字典中的最后一项添加进去。但在这个输出中,它排在我的键列表的第二个位置。当你向列表中添加项时,它们会被放在末尾,但在字典中情况并非总是如此。这取决于你使用的是哪个版本的 Python。(最新版本会保持字典项按添加顺序排列。)不过,正如前面提到的,字典中键的顺序并不重要。Python 会自动确定键的位置,所以你不需要去思考它。
为了防止程序在用户请求字典中没有的行星信息时崩溃,请按照 列表 5-2 中的新增代码修改程序。
listing5-2.py
--snip--
while True:
query = input("Which planet would you like information on? ")
➊ if query in planets.keys():
➋ print(planets[query])
else:
➌ print("No data available! Sorry!")
列表 5-2:字典查找的错误防护
将程序保存为 listing5-2.py,然后通过点击 运行 ▸ 运行模块 来运行它。通过正确输入一个行星名称来检查程序是否正常工作,然后再输入一个不在键列表中的行星名称。这是一个例子:
Which planet would you like information on? Venus
Venus takes 243 days to rotate
Which planet would you like information on? Tatooine
No data available! Sorry!
我们通过在程序尝试使用 query 中的键之前,检查该键是否存在于字典中,从而防止程序崩溃 ➊。如果该键存在,我们像之前一样使用查询 ➋。否则,我们会向用户发送一条消息,告诉他们我们字典中没有该信息 ➌。现在程序变得更加友好。
将列表放入字典中
我们的行星字典目前有些有限。如果我们想添加更多信息,比如行星是否有环和它有多少颗卫星呢?为此,我们可以使用列表来存储关于行星的多个信息,然后将该列表放入字典中。
例如,以下是金星的新条目:
"Venus": ["Venus takes 243 days to rotate", False, 0]
方括号标记了列表的开始和结束,列表中有三个项目:一个简短的描述,一个表示行星是否有环的 True 或 False 值,以及它拥有的卫星数量。因为金星没有环,所以第二项是 False。它也没有卫星,所以第三项是 0。
红色警报
True 和 False 的值需要以大写字母开头,并且不应该加引号。当你在 IDLE 中正确输入时,单词会变成橙色。
更改你的字典代码,使每个键都有一个列表,如示例 5-3 所示,其余代码保持不变。记住,字典条目之间由逗号分隔,因此所有列表的右括号后面都需要有逗号,除了最后一个列表。将更新后的程序保存为listing5-3.py。
我也为冥王星加入了信息。有些人猜测冥王星可能有环,探索仍在进行中。当你阅读本书时,我们对冥王星的理解可能已经发生了变化。
listing5-3.py
planets = { "Mercury": ["The smallest planet, nearest the Sun", False, 0],
"Venus": ["Venus takes 243 days to rotate", False, 0],
"Earth": ["The only planet known to have native life", False, 1],
"Mars": ["The second smallest planet", False, 2],
"Jupiter": ["The largest planet, a gas giant", True, 67],
"Saturn": ["The second largest planet is a gas giant", True, 62],
"Uranus": ["An ice giant with a ring system", True, 27],
"Neptune": ["An ice giant and farthest from the Sun", True, 14],
"Pluto": ["Largest dwarf planet in the Solar System", False, 5]
}
--snip--
示例 5-3:将列表放入字典中
通过选择运行 ▸ 运行模块来运行程序。现在,当你请求某颗行星的信息时,程序应该显示该行星的整个列表:
Which planet would you like information on? Venus
['Venus takes 243 days to rotate', False, 0]
Which planet would you like information on? Mars
['The second smallest planet', False, 2]
从字典中的列表提取信息
我们知道如何从字典中获取信息列表,所以下一步是从该列表中获取单个信息。例如,False项本身意义不大。如果我们能够将其从列表中分离出来,可以在旁边添加解释,以便更容易理解结果。我们之前在第四章中使用了列表中的列表来表示房间地图。现在,就像之前一样,我们将使用索引号从字典中的列表中获取条目。
因为planets[query]是整个列表,我们可以通过使用planets[query][0]来查看描述(列表中的第一个条目)。我们可以通过使用planets[query][1]来查看它是否有环。简而言之,以下是我们正在做的事情:
-
我们使用存储在变量
query中的行星名称,从planets字典中访问特定的列表。 -
我们使用索引号从列表中提取单个项。
修改你的程序,使其看起来像示例 5-4。像之前一样,只修改未灰显的行。将程序保存为listing5-4.py,并通过点击运行 ▸ 运行模块来运行它。
listing5-4.py
--snip--
while True:
query = input("Which planet would you like information on? ")
if query in planets.keys():
➊ print(planets[query][0])
➋ print("Does it have rings? ", planets[query][1])
else:
print("Databanks empty. Sorry!")
示例 5-4:显示从字典中存储的列表获取的信息
当你运行listing5-4.py程序时,你应该会看到类似以下内容:
Which planet would you like information on? Earth
The only planet known to have native life
Does it have rings? False
Which planet would you like information on? Saturn
The second largest planet is a gas giant
Does it have rings? True
这应该适用于字典中的每一颗行星!
当你输入字典中存在的行星名称时,程序现在会打印出该行星信息列表中的第一个项,即描述➊。在下一行,程序会询问该行星是否有环,并显示True或False的答案,这是该行星信息列表中的第二项➋。你可以通过用逗号分隔来在同一个print()指令中显示一些文本和一些数据。这样显示比打印整个列表要清晰得多,信息也更易于理解。
训练任务 #2
你能修改程序,让它同时告诉你这颗行星有多少颗卫星吗?
制作太空站对象字典
让我们将如何使用字典以及在字典中使用列表的知识应用到空间站上。空间站上需要的家具、生命支持设备、工具和个人物品众多,我们需要跟踪大量信息。我们将使用一个名为objects的字典来存储游戏中所有不同物品的信息。
我们将使用数字作为对象的键。这样比为每个对象使用一个单词更简单。而且,使用数字会让你更容易理解房间地图,如果你想像我们在第四章中那样打印出来的话。也减少了输入错误的风险。当我们稍后为谜题创建代码时,解决方案就不那么显而易见了,这意味着如果你在玩游戏之前构建游戏,会减少剧透。
你可能记得我们在第四章中使用了数字 0、1 和 2 来表示地板瓷砖、墙柱和土壤。我们将继续使用这些数字来表示这些物品,其余的物品将使用数字 3 到 81。
字典中的每个条目都是一个包含该物品信息的列表,类似于我们在本章前面创建的planets字典。列表中包含每个对象的以下信息:
一个对象图像文件 不同的对象可以使用相同的图像文件。例如,所有的访问卡都使用相同的图像。
一个阴影图像文件 我们使用阴影来增强游戏中的 3D 效果。两个标准阴影是images.full_shadow,它填充一个完整的瓷砖空间,适用于较大的物体;另一个是images.half_shadow,它填充一个半个瓷砖空间,适用于较小的物体。像仙人掌这样的物体有自己独特的轮廓,它们有专用的阴影图像文件,仅用于该物体。一些物品,比如椅子,其阴影包含在图像文件内。有些物品没有阴影,比如陨石坑和玩家可以携带的任何物品。当图像没有阴影时,我们在字典中的阴影文件名位置写None。None是 Python 中的一种特殊数据类型。像True和False一样,你不需要给它加引号,而且它应该以大写字母开头。正确输入时,None在代码中会变成橙色。
一个长描述 当你在游戏中检查或选择一个对象时,会显示一个长描述。一些长描述包含线索,而其他的则只是描述环境。
一个简短描述 通常只有几个词,例如“一个访问卡”,简短描述会在你与对象互动时显示在屏幕上。例如,“你丢下了一张访问卡。”简短描述仅在玩家可以拾取或使用的物品上需要,比如访问卡或自动售货机。
游戏可以重用objects字典中的物品。例如,如果一个房间由 60 个或更多相同的墙壁支柱构成,游戏只需重用同一个墙壁支柱对象。它只需要在字典中出现一次。
有些物品使用相同的图像文件,但有其他不同之处,这意味着我们必须在字典中将它们分开存储。例如,通行卡根据持有者的不同描述不同,门也有不同的描述,告诉你使用哪把钥匙。每张通行卡和每扇门在objects字典中都有自己的条目。
添加逃脱游戏中的第一个物品
打开你在第四章中创建的listing4-3.py。该列表包含了游戏地图和生成房间地图的代码。我们将对这个程序进行扩展,继续构建逃脱游戏。
首先,我们需要设置一些额外的变量。在冒险开始之前,一艘名为“波杜尔着陆器”的科研飞船坠毁在行星表面。我们将在这些新变量中存储随机坠机地点的坐标。我们现在添加这些变量,因为地图对象(编号 27)将需要它们来描述其位置。
将 Listing 5-5 中的新行添加到现有listing4-3.py文件中的VARIABLES部分,该部分由一个哈希框标记。我建议将它们添加到其他变量的末尾,紧接着MAP部分开始的位置,这样你的列表和我的列表就一致了。将程序保存为listing5-5.py。如果现在运行它,程序不会做任何新的操作,但如果你想尝试,可以输入 pgzrun listing5-5.py。
listing5-5.py
--snip--
###############
## VARIABLES ##
###############
--snip--
DEMO_OBJECTS = [images.floor, images.pillar, images.soil]
LANDER_SECTOR = random.randint(1, 24)
LANDER_X = random.randint(2, 11)
LANDER_Y = random.randint(2, 11)
###############
## MAP ##
###############
--snip--
Listing 5-5: 添加坠机地点变量
这些新指令创建了变量,用来记住波杜尔着陆器所着陆的区块(或房间编号),以及它在该区块中的x和y位置。这些指令使用了random.randint()函数,它会从你提供的两个数字之间随机选择一个数字。这些指令在游戏开始时执行一次,因此每次你玩游戏时,着陆器的位置都会不同,但游戏过程中不会改变。
现在让我们添加第一部分物品数据,如 Listing 5-6 所示。本部分提供了从物品 0 到 12 的数据。因为玩家无法拾取或使用这些物品,它们没有简短的描述。
将本段代码添加到现有程序中的MAKE MAP部分(listing5-5.py)之前。为了帮助你在代码中找到位置,你可以在 IDLE 中按 CTRL-F 搜索特定的单词或短语。例如,尝试搜索make map,查看在哪里开始添加 Listing 5-6 中的代码。搜索后,点击搜索对话框上的关闭按钮。记住,如果你在列表中迷失了方向,始终可以参考附录 A 中的完整游戏代码。
如果你不想手动输入数据,可以使用位于列表文件夹中的 data-chapter5.py 文件。它包含了 objects 字典,你可以将它复制并粘贴到你的程序中。你可以先粘贴前 12 项。
listing5-6.py
--snip--
assert len(GAME_MAP)-1 == MAP_SIZE, "Map size and GAME_MAP don't match"
###############
## OBJECTS ##
###############
objects = {
0: [images.floor, None, "The floor is shiny and clean"],
1: [images.pillar, images.full_shadow, "The wall is smooth and cold"],
2: [images.soil, None, "It's like a desert. Or should that be dessert?"],
3: [images.pillar_low, images.half_shadow, "The wall is smooth and cold"],
4: [images.bed, images.half_shadow, "A tidy and comfortable bed"],
5: [images.table, images.half_shadow, "It's made from strong plastic."],
6: [images.chair_left, None, "A chair with a soft cushion"],
7: [images.chair_right, None, "A chair with a soft cushion"],
8: [images.bookcase_tall, images.full_shadow,
"Bookshelves, stacked with reference books"],
9: [images.bookcase_small, images.half_shadow,
"Bookshelves, stacked with reference books"],
10: [images.cabinet, images.half_shadow,
"A small locker, for storing personal items"],
11: [images.desk_computer, images.half_shadow,
"A computer. Use it to run life support diagnostics"],
12: [images.plant, images.plant_shadow, "A spaceberry plant, grown here"]
}
###############
## MAKE MAP ##
###############
--snip--
列表 5-6:添加第一个对象
记住,代码的颜色可以帮助你发现错误。如果你的文本部分不是绿色的,说明你漏掉了开头的双引号。如果有过多的绿色,可能是你忘了关闭的双引号。有些列表会继续在下一行,而 Python 知道列表没有完成,直到看到闭合的括号。如果你在让任何代码示例工作时遇到困难,你可以使用我提供的代码版本(参见 “使用我的示例代码” 在 第 21 页),从任何点开始继续这个项目。
列表 5-6 看起来与我们之前的 planets 字典相似:我们使用大括号标记字典的开始和结束,每个字典条目是一个列表,因此它位于方括号内。主要的区别是这次键是数字而不是单词。
将你新的程序保存为 listing5-6.py。这个程序使用 Pygame Zero 进行图形处理,因此你需要通过输入 pgzrun listing5-6.py 来运行它。它应该和之前一样工作,因为我们添加了新的数据,但还没有使用这些数据。无论如何都值得运行一下程序,因为如果你在命令行窗口看到错误信息,你可以在继续之前修复新代码。
使用空间站浏览器查看对象
要查看对象,我们必须告诉游戏使用新的字典。将程序中 EXPLORER 部分的以下行更改为:
image_to_draw = DEMO_OBJECTS[room_map[y][x]]
改成如下:
image_to_draw = objects[room_map[y][x]][0]
这个小变化使得 Explorer 代码使用我们新的 objects 字典,而不是我们之前指定的 DEMO_OBJECTS 列表。
注意,我们现在使用的是小写字母而不是大写字母。在这个程序中,我使用大写字母表示常量,其值不会改变。DEMO_OBJECTS 列表从未改变:它仅用于查找图像文件名。但 objects 字典在游戏过程中有时会更改其内容。
另一个区别是 [0] 现在位于行末。这是因为当我们从 objects 字典中提取一个条目时,它会给我们一整个信息列表。但我们这里只想使用图像,它是该列表中的第一个项目,因此我们使用索引 [0] 来提取它。
保存程序并再次运行,你应该会看到房间看起来与之前相同。这是因为我们还没有添加任何新的对象,我们保持了楼层、墙壁和土壤的对象编号与之前使用的索引编号相同。
设计一个房间
让我们在房间显示中添加一些物品。在代码的EXPLORER部分,添加 Listing 5-7 中显示的新行:
listing5-7.py
--snip--
###############
## EXPLORER ##
###############
def draw():
global room_height, room_width, room_map
print(current_room)
generate_map()
screen.clear()
room_map[2][4] = 7
room_map[2][6] = 6
room_map[1][1] = 8
room_map[1][2] = 9
room_map[1][8] = 12
room_map[1][9] = 9
--snip--
Listing 5-7:在房间显示中添加一些物体
这些新指令在房间显示之前,将物体添加到room_map列表中的不同位置。
记住,room_map使用 y 坐标在前,x 坐标在后。第一个索引数字表示物体离房间后面的距离;数字越小,物体越靠近后面。最小的有效数字通常是 1,因为墙壁在第 0 行。
第二个数字表示物体离房间的左边有多远,通常从左到右。第 0 列通常是墙壁,所以 1 也是这个位置的最小有效数字。
等号另一边的数字是特定物体的键。你可以通过查看 Listing 5-6 中的objects字典来检查每个数字代表的物体。
所以这一行:
room_map[1][1] = 8
将物体 8(一组高书架)放置在房间的左上角。而这一行:
room_map[2][6] = 6
将一把椅子(物体 6)放置在距离顶部三行和距离左边七个位置的地方。(记住,索引编号是从 0 开始的。)
将你的程序保存为 listing5-7.py,然后输入 pgzrun listing5-7.py 运行它。图 5-1 显示了现在你应该看到的内容。

图 5-1:温馨!Explorer程序中显示的一些物体
因为Explorer程序只是一个示例,所以一些功能还没有实现。例如,一些物体下面有一个黑色的方块,因为那里没有地板瓦片。另外,所有的房间看起来都一样,因为我们已经把物体编码到EXPLORER部分,所以它们出现在我们显示的每个房间里。这意味着你不能再查看所有的房间,因为某些房间里无法容纳物体。因此,你不能使用箭头键查看所有的房间。程序也不能正确显示像床这样的宽物体。我们稍后会解决这些问题,但在此期间,我们可以继续构建和测试空间站。
训练任务#3
尝试修改你在Explorer程序中添加的代码,将家具重新定位到你喜欢的位置。玩这个代码是学习如何在房间中定位物体的好方法。如果你想玩一个更大的房间,可以将VARIABLES部分中的current_room值从 31 改为 40(这是游戏中最大的房间)。将程序保存为 mission5-3.py,并使用pgzrun mission5-3.py运行它。你需要保留现有的Explorer代码(listing5-7.py)的备份,以便在训练任务#4 中使用。
添加其余的物体
到目前为止,我们已经将对象 0 到 12 添加到objects字典中。游戏中总共有 81 个物品,所以现在让我们通过在清单 5-8 中添加新行来添加其余的物品。记得在添加字典中其余物品之前,在物品 12 后添加逗号。
当同一个文件名或类似的描述用于多个物品时,你可以直接复制粘贴它。要复制代码,点击并按住鼠标左键,在代码块的开头开始,移动鼠标并高亮选中代码,然后按 CTRL-C。接着点击鼠标到你想粘贴代码的位置,按 CTRL-V。记住,如果你想节省打字时间,你也可以复制并粘贴整个data-chapter5.py文件中的字典。
将程序保存为listing5-8.py。你可以通过输入 pgzrun listing5-8.py 来测试程序是否仍然有效,尽管你目前还看不到任何新内容。
这是清单 5-8:
listing5-8.py
###############
## OBJECTS ##
###############
objects = {
0: [images.floor, None, "The floor is shiny and clean"],
--snip--
12: [images.plant, images.plant_shadow, "A spaceberry plant, grown locally"],
➊ 13: [images.electrical1, images.half_shadow,
"Electrical systems used for powering the space station"],
14: [images.electrical2, images.half_shadow,
"Electrical systems used for powering the space station"],
15: [images.cactus, images.cactus_shadow, "Ouch! Careful on the cactus!"],
16: [images.shrub, images.shrub_shadow,
"A space lettuce. A bit limp, but amazing it's growing here!"],
17: [images.pipes1, images.pipes1_shadow, "Water purification pipes"],
18: [images.pipes2, images.pipes2_shadow,
"Pipes for the life support systems"],
19: [images.pipes3, images.pipes3_shadow,
"Pipes for the life support systems"],
➋ 20: [images.door, images.door_shadow, "Safety door. Opens automatically \
for astronauts in functioning spacesuits."],
21: [images.door, images.door_shadow, "The airlock door. \
For safety reasons, it requires two person operation."],
22: [images.door, images.door_shadow, "A locked door. It needs " \
+ PLAYER_NAME + "'s access card"],
23: [images.door, images.door_shadow, "A locked door. It needs " \
+ FRIEND1_NAME + "'s access card"],
24: [images.door, images.door_shadow, "A locked door. It needs " \
+ FRIEND2_NAME + "'s access card"],
25: [images.door, images.door_shadow,
"A locked door. It is opened from Main Mission Control"],
26: [images.door, images.door_shadow,
"A locked door in the engineering bay."],
➌ 27: [images.map, images.full_shadow,
"The screen says the crash site was Sector: " \
+ str(LANDER_SECTOR) + " // X: " + str(LANDER_X) + \
" // Y: " + str(LANDER_Y)],
28: [images.rock_large, images.rock_large_shadow,
"A rock. Its coarse surface feels like a whetstone", "the rock"],
29: [images.rock_small, images.rock_small_shadow,
"A small but heavy piece of Martian rock"],
30: [images.crater, None, "A crater in the planet surface"],
31: [images.fence, None,
"A fine gauze fence. It helps protect the station from dust storms"],
32: [images.contraption, images.contraption_shadow,
"One of the scientific experiments. It gently vibrates"],
33: [images.robot_arm, images.robot_arm_shadow,
"A robot arm, used for heavy lifting"],
34: [images.toilet, images.half_shadow, "A sparkling clean toilet"],
35: [images.sink, None, "A sink with running water", "the taps"],
36: [images.globe, images.globe_shadow,
"A giant globe of the planet. It gently glows from inside"],
37: [images.science_lab_table, None,
"A table of experiments, analyzing the planet soil and dust"],
38: [images.vending_machine, images.full_shadow,
"A vending machine. It requires a credit.", "the vending machine"],
39: [images.floor_pad, None,
"A pressure sensor to make sure nobody goes out alone."],
40: [images.rescue_ship, images.rescue_ship_shadow, "A rescue ship!"],
41: [images.mission_control_desk, images.mission_control_desk_shadow, \
"Mission Control stations."],
42: [images.button, images.button_shadow,
"The button for opening the time-locked door in engineering."],
43: [images.whiteboard, images.full_shadow,
"The whiteboard is used in brainstorms and planning meetings."],
44: [images.window, images.full_shadow,
"The window provides a view out onto the planet surface."],
45: [images.robot, images.robot_shadow, "A cleaning robot, turned off."],
46: [images.robot2, images.robot2_shadow,
"A planet surface exploration robot, awaiting set-up."],
47: [images.rocket, images.rocket_shadow, "A 1-person craft in repair."],
48: [images.toxic_floor, None, "Toxic floor - do not walk on!"],
49: [images.drone, None, "A delivery drone"],
50: [images.energy_ball, None, "An energy ball - dangerous!"],
51: [images.energy_ball2, None, "An energy ball - dangerous!"],
52: [images.computer, images.computer_shadow,
"A computer workstation, for managing space station systems."],
53: [images.clipboard, None,
"A clipboard. Someone has doodled on it.", "the clipboard"],
54: [images.bubble_gum, None,
"A piece of sticky bubble gum. Spaceberry flavour.", "bubble gum"],
55: [images.yoyo, None, "A toy made of fine, strong string and plastic. \
Used for antigrav experiments.", PLAYER_NAME + "'s yoyo"],
56: [images.thread, None,
"A piece of fine, strong string", "a piece of string"],
57: [images.needle, None,
"A sharp needle from a cactus plant", "a cactus needle"],
58: [images.threaded_needle, None,
"A cactus needle, spearing a length of string", "needle and string"],
59: [images.canister, None,
"The air canister has a leak.", "a leaky air canister"],
60: [images.canister, None,
"It looks like the seal will hold!", "a sealed air canister"],
61: [images.mirror, None,
"The mirror throws a circle of light on the walls.", "a mirror"],
62: [images.bin_empty, None,
"A rarely used bin, made of light plastic", "a bin"],
63: [images.bin_full, None,
"A heavy bin full of water", "a bin full of water"],
64: [images.rags, None,
"An oily rag. Pick it up by a corner if you must!", "an oily rag"],
65: [images.hammer, None,
"A hammer. Maybe good for cracking things open...", "a hammer"],
66: [images.spoon, None, "A large serving spoon", "a spoon"],
67: [images.food_pouch, None,
"A dehydrated food pouch. It needs water.", "a dry food pack"],
68: [images.food, None,
"A food pouch. Use it to get 100% energy.", "ready-to-eat food"],
69: [images.book, None, "The book has the words 'Don't Panic' on the \
cover in large, friendly letters", "a book"],
70: [images.mp3_player, None,
"An MP3 player, with all the latest tunes", "an MP3 player"],
71: [images.lander, None, "The Poodle, a small space exploration craft. \
Its black box has a radio sealed inside.", "the Poodle lander"],
72: [images.radio, None, "A radio communications system, from the \
Poodle", "a communications radio"],
73: [images.gps_module, None, "A GPS Module", "a GPS module"],
74: [images.positioning_system, None, "Part of a positioning system. \
Needs a GPS module.", "a positioning interface"],
75: [images.positioning_system, None,
"A working positioning system", "a positioning computer"],
76: [images.scissors, None, "Scissors. They're too blunt to cut \
anything. Can you sharpen them?", "blunt scissors"],
77: [images.scissors, None,
"Razor-sharp scissors. Careful!", "sharpened scissors"],
78: [images.credit, None,
"A small coin for the station's vending systems",
"a station credit"],
79: [images.access_card, None,
"This access card belongs to " + PLAYER_NAME, "an access card"],
80: [images.access_card, None,
"This access card belongs to " + FRIEND1_NAME, "an access card"],
81: [images.access_card, None,
"This access card belongs to " + FRIEND2_NAME, "an access card"]
}
➍ items_player_may_carry = list(range(53, 82))
# Numbers below are for floor, pressure pad, soil, toxic floor.
➎ items_player_may_stand_on = items_player_may_carry + [0, 39, 2, 48]
###############
## MAKE MAP ##
###############
--snip--
清单 5-8:完成 Escape 游戏的物品数据
一些物品的列表在程序中跨越了多行 ➊。这没问题,因为 Python 知道列表直到看到闭括号才算完成。要将字符串(或任何其他代码片段)分割成多行,你可以在行尾使用\ ➋。listing5-8.py中的换行只是为了让代码在书页上排版得更整齐:在屏幕上,如果你愿意,代码可以延伸到右侧。
对象 27 是一个显示贵宾犬坠毁地点的地图。它的长描述包括你在清单 5-5 中设置的贵宾犬位置变量。str()函数用于将这些变量中的数字转换为字符串,以便它们可以与其他字符串组合,构成长描述 ➌。
我们还设置了一些在游戏中需要的额外列表:items_player_may_carry存储玩家可以拾取的物品编号 ➍。这些物品编号是 53 到 81。因为它们是连续的,我们可以使用范围来设置items_player_may_carry列表。范围是一个数字序列,它从给定的第一个数字开始,直到最后一个数字之前的数字为止。(我们在第三章中使用了范围。)我们通过list(range(53 to 82))将该范围转化为列表,从而得到包含 53 到 81 之间所有数字的列表。
如果以后添加更多玩家可以携带的物品,可以将它们添加到这个列表的末尾。例如,要添加编号为 89 和 93 的新物品,玩家可以携带,可以使用items_player_may_carry = list(range(54, 82)) + [89, 93]。你也可以将新物品添加到objects列表的末尾,并扩展用于设置items_player_may_carry的范围。
另一个新列表是items_player_may_stand_on,它指定玩家是否被允许站在某个物品上➎。玩家只能站在足够小以便能被捡起的物品和不同类型的地面上。我们通过将不同地面类型的物品编号添加到items_player_may_carry列表中来创建这个列表。
完成了 F 第五章清单之后,你就完成了Escape游戏中的OBJECTS部分!但是我们还没有将物品放入游戏地图中。我们将在第六章开始做这件事。
训练任务 #4
试验一下你刚刚添加到游戏中的一些新物品。通过修改代码,你能否 . . .
-
将高书架换成一个垃圾箱(物品 62)?
-
将太空浆果植物换成一块小石头(物品 29)?
-
将右边的椅子换成一块有毒的地面(物品 48)?
为了理解哪个指令放置了哪个物品,你可以使用现有代码中的坐标,或者查阅objects字典中的物品编号(屏幕上或本章的清单中)。运行你的程序以确保它正常工作。
你准备好飞行了吗?
勾选以下框以确认你已经学会了本章的关键内容。
要从字典中获取信息,你需要使用该信息的键。键可以是一个单词或数字,也可以存储在变量中。
如果你尝试使用字典中没有的键,会导致错误。
为了避免错误,在程序尝试使用键之前,检查该键是否存在于字典中。
你可以将列表放入字典中。然后,你可以使用字典键后跟列表索引来获取列表中的特定项。例如:planets["Earth"][1]。
Escape游戏使用objects字典来存储游戏中所有物品的信息。字典中的每个项都是一个列表。
你可以使用该列表的索引号来访问物品的图片文件、阴影图片文件以及长短描述。

第六章:安装空间站设备**

在第五章中,你准备了所有将在任务中使用的设备信息。在本章中,你将把一些设备安装到空间站,并使用Explorer查看任何房间或行星表面的位置。这是你第一次有机会探索将成为你家的火星基地设计。
理解景物数据的字典
空间站上有两种不同类型的物体:
-
景物 是在Escape游戏中始终保持在同一位置的设备,包括家具、管道和电子设备。
-
道具 是在游戏中可以出现、消失或移动的物体。它们包括玩家可以创建和拾取的物品。道具还包括门,当门关闭时它们出现在房间中,打开时则消失。
用于定位景物和道具的数据是分开存储的,且组织方式不同。本章中,我们只会添加景物数据。
我们的程序已经知道所有游戏中物体的图片和描述,因为它们已经在你在第五章中创建的objects字典中。现在,我们将告诉程序将景物物体放置在空间站的位置。为此,我们将创建一个新的字典,称为scenery。这是我们为一个房间构建条目的方式:
room number: [[object number, y, x], [object number, y, x]]
字典的键将是房间号。对于每个房间号,字典存储一个列表,列表的开头和结尾有方括号。列表中的每一项是另一个列表,告诉程序将一个物体放置在房间中的位置。在这里,我将一个物体设置为红色,另一个设置为绿色,这样你可以看到它们的起始和结束位置。
每个物体需要以下三条信息:
物体编号 这是与objects字典中用作键的编号相同的编号。例如,编号 5 代表一张桌子。
物体的 y 位置 这是物体在房间中的位置,从后到前。后墙通常在 0 行,所以我们通常从 1 开始放置物体。最大有用数值通常是房间的高度减去 2:我们减去 1 是因为地图位置从 0 开始,再减去 1 是因为前墙占用了空间。实际上,最好在房间前面留更多空间,因为前墙可能会遮挡其他物体。你可以在第四章中添加的GAME_MAP代码中查看房间的大小。
物体的 x 位置 这告诉程序物体在房间中从左到右的具体位置。通常,墙壁的 x 位置是 0。通常有用的最大数值是房间的宽度减去 2。
为了更好地理解这些数字,让我们来看一下图 6-1,它展示了空间站中的一个房间的截图和地图。在这张图片中,水槽(S)位于从后向前数的第二排,因此它的y位置是 1。记住,最靠后的第一排墙壁的位置是y = 0。水槽的x位置是 3。它左边有两个瓷砖空间,墙壁的位置是x = 0。

图 6-1: 游戏中看到的空间站房间示例(左)和通过地图表示的房间(右)。T = 厕所,S = 水槽,P = 玩家。
让我们看看这个房间的数据。暂时不要输入这个代码。我会很快给你所有的景观数据。
scenery = {
--snip--
30: [[34,1,1], [35,1,3]],
--snip--
}
这段代码告诉程序房间 30 中的物体信息。房间 30 在左上角有编号 34 的厕所,位置是y = 1 和 x = 1,另外在位置y = 1 和 x = 3 有编号 35 的水槽,离厕所很近。
你可以在房间中多次使用同一个物品,只需要为每个位置添加一个列表,并为它们使用相同的物品编号。例如,如果你愿意,你可以将房间填满不同位置的厕所,尽管这会是一个相当奇怪的做法。
你不需要在景观数据中包括墙壁,因为程序在创建room_map列表时会自动将它们添加到房间中,正如你之前所看到的那样。
尽管将每个物品的信息放入一个列表中意味着要增加更多的括号,但这样一目了然,数据更容易理解。括号帮助你看到房间里有多少物品,哪些是物品编号,哪些是位置编号。
添加景观数据
打开listing5-8.py,它是第五章中的最后一个代码清单。这个清单包含了你的游戏地图和物体数据。现在我们将把景观数据添加到其中。
Listing 6-1 展示了景观数据。将这个新的SCENERY部分添加到MAKE MAP部分之前。确保括号和逗号的位置正确。记住,每个景观项需要一个由三个数字组成的列表,而且每个列表之间也要用逗号分隔。如果你不想全部手动输入数据,可以使用data-chapter6.py文件,它位于listings文件夹中。这个文件包含了你可以复制粘贴到程序中的景观字典。
listing6-1.py
--snip--
items_player_may_stand_on = items_player_may_carry + [0, 39, 2, 48]
###############
## SCENERY ##
###############
# Scenery describes objects that cannot move between rooms.
# room number: [[object number, y position, x position]...]
scenery = {
26: [[39,8,2]],
27: [[33,5,5], [33,1,1], [33,1,8], [47,5,2],
[47,3,10], [47,9,8], [42,1,6]],
28: [[27,0,3], [41,4,3], [41,4,7]],
29: [[7,2,6], [6,2,8], [12,1,13], [44,0,1],
[36,4,10], [10,1,1], [19,4,2], [17,4,4]],
30: [[34,1,1], [35,1,3]],
31: [[11,1,1], [19,1,8], [46,1,3]],
32: [[48,2,2], [48,2,3], [48,2,4], [48,3,2], [48,3,3],
[48,3,4], [48,4,2], [48,4,3], [48,4,4]],
33: [[13,1,1], [13,1,3], [13,1,8], [13,1,10], [48,2,1],
[48,2,7], [48,3,6], [48,3,3]],
34: [[37,2,2], [32,6,7], [37,10,4], [28,5,3]],
35: [[16,2,9], [16,2,2], [16,3,3], [16,3,8], [16,8,9], [16,8,2], [16,1,8],
[16,1,3], [12,8,6], [12,9,4], [12,9,8],
[15,4,6], [12,7,1], [12,7,11]],
36: [[4,3,1], [9,1,7], [8,1,8], [8,1,9],
[5,5,4], [6,5,7], [10,1,1], [12,1,2]],
37: [[48,3,1], [48,3,2], [48,7,1], [48,5,2], [48,5,3],
[48,7,2], [48,9,2], [48,9,3], [48,11,1], [48,11,2]],
38: [[43,0,2], [6,2,2], [6,3,5], [6,4,7], [6,2,9], [45,1,10]],
39: [[38,1,1], [7,3,4], [7,6,4], [5,3,6], [5,6,6],
[6,3,9], [6,6,9], [45,1,11], [12,1,8], [12,1,4]],
40: [[41,5,3], [41,5,7], [41,9,3], [41,9,7],
[13,1,1], [13,1,3], [42,1,12]],
41: [[4,3,1], [10,3,5], [4,5,1], [10,5,5], [4,7,1],
[10,7,5], [12,1,1], [12,1,5]],
44: [[46,4,3], [46,4,5], [18,1,1], [19,1,3],
[19,1,5], [52,4,7], [14,1,8]],
45: [[48,2,1], [48,2,2], [48,3,3], [48,3,4], [48,1,4], [48,1,1]],
46: [[10,1,1], [4,1,2], [8,1,7], [9,1,8], [8,1,9], [5,4,3], [7,3,2]],
47: [[9,1,1], [9,1,2], [10,1,3], [12,1,7], [5,4,4], [6,4,7], [4,1,8]],
48: [[17,4,1], [17,4,2], [17,4,3], [17,4,4], [17,4,5], [17,4,6], [17,4,7],
[17,8,1], [17,8,2], [17,8,3], [17,8,4],
[17,8,5], [17,8,6], [17,8,7], [14,1,1]],
49: [[14,2,2], [14,2,4], [7,5,1], [5,5,3], [48,3,3], [48,3,4]],
50: [[45,4,8], [11,1,1], [13,1,8], [33,2,1], [46,4,6]]
}
checksum = 0
check_counter = 0
for key, room_scenery_list in scenery.items():
for scenery_item_list in room_scenery_list:
➊ checksum += (scenery_item_list[0] * key
+ scenery_item_list[1] * (key + 1)
+ scenery_item_list[2] * (key + 2))
check_counter += 1
print(check_counter, "scenery items")
➋ assert check_counter == 161, "Expected 161 scenery items"
➌ assert checksum == 200095, "Error in scenery data"
print("Scenery checksum: " + str(checksum))
###############
## MAKE MAP ##
###############
--snip--
Listing 6-1: 添加景观数据
将你的清单保存为listing6-1.py,然后在命令行中使用pgzrun listing6-1.py运行它。我们已经添加了一些数据,但还没有告诉程序做任何处理,所以你不会看到任何变化。但是,如果你在输入数据时犯了错误,程序应该会停止并显示消息Error in scenery data。如果发生这种情况,请返回并仔细检查你的代码与书中的内容是否一致。首先检查你是否正确输入了校验和!
该列表的后半部分是一种安全措施,称为校验和。它通过对数据进行计算并将结果与正确答案进行对比,检查所有数据是否完整且正确。如果你输入的数据有错误,这段代码会阻止程序运行,直到你修正错误。这防止了游戏运行时出现错误。(虽然有些错误可能会被忽略,但这段代码能够捕捉大部分错误。)
程序使用 assert 指令来检查数据。第一条指令检查程序是否拥有正确数量的数据项。如果没有,程序会停止并显示错误消息 ➋。程序还检查校验和(计算结果)是否为预期的数字,如果不是,它会停止程序 ➌。注意,列表 6-1 中的一条指令跨越了三行 ➊:Python 知道我们没有完成指令,直到我们关闭最后一个括号。
提示
如果你想更改景观数据,重新设计房间或添加自己的房间,你需要关闭校验和。这是因为基于你更改的数据的计算将不同,因此校验和会失败,程序无法运行。你只需在以 assert 开头的两行 ➋➌ 前加上 # 符号来关闭它们。如你所知,# 符号用于注释,Python 会忽略同一行中它后面的所有内容。在你构建或测试程序时,它是一个非常方便的开关。
为星球表面添加外围围栏
你可能注意到我们还没有为房间 1 到 25 添加任何景观。我们的数据从房间 26 开始。如你所记,前 25 个位置是在星球表面外部。为了简化,我们仍然称它们为房间,尽管它们没有墙壁。
图 6-2 显示了地图上房间 1 到 25 的位置。一个栅栏,如图 6-2 所示的虚线,围绕着这些房间的外部。栅栏防止人们漫无目的地走出围墙,离开游戏地图。

图 6-2:为星球表面位置添加围栏
我们需要在以下位置添加围栏:
-
在房间 1、6、11、16 和 21 的左侧
-
在房间 1、2、3、4 和 5 的顶部
-
在房间 5、10、15、20、25 的右侧
每个外部房间也有一项星球表面景观,它是从包括岩石、灌木和陨石坑等适当项目的小范围中随机选择的。对于游戏来说,这些物品的放置位置无关紧要,因此它们也可以随机定位。
列表 6-2 显示了生成随机星球表面景观并添加围栏的代码。将代码添加到你刚刚创建的 SCENERY 部分的末尾,并将程序保存为 listing6-2.py。你可以使用 pgzrun listing6-2.py 来检查程序是否报告了任何错误。
listing6-2.py
--snip--
print("Scenery checksum: " + str(checksum))
for room in range(1, 26): # Add random scenery in planet locations.
➊ if room != 13: # Skip room 13.
➋ scenery_item = random.choice([16, 28, 29, 30])
➌ scenery[room] = [[scenery_item, random.randint(2, 10),
random.randint(2, 10)]]
# Use loops to add fences to the planet surface rooms.
➍ for room_coordinate in range(0, 13):
➎ for room_number in [1, 2, 3, 4, 5]: # Add top fence
➏ scenery[room_number] += [[31, 0, room_coordinate]]
➐ for room_number in [1, 6, 11, 16, 21]: # Add left fence
➑ scenery[room_number] += [[31, room_coordinate, 0]]
for room_number in [5, 10, 15, 20, 25]: # Add right fence
➒ scenery[room_number] += [[31, room_coordinate, 12]]
➓ del scenery[21][-1] # Delete last fence panel in Room 21
del scenery[25][-1] # Delete last fence panel in Room 25
###############
## MAKE MAP ##
###############
--snip--
列表 6-2:生成随机的行星表面景观
你不需要理解这些代码就能享受构建和玩逃脱游戏,但如果你想深入了解,我会更详细地解释这些代码。
列表 6-2 的第一部分添加了随机的景观。对于每个房间,random.choice() ➋ 随机选择一个景观项。就像random.randint()给我们一个随机数(就像掷骰子一样),random.choice()则给我们一个随机项(就像抓宝袋或者抽奖游戏)。这个项从列表[16, 28, 29, 30]中选择。这些物品编号分别代表灌木丛、大岩石、小岩石和陨石坑。
我们还为房间➌的scenery字典添加了一个新条目。这个条目包含了随机的景观项以及该项的随机y和x位置。y和x位置将景观项放置在房间内,但不会太靠近边缘。
!=运算符➊表示“不等于”,所以只有当房间编号不等于13 时才会添加景观。谁知道呢?也许在你的任务中,行星表面有个空旷的空间会很有用…
列表 6-2 的第二部分添加了围栏。所有的行星表面位置都是 13 个瓦片高,13 个瓦片宽,所以我们可以使用一个循环➍来添加顶部和侧面的围栏。循环的变量room_coordinate从 0 数到 12,每次循环时,围栏板会放置在相应房间的顶部和侧面。
在room_coordinate循环内,有三个room_number循环。第一个room_number循环➎在顶部房间的顶部行添加围栏。这里不是使用range(),而是通过一个列表进行循环。每次循环时,变量room_number从列表[1, 2, 3, 4, 5]中取出下一个数字。我们通过+=将一个景观项添加到该房间的景观列表中➏。这是景观项 31(一块围栏),位于房间的顶部行(位置y = 0)。room_coordinate的值用于x位置。这样就把顶部围栏添加到房间 1 到 5 的顶部行。
在room_coordinate循环内还有两个room_number循环。第一个循环给房间1、6、11、16和21添加左边的围栏➐。这次,程序使用room_coordinate变量作为y位置,使用0作为x位置➑。这样就把围栏板放置在这些房间的左边缘。第二个循环给房间5、10、15、20和25添加右边的围栏。这也使用room_coordinate作为围栏板的y位置,但使用12作为x坐标,将围栏放置在这些房间的右边缘➒。
我们不希望在外部区域与空间站墙壁相接的地方出现侧围栏面板。图 6-3 显示了房间 21 的地图。房间的左下角应该是墙面,因此这里不应该有围栏面板。然而,之前的循环添加了一个围栏面板,所以我们使用指令 ➓ 删除添加到该房间的最后一个景观项,并删除位于复合体另一侧的房间 25(见图 6-2)。与其编写代码避免在此处放置围栏面板,不如直接添加这两个面板再将它们删除。索引号 -1 是指向列表中最后一项的便捷快捷方式。

图 6-3:显示围栏如何接触到空间站旁边外部房间墙壁的地图
使用随机景观和循环来定位围栏,使我们能够探索一个大区域,而不必为 200 多个围栏面板和景观项目输入数据。
提示
如果你在定制游戏时不想在房间 1 到 25 中添加随机景观或围栏,可以删除清单 6-2 中显示的代码部分。
将景观加载到每个房间
现在我们已经将景观数据添加到程序中,接下来让我们添加一些代码,以便在空间站中看到景观!你可能还记得,generate_map() 函数会为你当前探索的房间创建 room_map 列表。room_map 列表用于显示和导航房间。
到目前为止,generate_map() 函数仅计算房间的大小、门的位置,并放置地板和墙壁。我们需要添加一些代码,从新字典中提取景观并将其添加到 room_map 中。但首先,我们需要对程序进行一个小但重要的调整。在程序的 VARIABLES 部分,靠近程序开始的位置,添加清单 6-3 中显示的新行。将程序保存为 listing6-3.py。
listing6-3.py
--snip--
###############
## VARIABLES ##
###############
--snip--
LANDER_SECTOR = random.randint(1, 24)
LANDER_X = random.randint(2, 11)
LANDER_Y = random.randint(2, 11)
TILE_SIZE = 30
###############
## MAP ##
###############
--snip--
清单 6-3:设置 TILE_SIZE 变量
这一行创建了一个变量来存储瓷砖的大小。使用它能让程序更易读,因为我们可以用一个更有意义的短语替代数字 30。我们不必再在代码中看到数字 30 并且记住它代表的意思,而是可以看到TILE SIZE,它能提示我们代码的作用。
接下来,找到程序中的 MAKE MAP 部分:它位于 EXPLORER 部分之前。在 MAKE MAP 部分末尾添加清单 6-4,以便将景观放置到当前房间。所有清单 6-4 中的代码都属于 generate_map() 函数,因此我们需要将第一行缩进四个空格,然后按示例缩进剩余的行。将程序保存为 listing6-4.py。
listing6-4.py
--snip--
def generate_map():
--snip--
➊ if current_room in scenery:
➋ for this_scenery in scenery[current_room]:
➌ scenery_number = this_scenery[0]
➍ scenery_y = this_scenery[1]
➎ scenery_x = this_scenery[2]
➏ room_map[scenery_y][scenery_x] = scenery_number
➐ image_here = objects[scenery_number][0]
➑ image_width = image_here.get_width()
➒ image_width_in_tiles = int(image_width / TILE_SIZE)
➓ for tile_number in range(1, image_width_in_tiles):
room_map[scenery_y][scenery_x + tile_number] = 255
###############
## EXPLORER ##
###############
--snip--
清单 6-4:为 generate_map() 函数添加的额外代码,将当前房间的景观添加到 room_map 列表中
让我们来逐步分析一下。➊这一行检查当前房间在scenery字典中是否有条目。这个检查非常重要,因为我们游戏中的某些房间可能没有任何景物,如果我们尝试使用一个不存在的字典键,Python 会抛出错误并停止执行。
然后,我们设置了一个循环➋,该循环遍历房间中的所有景物项,并将它们复制到一个名为this_scenery的列表中。第一次进入循环时,this_scenery包含第一个景物项的列表;第二次时,包含第二个景物项的列表,以此类推,直到遍历完当前房间的所有景物项。
每个景物项都有一个列表,包含它的对象编号、y位置和x位置。程序使用索引提取this_scenery中的这些信息,并将它们放入变量scenery_number ➌、scenery_y ➍和scenery_x ➎中。
现在程序已经拥有了所有需要的信息,可以将景物项添加到room_map中。你可能还记得,room_map在房间的每个位置存储着该物品的对象编号。它使用房间中的y位置和x位置作为列表索引。这个程序使用scenery_y和scenery_x的值作为列表索引,将scenery_number放入room_map中➏。
如果我们的所有物品都只有一个瓦片宽,那么我们只需要做这些就足够了。但有些物品更宽,占据多个瓦片。例如,一个宽的物品放置在一个瓦片内,可能会覆盖右边的两个瓦片,但目前程序只看到它占据的一个瓦片。
我们需要在room_map的额外空间中添加一些内容,以便程序知道玩家不能走到那些瓦片上。我使用数字 255 来表示一个没有物品的空白区域,但玩家也无法走动。
为什么选择数字 255?这是一个足够大的数字,如果你愿意,可以为游戏添加更多的对象,它允许objects字典中有 254 个项目。此外,对我来说,这个数字也很有意义:它是你能用一个字节数据表示的最大数字(在我上世纪 80 年代开始编写游戏时,这个问题非常重要,那时计算机的内存大约只有 65,000 字节,用来存储所有数据、图形和程序代码)。
首先,我们需要确定图片的宽度,这样才能知道它占据了多少个瓦片。我们使用scenery_number作为字典键,从objects字典中获取关于该对象的信息➐。我们知道,objects字典返回的是一个信息列表,其中第一个项目是图像。所以我们使用索引 0 来提取图像,并将其放入变量image_here中。
然后,我们可以使用 Pygame Zero 来找出图像的宽度,通过在其名称后添加get_width()➑。我们将这个数字放入一个名为image_width的变量中。因为我们需要知道图像覆盖了多少瓦片,程序将图像宽度(像素)除以瓦片大小 30,并将其转换为整数(一个整数)➒。我们必须将这个数字转换为整数,因为我们将在range()函数中使用它➓,而该函数只接受整数。如果我们不转换这个数字,宽度将是一个浮动小数——一个带小数点的数字。
最后,我们设置一个循环,在景物项的右侧添加值 255,无论瓦片是否被覆盖➓。
如果一张图片宽度为 90 像素,我们将其除以 30 的瓦片大小,并将结果 3 存储在image_width_in_tiles中。然后,循环使用range()计算到 2,因为我们为它提供了 1 到image_width_in_tiles的范围➓。我们将循环中的数字加到物体的* x *位置,并且room_map中的这些位置会标记为 255。现在,覆盖三个瓦片的大型物体将在右边的下两个空间标记为 255。
现在,我们的程序包含了所有景物,并可以将其添加到room_map中,准备显示。接下来,我们将对EXPLORER部分做一些小修改,以便可以巡游空间站。
更新探险者以巡游空间站
程序中的EXPLORER部分让你可以查看空间站中的所有房间,并使用箭头键在地图上移动。让我们更新这一部分,这样你就可以看到所有的景物了。
如果你的Explorer代码中包含任何用于将景物添加到room_map的行,你现在需要将它们关闭。尽管这些行是实验房间设计的好方法,但它们会将相同的景物强行放入每个房间并覆盖真正的房间设计。由于这些行可能包含你对房间设计的构思,所以不妨将它们注释掉,这样 Python 就会忽略它们。点击并拖动鼠标,选中所有行,然后点击格式 ▸ 注释区域(或使用快捷键 ALT-3)。注释符号将被添加到选中行的开头,如清单 6-5 所示:
listing6-5.py
--snip--
###############
## EXPLORER ##
###############
def draw():
global room_height, room_width, room_map
print(current_room)
generate_map()
screen.clear()
## room_map[2][4] = 7
## room_map[2][6] = 6
## room_map[1][1] = 8
## room_map[1][2] = 9
## room_map[1][8] = 12
## room_map[1][9] = 10
--snip--
清单 6-5:注释掉 EXPLORER 部分的代码
现在,我们需要对显示房间的代码做一个小修改,使其不再尝试为标记为 255 的地板空间绘制图像。这个空间将由其左侧的图像覆盖,并且我们没有为 255 在objects字典中创建条目。
清单 6-6 显示了你需要在程序的EXPLORER部分添加的新行,如图所示。if语句确保只有当物体编号不等于 (!=) 255 时,绘制物体的指令才会执行。
添加该行后,将其后的现有代码缩进四个空格。缩进告诉 Python 这些指令属于 if 指令。你可以在接下来的两行开头键入四个空格,或者你可以选中这些行,然后点击 格式 ▸ 缩进区域。
listing6-6.py
--snip--
###############
## EXPLORER ##
###############
--snip--
for y in range(room_height):
for x in range(room_width):
if room_map[y][x] != 255:
image_to_draw = objects[room_map[y][x]][0]
screen.blit(image_to_draw,
(top_left_x + (x*30),
top_left_y + (y*30) - image_to_draw.get_height()))
--snip--
Listing 6-6: 更新 Explorer,使其不再尝试显示图像 255
现在你已经准备好参观基地了。将程序保存为 listing6-6.py,并通过输入 pgzrun listing6-6.py 来运行它。使用方向键在地图上移动,熟悉空间站的布局。和之前一样,Explorer 程序允许你在地图上朝任意方向移动,即使在游戏中遇到墙壁也不受影响。
所有景物应该已经布置好在房间里。宽物体现在应该正确显示,你也应该能够再次查看所有房间,因为你之前在 Listing 6-5 中所做的更改。某些物体下方仍然会有黑色方块,因为下面没有地板瓷砖,但我们将在 第八章 中解决这个问题。
空间站的地图和景物现在已经完成。是时候进入空间站了。在下一章中,你将传送到表面,最终踏上火星。
训练任务 #1
你能将自己的房间设计添加到景物数据中吗?房间编号 43 已经为空,供你填写。它的大小是 9 × 9 瓷砖,所以你可以将物体放置在每个方向的 1 到 7 的位置(记住墙壁!)。你可以以你在 第五章 中的 Explorer 创建的房间为基础,或者发明一个新的布局。记住,你需要关闭 assert 指令,以防当 scenery 数字不匹配时,校验和报告错误。
你的程序的 objects 字典(见 第五章)会告诉你每个物体的数量。使用 1 到 47 之间的物体编号,确保在完成并开始玩 Escape 游戏时,不会出现影响代码的任何问题。
如果你遇到困难,可以尝试构建我的示例,该示例显示在 任务回顾 中,位于 第 110 页。将 VARIABLES 部分中的 current_room 的值更改为 43,这样你第一次运行程序时就能看到重新设计的房间。记得在完成后将 current_room 改回 31。
你适合飞行吗?
勾选以下框以确认你已经学习了本章的关键内容。
在 Escape 游戏中无法移动的物品被称为 景物。
scenery 字典使用房间编号作为键,并提供每个房间中固定物品的列表。
每个景物项目都存储为一个包含物体编号、y 位置和 x 位置的列表。
校验和用于检查数据是否已被更改或输入错误。
可以使用循环将物品添加到scenery字典中。有些景物也可以随机放置。
generate_map()函数从当前房间的scenery字典中获取物品,并将它们放入room_map列表中。然后,这些物品就可以在房间中显示出来。
在room_map中的数字 255 表示一个被大物体覆盖的空间,前提是物体不从该空间开始。

第七章:进入空间站

现在我们已经为空间站配备了景观、生命支持系统和其他设备,是时候搬进来了。在这一章中,你将第一次看到自己在空间站的样子,并且能够四处走动,探索各个房间。刚开始你可能会因为旅途而感到有些僵硬,但很快你就会在基地里四处行走。
你将学习如何给宇航员动画,并使用键盘控制让他们移动。你还将添加代码,使宇航员能够在房间之间移动。火星上有生命吗?现在有了。
到达空间站
我们将在本章中以第六章的清单 6-6 为起点,因此请打开listing6-6.py。我们将添加代码,展示你穿着太空服的样子在空间站内。最终,你将能够使用箭头键进行移动。
禁用探索者部分的房间导航控制
到目前为止,我们一直在使用EXPLORER部分的箭头键来显示地图上的不同房间。现在我们将开始使用这些键来移动宇航员在房间内。首先,我们需要禁用现有的控制功能。向下滚动到程序中的EXPLORER部分,并高亮显示清单 7-1 中的指令。点击格式 ▸ 注释区域,将这些指令变为注释,这样程序就会忽略它们。(如果你更喜欢,也可以直接删除它们。)将程序保存为listing7-1.py。
listing7-1.py
--snip--
##def movement():
## global current_room
## old_room = current_room
##
## if keyboard.left:
## current_room -= 1
## if keyboard.right:
## current_room += 1
## if keyboard.up:
## current_room -= MAP_WIDTH
## if keyboard.down:
## current_room += MAP_WIDTH
##
## if current_room > 50:
## current_room = 50
## if current_room < 1:
## current_room = 1
##
## if current_room != old_room:
## print("Entering room:" + str(current_room))
##
##clock.schedule_interval(movement, 0.08)
--snip--
清单 7-1:禁用EXPLORER部分的键盘控制
现在我们可以添加代码,使用箭头键来移动宇航员。
添加新变量
让我们从设置一些变量开始。最重要的变量是你传送进入的起始坐标。像之前一样,我们将变量添加到程序的VARIABLES部分,靠近程序的开始位置。在清单 7-2 中添加新行。将程序保存为listing7-2.py。
listing7-2.py
--snip--
TILE_SIZE = 30
➊ player_y, player_x = 2, 5
➋ game_over = False
➌ PLAYER = {
"left": [images.spacesuit_left, images.spacesuit_left_1,
images.spacesuit_left_2, images.spacesuit_left_3,
images.spacesuit_left_4
],
"right": [images.spacesuit_right, images.spacesuit_right_1,
images.spacesuit_right_2, images.spacesuit_right_3,
images.spacesuit_right_4
],
"up": [images.spacesuit_back, images.spacesuit_back_1,
images.spacesuit_back_2, images.spacesuit_back_3,
images.spacesuit_back_4
],
"down": [images.spacesuit_front, images.spacesuit_front_1,
images.spacesuit_front_2, images.spacesuit_front_3,
images.spacesuit_front_4
]
}
➍ player_direction = "down"
➎ player_frame = 0
➏ player_image = PLAYER[player_direction][player_frame]
player_offset_x, player_offset_y = 0, 0
--snip--
清单 7-2:添加玩家变量
VARIABLES部分已经包含了current_room的值,它是你开始时所在的房间。(如果你在第六章中实验时更改了current_room的值,请确保将其改回 31。)我们创建了新的player_y和player_x变量➊,用于存储你在房间中的起始位置的数字。这里,我们在一行中设置了两个变量。数字按它们的列出顺序放入变量中,所以2被放入player_y(第一个数字放入第一个变量),而5被放入player_x。这些变量会随着你在空间站中房间的移动而变化,并且将用于检查你的位置并在正确的地方绘制你。你的位置信息使用与景观位置相同的瓦片坐标来衡量。
我们还设置了一个game_over变量 ➋ 用于告诉程序游戏是否已经结束。在程序开始时,我们将该变量设置为False。它会保持False,直到游戏结束后变为True。程序会检查这个变量,看看玩家是否可以继续移动。如果玩家在死后还继续移动,那就显得很奇怪了!
接下来,我们将设置玩家的行走动画图像。动画是一种视觉技巧。你从一系列相似的图片开始,每张图片之间有细微的差别,展示了小的运动。当你快速切换这些图片时,可以欺骗眼睛,让它认为图像在移动。在我们的游戏中,我们将使用一系列宇航员行走的图片,展示宇航员腿部的不同位置。当我们快速切换这些图片时,宇航员的腿看起来就像在移动。
提示
使动画生效的关键是确保图像足够相似。如果图像差别太大,效果就无法实现。
动画中的每一张图片称为帧。表 7-1 展示了我们将使用的动画帧。我们将从 0 开始编号帧,这将是宇航员不行走时的静止位置。当玩家向上走时,我们会看到他们的背面,因为他们是在背离我们走出房间。
表 7-1: 宇航员的动画帧
| 键 | 帧 0 | 帧 1 | 帧 2 | 帧 3 | 帧 4 |
|---|---|---|---|---|---|
left |
![]() |
![]() |
![]() |
![]() |
![]() |
right |
![]() |
![]() |
![]() |
![]() |
![]() |
up |
![]() |
![]() |
![]() |
![]() |
![]() |
down |
![]() |
![]() |
![]() |
![]() |
![]() |
PLAYER 字典 ➌ 存储动画帧。方向名称——上、下、左、右——是字典的键。每个字典条目是一个列表,其中包含玩家站立的图像,以及该方向行走的四个动画帧(参见表 7-1)。PLAYER 字典将与玩家面朝的方向 ➍ 以及动画帧的编号 ➎ 一起使用,以在玩家行走或静止时显示正确的图像。player_image 变量 ➏ 存储当前的宇航员图像。
提示
附录 B 在书的后面描述了Escape程序中的重要变量,如果你不记得某个变量的作用,可以去那里查看。
传送到太空站
准备好传送下去吧!有了起始坐标后,让我们添加代码让你出现在太空站上。
清单 7-3 显示了你需要添加到程序的 EXPLORER 部分的代码行。如前所述,你只需要添加新行,不要更改其他行。只需用它们来帮助你理解程序代码。第一行新代码 ➊ 缩进了八个空格,因为它在一个函数内,且在一个循环内。将你的程序保存为 listing7-3.py。
listing7-3.py
--snip--
for y in range(room_height):
for x in range(room_width):
if room_map[y][x] != 255:
image_to_draw = objects[room_map[y][x]][0]
screen.blit(image_to_draw,
(top_left_x + (x*30),
top_left_y + (y*30) - image_to_draw.get_height()))
➊ if player_y == y:
➋ image_to_draw = PLAYER[player_direction][player_frame]
➌ screen.blit(image_to_draw,
(top_left_x + (player_x*30)+(player_offset_x*30),
top_left_y + (player_y*30)+(player_offset_y*30)
- image_to_draw.get_height()))
--snip--
清单 7-3:在房间中绘制玩家
这些新指令会将你绘制在房间中。y 循环从后往前绘制房间。x 循环在每一行中从左到右绘制景物。
每一行绘制完成后,程序会检查玩家是否站在该行 ➊ 中。此指令应该与 for x in range(room_width) 行对齐,而不是进一步缩进,因为它不在 x 循环内。它将在 x 循环完成后执行一次。
如果玩家位于程序刚绘制的那一行中,下一行 ➋ 会将玩家的图像放入变量 image_to_draw 中。图像来自 PLAYER 动画帧字典,使用玩家的方向和动画帧编号。
最后一行新代码 ➌ 使用你刚设置的 image_to_draw 变量绘制玩家图像,该变量包含图像。它还使用玩家的 x 和 y 位置变量来计算图像在屏幕上的绘制位置。第三章 解释了如何计算屏幕上的位置(见 “计算绘制每个物品的位置” 第 56 页)。player_offset_x 和 player_offset_y 变量在 清单 7-2 中设置,用于在玩家走过瓷砖时将其部分位置放置在瓷砖之间。你稍后会了解更多关于这些变量的内容。
准备好传送吧!做好准备!深呼吸。
使用 pgzrun 运行你的程序 listing7-3.py。如果传送成功,你应该在太空站上(见 图 7-1)。如果没有,检查你在本章中所做的程序更改。
传送的一个副作用是,刚开始你无法移动。随着代码的增加,你会发现这个副作用会逐渐消失。

图 7-1:宇航员到达了!
添加移动代码
现在,我们将添加一个全新的部分,称为 游戏循环。这是程序的核心。game_loop() 函数将每秒运行多次,并允许你移动。稍后在书中,我们将在这里添加更多指令,让你可以与发现的物体进行交互。
将这个新部分添加到 MAKE MAP 和 EXPLORER 部分之间。清单 7-4 会展示它的样子。将程序保存为 listing7-4.py。
listing7-4.py
--snip--
for tile_number in range(1, image_width_in_tiles):
room_map[scenery_y][scenery_x + tile_number] = 255
###############
## GAME LOOP ##
###############
➊ def game_loop():
➋ global player_x, player_y, current_room
global from_player_x, from_player_y
global player_image, player_image_shadow
global selected_item, item_carrying, energy
global player_offset_x, player_offset_y
global player_frame, player_direction
➌ if game_over:
return
➍ if player_frame > 0:
player_frame += 1
time.sleep(0.05)
if player_frame == 5:
player_frame = 0
player_offset_x = 0
player_offset_y = 0
➎ # save player's current position
old_player_x = player_x
old_player_y = player_y
➏ # move if key is pressed
if player_frame == 0:
if keyboard.right:
from_player_x = player_x
from_player_y = player_y
player_x += 1
player_direction = "right"
player_frame = 1
elif keyboard.left: #elif stops player making diagonal movements
from_player_x = player_x
from_player_y = player_y
player_x -= 1
player_direction = "left"
player_frame = 1
elif keyboard.up:
from_player_x = player_x
from_player_y = player_y
player_y -= 1
player_direction = "up"
player_frame = 1
elif keyboard.down:
from_player_x = player_x
from_player_y = player_y
player_y += 1
player_direction = "down"
player_frame = 1
➐ # If the player is standing somewhere they shouldn't, move them back.
# Keep the 2 comments below - you'll need them later
if room_map[player_y][player_x] not in items_player_may_stand_on: #\
# or hazard_map[player_y][player_x] != 0:
player_x = old_player_x
player_y = old_player_y
➑ player_frame = 0
➒ if player_direction == "right" and player_frame > 0:
player_offset_x = -1 + (0.25 * player_frame)
if player_direction == "left" and player_frame > 0:
player_offset_x = 1 - (0.25 * player_frame)
if player_direction == "up" and player_frame > 0:
player_offset_y = 1 - (0.25 * player_frame)
if player_direction == "down" and player_frame > 0:
player_offset_y = -1 + (0.25 * player_frame)
###############
## EXPLORER ##
###############
--snip--
清单 7-4:添加玩家移动
在程序的最后,你还需要添加一个名为START的新部分,它将使game_loop()函数每 0.03 秒运行一次。清单 7-5 展示了你需要添加的行。这条指令没有缩进,因为它不属于任何函数。Python 会按照程序中出现的顺序从上到下执行不在函数中的指令。这条指令会在所有变量、地图、景物和道具数据被设置好,并且函数在上面被定义之后执行。将你的程序保存为listing7-5.py。
listing7-5.py
--snip--
###############
## START ##
###############
clock.schedule_interval(game_loop, 0.03)
--snip--
清单 7-5:使 game_loop() 函数定期运行
使用 pgzrun 运行程序,命令为 listing7-5.py。你应该已经在房间里(如图 7-1 所示),并且能够使用箭头键进行移动!你可能会注意到当你向上走时,你的腿会消失。这是瞬移的副作用,当我们在第八章改进绘制房间的代码时,这个问题会消失。
此时,如果你走出房间,程序可能不会正常工作,但它应该能阻止你穿过墙壁或家具。如果你能够穿过物体,请再次检查你刚刚添加的代码。如果仍然有问题,仔细检查在程序的OBJECTS部分末尾设置items_player_may_stand_on列表的那一行。
理解运动代码
如果你想玩这个游戏并自定义它的设计,你不需要理解本章节中的代码是如何工作的。你只需要替换图像和地图、景物及道具的数据。这段运动代码,以及你将在本章节稍后添加的在房间间移动的代码,应该仍然能够正常运行。然而,如果你想理解这些代码是如何工作的,并且希望了解如何为程序添加动画效果,我现在会详细解释。这段代码是游戏的真正引擎,所以在很多方面,它是最令人兴奋的部分!
如果你有一种似曾相识的感觉,那是因为你已经见过大部分代码。在第二章中,你使用代码通过键盘控制改变玩家的位置,并使用名为game_loop()的函数控制运动。让我们回顾一下,并看看清单 7-4 中的新内容。
在清单 7-4 中,我们在这个新章节的开始定义了一个名为game_loop()的函数➊。我们在程序末尾添加的clock.schedule_interval()函数(见清单 7-5)让game_loop()函数每 0.03 秒执行一次。每次game_loop()函数运行时,它会检查你是否按下了箭头键或正在行走,如果是,它会更新你的当前位置。
在 game_loop() 开始时,我们告诉 Python 哪些变量是全局变量 ➋(如需复习为什么要这样做,请参考 “理解太空漫步示例” 在 第 27 页)。这些变量有些目前还未使用,但稍后会用到。
然后我们检查 game_over 变量。如果它被设置为 True ➌,game_loop() 函数将结束,不再执行其余的指令,因为游戏已经结束。这个变量会在游戏结束时阻止玩家继续移动。目前它不会起作用,因为程序中没有任何部分会导致游戏结束。
game_loop() 函数检查玩家是否已经在行走 ➍。走一步需要四帧动画来跨越一个屏幕格子。如果玩家在移动,player_frame 变量的值在 1 和 4 之间,表示当前使用的动画帧。如果玩家在行走,程序会将 player_frame 变量加 1,切换到下一个动画帧。这意味着 EXPLORER 部分中的 draw() 函数将在下一次运行时显示下一个动画帧。
当 player_frame 达到 5 时,意味着所有动画帧都已经显示完毕,动画结束。在这种情况下,程序将重置 player_frame 为 0,结束动画。当动画结束时,程序还会重置 player_offset_x 和 player_offset_y 变量。我稍后会告诉你这些变量的作用。
接下来,我们检查玩家是否按下了某个键来启动新的行走动画。在允许玩家移动之前,我们通过将 x 位置存储在 old_player_x 变量中,并将 y 位置存储在 old_player_y 变量中来保存他们的当前位置 ➎。我们将使用这些变量来将玩家移回原位,如果他们试图走到不该去的地方,例如走进墙壁或柱子里。
程序接着使用一段熟悉的代码来改变玩家的 x 和 y 位置变量,前提是玩家按下了箭头键 ➏。我们以格子为单位来衡量玩家的位置,这与我们用来定位场景的单位相同。这与我们在第一章中使用像素作为度量单位不同。
当玩家按下右箭头键时,程序会将 x 位置加 1。如果玩家按下左箭头键,程序会将 x 位置减 1。类似的代码用于在玩家按下上下箭头键时改变 y 位置。
当玩家移动时,全局变量 from_player_x 和 from_player_y 存储玩家行走的起始位置。这些变量稍后将用于检查玩家在行走过程中是否被障碍物撞到。同时,player_direction 变量也会被设置为玩家移动的方向,player_frame 设置为 1,即动画序列中的第一帧。
如第一章所示,我们使用elif来结合我们对按键的检查。这确保玩家不能同时改变x和y位置,从而进行对角线移动。在我们的 3D 房间中,对角线行走会让玩家穿过障碍物,挤过不可能的缝隙。
在移动玩家后,我们检查新位置是否把他们放在允许站立的地方 ➐。我们通过使用room_map查看他们站立位置的物品,并与items_player_may_stand_on列表进行比较来实现这一点。这里也有一些被注释掉的代码,我们稍后需要启用它来防止玩家穿越危险区域。
我们可以使用关键字in来检查某个元素是否在列表中。通过与not一起使用,我们可以看到某个元素是否缺失在列表中。以下代码的意思是:“如果玩家站立的地图位置中的数字不在物品列表中,玩家可以站立在……”
if room_map[player_y][player_x] not in items_player_may_stand_on:
如果玩家站在不在items_player_may_stand_on列表中的物体上,我们会将玩家的x和y位置重置为他们移动前的位置。
所有这些过程发生得非常快,以至于玩家几乎察觉不到。如果他们尝试走进墙壁,看起来就像他们从没移动过一样!这种方式比在每次移动前都检查是否允许更简单地防止玩家穿墙。
如果玩家的位置必须重置,程序还会将player_frame变量设置为 0 ➑。这会再次关闭玩家的动画。
当你按下右箭头键时,宇航员会向右走一个瓦片。这需要四个帧来动画化,因此在此动画播放过程中,宇航员会显示在瓦片的中途位置。player_offset_x和player_offset_y变量用于计算绘制宇航员的位置。这些变量是在game_loop()函数的末尾计算的 ➒。draw()函数(见列表 7-3)将偏移值乘以瓦片的大小(30 像素),因为图像是以像素为单位绘制的。例如,如果偏移量是 0.25 瓦片,宇航员大约会被绘制在距离新瓦片中心 7 个像素的位置。计算机会四舍五入这个数值,因为你不能用半个像素来定位物体。
查看图 7-2 的左侧。对于宇航员向左行走的第一个动画帧,我们需要将四分之三的瓦片添加到玩家的新瓦片位置(0.75)。对于第二个动画帧,我们在绘制之前将半个瓦片(0.5)添加到玩家的新瓦片位置。对于第三个动画帧,我们将四分之一的瓦片添加到玩家的新瓦片位置。

图 7-2:理解宇航员在动画过程中的定位
我们可以通过帧编号计算这些偏移量。以下是向左行走的计算方法:
player_offset_x = 1 - (0.25 * player_frame)
通过自己算一算,检查这个计算是否合理。例如,当动画帧为 2 时,计算如下:
0.25 × 2 = 0.5
1 − 0.5 = 0.5
在图 7-2 中,0.5 是帧 2 的正确偏移量。
当玩家向右走时,我们需要从玩家的位置中减去一部分瓦片,因此偏移量是负数。请查看图 7-2 的右侧。对于帧 1,添加−0.75 将宇航员放置在其新位置的左侧三分之四的位置。
我们也可以通过帧编号来计算向右行走的* x *偏移量。这里是公式:
player_offset_x = -1 + (0.25 * player_frame)
训练任务 #1
你能检查一下公式是否有效吗?使用它来查找帧 1 和帧 3 的偏移值,并检查它们是否与图 7-2 中的偏移值相符。
- y 方向的偏移量计算方式相同。当宇航员向上移动时,我们使用与向左偏移量相同的公式计算 y 偏移量。当宇航员向下移动时,我们使用与向右偏移量相同的公式计算 y *偏移量。
总结一下,game_loop()函数的作用如下:
-
如果你没有在行走,按下一个键时会启动行走动画。
-
如果你正在行走,它会计算下一个动画帧,以及在绘制你时用于部分跨越瓦片的位置信息。
-
如果你已经到达动画序列的末尾,它会重置动画,这样你就可以继续移动了。移动非常流畅,所以如果你按住一个键,你将会在动画帧 1 到 4 之间循环,直到停止行走才会看到站立姿势。
房间间移动
现在你已经站起来了,应该完全探索一下空间站。让我们在game_loop()函数中添加一些代码,允许你走进下一个房间。把新代码添加到清单 7-6 中,位置是在检查按键之后和检查玩家是否站在不该站的位置之前。确保包括带有注释符号(#)的指令。我们稍后需要它们。
清单 7-6 中的灰色行告诉你在哪些地方添加新代码。将程序保存为listing7-6.py。使用 pgzrun 运行 listing7-6.py,然后在空间站内四处走走!现在正是一个好时机,可以环顾四周,在门装上之前,某些区域尚未锁定。
listing7-6.py
--snip--
def game_loop():
--snip--
player_direction = "down"
player_frame = 1
# check for exiting the room
➊ if player_x == room_width: # through door on RIGHT
#clock.unschedule(hazard_move)
➋ current_room += 1
➌ generate_map()
➍ player_x = 0 # enter at left
➎ player_y = int(room_height / 2) # enter at door
➏ player_frame = 0
➐ #start_room()
➑ return
➒ if player_x == -1: # through door on LEFT
#clock.unschedule(hazard_move)
current_room -= 1
generate_map()
player_x = room_width - 1 # enter at right
player_y = int(room_height / 2) # enter at door
player_frame = 0
#start_room()
return
➓ if player_y == room_height: # through door at BOTTOM
#clock.unschedule(hazard_move)
current_room += MAP_WIDTH
generate_map()
player_y = 0 # enter at top
player_x = int(room_width / 2) # enter at door
player_frame = 0
#start_room()
return
if player_y == -1: # through door at TOP
#clock.unschedule(hazard_move)
current_room -= MAP_WIDTH
generate_map()
player_y = room_height - 1 # enter at bottom
player_x = int(room_width / 2) # enter at door
player_frame = 0
#start_room()
return
# If the player is standing somewhere they shouldn't, move them back.
if room_map[player_y][player_x] not in items_player_may_stand_on: #\
# or hazard_map[player_y][player_x] != 0:
player_x = old_player_x
--snip--
清单 7-6:启用玩家在房间间移动
为了查看这段代码是如何工作的,让我们使用一个示例房间地图。图 7-3 显示了一个 9 个瓦片宽和 9 个瓦片高的房间,每面墙都有出口。我们将使用这张图来理解玩家离开房间时的位置。
如你所知,地图上的位置从左上角的 0 开始编号。黄色方块表示玩家走出房间后可能的位置:
-
如果玩家的* y *位置是−1,说明他们走出了顶部出口。
-
如果玩家的* x *位置是−1,说明他们走出了左侧出口。
-
如果玩家的* y *位置与
room_height变量相同,他们已经走出了底部。瓦片位置从 0 开始编号,所以如果玩家进入了一个有 9 行的房间的第 9 行,他们已经离开了房间。 -
类似地,如果玩家的* x *位置与
room_width变量相同,则表示他们已经从右侧出口走了出去。

图 7-3:判断玩家是否走过出口
新的代码行检查玩家的位置是否意味着他们已经走出了房间。如果玩家的* x *位置与room_width相同 ➊,他们就在右侧门外,如图 7-3 所示。
当玩家离开房间时,我们需要改变他们所在的房间编号,这个编号保存在current_room变量中。当他们通过右侧门时,房间编号会增加 1 ➋。再次查看房间地图(翻回图 4-1 在第 60 页),以确认这一点:房间编号是从左到右递增的。例如,如果玩家在房间 33 中,走过右侧出口,他们将进入房间 34。
然后程序生成一个新的room_map列表 ➌,用于显示和导航新房间。玩家被重新定位到房间的另一侧 ➍,看起来像是他们已经走过了门口。如果玩家从房间的右侧出口出去,他们将从左侧进入下一个房间 ➍。
房间有很多不同的大小,所以我们还需要改变玩家的* y *位置,将他们放置在门口的中央。否则,玩家可能会从墙壁里出来!我们将玩家的位置设置为房间高度的一半 ➎,这样他们就正好站在门口的中央。当他们进入房间时,我们也会重置玩家动画 ➏。
我在这里包含了几个我们稍后需要的功能,所以确保你包括了clock.unschedule(hazard_move) ➊和start_room() ➐指令。start_room()函数将在玩家进入新房间时显示房间名称。我们将在书中的后面部分进一步讨论这些指令。
最后,return指令退出game_loop()函数 ➑。此时函数中的任何进一步指令都不会执行。当函数重新开始时,它将像往常一样从头开始。
下一个代码块 ➒ 检查玩家是否走过左侧门。要通过左侧门,程序执行以下操作:
-
检查
player_x变量是否包含-1(见图 7-3)。 -
从当前房间编号中减去 1,进入左侧房间。
-
将玩家的* x 位置设置为刚好位于右侧门口内部。这个位置是
room_width减去 1。(你可以在图 7-3 中检查这一点。在一个room_width为 9 的房间中,玩家的 x *位置应该是 8。) -
使用
room_height将玩家的y位置设置为中间。这与通过右侧出口行走的方式相同。
对于上下出口➓,使用了相同的代码结构。但是,程序会检查玩家的y位置,看看他们是否使用了出口,并将他们的新位置设置为通过上方或下方的门进入。
这次,我们通过 5 来改变房间号,而不是 1,因为游戏地图宽度是 5 个房间(见图 4-1)。例如,如果你在房间 37,走过上方出口,你将进入房间 32(37 减去 5)。如果你在房间 37,走过下方出口,你将进入房间 42(37 加上 5)。我们之前将数字 5 存储在变量MAP_WIDTH中,程序在这里使用它。
现在你可以自由探索空间站了。在下一章,我们将修复房间显示中的剩余几个错误。
你准备好飞行了吗?
勾选以下框以确认你已经掌握了本章的关键知识点。
玩家在逃脱游戏中的位置是以格子为单位的,就像风景一样。
game_loop()函数控制玩家移动,并计划每 0.03 秒运行一次。
如果玩家移动到一个不允许的位置,他们会迅速被送回到之前的位置,快到你几乎看不见他们移动。
程序检查玩家的x和y位置,以查看他们是否已走出出口。如果是,他们将在下一个房间的相对出口中间出现。
动画帧存储在PLAYER字典中,并为每个方向列出了图像。字典的键是方向名称,索引号获取所需的特定帧。
帧 0 是静止位置。帧 1、2、3 和 4 显示宇航员行走的动画。
game_loop()函数在玩家行走时增加使用的动画帧数。
player_offset_x和player_offset_y变量用于在进入新格子时正确地定位宇航员。

第八章:修复空间站

当你在空间站内四处漫游时,你一定注意到有些地方看起来不太对。为了快速启动程序,我们使用EXPLORER部分来显示房间。然而,它也有一些缺点:
-
有时,场景下方会显示一片空白,因为那里没有地板。
-
当你走到房间前方时,前墙会遮挡住宇航员。
-
宇航员的腿在走到屏幕后方时消失。
-
所有房间都绘制在游戏窗口的左上角。这让它看起来不均匀且不一致,因为房间右边的空间远比左边的多,宽的房间在右侧的空间比窄房间少。
-
没有阴影,导致更难理解房间中物体的位置。
在本章中,我们将修复这些故障,并添加一个在窗口顶部显示信息的函数。这些信息将向玩家提供有关空间站以及他们在游戏中进展的情况。
在你阅读本章时,你将学会如何将信息传递给 Python 函数,并发现如何使用 Pygame Zero 绘制矩形。到本章结束时,空间站将变得很棒!
将信息传递给一个函数
第一次,我们需要将信息传递给函数。你已经看过如何通过将信息放入括号中传递给print()函数。例如,你可以像这样输出一条信息:
print("Learn your emergency evacuation drill")
当该指令运行时,print()函数接收你放入括号中的信息,并在命令行窗口或 Python Shell 中显示出来。
我们还可以将信息传递给我们自己创建的函数。
创建一个接收信息的函数
为了实验函数,我们将构建一个可以将两个数字相加的函数。点击文件 ▸ 新建打开一个新窗口,并在清单 8-1 中输入程序。
listing8-1.py
➊ def add(first_number, second_number):
➋ total = first_number + second_number
➌ print(first_number, "+", second_number, "=", total)
➍ add(5, 7)
add(2012, 137)
add(1234, 4321)
清单 8-1:将信息传递给一个函数
将程序保存为listing8-1.py。因为它没有使用任何 Pygame Zero 功能,你可以通过点击运行 ▸ 运行模块或按 F5 来运行它。(如果使用 Pygame Zero 运行,结果会显示在命令行窗口,游戏窗口将为空。)
当你运行程序时,你应该能看到以下输出:
5 + 7 = 12
2012 + 137 = 2149
1234 + 4321 = 5555
我们创建了一个名为add()的新函数➊。在定义了add()之后,我们可以通过使用其名称➍来运行它,并通过将数字放入括号中,使用逗号分隔它们➍,向它发送数字。然后,函数会将这两个数字相加。
工作原理
为了使函数能够接收数字,我们在定义它时给它提供了两个变量来存储这些数字。我将它们命名为first_number和second_number➊,以便让程序更容易理解,但这些变量名可以是任何名字。这些是局部变量:它们只在这个函数内部有效。
当你使用这个函数时,它会获取接收到的第一个项目,并将其放入变量first_number中。第二个项目会放入second_number中。
当然,两个数字相加的顺序并不重要,所以传递数字的顺序也不重要。指令add(5, 7)和add(7, 5)会得到相同的结果。但有些函数需要你按照函数预期接收信息的顺序来传递参数。例如,如果函数是做减法运算的,那么如果你传递数字的顺序错误,你将得到不同的结果。知道函数期望接收什么信息的唯一方法是查看它的代码。
函数体非常简单。它创建了一个新变量total,用于存储两个数字相加的结果➋。然后程序打印一行,内容包括第一个数字、加号、第二个数字、等号和总和➌。
在最后三条指令中,我们将三个数字对传递给函数进行加法运算➍。
这个简单的演示向你展示了如何将信息(或参数)传递给函数。你可以创建接受比两个参数更多参数的函数,甚至可以接受列表、字典或图像。函数使得重复使用指令集变得简单,而传递参数意味着我们可以使用不同的信息重复使用这些指令。例如,清单 8-1 使用了三次相同的print()指令,来显示三个不同数字对的和。在这种情况下,我们避免了重复使用print()指令和设置total变量的指令。更复杂的函数可以避免重复大量代码,这使得程序更容易编写和理解。
训练任务 #1
尝试修改程序,使其从一个数字中减去另一个数字,而不是加法运算。当你改变传递给新函数的数字顺序时,会发生什么呢?你可能需要修改的不仅仅是计算部分,确保函数易于使用。
现在我们准备向Escape游戏添加一些新函数,用来在空间站上绘制物体。
添加阴影、墙壁透明度和颜色的变量
为了修复我们的空间站,我们将使用新学到的函数知识,为Escape游戏创建新的显示函数。在创建这些新函数之前,我们需要为函数使用设置新的变量。
打开 listing7-6.py,这是你在第七章保存的最后一个列表。在程序的开头找到 VARIABLES 部分,并添加列表 8-2 中显示的新行。将程序保存为 listing8-2.py。像往常一样,运行程序(使用 pgzrun listing8-2.py)检查是否有新的错误。
listing8-2.py
--snip--
###############
## VARIABLES ##
###############
--snip--
player_image = PLAYER[player_direction][player_frame]
player_offset_x, player_offset_y = 0, 0
➊ PLAYER_SHADOW = {
"left": [images.spacesuit_left_shadow, images.spacesuit_left_1_shadow,
images.spacesuit_left_2_shadow, images.spacesuit_left_3_shadow,
images.spacesuit_left_3_shadow
],
"right": [images.spacesuit_right_shadow, images.spacesuit_right_1_shadow,
images.spacesuit_right_2_shadow,
images.spacesuit_right_3_shadow, images.spacesuit_right_3_shadow
],
"up": [images.spacesuit_back_shadow, images.spacesuit_back_1_shadow,
images.spacesuit_back_2_shadow, images.spacesuit_back_3_shadow,
images.spacesuit_back_3_shadow
],
"down": [images.spacesuit_front_shadow, images.spacesuit_front_1_shadow,
images.spacesuit_front_2_shadow, images.spacesuit_front_3_shadow,
images.spacesuit_front_3_shadow
]
}
➋ player_image_shadow = PLAYER_SHADOW["down"][0]
➌ PILLARS = [
images.pillar, images.pillar_95, images.pillar_80,
images.pillar_60, images.pillar_50
]
➍ wall_transparency_frame = 0
➎ BLACK = (0, 0, 0)
BLUE = (0, 155, 255)
YELLOW = (255, 255, 0)
WHITE = (255, 255, 255)
GREEN = (0, 255, 0)
RED = (128, 0, 0)
###############
## MAP ##
###############
--snip--
列表 8-2:为新的显示函数添加所需的变量
我们添加了一个与 PLAYER 字典类似的 PLAYER_SHADOW 字典 ➊。它包含了宇航员阴影的动画帧。当宇航员移动时,阴影的形状也会发生变化。player_image_shadow ➋ 存储着当前的宇航员阴影,就像 player_image 变量存储当前的宇航员动画帧(或站立图像)。
在本章后面,我们将添加动画,使得当你走到前墙后面时,前墙会渐隐,这样你仍然可以看到宇航员。在这里,我们设置了一个包含动画帧的列表 ➌ 和一个 wall_transparency_frame 变量,用来记住当前显示的动画帧 ➍。你稍后将学习更多关于这些是如何工作的。
我们还设置了一些名称,可以用来引用颜色编号 ➎。Pygame Zero 中的颜色存储为元组。元组像一个列表,但其中的内容不可更改,并且使用圆括号而不是方括号。你已经在绘制屏幕坐标时看到过元组的使用(见第一章)。颜色作为三个数字存储,分别指定了颜色中的红色、绿色和蓝色的量,顺序是这样的。每种颜色的范围从 0 到 255。这个颜色是亮红色:
(255, 0, 0)
红色达到了最大值(255),而绿色(0)和蓝色(0)则没有出现在颜色中。
因为我们已经设置了这些颜色变量,现在可以使用名称BLACK来代替使用元组(0, 0, 0)来表示黑色。使用颜色名称会让程序更易于阅读。
表 8-1 展示了一些你可能在程序中使用的颜色组合。你也可以尝试不同的数字,发明你自己的颜色。
表 8-1: 一些示例 RGB 颜色值
| 红色 | 绿色 | 蓝色 | 描述 |
|---|---|---|---|
| 255 | 0 | 0 | 亮红色 |
| 0 | 255 | 0 | 亮绿色 |
| 0 | 0 | 255 | 亮蓝色 |
| 0 | 0 | 50 | 极暗蓝色(几乎是黑色!) |
| 255 | 255 | 255 | 白色(所有颜色的最大强度) |
| 255 | 255 | 150 | 奶油黄色(比白色少一点蓝色) |
| 230 | 230 | 230 | 银色(稍微调暗的白色) |
| 200 | 150 | 200 | 淡紫色 |
| 255 | 100 | 0 | 橙色(最大红色,略带绿色) |
| 255 | 105 | 180 | 粉色 |
删除资源管理器部分
我们需要添加一个新的 DISPLAY 部分,其中包含一些新的函数,将改善游戏在屏幕上的显示效果。EXPLORER 部分帮助我们快速启动了程序,但我们将在本章中构建一个新的、更好的 draw() 函数,替代我们目前使用的那个。为了避免程序中仍然残留 EXPLORER 代码带来的问题,我们将删除它。你的 EXPLORER 部分可能比我的 图 8-1 中的多一些或少一些,具体取决于你是否在前几章删除了一些内容。
要删除整个 EXPLORER 部分,请按照以下步骤操作:
-
找到代码接近结尾处的
EXPLORER部分。 -
点击
EXPLORER注释框的起始位置,按住鼠标按钮,并拖动鼠标至该部分的底部(见 图 8-1)。该部分结束的位置在START部分开始之前。 -
按下键盘上的 DELETE 或 BACKSPACE 键。
在 EXPLORER 部分中有一个我们仍然需要的指令:它运行 generate_map() 函数来设置第一个房间的地图。你需要将该指令作为单独的一行添加到程序的末尾,如 清单 8-3 所示。
listing8-3.py
--snip--
###############
## START ##
###############
clock.schedule_interval(game_loop, 0.03)
generate_map()
清单 8-3:为第一个房间生成地图
generate_map() 这一行将在变量设置好后运行,并为当前房间生成地图。

图 8-1:删除 EXPLORER 部分
将新清单保存为 listing8-3.py,并使用 pgzrun listing8-3.py 运行它。如果一切顺利,命令行窗口应该不会显示任何错误信息。游戏窗口显示的是漆黑的太空,因为我们还没有添加绘制任何内容的新代码。
添加 DISPLAY 部分
现在,我们将添加新的 DISPLAY 部分,来替代已删除的 EXPLORER 部分。这个部分包含了更新屏幕显示的大部分代码。它包括绘制房间、显示信息和改变前墙透明度的代码。
添加绘制对象的函数
首先,我们将创建一些函数,用于在特定的瓦片位置绘制对象、阴影或玩家。在 GAME LOOP 和 START 部分之间,添加新的 DISPLAY 部分,如 清单 8-4 所示。将该程序保存为 listing8-4.py 并使用 pgzrun listing8-4.py 运行。再次提醒,游戏窗口中还不会显示任何内容。
如果命令行窗口中有任何错误,你可以利用它们来帮助修复程序。最好在添加代码时进行测试,而不是一次性添加大量代码后再去找错误。
listing8-4.py
--snip--
if player_direction == "down" and player_frame > 0:
player_offset_y = -1 + (0.25 * player_frame)
###############
## DISPLAY ##
###############
➊ def draw_image(image, y, x):
➋ screen.blit(
image,
(top_left_x + (x * TILE_SIZE),
top_left_y + (y * TILE_SIZE) - image.get_height())
)
➌ def draw_shadow(image, y, x):
screen.blit(
image,
(top_left_x + (x * TILE_SIZE),
top_left_y + (y * TILE_SIZE))
)
def draw_player():
➍ player_image = PLAYER[player_direction][player_frame]
➎ draw_image(player_image, player_y + player_offset_y,
player_x + player_offset_x)
➏ player_image_shadow = PLAYER_SHADOW[player_direction][player_frame]
➐ draw_shadow(player_image_shadow, player_y + player_offset_y,
player_x + player_offset_x)
###############
## START ##
###############
clock.schedule_interval(game_loop, 0.03)
generate_map()
清单 8-4:在 DISPLAY 部分添加第一个函数
第一个新的函数 draw_image() ➊ 用于在屏幕上绘制指定的图像。当我们使用它时,我们会传入要绘制的图像和物体在房间中的 y 和 x 瓷砖位置。该函数会根据房间中的瓷砖位置计算出在屏幕上绘制图像的位置(像素位置)。例如,我们可以这样使用这个函数:
draw_image(player_image, 5, 2)
这行代码将玩家的图像绘制在房间中位置 y = 5 和 x = 2 处。
当我们定义 draw_image() 函数时,我们设置它来接收图像名称 image,将 y 位置赋值给 y 变量,将 x 位置赋值给 x 变量 ➊。虽然 draw_image() 函数有几行代码,但它的唯一指令是 screen.blit(),该指令根据我们指定的位置绘制图像 ➋。这个指令几乎与我们在旧版 EXPLORER 部分中使用的相同,所以请查看 第三章 以复习它的工作原理。
提示
确保所有括号的位置正确。你需要在所有 screen.blit() 参数周围加上一对括号,并且在 y 和 x 位置的括号周围也要加上一对,因为它们组成一个元组。你还需要在位置计算的乘法部分加上一对括号。如果程序无法运行,首先检查错误时,要数一数开括号和闭括号的数量,确保它们的数量相同。
然后我们添加了一个新的 draw_shadow() 函数 ➌。这个函数与绘制图像的函数类似,不同之处在于计算图像在屏幕上的位置时,并不会减去图像的高度。这就是将阴影绘制在主图像 下方 的原因。图 8-2 显示了基于相同瓷砖位置的宇航员和他们的阴影。请记住,传递给 screen.blit() 的 y 位置是图像的顶部边缘。

图 8-2:计算图像和阴影的位置
第三个新的函数 draw_player() 用于绘制宇航员。首先,它将正确的宇航员动画帧放入 player_image ➍。然后使用新的 draw_image() 函数来绘制 ➎。draw_image() 函数需要以下参数:
-
变量
player_image包含需要绘制的图像。 -
将
player_y和player_offset_y变量相加后的结果。这是 y 位置的瓷砖数,可能包括小数部分(例如 5.25)。 -
将
player_x和player_offset_x相加后的结果,用于计算 x 位置的瓷砖数。(有关如何使用偏移变量进行动画的更多信息,请参见“理解移动代码”第 119 页)
我们使用类似的代码来绘制玩家的阴影:将正确的动画帧从 PLAYER_SHADOW 字典中取出并放入 player_image_shadow ➏。然后使用 draw_shadow() 函数将其绘制出来 ➐。draw_shadow() 函数使用与 draw_image() 函数相同的瓷砖位置。
绘制房间
现在我们已经创建了绘制对象和玩家的函数,可以添加绘制房间的代码了。列表 8-5 中的新draw()函数为场景和玩家添加了阴影,并修复了之前看到的视觉故障。
将新的代码添加到DISPLAY部分的末尾,将程序保存为listing8-5.py,并使用pgzrun listing8-5.py运行它。就像你打开了灯光,阴影出现在物体前面。游戏看起来还不完全正确,因为所有房间都会绘制在窗口的左上角,有时当你离开房间时,房间不会被正确清除。我们稍后会解决这个问题。在这一点上,你不应该看到任何错误信息。
listing8-5.py
--snip--
def draw_player():
player_image = PLAYER[player_direction][player_frame]
draw_image(player_image, player_y + player_offset_y,
player_x + player_offset_x)
player_image_shadow = PLAYER_SHADOW[player_direction][player_frame]
draw_shadow(player_image_shadow, player_y + player_offset_y,
player_x + player_offset_x)
def draw():
if game_over:
return
➊ # Clear the game arena area.
box = Rect((0, 150), (800, 600))
screen.draw.filled_rect(box, RED)
box = Rect ((0, 0), (800, top_left_y + (room_height - 1)*30))
➋ screen.surface.set_clip(box)
floor_type = get_floor_type()
➌ for y in range(room_height): # Lay down floor tiles, then items on floor.
for x in range(room_width):
draw_image(objects[floor_type][0], y, x)
# Next line enables shadows to fall on top of objects on floor
if room_map[y][x] in items_player_may_stand_on:
draw_image(objects[room_map[y][x]][0], y, x)
➍ # Pressure pad in room 26 is added here, so props can go on top of it.
if current_room == 26:
draw_image(objects[39][0], 8, 2)
image_on_pad = room_map[8][2]
if image_on_pad > 0:
draw_image(objects[image_on_pad][0], 8, 2)
➎ for y in range(room_height):
for x in range(room_width):
item_here = room_map[y][x]
# Player cannot walk on 255: it marks spaces used by wide objects.
if item_here not in items_player_may_stand_on + [255]:
image = objects[item_here][0]
➏ if (current_room in outdoor_rooms
and y == room_height - 1
and room_map[y][x] == 1) or \
(current_room not in outdoor_rooms
and y == room_height - 1
and room_map[y][x] == 1
and x > 0
and x < room_width - 1):
# Add transparent wall image in the front row.
image = PILLARS[wall_transparency_frame]
draw_image(image, y, x)
➐ if objects[item_here][1] is not None: # If object has a shadow
shadow_image = objects[item_here][1]
# if shadow might need horizontal tiling
➑ if shadow_image in [images.half_shadow,
images.full_shadow]:
shadow_width = int(image.get_width() / TILE_SIZE)
# Use shadow across width of object.
for z in range(0, shadow_width):
draw_shadow(shadow_image, y, x+z)
else:
draw_shadow(shadow_image, y, x)
➒ if (player_y == y):
draw_player()
➓ screen.surface.set_clip(None)
###############
## START ##
###############
clock.schedule_interval(game_loop, 0.03)
generate_map()
列表 8-5:新的 draw() 函数
与第七章中的移动代码一样,即使你想自定义程序,你也不需要知道draw()函数是如何工作的。我将在下一部分解释draw()函数,所以如果你暂时不想知道它是如何工作的,可以跳到“在屏幕上定位房间”,第 141 页(page 141)。
理解新的 DRAW() 函数
你可以将新的draw()函数看作是之前EXPLORER部分代码的一个更复杂版本。我将为你概述每一部分的工作原理。
清理游戏场地
程序通过清理游戏场地 ➊ 来开始绘制太空站的位置。它通过绘制一个大红色矩形来清除之前的屏幕显示。顶部和底部的区域提供玩家信息是独立的,因此它们不会被更改。
将矩形显示在屏幕上有两个步骤。首先,你使用一个名为Rect的 Pygame 对象来创建这个形状,它的工作方式如下(不要键入此内容):
box = Rect((left position, top position), (width, height))
名称几乎可以是任何你喜欢的名字,但在我的程序中,我使用box这个名字。位置和大小是元组,因此它们用括号括起来。
然后,你通过使用类似于以下指令的代码在屏幕上绘制你创建的矩形(再次提醒,不要键入此内容):
screen.draw.filled_rect(box, color)
括号中的第一个项目是你之前创建的 box 矩形。第二个项目是你想要绘制的矩形的颜色。这可以是由红色、绿色和蓝色构成的数字元组。在列表 8-5 中,我使用了我们在VARIABLES部分之前设置的RED名称。
你还可以使用矩形形状创建一个裁剪区域 ➋。这就像一个不可见的窗口,你通过它查看屏幕。如果程序绘制的东西超出了这个窗口,它就无法被看到。我设置了一个裁剪区域,区域的高度与房间一致,以防止玩家的阴影在站在前门口时从游戏底部溢出。
绘制房间
房间分为两个阶段绘制。首先,程序绘制地板瓷砖和玩家可以走动的区域 ➌。先绘制它们能使景物、玩家和阴影绘制在其上面。这解决了景物下出现黑洞的问题,因为在景物绘制之前,地板瓷砖已经存在于这些空间中。
接着,程序使用新的循环添加房间中的景物,包括它的阴影 ➎。由于这些是在整个房间的地板绘制完之后绘制的,阴影会覆盖在地板瓷砖和地面上的物品之上。阴影是透明的,因此你仍然可以看到阴影下方的物体。景物绘制的循环还会添加透明墙 ➏,并将玩家绘制在地面之上 ➒。
一如既往,房间是从后到前绘制的,以确保房间前方的物体看起来位于房间后方物体之前。
我们还添加了一小段代码,用于一个在游戏中只会在一个地方使用的特殊物体。26 号房间的地板上有一个压力垫,你在游戏中可能想要将物品放在上面(也许是重物,或者是你可以让它变重的物品...)。这里的特殊代码确保了地板垫和它上面的物体都能被显示出来。
在地板瓷砖绘制完后,draw()函数会检查当前房间是否是 26 号房间:如果是,它会先绘制地板垫,然后绘制任何在其上的物体 ➍。
红色警报
如果你正在用自己的地图自定义游戏,请删除这段代码以移除游戏中的地板垫。从注释行 ➍开始,并删除直到(包括)draw_image(objects[image_on_pad][0], 8, 2) 指令。
使前墙透明
当程序绘制房间的前排(当y循环等于room_height - 1时),它会检查是否需要绘制半透明墙壁,而不是从房间地图中获取的实体墙物体 ➏。如果玩家站在墙后面,则使用半透明墙壁(见图 8-3)。
在行星表面,程序使整个墙体透明。在太空站内部,只有当透明墙面板不位于底部角落位置时才使用透明墙面板(见图 8-3)。角落位置总是使用实体墙面板。原因是,如果你看到实体墙面板从倒数第二行开始,效果看起来会很奇怪。
稍后我们将添加代码,通过改变wall_transparency_frame中的数值来为墙壁添加透明度动画。此时在游戏中你还看不到半透明的墙壁。

图 8-3:如最终游戏中所见,房间前面的透明墙
添加阴影
如果一个物体有阴影,那么阴影会从objects字典中取出并放入shadow_image ➐。然后程序检查是否应该使用half_shadow或full_shadow,分别填充半块或整块瓷砖。这两种标准阴影适用于不需要特殊阴影轮廓的方块状物品(如电气单元和墙壁)。程序会检查shadow_image是否位于一个包含这两种标准图像的列表中 ➑。
这是一种简单且易于阅读的方式,用来检查shadow_image是否是两种情况之一。如果你检查的是三种或更多的情况,使用这种技巧可以让程序比使用大量if比较和结合==与or更加易读。
如果阴影是标准图像之一,程序将计算阴影应该有多宽,以瓷砖为单位。这是通过获取投射阴影的物体的宽度,并将其除以瓷砖的宽度(30 像素)来计算的。例如,一个宽度为 90 像素的图像,将是 3 个瓷砖宽。
程序接着创建一个循环,用变量z来绘制标准阴影图像。z从 0 开始,一直到阴影宽度减去 1。因为range不包括最后一项:range(0, 3)会返回 0、1 和 2 这三个数值。z值会加到主循环中的x位置,并用于绘制阴影瓷砖。图 8-4 展示了一个宽度为 3 个瓷砖的物体。z循环取值 0、1 和 2,用来把阴影绘制在正确的位置。
通过在铺设地板后绘制玩家角色,确保了宇航员的腿部在屏幕上移动时不会消失 ➒。

图 8-4:宽度为 3 个瓷砖的物体下方可以有一个标准阴影,这个阴影会被使用三次。
draw()函数的最后,关闭了限制区域,避免阴影溢出游戏区域的底部 ➓。
将房间定位到屏幕上的位置
现在让我们解决房间出现在屏幕左上角的问题。程序使用两个变量来定位房间:top_left_x 和 top_left_y。目前,这两个变量的值分别为 100 和 150,这意味着房间总是绘制在窗口的左上角。我们将添加一些代码,根据房间的大小改变这些变量的值,使房间居中显示在窗口中(见图 8-5)。这样屏幕布局看起来会更好,也会让游戏更容易操作。

图 8-5:房间居中显示在窗口中
将清单 8-6 中所示的新行添加到generate_map()函数的末尾,该函数位于程序的MAKE MAP部分。由于它们位于一个函数内,你需要将每行缩进四个空格。
将程序保存为listing8-6.py并使用 pgzrun listing8-6.py 运行。如图 8-5 所示,现在每个房间应该已经居中显示在屏幕上。
listing8-6.py
--snip--
def generate_map():
--snip--
for tile_number in range(1, image_width_in_tiles):
room_map[scenery_y][scenery_x + tile_number] = 255
➊ center_y = int(HEIGHT / 2) # Center of game window
➋ center_x = int(WIDTH / 2)
➌ room_pixel_width = room_width * TILE_SIZE # Size of room in pixels
➍ room_pixel_height = room_height * TILE_SIZE
➎ top_left_x = center_x - 0.5 * room_pixel_width
➏ top_left_y = (center_y - 0.5 * room_pixel_height) + 110
Listing 8-6:创建变量将房间放置在游戏窗口的中央
这些指令位于generate_map()函数内,该函数在玩家进入每个房间时设置room_map列表。generate_map()函数现在还会设置top_left_x和top_left_y变量,记住房间应当在窗口中的绘制位置。
Listing 8-6 中的新代码首先计算出窗口的中心位置。HEIGHT和WIDTH变量存储窗口的尺寸(像素)。将它们除以 2 可以得到窗口中心的坐标。我们将这些坐标存储在center_y ➊和center_x ➋变量中。
程序接着计算房间图像的宽度(像素)➌。它将是房间的瓷砖数乘以单个瓷砖的大小。计算结果存储在room_pixel_width中。对于房间高度,程序进行类似的计算➍。
为了将房间图像放置在房间的中央,我们希望房间的左半部分在中心线的左侧,右半部分在右侧。因此,我们从中心线➎开始,减去房间宽度的一半像素,然后在那里开始绘制房间。
对于top_left_y,我们使用类似的计算方法,只是我们在结果上加上了 110➏。我们需要加上 110,因为最终的屏幕布局会在屏幕的顶部使用一个信息面板区域。我们稍微将房间图像向下移动一些,为面板腾出空间。
使前墙逐渐显现与消失
此时,游戏中有一些死角,玩家在这些地方无法被看到。在房间中央,我们可以通过确保物体不太高以免遮挡玩家来避免这种情况。然而,我们仍然需要在房间前面放置一面高墙。
用墙壁将玩家挡在房间前面可能会引发各种问题:如果你掉了什么东西,你将无法找到它;或者如果有什么东西在伤害你,你将看不见它!解决方法是让墙壁在玩家接近时逐渐消失。
draw()函数已经使用动画帧绘制了前墙的柱子。墙壁动画共有五帧(编号从 0 到 4),存储在PILLARS列表中。第一帧是实心墙,而最后一帧显示的是墙壁最透明的状态(见表 8-2)。随着动画帧数的增加,墙壁变得越来越透明。当前帧存储在变量wall_transparency_frame中。
由于透明度在图像中的表现方式,当透明墙被绘制在玩家上方时,玩家将透过墙壁可见。
表 8-2:前墙的动画帧
| 帧编号 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 图像 | ![]() |
![]() |
![]() |
![]() |
![]() |
Listing 8-7 展示了一个新函数 adjust_wall_transparency(),它会使墙壁渐显或渐隐。将它添加到 DISPLAY 部分的末尾,紧接着你刚完成的 draw() 函数,并放在 START 部分之前。你还需要在程序的末尾、函数外部添加一行代码,安排它定期运行。这一行也在 Listing 8-7 中。
将更新后的程序保存为 listing8-7.py 并使用 pgzrun listing8-7.py 运行它。如果你走到前面的墙后面,它现在会渐变成透明,这样你就可以被看到(请参见本章前面的 Figure 8-3)。当你再次走开时,墙壁会恢复为不透明。
listing8-7.py
--snip--
###############
## DISPLAY ##
###############
--snip--
screen.surface.set_clip(None)
def adjust_wall_transparency():
global wall_transparency_frame
➊ if (player_y == room_height - 2
➋ and room_map[room_height - 1][player_x] == 1
➌ and wall_transparency_frame < 4):
➍ wall_transparency_frame += 1 # Fade wall out.
➎ if ((player_y < room_height - 2
➏ or room_map[room_height - 1][player_x] != 1)
➐ and wall_transparency_frame > 0):
➑ wall_transparency_frame -= 1 # Fade wall in.
###############
## START ##
###############
clock.schedule_interval(game_loop, 0.03)
generate_map()
➒ clock.schedule_interval(adjust_wall_transparency, 0.05)
Listing 8-7: 当你靠近前墙时让它变得透明
我们在 Listing 8-7 中添加的最后一行让 adjust_wall_``transparency() 函数每 0.05 秒运行一次 ➒。这使得墙壁在玩家在房间里走动时根据需要渐显或渐隐。
让我们看看这个新函数是如何工作的。如果玩家站在墙后面,以下两个语句成立:
-
它们的 y 位置将等于
room_height - 2➊。正如 Figure 8-6 所示,地图的底行是room_height - 1。所以我们检查玩家是否在这一行之上。 -
在房间底行中,有一块墙与玩家的 x 位置对齐 ➋。 在 Figure 8-6 中,红色方块标记了一个我们无法看到玩家的位置。它们前方的底行包含一个表示墙壁的 1。绿色方块显示了我们能看到玩家的位置,因为他们在门口。这里,房间底行地图包含 0。

Figure 8-6: 判断玩家是否在墙后面
如果玩家在墙后面 ➊ ➋ 且墙壁的透明度没有设置为最大值 ➌,则墙壁的透明度增加 1 ➍。
如果以下任一条件成立,则意味着玩家 没有 被墙壁遮挡:
-
它们的 y 位置小于
room_height - 2➎。如果玩家离房间后面远一些,至少部分是可以被看到的。 -
在与它们的 x 位置对齐的房间底行没有墙体 ➏。
在这些情况下,如果墙壁的透明度设置超过最低值 ➐,它会减少 ➑。
draw() 函数使用 wall_transparency_frame 的值来计算从 PILLARS 列表中的动画帧中选择哪个图像用于前排。
效果是墙壁会逐渐淡入淡出,取决于玩家是否站在墙后。这种淡入淡出的速度足够快,玩家不会被延迟,但也不会太快到让它瞬间消失,这样就不会分散注意力。
显示提示、技巧和警告
有时候,Escape游戏会使用文本告诉你发生了什么。例如,它可能会使用文本告诉你当你对某个物体做了什么时发生了什么,或者为你提供该物体的描述。
程序中DISPLAY部分的最终函数将消息写在游戏窗口的顶部。这里有两行文本:
-
第一行,位于窗口顶部 15 像素的位置,告诉玩家他们正在做什么。例如,它显示物体描述,并告诉玩家当他们使用物体时会发生什么。
-
第二行,位于窗口顶部 50 像素的位置,用于显示重要消息。
这些文本行被这样分隔,以免重要消息被不那么重要的消息覆盖。如果游戏需要告诉你一个生死攸关的情况,你肯定不希望那个消息被告知你进入的新房间的消息所取代!
将列出 8-8 中的新代码添加到DISPLAY部分的末尾,在你添加墙壁透明代码之后(在列出 8-7 中)。将该列表保存为listing8-8.py。你可以通过运行 pgzrun listing8-8.py 来测试它,但目前你还看不出任何区别。稍后,我们将添加一些指令,使用这个新的show_text()函数。
listing8-8.py
--snip--
if ((player_y < room_height - 2
or room_map[room_height - 1][player_x] != 1)
and wall_transparency_frame > 0):
wall_transparency_frame -= 1 # Fade wall in.
➊ def show_text(text_to_show, line_number):
if game_over:
return
➋ text_lines = [15, 50]
➌ box = Rect((0, text_lines[line_number]), (800, 35))
➍ screen.draw.filled_rect(box, BLACK)
➎ screen.draw.text(text_to_show,
(20, text_lines[line_number]), color=GREEN)
###############
## START ##
###############
--snip--
列出 8-8:添加文本显示功能
我们将像这样使用show_text() ➊函数(不要直接输入此内容):
show_text("message", line number)
行号将是 0,表示第一行,或者是 1,表示第二行,该行保留给重要消息。在函数开始时,消息被放入变量text_to_show中,行号放入line_number中➊。
我们使用一个名为text_lines的列表来记录两行文本的垂直位置(以像素为单位)➋。我们还定义了一个框➌,并用黑色填充它➍,以便在绘制新消息之前清除文本行。
最后,我们使用 Pygame Zero 中的screen.draw.text()函数将文本显示在屏幕上➎。此函数需要文本、文本的* x 和 y *位置,以及文本颜色。位置数字放在括号内(它们组成一个元组)。
在列出 8-8 中➎,* x *位置从左边起 20 像素,垂直位置取自text_lines列表,使用line_number中的数字作为列表索引。
进入房间时显示房间名称
为了测试 show_text() 函数,让我们添加 start_room() 函数,它会在你走进房间时显示房间名称。将此函数放在 GAME LOOP 部分,位于 game_loop() 函数之前,如 列表 8-9 所示。保存你的程序为 listing8-9.py。当你运行它时,暂时不会看到任何新内容。
listing8-9.py
--snip--
###############
## GAME LOOP ##
###############
def start_room():
show_text("You are here: " + room_name, 0)
def game_loop():
--snip--
列表 8-9:添加 start_room() 函数
此函数使用 room_name 变量,这是我们在 generate_map() 函数中设置的。它包含当前房间的名称,该名称来自 GAME_MAP 列表。房间名称与文本 "You are here: " 结合,并发送到 show_text() 函数。
现在,我们需要设置新的 start_room() 函数,以便每当玩家进入新房间时运行。在 第七章 的 列表 7-6 中,我们已经包含了这段代码,但将其注释掉了。现在,我们准备好了!在我们有 #start_room() 代码的地方,我们希望将其替换为 start_room()。那个 # 充当了“关闭开关”,告诉 Python 忽略该指令。为了启用该指令,我们需要移除 # 符号。
我们不会手动查找所有需要更改的行,而是让 IDLE 为我们完成这项工作。请按照以下步骤操作,并参考 图 8-7:
-
在 IDLE 中点击 编辑 ▸ 替换(或按 CTRL-H)以显示替换文本对话框。
-
在“查找”框中输入 #start_room()。
-
在“替换为”框中输入 start_room()。
-
点击 全部替换。

图 8-7:当玩家进入新房间时启用 start_room() 函数
IDLE 应该在四个地方替换指令,并跳转到列表中的最后一处,如 列表 8-10 所示(无需输入此列表)。
将列表保存为 listing8-10.py,并使用 pgzrun listing8-10.py 运行程序。当你进入每个新房间时,应该会显示一条消息。这是通过走过门触发的,所以在第一个房间时不会生效。
listing8-10.py
--snip--
if player_y == -1: # through door at TOP
#clock.unschedule(hazard_move)
current_room -= MAP_WIDTH
generate_map()
player_y = room_height - 1 # enter at bottom
player_x = int(room_width / 2) # enter at door
player_frame = 0
start_room()
return
--snip--
列表 8-10:当玩家离开房间时启用 start_room() 函数
这完成了 Escape 游戏的 DISPLAY 部分!稍后我们将做一些小改动来显示敌人,但除此之外,我们已经为游戏的其余部分打下了基础。
在下一章中,我们将开始拆解你的个人物品,并为游戏添加道具。
你准备好飞行了吗?
勾选以下框来确认你已经学会了本章的关键课程。
发送给函数的信息称为 参数。
要将信息发送到函数,你需要将其放在函数名后的括号中。如果需要传递多个参数,可以用逗号分隔它们。例如:add(5, 7)。
为了使一个函数能够接收信息,在定义函数时,你需要设置局部变量来接收参数。
程序的DISPLAY部分绘制房间,动画化透明墙,并显示文本信息。
show_text()函数需要两个参数:你想显示的字符串和行号(0 或 1)。第 1 行保留给重要信息。
你通过为矩形的位置和大小提供 Python 元组来定义一个 Rect。
screen.draw.filled_rect()函数绘制一个填充的矩形。
Pygame Zero 中的颜色使用 RGB(红、绿、蓝)格式。例如(255, 100, 0)是橙色:最大红色,一点绿色,没有蓝色。
如果你想在整个程序中替换某些代码,可以使用 IDLE 中的“全部替换”选项。

第九章:拆包你的个人物品**

现在空间站已经投入使用,是时候开始拆包你的个人物品以及你在执行任务时需要的各种工具和设备了。
在本章中,你将编写代码来处理可以在房间之间移动的物体(道具)。当你玩游戏时,你可以发现新物品,捡起它们,移动它们,并用它们解决谜题。
添加道具信息
你在第五章中已经添加了一些关于道具的信息,当时你将图片文件名和描述添加到 objects 字典中。objects 字典包含关于物品的是什么的信息。在这一章中,我们将添加信息,告诉游戏道具放在哪里。
你可能会想,为什么我们要将道具和景物分开处理。我们这样做是因为它们的信息以不同的方式使用:scenery 字典使用房间作为键来存储信息。这是有道理的,因为程序需要一次性获取房间内所有景物的信息。在将景物信息添加到房间地图之后,scenery 字典不再需要,直到玩家进入新的房间。
相比之下,道具是会移动的,所以在任何时候,任何房间中可能都需要用到道具的信息。如果这些信息埋藏在一长串景物项中,它就更难以查找和修改。
我们将创建一个新的字典,叫做 props,来存储关于道具的信息。我们将使用物品编号作为键,每一项将是一个包含以下内容的列表:
-
道具所在的房间编号
-
道具在房间中的 y 位置(以瓦片为单位)
-
道具在房间中的 x 位置(以瓦片为单位)
例如,以下是锤子的条目,它是物品 65:
65: [50, 1, 7]
它在房间 50,y 位置 1 和 x 位置 7。
不在游戏世界中或由玩家携带的物品的房间编号为 0,这不是游戏中的真实位置。例如,一些物品在它们被创建之前,或者在被销毁之后才会出现在游戏世界中。这些物品会存储在房间 0。
提示
props 和 objects 字典使用相同的键。如果你想知道 props 字典中物品 65 是什么,可以在 objects 字典中查看它的详细信息。
清单 9-1 展示了将道具信息添加到游戏中的代码。打开 listing8-10.py,这是你在上一章的最终程序。将新的 PROPS 部分添加到 show_text() 函数之后,DISPLAY 部分和 START 部分之前。只添加新行,并将新程序保存为 listing9-1.py。如果你不想手动输入数据,可以从 data-chapter9.py 文件中复制并粘贴数据。
你可以使用 pgzrun listing9-1.py 运行程序。它目前还不会做任何新操作,但你可以检查命令行窗口中是否有任何错误消息。
listing9-1.py
--snip--
screen.draw.text(text_to_show,
(20, text_lines[line_number]), color=GREEN)
###############
## PROPS ##
###############
# Props are objects that may move between rooms, appear or disappear.
# All props must be set up here. Props not yet in the game go into room 0.
# object number : [room, y, x]
➊ props = {
20: [31, 0, 4], 21: [26, 0, 1], 22: [41, 0, 2], 23: [39, 0, 5],
24: [45, 0, 2],
➋ 25: [32, 0, 2], 26: [27, 12, 5], # two sides of same door
40: [0, 8, 6], 53: [45, 1, 5], 54: [0, 0, 0], 55: [0, 0, 0],
56: [0, 0, 0], 57: [35, 4, 6], 58: [0, 0, 0], 59: [31, 1, 7],
60: [0, 0, 0], 61: [36, 1, 1], 62: [36, 1, 6], 63: [0, 0, 0],
64: [27, 8, 3], 65: [50, 1, 7], 66: [39, 5, 6], 67: [46, 1, 1],
68: [0, 0, 0], 69: [30, 3, 3], 70: [47, 1, 3],
➌ 71: [0, LANDER_Y, LANDER_X], 72: [0, 0, 0], 73: [27, 4, 6],
74: [28, 1, 11], 75: [0, 0, 0], 76: [41, 3, 5], 77: [0, 0, 0],
78: [35, 9, 11], 79: [26, 3, 2], 80: [41, 7, 5], 81: [29, 1, 1]
}
checksum = 0
for key, prop in props.items():
➍ if key != 71: # 71 is skipped because it's different each game.
checksum += (prop[0] * key
+ prop[1] * (key + 1)
+ prop[2] * (key + 2))
➎ print(len(props), "props")
assert len(props) == 37, "Expected 37 prop items"
print("Prop checksum:", checksum)
➏ assert checksum == 61414, "Error in props data"
➐ in_my_pockets = [55]
selected_item = 0 # the first item
item_carrying = in_my_pockets[selected_item]
###############
## START ##
###############
--snip--
清单 9-1:向 Escape 中添加道具信息
我们通过创建一个字典来存储道具信息,开始新的PROPS部分➊。这个字典列出了所有道具的位置,从一些门(20 到 24 号)开始,包括一艘救援船(40 号)和从 53 号开始的可携带物品。
有一个小问题需要注意。我们将门算作道具而不是场景,因为门并非总是存在:当它们打开时,会从房间中移除。大多数门在打开时会一直保持打开状态,直到游戏结束。然而,连接 27 号房间和 32 号房间的门也可以关闭,这意味着玩家可以从两边看到它。因此,我们需要两个道具来表示这扇门➋,分别显示在 27 号房间的顶部和 32 号房间的底部。这两个门的物体编号是 25 和 26。
道具 71 是贵宾犬着陆器,它在游戏开始前已迫降在行星表面。我们使用程序中VARIABLES部分的LANDER_Y和LANDER_X变量➌来定位着陆器,因为它的位置在每局游戏中都会变化。贵宾犬着陆时撞击力极大,它可能已经被火星土壤覆盖。它会一直待在 0 号房间,直到玩家能够将它挖出来。
与场景信息类似(见第六章),我在这里使用了校验和来帮助你检查输入数据时是否有错误。如果这里出错,可能无法将游戏进行到底。唯一没有参与校验和计算的是编号 71 的道具,因为它的位置在每个游戏中使用不同的随机数➍。
如果你想更改道具数据,最简单的方法是像这样注释掉两个校验和指令➏,以关闭它们:
#assert len(props) == 37, "Expected 37 prop items"
#assert checksum == 61414, "Error in props data"
程序会在命令行窗口显示校验和总数和数据项数量➎,因此如果你更改了道具数据,可以利用这些信息来更新两个assert指令中的数字,以确保它们与你自定义的数据一致。如果你这么做,可以继续使用这些行,而不是将其注释掉。
程序还设置了两个新的变量和一个稍后在本章中需要的列表。in_my_pockets ➐ 列表存储玩家拾取的所有物品,也就是他们的库存。这些物品中总有一个被选中,以便玩家准备对其进行操作。selected_item变量存储该物品在in_my_pockets列表中的索引号。item_carrying变量存储玩家选中的物品的物体编号。你可以将item_carrying变量理解为玩家手中的物体编号。稍后我会在本章中详细讲解这些变量。
向房间地图添加道具
我们已经添加了有关道具位置的信息,接下来让我们显示道具。当道具位于当前房间时,它们会在玩家进入房间时被放入room_map列表中。然后,draw()函数使用该列表来绘制房间。
我们将把添加道具到房间地图的指令放入程序的MAKE MAP部分,位于generate_map()函数内。我们只需要将这些指令添加在你在第八章中为计算top_left_x和top_left_y变量所添加的指令之后,就在GAME LOOP部分的开始之前。
因为新指令都是generate_map()函数的一部分,所以你需要将它们缩进至少四个空格。
将列表 9-2 中的新指令添加到程序中,并将其保存为listing9-2.py。运行程序时使用命令pgzrun listing9-2.py。你应该能看到一些新的物体出现在某些房间中,如图 9-1 所示。

图 9-1:刚才那扇门好像不见了!不过那个气瓶可能会派上用场。
listing9-2.py
--snip--
top_left_x = center_x - 0.5 * room_pixel_width
top_left_y = (center_y - 0.5 * room_pixel_height) + 110
➊ for prop_number, prop_info in props.items():
➋ prop_room = prop_info[0]
prop_y = prop_info[1]
prop_x = prop_info[2]
➌ if (prop_room == current_room and
➍ room_map[prop_y][prop_x] in [0, 39, 2]):
➎ room_map[prop_y][prop_x] = prop_number
➏ image_here = objects[prop_number][0]
image_width = image_here.get_width()
image_width_in_tiles = int(image_width / TILE_SIZE)
➐ for tile_number in range(1, image_width_in_tiles):
room_map[prop_y][prop_x + tile_number] = 255
###############
## GAME LOOP ##
###############
--snip--
列表 9-2:将道具添加到当前房间的房间地图中
在新代码中,我们首先设置了一个循环,遍历props字典中的条目➊。对于每个条目,字典的键存入变量prop_number,而包含位置信息的列表存入prop_info列表中。
为了让程序更易于阅读,我设置了一些变量来存储prop_info列表中的信息➋。程序提取房间编号的信息(并将其存入prop_room)以及y和x位置(分别存入prop_y和prop_x变量)。
我们添加了一个检查,查看prop_room是否与玩家所在的房间匹配➌,以及道具是否放置在地板上➍。地板检查会将三种不同的地面类型放入一个列表中(0 表示室内,2 表示土壤,39 表示房间 26 中的压力垫)。程序检查道具的位置,以查看该位置在房间地图中的内容。如果是这些地面类型之一,意味着物体正坐落在地板上,完全可见。如果不是,那么道具被隐藏在景物中,暂时不可见。例如,如果道具位置上有一个柜子而不是地板,那么道具将不会显示在屏幕上。不过,玩家仍然可以通过检查该位置的柜子来找到道具。
如果道具在房间内并且在地板上,房间地图将更新道具编号➎。
有些道具,比如门,比一个方块宽。因此,我们会将数字 255 添加到道具覆盖的除第一个方块以外的所有方块上➐。这与我们在generate_map()函数中标记宽大景物的代码类似(见列表 6-4 在第 106 页)。
从函数获取信息:掷骰子
在第八章中,你学习了如何将信息(或参数)传递给函数。让我们更仔细地看看如何从函数中获取信息返回。我们将使用这个技巧来创建一个函数,告诉我们玩家站在什么物体上。
示例 9-3 展示了一个简单的程序,它从函数返回一个数字并将其放入一个变量中。这不是Escape游戏的一部分,所以请首先通过点击文件 ▸ 新建来创建一个新文件。
将程序保存为listing9-3.py。这个程序不使用 Pygame Zero,因此你可以通过点击运行 ▸ 运行模块来在脚本窗口中运行它。该程序模拟了一个 10 面骰子。
listing9-3.py
➊ import random
➋ def get_number():
➌ die_number = random.randint(1, 10)
➍ return die_number
➎ random_number = get_number()
➏ print(random_number)
示例 9-3:一个 10 面骰子模拟器演示了如何从函数返回一个数字。
该程序首先告诉 Python 使用random模块 ➊,这个模块为 Python 提供了用于随机选择的新函数。然后我们创建了一个名为get_number()的新函数 ➋,它生成一个 1 到 10 之间的随机数 ➌,并将结果放入名为dice_number的变量中。
通常,当你调用一个函数时(在 Python 术语中称为调用函数),你会使用其名称,像这样:
get_number()
这一次,我们不仅调用了函数,而且告诉 Python 将函数的结果放入一个名为random_number的变量中 ➎。当函数通过return命令 ➍返回结果时,结果就会进入random_number变量。程序的主部分随后可以打印出它的值 ➏。
这段代码展示了从函数中获取信息的方法:当函数开始时,设置一个变量来存储信息 ➎,并在函数结束时通过return指令将信息返回 ➍。你也可以返回字符串和列表,而不仅仅是数字。尽可能地,这种方式是使程序的其他部分能够使用来自函数的信息的最佳方法。这种技巧使得程序的主部分能够从函数的局部变量(在这个例子中是dice_number)中获取信息,而这个变量通常只在函数内部可见。
你不会再需要这个程序了,所以在你完成实验后,可以将它关闭。
从房间地图中查找物体编号
稍后,我们将添加代码,允许你在空间站中捡起物体。首先,我们需要一种方法来找出哪个物体被捡起了。
当玩家与景物或道具互动时,我们需要找到他们正在使用的物体的编号。通常,这很简单。如果房间地图显示玩家所在位置的道具编号是 65,那就是一把锤子。程序可以显示锤子的描述,并允许玩家捡起或使用它。
确定物体编号对于跨越多个瓷砖的宽大物体来说比较棘手。我们使用数字 255 来标记被宽物体覆盖的瓷砖,但该数字并不对应于道具。程序需要通过在房间地图中向左移动,直到找到一个不为 255 的数字,从而计算出真实的物体编号。
例如,如果玩家检查门的最右侧第三部分,程序会看到该位置包含 255,因此会检查左边的位置。该位置也包含 255,所以程序会继续检查更左边的位置。如果该位置包含其他数字而不是 255,程序就知道它找到了真实的物体编号,举例来说,可能是 20(其中一个门)。使用物体编号 20,程序随后可以让玩家检查或打开这扇门。
我们将创建两个函数来计算物体编号,如 Listing 9-4 所示。你需要将这些添加到 Listing 9-2,因此如果需要,请点击文件 ▸ 打开再次打开listing9-2.py。我们将开始一个新的程序部分,称为PROP INTERACTIONS。将其放在PROPS部分后面。这个新部分将用于编写拾取和丢弃道具的代码。
将更新后的程序保存为listing9-4.py。它现在还不会做任何新功能,但你可以使用 pgzrun listing9-4.py 运行它,检查是否没有添加错误。在命令行窗口中查找任何错误信息。
listing9-4.py
--snip--
in_my_pockets = [55]
selected_item = 0 # the first item
item_carrying = in_my_pockets[selected_item]
#######################
## PROP INTERACTIONS ##
#######################
➊ def find_object_start_x():
➋ checker_x = player_x
➌ while room_map[player_y][checker_x] == 255:
➍ checker_x -= 1
➎ return checker_x
➏ def get_item_under_player():
➐ item_x = find_object_start_x()
➑ item_player_is_on = room_map[player_y][item_x]
➒ return item_player_is_on
--snip--
Listing 9-4:查找真实物体编号
在我们讲解这个代码如何工作的之前,我将解释一下游戏循环是如何让玩家与道具和场景进行交互的:
-
当玩家按下移动键时,程序会改变玩家的位置(即使这会将他们移到不可能的位置,比如墙内)。
-
程序通过使用玩家当前位置的物体来执行玩家要求的任何操作。这意味着玩家和物体此时位于房间中的同一位置。
-
如果玩家站在一个不允许站的位置(例如在墙内),程序会将他们移回原来的位置。
整个过程发生得非常快,你甚至看不到玩家进入墙壁或其他场景物体的过程。这样,玩家可以通过按下移动键和动作键来检查或使用场景物体。例如,你可以走到墙壁前,按下空格键来检查墙壁,并查看它的描述。这个过程也适用于玩家站立的物体,比如地上的道具。
我们在 Listing 9-4 中添加的第一个新函数是find_object_start_x() ➊。这个函数通过向左移动来找到玩家当前位置的物体的起始位置,如果位置包含 255,则继续向左查找直到找到真实的物体编号。
为此,函数将变量checker_x设置为与玩家的* x 位置相同 ➋。我们使用一个循环,只要房间地图中checker_x的 x 位置和玩家的 y *位置包含 255,循环就会继续。在该循环内部,有一条指令将checker_x减少 1 ➍,使其向左移动 1 个瓦片。当循环结束时,checker_x包含物品开始的左侧位置。然后,该数字会返回 ➎ 给启动该函数的指令。
第二个新函数是get_item_under_player() ➏,它计算玩家当前位置的物品是什么。它使用第一个函数来查找物品开始的位置,并将* x *位置存储在变量item_x中 ➐。然后,它查看该位置的房间地图数据,以确定那里是什么物品 ➑,并将该编号返回给启动该函数的指令 ➒。
捡起物品
现在这些函数已经到位,我们可以创建几个函数来捡起物品,并将它们存入玩家的背包中。接着,我们将添加一些键盘控制。
捡起道具
将列表 9-5 中显示的两个函数添加到程序中PROP INTERACTIONS部分的末尾,紧跟在你在列表 9-4 中添加的代码之后。
将此程序保存为listing9-5.py。你可以通过使用 pgzrun listing9-5.py 运行程序来检查是否有任何错误,但目前你不会看到任何变化。此代码添加了一些新函数,但不包括任何键控来启用玩家使用它们。
listing9-5.py
--snip--
item_player_is_on = room_map[player_y][item_x]
return item_player_is_on
def pick_up_object():
global room_map
➊ item_player_is_on = get_item_under_player()
➋ if item_player_is_on in items_player_may_carry:
➌ room_map[player_y][player_x] = get_floor_type()
➍ add_object(item_player_is_on)
show_text("Now carrying " + objects[item_player_is_on][3], 0)
sounds.pickup.play()
time.sleep(0.5)
➎ else:
show_text("You can't carry that!", 0)
➏ def add_object(item): # Adds item to inventory.
global selected_item, item_carrying
➐ in_my_pockets.append(item)
➑ item_carrying = item
➒ selected_item = len(in_my_pockets) - 1
display_inventory()
➓ props[item][0] = 0 # Carried objects go into room 0 (off the map).
def display_inventory():
print(in_my_pockets)
--snip--
列表 9-5:添加捡起物品的函数
函数pick_up_object()将在玩家按下获取键(G)时开始执行,来捡起物品。它首先将玩家位置上物品的编号存入变量item_player_is_on ➊。若该物品可以携带 ➋,其余的代码将执行捡起操作。
为了从地板上移除物品,程序会将玩家位置的房间地图替换为地面(土壤或地板砖)的物品编号 ➌。get_floor_type()函数用于查找该房间中地面的类型。当房间重新绘制时,物品将从地板上消失,看起来像是被捡起来了。然后,物品会通过add_object()函数被添加到玩家携带的物品列表中 ➍。
然后,我们在屏幕上显示一条信息,告诉玩家他们捡起了一个物品,并播放一个音效。我们使用time.sleep(0.5)指令添加了半秒的短暂延迟,以确保确认信息不会在玩家长按按键时被覆盖。
如果物品不能被携带,我们会显示一条信息,告诉玩家他们无法携带它 ➎。例如,风景物品是无法携带的,我们需要告诉玩家这一点。否则,他们可能会认为按错了键或程序没有正常工作。
add_object()函数将物品添加到in_my_pockets列表中,这个列表存储了玩家携带的物品(他们的背包)。函数开始时,传入的物品编号被赋值给局部变量item ➏。物品通过append()方法被添加到in_my_pockets_list的末尾 ➐。
我们使用全局变量item_carrying来存储玩家手中物品的对象编号,所以它被设置为这个物品的对象编号 ➑。我们将selected_item变量设置为列表中的最后一个物品,也就是说,玩家刚刚捡起的物品被选中 ➒。这些变量将在后续使用物品时非常重要,尤其是在display_inventory()函数显示物品列表时。现在,这个函数只会在命令行窗口中打印出物品列表。
最后,我们将物品在props字典中的位置设置为房间 0 ➓。这意味着刚捡起的物品在游戏地图中不会显示。如果我们不这么做,物品将在玩家下次进入房间时重新出现在房间内。
添加键盘控制
为了让新功能发挥作用,我们还需要添加键盘控制。我们将使用 G 键作为获取键。
将新的指令,见于清单 9-6,放入程序中GAME LOOP部分的game_loop()函数中。新指令应该在退出检查完成后,并在玩家被移动回他们不该站立的位置之前添加。
listing9-6.py
--snip--
player_frame = 0
start_room()
return
➊ if keyboard.g:
➋ pick_up_object()
# If the player is standing somewhere they shouldn't, move them back.
if room_map[player_y][player_x] not in items_player_may_stand_on: #\
# or hazard_map[player_y][player_x] != 0:
--snip--
清单 9-6:添加键盘控制
你需要将第一条新指令缩进四个空格 ➊,因为它在game_loop()函数内。第二条指令需要再缩进四个空格 ➋,因为它属于上面那个if指令。这些指令在玩家按下 G 键时运行pick_up_object()函数 ➋ ➊。
将清单保存为listing9-6.py。当你运行pgzrun listing9-6.py时,你应该能够捡起物品。
从第一个房间的空气罐开始测试。只需走到它上面并按下 G 键。你会听到声音并看到消息,物品会从房间中消失。
每次你捡起物品时,命令行窗口(你输入pgzrun指令的地方)也会显示物品清单,像这样:
[55, 59]
每次你都会看到一个新物品被添加到列表末尾。游戏开始时,物品 55,溜溜球,就在你的口袋里。
添加背包功能
现在,你可以捡起你在太空站周围找到的道具。我们应该添加一个简单的方法来查看你携带的物品,并选择不同的物品来使用。我们将创建一个新的display_inventory()函数,在游戏窗口顶部显示一个条形区域,显示玩家正在携带的物品。
接下来,我们将添加控件,以便玩家可以按 TAB 键选择列表中的下一个项。选中的项周围会画一个框,并在下方显示其描述。图 9-2 展示了它的样子。

图 9-2: 游戏窗口顶部的物品栏
显示物品栏
Listing 9-7 展示了需要添加的代码。Listing 9-5 中包含了部分display_inventory()函数的代码。请将其替换为新代码。将此列表保存为listing9-7.py。当你使用 pgzrun 运行 listing9-7.py 程序时,你将能够在屏幕顶部看到收集到的物品添加到物品栏中。
listing9-7.py
--snip--
selected_item = len(in_my_pockets) - 1
display_inventory()
props[item][0] = 0 # Carried objects go into room 0 (off the map).
def display_inventory():
➊ box = Rect((0, 45), (800, 105))
screen.draw.filled_rect(box, BLACK)
➋ if len(in_my_pockets) == 0:
return
➌ start_display = (selected_item // 16) * 16
➍ list_to_show = in_my_pockets[start_display : start_display + 16]
➎ selected_marker = selected_item % 16
➏ for item_counter in range(len(list_to_show)):
item_number = list_to_show[item_counter]
image = objects[item_number][0]
➐ screen.blit(image, (25 + (46 * item_counter), 90))
box_left = (selected_marker * 46) - 3
➑ box = Rect((22 + box_left, 85), (40, 40))
screen.draw.rect(box, WHITE)
item_highlighted = in_my_pockets[selected_item]
description = objects[item_highlighted][2]
➒ screen.draw.text(description, (20, 130), color="white")
###############
## START ##
###############
clock.schedule_interval(game_loop, 0.03)
generate_map()
clock.schedule_interval(adjust_wall_transparency, 0.05)
➓ clock.schedule_unique(display_inventory, 1)
Listing 9-7:显示物品栏
新的display_inventory()函数首先会在物品栏区域绘制一个黑色框,清除原有内容➊。如果玩家没有携带任何物品,函数会直接返回,不进行任何操作,因为没有物品需要显示➋。
屏幕上只能显示 16 个物品,但玩家可能携带更多物品。如果in_my_pockets列表太长,无法在屏幕上完全显示,程序会一次显示 16 个物品。玩家可以按 TAB 键选择屏幕上显示的任意物品,按从左到右的顺序移动。如果选中了最后一个物品并按下 TAB 键,则会显示列表的下一个部分。如果玩家在列表中的最后一项按下 TAB 键,列表的开头会再次出现。
我们将当前显示在屏幕上的in_my_pockets列表部分存储在另一个名为list_to_show的列表中,并使用循环来显示它➏。该循环将数字放入一个名为item_counter的变量中,item_counter用于每次提取正确的图像并确定绘制位置➐。
精妙之处在于确定哪些项应该放入list_to_show。在start_display变量中,我们存储了in_my_pockets中程序应绘制的第一个项的索引号➌。//运算符将选中的项编号除以 16 并向下取整。然后,将结果乘以 16 以获得批次中第一个项的索引号。例如,如果选中的项是编号 9,你会将 9 除以 16(得到 0.5625),向下取整(得到 0),然后乘以 16(仍然是 0),得到结果 0。这样就得到了列表的起始位置,这很有道理,因为我们知道屏幕上有 16 个位置,而 9 小于 16。如果你想查看包含第 22 项的项目组,你会将 22 除以 16(得到 1.375),向下取整(得到 1),然后乘以 16,得到结果 16。那就是下一个批次的起始位置,因为第一个批次的索引号范围是从 0 到 15。
我们使用一种叫做列表切片的技巧创建了list_to_show列表,这其实只是使用列表的一部分。当你给 Python 两个列表索引并用冒号分隔时,程序会切出那部分列表。我们使用的这一部分从start_display索引开始,15 个元素后结束 ➍。列表切片会省略最后一个元素,所以我们使用start_display + 16作为结束点。
我们还需要另一个计算来确定从新列表中要突出显示的选中项 ➎。该物品的索引值介于 0 和 15 之间,我们将其存储在selected_marker中。我们通过将选中物品的编号除以 16 后的余数来计算它。例如,如果选中的物品编号是 18,当显示第二组物品时,它将位于索引 2 处。(记住,第一项的索引是 0。)Python 有一个取余运算符 %,可以用来获取除法后的余数。
为了在屏幕上突出显示选中的项,我们用一个位于其左边缘的矩形框将其框起来 ➑。与您之前看到的填充矩形不同(例如 ➊),这个指令绘制了一个边框为白色的空心框。
被选中项的描述会显示在库存下方 ➒,这样玩家就可以通过 TAB 键浏览他们的物品,重新查看描述。
最后,当程序第一次运行时,它需要显示库存。这是通过略微延迟 ➓来调度的,以避免在 Pygame Zero 启动完成之前尝试使用screen.blit()指令时可能出现的问题。clock.schedule_interval()用于定期运行一个函数,而clock.schedule_unique()则用于在延迟后只运行一次一个函数。
添加 TAB 键盘控制
当你运行程序时,你可以看到库存,但你还没有办法在物品间切换,所以最新的物品始终被选中。我们来添加一个键盘控制,允许你通过 TAB 键在库存中选择不同的物品。
将 Listing 9-8 中的新指令放入game_loop()函数中,紧接在你在 Listing 9-6 中添加键盘控制以获取物品之后。你需要至少缩进四个空格,因为它们是在game_loop()函数内部。
将这个列表保存为 listing9-8.py。当你使用pgzrun listing9-8.py运行程序时,你将能够按 TAB 键在库存中选择不同的物品。(TAB 键通常位于键盘的左侧,可能上面有两个箭头的图标。)
在测试新的键盘控制之前,先捡起一些物品,或者跳到下一节以填充更多物品,供测试使用。
listing9-8.py
--snip--
if keyboard.g:
pick_up_object()
➊ if keyboard.tab and len(in_my_pockets) > 0:
➋ selected_item += 1
➌ if selected_item > len(in_my_pockets) - 1:
selected_item = 0
➍ item_carrying = in_my_pockets[selected_item]
➎ display_inventory()
➏ if keyboard.d and item_carrying:
➐ drop_object(old_player_y, old_player_x)
➑ if keyboard.space:
➒ examine_object()
--snip--
Listing 9-8: 启用 TAB 键选择库存中的物品
第一段指令在玩家按下 TAB 键时运行,但仅当in_my_pockets列表包含一些物品(即其长度大于 0)时 ➊。
要选择背包中的下一个物品,我们在按下 TAB 键时将selected_item变量加 1 ➋。这个变量存储一个索引号(从 0 开始),所以程序会从列表的长度中减去 1,检查selected_item是否已经超出了列表的末尾 ➌。如果超出,选中的物品会重新设置为第一个物品,即 0。
我们将变量item_carrying设置为选中物品的对象编号(从in_my_pockets列表中获取)➍。例如,如果in_my_pockets列表包含对象编号 55 和 65,而selected_item为 0,item_carrying将包含 55(in_my_pockets中的第一个物品)。最后,背包通过你之前创建的display_``inventory()函数来显示 ➎。
在我们处理这部分程序时,我们还添加了键盘控制来丢弃和检查物品。当玩家按下 D 键且item_carrying变量不为False时,drop_object()函数会执行 ➏。此函数会将玩家之前的 y 和 x 位置作为丢弃物品的位置 ➐。请记住,玩家当前的位置可能处于墙内,因为我们处于游戏循环的某个阶段。我们知道他们在移动前的最后一个位置是一个安全的地方,可以丢下物品。
我们还添加了当按下空格键时启动examine_object()函数的指令 ➒。
还不要在游戏中按 D 或空格键:按下它们会导致程序崩溃,因为我们还没有添加相应的函数。我们会很快添加这些函数。
测试背包
我们想要正确地测试程序,但目前你的背包里物品不多。为了节省时间,我们将调整代码,给你一个更完整的背包,这样你就可以测试显示和 TAB 控件。
我们将在游戏开始时填充in_my_pockets列表。最快的方法是更改程序中PROPS部分设置该列表的指令,如下所示(但不要现在就这么做!):
in_my_pockets = items_player_may_carry
这意味着你开始游戏时会携带所有可以携带的物品。不过,如果你这么做,可能会影响你玩游戏的乐趣。你将携带一些你可能希望稍后才看到的物品,这会使得一些谜题的解法变得很明显。
相反,我建议你创建一个这样的测试列表:
in_my_pockets = [55, 59, 61, 64, 65, 66, 67] * 3
这一行创建了一个列表,包含该物品序列三次。你最终会得到一个背包,里面每个物品都有三个(这在真实的游戏中是不可能的),但它能让你测试当背包中有超过 16 个物品时,是否能够正常工作。
测试完成后,请将代码改回原样。否则,在玩游戏时你可能会遇到意外的结果。以下是该行代码应该呈现的样子:
in_my_pockets = [55]
丢弃物品
能够收集散落在空间站各处的物品非常有趣,但有时你会想把它们放下,以便你可以使用它们或将其放在某个地方。我们需要两个新函数来丢放物品,它们的工作方式有点像拾取物品函数的反向操作。
drop_object() 函数(pick_up_object() 函数的反向操作)将允许你将物品丢放到玩家最近站立的位置。你已经在 Listing 9-8 中为此功能添加了键盘控制。
remove_object() 函数类似于 add_object() 函数的反向操作:它从库存中取出物品并更新库存。
将新的函数添加到程序的 PROP INTERACTIONS 部分的末尾,如 Listing 9-9 中所示。将新程序保存为 listing9-9.py。
当你运行程序并使用 pgzrun listing9-9.py 时,你将能够丢放物品。包括你在游戏开始时携带的悠悠球以及你在探索空间站时捡到的任何新物品。
listing9-9.py
--snip--
description = objects[item_highlighted][2]
screen.draw.text(description, (20, 130), color="white")
➊ def drop_object(old_y, old_x):
global room_map, props
➋ if room_map[old_y][old_x] in [0, 2, 39]: # places you can drop things
➌ props[item_carrying][0] = current_room
props[item_carrying][1] = old_y
props[item_carrying][2] = old_x
➍ room_map[old_y][old_x] = item_carrying
show_text("You have dropped " + objects[item_carrying][3], 0)
sounds.drop.play()
➎ remove_object(item_carrying)
time.sleep(0.5)
➏ else: # This only happens if there is already a prop here
show_text("You can't drop that there.", 0)
time.sleep(0.5)
def remove_object(item): # Takes item out of inventory
global selected_item, in_my_pockets, item_carrying
➐ in_my_pockets.remove(item)
➑ selected_item = selected_item - 1
➒ if selected_item < 0:
selected_item = 0
➓ if len(in_my_pockets) == 0: # If they're not carrying anything
item_carrying = False # Set item_carrying to False
else: # Otherwise set it to the new selected item
item_carrying = in_my_pockets[selected_item]
display_inventory()
###############
## START ##
###############
--snip--
Listing 9-9: 添加丢放物品的函数
drop_object() 函数需要两项信息:玩家的旧 y 和 x 位置。如果玩家这次通过 game_loop() 函数移动过,那么这些就是玩家尝试移动之前所在的位置。如果没有,这些数字将与玩家当前所在的位置相同。我们知道这是一个合理的放置物品的位置,不会把物品放到墙内。玩家的旧位置会存储在函数中的 old_y 和 old_x 变量中 ➊。
程序会检查玩家旧位置的房间地图是否属于地面类型。如果是,那么就可以在这里丢下物品,接着会使用丢放指令。如果不是 ➏,玩家会看到一条消息,告诉他们不能在该位置丢放物品。例如,如果该位置已有物品,则会出现这种情况。
如果玩家可以丢下物品,我们需要更新 props 字典。变量 item_carrying 包含玩家正在携带的物品的编号。它在 props 字典中的条目是一个列表。列表的第一个元素(索引 0)是物品所在的房间,第二个元素(索引 1)是物品的 y 位置,第三个元素是物品的 x 位置(索引 2)。这些值将被设置为当前房间和玩家的旧位置 ➌。
当前房间的房间地图也需要更新,以便房间中包含丢放的物品 ➍。游戏会显示一条消息并播放声音,告诉玩家他们已经成功丢放了物品,然后物品会通过 remove_object() 函数从库存中移除 ➎。
remove_object() 函数从玩家的物品栏中移除一个物品,并更新 selected_item 变量。传递给这个函数的物体编号存储在 item 变量中,然后 remove() ➐ 将它从 in_my_pockets 列表中移除。现在,选定的物品已被移除,选定物品的数量减少 1 ➑,因此列表中的前一个物品会被选中。如果这意味着选定的物品数量小于 0,则选定物品会重置为 0 ➒。如果玩家从物品栏中丢掉第一个物品,就会发生这种情况。
如果玩家的手现在是空的,item_carrying 变量会被设置为 False ➓。否则,它会设置为玩家所选物品的编号。最后,display_inventory() 会重新绘制物品栏,显示物品已经被移除。
训练任务 #1
现在是进行安全演练的时候了。你能把气瓶拿起来并送到医务室吗?将它放在中间的床附近。为了测试程序是否正常工作,交付后离开房间,再回来确认它还在那里。
检查物体
当你探索太空站时,你会想要仔细研究物体,看看它们如何帮助完成任务。examine 指令会显示物体的详细描述,并适用于场景和道具。通过检查物体,你有时也能发现其他物体。例如,当你检查一个橱柜时,你可能会发现橱柜里有东西。
按下空格键触发 examine_object() 函数。(你在 Listing 9-8 中添加了键盘控制。)将这个新函数,如 Listing 9-10 所示,放在你在 Listing 9-9 中添加的 remove_object() 函数之后。
将你的程序保存为 listing9-10.py。使用 pgzrun listing9-10.py 运行程序。现在你可以通过走到物体旁边或站在物体上并按下空格键来检查物体。例如,当你站在房间后面的墙边时,如果按上箭头键和空格键,你就可以检查墙壁。
listing9-10.py
--snip--
item_carrying = in_my_pockets[selected_item]
display_inventory()
def examine_object():
➊ item_player_is_on = get_item_under_player()
➋ left_tile_of_item = find_object_start_x()
➌ if item_player_is_on in [0, 2]: # don't describe the floor
return
➍ description = "You see: " + objects[item_player_is_on][2]
➎ for prop_number, details in props.items():
# props = object number: [room number, y, x]
➏ if details[0] == current_room: # if prop is in the room
# If prop is hidden (= at player's location but not on map)
if (details[1] == player_y
and details[2] == left_tile_of_item
and room_map[details[1]][details[2]] != prop_number):
➐ add_object(prop_number)
➑ description = "You found " + objects[prop_number][3]
sounds.combine.play()
➒ show_text(description, 0)
➓ time.sleep(0.5)
###############
## START ##
###############
--snip--
Listing 9-10: 添加代码以检查物体
清单 9-10 基于你在本章中已经完成的添加函数的工作。我们首先获取玩家想要检查的物品的编号,并将其存储在item_player_is_on ➊中。在game_loop()函数的这一部分,玩家的位置将会在他们想要检查的物品上,或者可能是在物品内,如果它是一个景物。我们将物品的起始x位置放入变量left_tile_of_item ➋中。如果玩家当前位置没有物品可以检查,函数将结束而不执行任何进一步的操作 ➌。忽略一个空白区域比描述地板要自然,尤其是在你操作失误的情况下。如果玩家的位置有物品,物品的描述将存入description变量中,取自objects字典中的长描述 ➍。
程序接着检查玩家正在检查的物品内部是否隐藏有道具。我们使用循环遍历props字典中的所有物品 ➎。如果一个物品位于玩家所在房间的当前位置,但房间地图在该位置上没有包含道具编号 ➏,则说明该物品被隐藏。因此,我们将隐藏的物品添加到玩家的背包中 ➐,并给玩家一个消息,告诉他们发现了什么。这个消息使用物品的简短描述,告诉玩家他们找到了什么 ➑。
在函数的末尾,显示了描述 ➒,我们在这里加入了一个短暂的暂停,以防玩家按住键时描述立即被覆盖 ➓。
如果你希望在自己的游戏设计中将道具隐藏在景物内,确保给玩家一个明确的提示,告诉他们哪里有隐藏的物品。在Escape中,你可能会在橱柜里找到物品。如果你发现某些东西不寻常,通常建议检查它以了解更多信息,你也许会找到一些其他有趣的东西。不过,你不需要检查每一把椅子、床和墙面板。
如果你决定将道具隐藏在宽敞的景物中(例如床上),确保将道具隐藏在景物的x位置,而不是隐藏在房间地图上被 255 覆盖的空间中。
训练任务 #2
你能找到 MP3 播放器吗?它在你在第四章中命名为FRIEND2的人的卧室里。如果你使用的是我的代码,它就在 Leo 的卧室里。
现在所有道具都已经拆开,你可以放松地玩你的溜溜球,看看还能发现什么。在下一章中,你将向程序中添加一个新部分,使你能够使用遇到的道具。
你准备好飞行了吗?
勾选以下框以确认你已经学会了本章的关键内容。
道具位置的信息存储在props字典中。
道具编号是字典的键,每个条目包含一个列表,列出了房间编号以及道具的y和x位置。
要从函数接收一个数字,在调用函数时设置一个变量来存储该信息。例如,变量名 = 函数名()。
要从函数中返回一个数字(或其他任何内容),使用 return 指令。
// 操作符用于除法,并将结果向下舍入,去除答案中的任何小数。
% 操作符可以在除法后返回余数:5 % 2 结果是 1。
你可以更改变量和列表的值以帮助测试程序,例如,在开始时创建一个完整的库存。记得测试后要恢复它们!
你可以将道具隐藏在场景中,但要确保它们位于场景开始的位置,并给玩家一个强烈的提示,告诉他们在哪些地方值得寻找。

第十章:让自己变得有用**

你已经将道具添加到游戏中,所以在本章中,你将添加代码来使宇航员能够 使用 物品并将它们组合成新的物品。这些技能将对你的任务至关重要。你将有机会进行演练,以便在任何情况下都能做好准备。
本章的代码比你最近看到的一些列表更简单,并且包含了 Escape 游戏中许多谜题的答案。为了避免剧透过多,我不会在这里解释每个项目和解决方案。例如,有时你可能会在代码中看到一个物品编号,但我不会告诉你该物品的名称。
如果你在玩游戏时遇到卡住的地方,可以阅读这段代码,并通过参考 objects 字典(参见 列表 5-6 和 列表 5-8)来帮助辨认物品。虽然这应该是最后的办法,但通过像宇航员一样思考,你是可以解决所有难题的。问问自己:你能接触到哪些对你有用的东西?你如何让某物变得更有用?
添加用于使用物品的键盘控制
我们将从在 game_loop() 函数中添加键盘控制开始。打开 listing9-10.py,这是你在 第九章中的最后一个列表。我们将在这个列表的基础上进行构建。
列表 10-1 显示了需要添加到 game_loop() 函数中的新指令。将它们添加到你在上一章中添加的 drop 和 examine 键盘控制之后。当玩家按下 U 键时,这些指令会启动 use_object() 函数。将程序保存为 listing10-1.py。不要尝试运行程序:它不会做任何新事情,但如果按下 U 键,程序会崩溃。
listing10-1.py
--snip--
if keyboard.space:
examine_object()
if keyboard.u:
use_object()
--snip--
列表 10-1:添加用于使用物品的键盘控制
添加用于使用物品的标准消息
使用物品的函数较长,因此我为它提供了一个单独的程序部分。将新的 USE OBJECTS 部分放在你在 第九章 中添加的 PROP INTERACTIONS 部分之后。 列表 10-2 显示了这个新部分的开始。在 examine_object() 函数结束后但在 START 部分之前添加这段代码。
listing10-2.py
--snip--
show_text(description, 0)
time.sleep(0.5)
#################
## USE OBJECTS ##
#################
def use_object():
global room_map, props, item_carrying, air, selected_item, energy
global in_my_pockets, suit_stitched, air_fixed, game_over
➊ use_message = "You fiddle around with it but don't get anywhere."
➋ standard_responses = {
4: "Air is running out! You can't take this lying down!",
6: "This is no time to sit around!",
7: "This is no time to sit around!",
32: "It shakes and rumbles, but nothing else happens.",
34: "Ah! That's better. Now wash your hands.",
35: "You wash your hands and shake the water off.",
37: "The test tubes smoke slightly as you shake them.",
54: "You chew the gum. It's sticky like glue.",
55: "The yoyo bounces up and down, slightly slower than on Earth",
56: "It's a bit too fiddly. Can you thread it on something?",
59: "You need to fix the leak before you can use the canister",
61: "You try signalling with the mirror, but nobody can see you.",
62: "Don't throw resources away. Things might come in handy...",
67: "To enjoy yummy space food, just add water!",
75: "You are at Sector: " + str(current_room) + " // X: " \
+ str(player_x) + " // Y: " + str(player_y)
}
# Get object number at player's location.
➌ item_player_is_on = get_item_under_player()
➍ for this_item in [item_player_is_on, item_carrying]:
➎ if this_item in standard_responses:
➏ use_message = standard_responses[this_item]
➐ show_text(use_message, 0)
time.sleep(0.5)
###############
## START ##
###############
--snip--
列表 10-2:添加用于使用物品的第一条指令
列表 10-2 显示了 use_object() 函数的第一部分。我们将在本章的进一步列表中充实这个部分。在函数的末尾,程序会向玩家显示一条信息,告诉他们在尝试使用物品时发生了什么 ➐。该信息会保存在 use_message 变量中。在该函数的开始部分,我们将它设置为一个错误信息 ➊。稍后,如果玩家成功使用了物品,它将被更改为成功的消息。
一些对象在游戏中没有实际功能,但当玩家尝试使用它们时会奖励玩家一条信息。这些信息可能包括线索以及丰富游戏故事情节。字典standard_responses包含在玩家使用某些对象时显示的消息➋。字典的键是对象编号。例如,如果他们想使用床(懒骨头!),这是对象 4,他们会看到一条消息:“你不能躺着做这件事!”
变量item_the_player_is_on存储玩家所在房间中物体的位置编号➌。玩家可以使用他们携带或站在上的物体。我们设置了一个循环,遍历一个包含两个项目的列表:玩家站在上面的物体编号和玩家携带的物体编号➍。如果它们中的任何一个是standard_responses字典的键➎,则use_message将更新为该字典中该对象的消息➏。如果两者都具有标准消息,程序会优先使用玩家携带的物品。
将文件保存为listing10-2.py。使用pgzrun listing10-2.py运行它。为了测试它是否正常工作,按 U 键使用你携带的悠悠球。
添加游戏进度变量
我们需要在程序中添加一些新变量,以存储关于玩家在游戏中进度的重要数据:
-
air,存储你拥有的空气量,按百分比表示 -
energy,存储你的能量,按百分比表示,并在你受伤时减少 -
suit_stitched,根据套装是否已修复,存储True或False值 -
air_fixed,根据气罐是否已修复,存储True或False值
将这些变量添加到VARIABLES部分的末尾,如列表 10-3 所示。将更新后的程序保存为listing10-3.py。运行时程序不会做任何新事情:我们设置了一些变量,但暂时没有对它们进行任何操作。
listing10-3.py
--snip--
GREEN = (0, 255, 0)
RED = (128, 0, 0)
air, energy = 100, 100
suit_stitched, air_fixed = False, False
launch_frame = 0
###############
## MAP ##
###############
--snip--
列表 10-3:添加游戏进度变量
添加特定对象的操作
use_object()函数的下一个阶段是检查特定对象,看看是否可以对其执行操作。这些检查将覆盖之前可能设置的任何标准消息,并显示在列表 10-4 中。因为这些指令位于use_object()函数内,所以它们的缩进至少为四个空格。将你的程序保存为listing10-4.py。使用pgzrun listing10-4.py运行它。
listing10-4.py
--snip--
if this_item in standard_responses:
use_message = standard_responses[this_item]
➊ if item_carrying == 70 or item_player_is_on == 70:
use_message = "Banging tunes!"
sounds.steelmusic.play(2)
➋ elif item_player_is_on == 11:
➌ use_message = "AIR: " + str(air) + \
"% / ENERGY " + str(energy) + "% / "
if not suit_stitched:
use_message += "*ALERT* SUIT FABRIC TORN / "
if not air_fixed:
use_message += "*ALERT* SUIT AIR BOTTLE MISSING"
if suit_stitched and air_fixed:
use_message += " SUIT OK"
show_text(use_message, 0)
sounds.say_status_report.play()
time.sleep(0.5)
# If "on" the computer, player intention is clearly status update.
# Return to stop another object use accidentally overriding this.
➍ return
elif item_carrying == 60 or item_player_is_on == 60:
➎ use_message = "You fix " + objects[60][3] + " to the suit"
air_fixed = True
air = 90
air_countdown()
remove_object(60)
elif (item_carrying == 58 or item_player_is_on == 58) \
and not suit_stitched:
use_message = "You use " + objects[56][3] + \
" to repair the suit fabric"
suit_stitched = True
remove_object(58)
elif item_carrying == 72 or item_player_is_on == 72:
use_message = "You radio for help. A rescue ship is coming. \
Rendezvous Sector 13, outside."
props[40][0] = 13
elif (item_carrying == 66 or item_player_is_on == 66) \
and current_room in outdoor_rooms:
use_message = "You dig..."
if (current_room == LANDER_SECTOR
and player_x == LANDER_X
and player_y == LANDER_Y):
add_object(71)
use_message = "You found the Poodle lander!"
elif item_player_is_on == 40:
clock.unschedule(air_countdown)
show_text("Congratulations, "+ PLAYER_NAME +"!", 0)
show_text("Mission success! You have made it to safety.", 1)
game_over = True
sounds.take_off.play()
game_completion_sequence()
elif item_player_is_on == 16:
energy += 1
if energy > 100:
energy = 100
use_message = "You munch the lettuce and get a little energy back"
draw_energy_air()
elif item_carrying == 68 or item_player_is_on == 68:
energy = 100
use_message = "You use the food to restore your energy"
remove_object(68)
draw_energy_air()
if suit_stitched and air_fixed: # open airlock access
if current_room == 31 and props[20][0] == 31:
open_door(20) # which includes removing the door
sounds.say_airlock_open.play()
show_text("The computer tells you the airlock is now open.", 1)
elif props[20][0] == 31:
props[20][0] = 0 # remove door from map
sounds.say_airlock_open.play()
show_text("The computer tells you the airlock is now open.", 1)
show_text(use_message, 0)
time.sleep(0.5)
###############
## START ##
###############
--snip--
列表 10-4:添加使用特定对象的功能
列表 10-4 包括一系列指令,检查正在使用的对象是否是特定的对象编号。如果是,便执行该对象的操作。
例如,如果玩家携带或站在物品 70 ➊上,它是一个 MP3 播放器,玩家会看到一条消息“Banging tunes!”并听到一些音乐。如果玩家正在使用计算机➋,则显示的消息是通过组合air和energy变量中的信息生成的,并在西装或气瓶出现故障时添加警报。这里还有一个计算机语音音效,说“状态报告!”
我在这组指令的结尾添加了return指令➍,防止玩家在本意使用计算机时意外使用其他物品。如果没有包含这个return指令,玩家可能会使用他们携带的其他道具,而不是计算机。保持控制简洁意味着玩家可能会对他们想要使用的物品产生一些歧义,但游戏的设计是优先考虑那些帮助玩家完成游戏的结果。
在几个地方,我使用了objects字典中的简短描述,而不是直接将物品名称输入到字符串中➎。这样做是为了防止你在代码中看到任何剧透!
行尾的\符号➌告诉 Python 代码将在下一行继续。有些行比较长,因此我使用这个符号将它们分开,以便它们能够适应书页。
尝试通过走到其中一个计算机终端并按下 U 键来测试一些新代码。你将看到状态更新。如果你能找到 MP3 播放器,你也可以听听看。
红色警报
当你输入物品编号和清单 10-4 中的其他代码时,要特别小心。如果你在这里犯了错误,可能无法完成游戏中的谜题!
物品组合
游戏中的一些谜题要求你将物品一起使用。例如,你可能会用一个物品作为工具对另一个物品进行操作,或者将两个物品组合在一起。例如,其中一个谜题要求你将 GPS 模块插入定位系统。当你找到这两个部件时,你需要将它们组合成一个工作中的定位系统。要将两个物品一起使用,你需要在你的背包中选择一个,并走到或走进另一个物品。你可能需要将物品从背包中丢到地上,这样你才能用你携带的其他物品来操作它。
在Escape游戏引擎中,组合物品被称为配方。一个配方包含三个物品编号,按顺序列出。前两个是被组合的物品,而第三个是它们组合后产生的物品编号。这里有一个例子:
[73, 74, 75]
物品 73(GPS 模块)加上物品 74(定位系统)组合成物品 75(一个工作中的定位系统)。
当你组合物品时,新物品会进入你的背包。如果组合的物品是道具,它们将从游戏中移除。有时其中一个物品是场景道具,因此仍会保留在游戏中。
清单 10-5 展示了食谱列表。将其添加到程序中PROPS部分的末尾,在那里设置道具的信息。将文件保存为listing10-5.py。如果你运行该清单,它目前不会做任何新操作,但它会检查新数据是否正确。
listing10-5.py
--snip--
in_my_pockets = [55]
selected_item = 0 # the first item
item_carrying = in_my_pockets[selected_item]
RECIPES = [
[62, 35, 63], [76, 28, 77], [78, 38, 54], [73, 74, 75],
[59, 54, 60], [77, 55, 56], [56, 57, 58], [71, 65, 72],
[88, 58, 89], [89, 60, 90], [67, 35, 68]
]
checksum = 0
check_counter = 1
for recipe in RECIPES:
checksum += (recipe[0] * check_counter
+ recipe[1] * (check_counter + 1)
+ recipe[2] * (check_counter + 2))
check_counter += 3
print(len(RECIPES), "recipes")
assert len(RECIPES) == 11, "Expected 11 recipes"
assert checksum == 37296, "Error in recipes data"
print("Recipe checksum:", checksum)
#######################
## PROP INTERACTIONS ##
#######################
--snip--
清单 10-5:为逃脱游戏添加食谱
现在,在use_object()函数的末尾添加代码来使用食谱,如清单 10-6 所示。将其添加到你的use_object()函数中,并将程序保存为listing10-5.py。当你运行程序时,使用 pgzrun listing10-5.py,你将能够合并物品。
listing10-6.py
--snip--
sounds.say_airlock_open.play()
show_text("The computer tells you the airlock is now open.", 1)
➊ for recipe in RECIPES:
➋ ingredient1 = recipe[0]
ingredient2 = recipe[1]
combination = recipe[2]
➌ if (item_carrying == ingredient1
and item_player_is_on == ingredient2) \
➍ or (item_carrying == ingredient2
and item_player_is_on == ingredient1):
➎ use_message = "You combine " + objects[ingredient1][3] \
+ " and " + objects[ingredient2][3] \
+ " to make " + objects[combination][3]
➏ if item_player_is_on in props.keys():
➐ props[item_player_is_on][0] = 0
➑ room_map[player_y][player_x] = get_floor_type()
➒ in_my_pockets.remove(item_carrying)
➓ add_object(combination)
sounds.combine.play()
show_text(use_message, 0)
time.sleep(0.5)
--snip--
清单 10-6:在游戏中合并物品
你可能会发现你可以理解这段新代码:它主要是将你之前见过的想法结合起来。我们使用一个循环来遍历RECIPES列表中的所有物品 ➊,每次都会将一个新的食谱添加到recipe列表中。我们将配料和合成物品的编号存入变量中,以便更容易理解函数 ➋。
程序会检查玩家是否携带了第一个配料并站在第二个配料上 ➌,或者反过来 ➍。如果是这样,使用消息将更新,告诉玩家他们合成了什么以及做出了什么 ➎。
当合成物品制作完成时,它通常会替换原来的配料物品。然而,如果其中一个物品是景物而不是道具,它将保留在游戏中。所以程序会检查玩家所站的物品是否为道具 ➏,如果是,它的房间号将被设置为 0,从游戏中移除 ➐。如果是道具,它还会从当前房间的房间地图中删除 ➑。
玩家携带的物品会从玩家的背包中移除 ➒,而新创建的物品会被添加到背包中 ➓。
训练任务 #1
让我们做一个简单的测试来检查合成代码是否正常工作。我们将需要对代码做一些修改来进行这个测试。在PROPS部分,修改设置in_my_pockets的那一行,以便你携带了物品 73 和 74:
in_my_pockets = [55, 73, 74]
现在运行程序:你将携带 GPS 模块和定位系统。丢掉其中一个并站在上面。选择背包中的另一个物品,然后按 U 键。物品应该合成一个工作中的 GPS 系统!你可以用它查看游戏中的位置。为了确保代码正常工作,试着交换物品,这次站在另一个物品上。
确保之后将代码改回:
in_my_pockets = [55]
添加游戏完成序列
程序的USE OBJECTS部分还有一个最终功能,这是当玩家完成游戏时播放的一个简短动画:宇航员乘坐救援船起飞。将此功能添加到USE OBJECTS部分的末尾,如清单 10-7 所示:
listing10-7.py
--snip--
show_text(use_message, 0)
time.sleep(0.5)
def game_completion_sequence():
global launch_frame #(initial value is 0, set up in VARIABLES section)
box = Rect((0, 150), (800, 600))
screen.draw.filled_rect(box, (128, 0, 0))
box = Rect ((0, top_left_y - 30), (800, 390))
screen.surface.set_clip(box)
for y in range(0, 13):
for x in range(0, 13):
draw_image(images.soil, y, x)
launch_frame += 1
if launch_frame < 9:
draw_image(images.rescue_ship, 8 - launch_frame, 6)
draw_shadow(images.rescue_ship_shadow, 8 + launch_frame, 6)
clock.schedule(game_completion_sequence, 0.25)
else:
screen.surface.set_clip(None)
screen.draw.text("MISSION", (200, 380), color = "white",
fontsize = 128, shadow = (1, 1), scolor = "black")
screen.draw.text("COMPLETE", (145, 480), color = "white",
fontsize = 128, shadow = (1, 1), scolor = "black")
sounds.completion.play()
sounds.say_mission_complete.play()
###############
## START ##
###############
--snip--
清单 10-7:发射!
探索物品
现在你可以探索在太空站中找到的物品,并尝试使用它们来看看它们的作用。不过,在你找到所有道具并开始工作之前,你需要先解决如何打开那些封闭太空站各个部分的安全门。在下一章中,你将通过设计门机制来完成太空站的设置,确保在使用正确的通行证时门会打开。
你还可以运用本章学到的知识,在Escape游戏代码中添加你自己的谜题。最简单的方法是使用标准消息(Listing 10-2)作为线索,并使用配方(Listing 10-5)来组合物品。你还可以添加简单的指令(Listing 10-4)来检查玩家是否携带某个特定物品,然后增加他们的air或energy变量,显示消息,或在游戏中执行其他操作。祝你冒险愉快!
你准备好飞行了吗?
勾选以下框以确认你已经学会了本章的关键内容。
使用物品的说明进入use_object()函数。
standard_responses字典包含当玩家使用特定物品时的消息。
对于许多物品,当玩家使用它们时,会有特定的说明来更新不同的列表或变量。
RECIPES列表储存了玩家如何在游戏中组合物品的详细信息。
在一个配方中,前两个项目是原料,第三个项目是它们所制成的物品。
第十一章:启动安全门**

在空间站中,门限制了对某些区域的访问,确保宇航员只能进入他们有资格工作的区域。许多门需要个人访问卡才能打开,而工程舱的门只能通过任务控制室的按钮打开。工程舱的门还带有定时器,可以自动关闭,以增强安全性。
这些门还执行安全规则,要求宇航员在进入气闸之前必须穿戴好宇航服,并且在行星表面的大门打开之前必须有一个同伴陪同。监控摄像头的录像显示,一些宇航员找到了绕过同伴要求的方法,这样他们就能独自享受在行星表面漫步的宁静。
当你安装道具时,你在空间站里安装了这些门。在本章中,你将添加开关门的代码,并增加一些其他的技巧和谜题,让游戏更加有趣。
规划安全门的位置
门显然是空间站设计中至关重要的一部分,但它们对游戏设计同样重要。最明显的是,它们呈现了一个具有挑战性的谜题:玩家需要找到一种方法来打开锁住的门。
这些门也帮助我们讲述一个故事,在这个故事中,英雄必须利用他们的生存训练和逻辑思维来克服障碍。只有当玩家稍微思考一下谜题时,游戏中的解谜才会令人满意。因此,控制玩家看到不同谜题元素的时机非常重要。想象一下,你进入一个房间,另一侧的出口被熊熊火焰阻挡。如果你已经拿着灭火器,你只需拿出来并使用,根本没有挑战。更吸引人的是,你看到威胁(或谜题),然后必须想出解决办法。通过封锁地图的部分区域,我们可以引导玩家在看到解决方案之前先看到问题。我们不能确定他们会注意到我们放在他们面前的每一项内容,但我们可以给他们一个体验游戏最佳状态的机会。
门还使我们能够从地图中获得更多的价值。尽管在输入后可能感觉不到,游戏地图并不巨大。通过要求玩家多次穿越困难的房间,我们可以提供更丰富的体验和更长的游戏时长。例如,如果我们把一把钥匙放在走廊的尽头,我们可以引导玩家沿着走廊重新走一遍,并使用他们之前经过的门上的钥匙。
图 11-1 显示了游戏中门的位置。虽然不想剧透太多,玩家在进入空间站的右上角区域(通过房间 34)之前,是无法进入房间 36 的。玩家也无法在进入房间 40 之前访问房间 27。通过在锁住的房间中战略性地放置物品,包括访问卡,我们可以引导玩家进行游戏并推进故事情节。

图 11-1:游戏地图,门用红色标出
当你设计自己的游戏时,要仔细考虑道具的摆放位置。这是确保游戏为玩家提供愉快挑战的最重要元素之一。
门的位置设置
我在 Escape 中将所有门的位置都设置在房间的顶部或底部出口,这是因为游戏采用了自上而下的视角。如果门设置在侧边出口,玩家只能看到门的顶部表面,我们需要确保像门这样的关键元素能够清晰可见。
大多数门都位于房间的顶部,且在玩家打开后保持开启。唯一的例外是房间 32 和房间 27 之间的门,它有一个定时器机制,会自动关闭。这一定时器带来了额外的挑战:玩家必须在门关闭前迅速从开门的开关处跑到房间。
Escape 中的门是对象 20 到 26。它们的图像和描述在 objects 字典中进行设置(参见 第 85 页 的 “制作空间站对象字典”)。门的位置在 props 字典中进行设置(参见 第 151 页 的 “添加道具信息”)。每扇门都有一个 x 坐标,它将门放置在房间的门口。要计算门的 x 坐标,只需将房间宽度除以 2,向下取整,然后减去 1。
现在让我们添加一些控制,允许玩家打开门。
添加访问控制
为了让玩家能够打开门,我们需要在程序的 USE OBJECTS 部分的 use_object() 函数中添加一些指令。一个新的代码片段将在玩家按下某个房间内的按钮时打开通往工程区的定时门。你将在处理对象 16 和 68 的指令之间添加这段代码。
另一个新增的代码功能将使玩家能够使用访问卡打开门:将此代码放在使用配方的代码之后。
清单 11-1 显示了需要添加的新代码。因为这些指令是 use_object() 函数的一部分,所以第一条指令缩进了四个空格。你的新 elif 指令应与其上方的 elif 指令对齐。
打开上一章节中的listing10-7.py并将这些新代码行添加到其中。将程序保存为listing11-1.py。你可以使用 pgzrun listing11-1.py 来运行它,但我们还没有添加所有必要的代码来使门正常工作。不过,你应该不会看到任何错误消息。
listing11-1.py
--snip--
elif item_player_is_on == 16:
energy += 1
if energy > 100:
energy = 100
use_message = "You munch the lettuce and get a little energy back"
draw_energy_air()
➊ elif item_player_is_on == 42:
➋ if current_room == 27:
➌ open_door(26)
➍ props[25][0] = 0 # Door from RM32 to engineering bay
props[26][0] = 0 # Door inside engineering bay
➎ clock.schedule_unique(shut_engineering_door, 60)
use_message = "You press the button"
show_text("Door to engineering bay is open for 60 seconds", 1)
sounds.say_doors_open.play()
sounds.doors.play()
elif item_carrying == 68 or item_player_is_on == 68:
energy = 100
use_message = "You use the food to restore your energy"
remove_object(68)
draw_energy_air()
--snip--
for recipe in RECIPES:
ingredient1 = recipe[0]
ingredient2 = recipe[1]
--snip--
add_object(combination)
sounds.combine.play()
# {key object number: door object number}
➏ ACCESS_DICTIONARY = { 79:22, 80:23, 81:24 }
➐ if item_carrying in ACCESS_DICTIONARY:
door_number = ACCESS_DICTIONARY[item_carrying]
➑ if props[door_number][0] == current_room:
use_message = "You unlock the door!"
➒ sounds.say_doors_open.play()
sounds.doors.play()
open_door(door_number)
show_text(use_message, 0)
time.sleep(0.5)
--snip--
清单 11-1:添加开门功能
打开工程舱门的按钮是物品 42。工程舱外有一个按钮提供通道,工程舱内另有一个按钮,确保玩家不会被困在里面。
如果玩家使用按钮 ➊,打开门的代码会运行。如果他们使用房间内的按钮 ➋,则会使用open_door()函数来展示门的打开 ➌。我们很快会添加这个函数。
props字典被更新,用于将门的房间号更改为 0,从而将门从房间中移除(也从游戏中移除) ➍。这个门是基于计时器工作的,所以程序会安排在 60 秒后关闭门的函数 ➎。如果你觉得在限定时间内赶到房间太困难,可以将 60 改为更大的数字。无论你使用的是 PC、Raspberry Pi 3,还是运行稍微慢一点的 Raspberry Pi 2,这个数字都应该足够给你足够的时间。
第二部分代码使玩家可以使用钥匙来打开门。我们创建了一个新的字典 ACCESS_DICTIONARY,该字典将门禁卡号作为键,门号作为数据 ➏。举例来说,物品 79(门禁卡)用来打开门 22。
提示
在Escape中,用于打开门的物品都是门禁卡,但如果你修改游戏,可以使用任何物品。你可以使用撬棍来撬开门,或者(如果你制作的是一个幻想世界中的游戏)使用不同的魔法咒语。只要确保玩家能够合理地猜测该使用什么物品即可。
当玩家按下 U 键时,如果他们选择了字典中用于解锁门的某个物品 ➐,并且他们站在解锁门的同一房间里 ➑,门就会打开。我们还会播放一段计算机声音效果,提示“门已打开” ➒。这只是一个录音,像游戏中的任何其他声音一样。
制作门的开关功能
我们将把开门、关门和动画效果的函数放入程序的一个新部分 DOORS。你需要在USE OBJECTS部分之后、START部分之前添加这个新部分。
清单 11-2 展示了你需要添加的前两个函数,以启动DOORS部分。添加新的代码行,并将程序保存为listing11-2.py。DOORS部分仍不完整:你可以运行程序(使用 pgzrun listing11-2.py)来检查错误,但门暂时还无法正常工作。
listing11-2.py
--snip--
sounds.completion.play()
sounds.say_mission_complete.play()
###############
## DOORS ##
###############
➊ def open_door(opening_door_number):
global door_frames, door_shadow_frames
global door_frame_number, door_object_number
➋ door_frames = [images.door1, images.door2, images.door3,
images.door4, images.floor]
# (Final frame restores shadow ready for when door reappears).
door_shadow_frames = [images.door1_shadow, images.door2_shadow,
images.door3_shadow, images.door4_shadow,
images.door_shadow]
door_frame_number = 0
door_object_number = opening_door_number
➌ do_door_animation()
➍ def close_door(closing_door_number):
global door_frames, door_shadow_frames
global door_frame_number, door_object_number, player_y
➎ door_frames = [images.door4, images.door3, images.door2,
images.door1, images.door]
door_shadow_frames = [images.door4_shadow, images.door3_shadow,
images.door2_shadow, images.door1_shadow,
images.door_shadow]
door_frame_number = 0
door_object_number = closing_door_number
# If player is in same row as a door, they must be in open doorway
➏ if player_y == props[door_object_number][1]:
➐ if player_y == 0: # if in the top doorway
➑ player_y = 1 # move them down
else:
➒ player_y = room_height - 2 # move them up
➓ do_door_animation()
###############
## START ##
###############
--snip--
清单 11-2:设置门的动画效果
open_door() 和 close_door() 函数设置了门的开关动画。你已经在 清单 11-1 中见过 open_door() ➊。在 清单 11-2 中,我们定义了该函数,这样它就能在玩家使用钥匙打开门时运行。
门的动画共有五帧,编号为 0 到 4,如 表 11-1 所示。我们将动画的图片存储在一个名为 door_frames 的列表中 ➋➎,并将帧编号存储在变量 door_frame_number 中。在 open_door() 和 close_door() 函数中,我们将帧编号设置为 0,即第一帧。
在变量 door_object_number 中,我们存储将要开关的门的对象编号。在设置好变量和列表后,函数 do_door_animation() 会开始执行并使用这些数据进行动画 ➌➓。我们稍后会添加该函数。
关闭门 ➍ 的函数与开门 ➊ 的函数相似,只有两个区别:动画帧不同,并且有一个检查来防止门关上时压到玩家。
如果玩家与门的 y 位置相同 ➏,这意味着玩家正站在门口。如果玩家位于顶部行 ➐,我们会将其 y 位置设置为 1 ➑,让他移到下一行。如果玩家不在顶部行,我们会将其 y 位置设置为倒数第二行 ➒,也就是门的内部。
这意味着宇航员会主动跳开门的路径,但比起他们被困在门里面,这样显得更为真实!
表 11-1:门的动画帧
| 帧编号 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 开门 | ![]() |
![]() |
![]() |
![]() |
最终帧为地面砖(没有门)。 |
| 关闭 | ![]() |
![]() |
![]() |
![]() |
![]() |
添加门的动画
do_door_animation() 函数将管理门的开关动画。
将 do_door_animation() 函数放在程序的 DOORS 部分,紧跟着你在 清单 11-2 中添加的 close_door() 函数。添加 清单 11-3 中的新行,并将程序保存为 listing11-3.py。你可以通过运行 pgzrun listing11-3.py 来启动这个版本的游戏。现在,用钥匙打开的门应该已经能正常工作了。我会很快告诉你如何在训练任务 #1 中进行测试。
listing11-3.py
--snip--
player_y = room_height - 2 # move them up
do_door_animation()
def do_door_animation():
global door_frames, door_frame_number, door_object_number, objects
➊ objects[door_object_number][0] = door_frames[door_frame_number]
objects[door_object_number][1] = door_shadow_frames[door_frame_number]
➋ door_frame_number += 1
➌ if door_frame_number == 5:
➍ if door_frames[-1] == images.floor:
➎ props[door_object_number][0] = 0 # remove door from props list
# Regenerate room map from the props
# to put the door in the room if required.
➏ generate_map()
➐ else:
➑ clock.schedule(do_door_animation, 0.15)
###############
## START ##
###############
--snip--
清单 11-3:添加门的动画
objects字典包含了许多内容,其中之一是用于特定物体的图像。这个新函数首先通过将字典中门的图像更改为当前的动画帧 ➊ 来开始。当房间被重新绘制时,它将使用该动画帧。
然后,该函数将动画帧编号增加 1 ➋,以便下次函数运行时显示下一个动画帧。如果此时帧为 5,意味着我们已经到达动画的结尾 ➌。在这种情况下,我们检查门是否已打开(而不是关闭),通过查看最后一帧是否是地板瓷砖,表示没有门 ➍。(-1的索引会给你列表中的最后一项。)
如果门现在已经完全打开,物品数据会被更新,通过将门的房间编号更改为 0 ➎ 来从游戏中移除此门。如果当前动画帧是最后一帧,无论门是开还是关,都会生成一个新的房间地图 ➏,确保门在当前房间中正确地被添加或移除。
如果当前帧不是最终的动画帧 ➐,该函数将在 0.15 秒后 ➑ 重新运行,以显示序列中的下一个帧。
你可能会想,为什么我没有将两个if语句合并在一起 ➌➍。原因是generate_map()函数需要在动画结束时运行,无论门是开还是关。如果我们合并了这两个if语句,这个函数只会在门打开时运行。
训练任务 #1
在程序的这个阶段,门应该完全功能正常。你能测试它们是否正常工作吗?在社区房间找到门的门禁卡并使用它。站在社区房间里,通过选择库存中的门禁卡并按 U 键来使用它。如果你需要提示,可以查看图 11-1 中的地图。社区房间是 39 号,门禁卡在 41 号房间。记住,有时人们会把东西整理起来,钥匙可能不会明目张胆地摆在眼前。
关闭定时门
接下来,我们需要添加一个新函数,名为shut_engineering_door(),以便在工程舱门自动关闭时使用。这个函数设置为在门打开后的 60 秒延迟后运行(参见 Listing 11-1),给玩家一分钟的时间从按钮跑到门口,门就会关闭!
将此函数放在程序的DOORS部分,在你刚刚添加的do_door_animation()函数之后。添加 Listing 11-4 中的新行,并将程序保存为listing11-4.py。然后使用pgzrun listing11-4.py运行此程序,你应该不会看到任何错误信息。定时门应该现在能正常工作,不过我稍后会向你展示一个更简单的测试方法。
listing11-4.py
--snip--
else:
clock.schedule(do_door_animation, 0.15)
def shut_engineering_door():
global current_room, door_room_number, props
➊ props[25][0] = 32 # Door from room 32 to the engineering bay.
➋ props[26][0] = 27 # Door inside engineering bay.
➌ generate_map() # Add door to room_map for if in affected room.
➍ if current_room == 27:
➎ close_door(26)
➏ if current_room == 32:
➐ close_door(25)
show_text("The computer tells you the doors are closed.", 1)
sounds.say_doors_closed.play()
###############
## START ##
###############
--snip--
Listing 11-4: 自动关闭工程门的代码
shut_engineering_door()函数有两个门属性要操作,物体 25 和 26,因为玩家可以根据所在房间的不同,从任何一侧看到这个门。我们首先要做的是更新props字典,以便这些门出现在房间中➊➋。
然后我们调用generate_map()函数➌。如果玩家在包含这些门的房间中,该函数将更新当前房间的房间地图。在其他情况下,generate_map()函数仍会运行,但不会有任何变化。
如果玩家在工程舱(房间 27)➍,他们需要看到 26 号门关闭➎,因此程序启动动画。如果玩家在门的另一边,在房间 32 ➏,我们需要展示 25 号门关闭➐。
红色警报
不要混淆门号和房间号。门号是物体编号,与它所在的房间无关。
为了测试工程舱门是否正常工作,我们需要运行游戏,按下按钮,然后快速跑到工程舱。因此,为了节省时间,我们将设计一种解决方案,使我们能够更快速地穿越空间站。
添加传送门
在你仍在构建空间站时,能够瞬间跳转到任何房间可能会对你有所帮助。利用最新的分子传输技术,我们可以安装一个传送门,允许你输入房间号并直接到达那里。这对测试游戏时是一个巨大的好处,但它是受限技术,在真实的空间站任务中并未批准使用。在你完成游戏构建之前,你需要将其移除。我信任你处理这种高度机密的技术。
将传送门代码与其他玩家控制代码放置在game_loop()函数中,即程序的GAME LOOP部分。我建议你在启动use_object函数的指令后添加它。因为这些指令在函数内部,你需要将if指令缩进四个空格,然后再将其下方的指令缩进四个空格。
在清单 11-5 中添加新的指令,然后将文件保存为listing11-5.py。你可以使用 pgzrun listing11-5.py 来运行这个程序。
listing11-5.py
--snip--
if keyboard.u:
use_object()
## Teleporter for testing
## Remove this section for the real game
➊ if keyboard.x:
➋ current_room = int(input("Enter room number:"))
➌ player_x = 2
player_y = 2
➍ generate_map()
➎ start_room()
sounds.teleport.play()
## Teleport section ends
--snip--
清单 11-5:添加传送门
当你按下 X 键➊时,程序会要求你输入一个房间号➋。这个请求会出现在命令行窗口中,在这里你输入pgzrun指令来运行程序。你可能需要点击这个窗口将其置于前端,并且之后需要点击游戏窗口才能再次进行游戏。
input()函数会将你输入的内容存入一个字符串中。因为我们需要将输入转换为数字,所以我们使用int()函数将其转换为整数(或整数)➋。
你输入的数字会进入current_room变量。这里没有错误检查,所以如果你没有输入有效的房间号,程序可能会崩溃。例如,如果你输入的是文本而不是数字,程序会冻结。
你会被传送到你选择的房间内的y = 2,x = 2 ➌位置。这通常是一个相对安全的地方,但如果传送器将你放置在一些场景中,你通常可以直接走出来。房间地图会重新生成 ➍,房间会重新启动 ➎,完成你传送到新目的地的过程。
训练任务 #2
使用传送器传送到 27 号房间,以便测试工程舱中的门。使用房间顶部的按钮打开门(在走向按钮时按下 U),并在房间里等待门关闭。再次打开门,但这次离开房间,检查从另一侧看到时门是否仍然会关闭。门的动画应该能正常工作。
激活气闸安全门
作为一个安全功能,通往星球表面的气闸门使用重量传感器来打开它。必须有一名宇航员站在压力垫上才能打开门,从而使另一名宇航员能够穿过它。这一设计确保了宇航员在没有空间站支援的情况下不能单独进入星球表面。
为了启用这个安全功能,我们需要在程序的DOORS部分添加一个新函数。清单 11-6 显示了新函数的代码,用于动画化门。在你在清单 11-4 中添加的shut_engineering_door()函数后添加这段代码。将更新后的程序保存为listing11-6.py。你可以使用pgzrun listing11-6.py来运行你的程序,但气闸门尚未激活。
listing11-6.py
--snip--
show_text("The computer tells you the doors are closed.", 1)
sounds.say_doors_closed.play()
def door_in_room_26():
global airlock_door_frame, room_map
➊ frames = [images.door, images.door1, images.door2,
images.door3,images.door4, images.floor
]
shadow_frames = [images.door_shadow, images.door1_shadow,
images.door2_shadow, images.door3_shadow,
images.door4_shadow, None]
➋ if current_room != 26:
clock.unschedule(door_in_room_26)
return
# prop 21 is the door in Room 26.
➌ if ((player_y == 8 and player_x == 2) or props[63] == [26, 8, 2]) \
and props[21][0] == 26:
➍ airlock_door_frame += 1
➎ if airlock_door_frame == 5:
props[21][0] = 0 # Remove door from map when fully open.
room_map[0][1] = 0
room_map[0][2] = 0
room_map[0][3] = 0
➏ if ((player_y != 8 or player_x != 2) and props[63] != [26, 8, 2]) \
and airlock_door_frame > 0:
if airlock_door_frame == 5:
# Add door to props and map so animation is shown.
props[21][0] = 26
room_map[0][1] = 21
room_map[0][2] = 255
room_map[0][3] = 255
airlock_door_frame -= 1
➐ objects[21][0] = frames[airlock_door_frame]
objects[21][1] = shadow_frames[airlock_door_frame]
###############
## START ##
###############
--snip--
清单 11-6:在气闸中添加重量激活门
我已经向游戏中添加了door_in_room_26()函数,以启用一个特定的谜题。为了避免直接告诉你解决方案并破坏谜题的乐趣,我在这里不会覆盖代码中的所有内容,但我相信如果你想的话,可以自己搞明白!
我们将门的动画帧存储在列表frames中,包括显示门关闭的第一帧和显示空地板瓷砖而不是门的最后一帧➊。
我们将气闸门的动画帧存储在airlock_door_frame变量中。如果玩家站在压力垫上(位置y = 8,x = 2),且门在房间中➌,则动画帧编号会增加,门会稍微打开一些 ➍。如果动画帧现在是 5 ➎,则门完全打开,props字典和房间地图会更新,将门从房间中移除。
我们添加了另一段代码,用于在玩家不站在压力垫上且门已经至少部分打开时关闭门 ➏,因此当玩家离开压力垫时,门会关闭。程序只会显示当前房间地图中存在的道具,因此第一条指令将门(对象 21)添加到房间地图中,尽管第一帧动画将显示门完全打开。
最后,我们将objects字典中门的图像文件更改为当前的动画帧 ➐。门的阴影图像也会更新。因此,当房间绘制时,门的图像会显示其当前的动画帧。
这个气闸程序创建了一个平滑的效果,当玩家踩上压力垫时,门会滑开;但当玩家离开时,它又会迅速关闭。如果他们在门关闭时重新踩上垫子,门会重新打开。
为了使气闸程序正常工作,我们还需要添加指令,让door_in_room_26()函数在玩家进入房间时每 0.05 秒运行一次。当door_in_room_26()函数启动时,它会检查玩家是否还在 26 号房间。如果玩家已经离开该房间,Listing 11-6 中的第➋条指令会停止定期运行该函数并退出函数(使用return指令),以便门的动画停止。
我们将把启动door_in_room_26()函数的代码放入start_room()函数中,该函数位于GAME LOOP部分的顶部。start_room()函数在玩家进入房间时运行。Listing 11-7 展示了需要添加的新指令。
listing11-7.py
--snip--
###############
## GAME LOOP ##
###############
def start_room():
global airlock_door_frame
show_text("You are here: " + room_name, 0)
if current_room == 26: # Room with self-shutting airlock door
airlock_door_frame = 0
clock.schedule_interval(door_in_room_26, 0.05)
--snip--
Listing 11-7: 为气闸安排门动画
将程序保存为listing11-7.py,并使用 pgzrun listing11-7.py 运行它。在游戏中,按 X 键使用传送器并传送到 26 号房间。现在你可以测试压力垫是否按预期工作(参见 Figure 11-2)。尝试在上面走动、离开或穿过它,观察门的表现。
请注意,如果你从房间底部的出口离开,门会出现并挡住你回来的路。(通常,你只会通过打开那扇门并将其从游戏中移除来进入气闸舱。)当你传送到房间时,像这样的奇怪情况可能会发生。它会扰乱时空连续性。

Figure 11-2: 踩在压力垫上会打开门。
为你自己的游戏设计移除出口
如果你在自己的地图设计中需要关闭出口,可能也需要移动或移除这些出口中的门。要从游戏中移除一扇门,可以在props字典中将该门的条目修改为其第一个数字为 0,或者删除字典中的条目。
如果你在自定义游戏,你可能还想移除一些自定义代码,这些代码启用了工程舱和气闸的特殊门。要禁用压力垫门,请删除 Listings 11-6 和 11-7 中的新增代码。要移除通往工程舱的定时门,请删除 Listing 11-4 中的代码,同时还需要移除 Listing 11-1 中的第一部分新代码(使用对象 42 按下按钮)。
任务完成了吗?
你现在已经完成了空间站的建设,并且它已经完全功能化。看来你现在可以开始新生活,进行实验并探索红色星球了。
等等!这是什么?前方可能有麻烦。
你准备好飞行了吗?
勾选以下框以确认你已经学会了本章的关键知识点。
门可以封闭游戏地图的某些部分,因此玩家可以按正确的顺序发现谜题元素。
门需要放置在房间的顶部或底部。
通过访问卡打开的门将保持打开状态。
你可以使用提供的功能添加自动关闭的门,比如工程舱里的门。
门的位置是通过 props 字典进行设置的。它们的图像和描述存储在 objects 字典中。
为了给门添加动画,程序会更改 objects 字典中的门图像。当房间被重新绘制时,门会使用新的图像。
如果一扇门从两边都能看到,则需要用两个门道具表示:每个房间里各有一个门。
ACCESS_DICTIONARY 用于记住哪些访问卡可以解锁哪些门。你可以通过在这个字典中做出修改,使用其他物品来打开门。
要调整游戏的难度,你可以更改工程门关闭前的延迟时间。
传送器使你能够传送到任何房间进行测试。
Python 中的 input() 函数将你输入的内容视为字符串。为了让玩家输入数字,可以使用 int() 函数将输入内容转换为整数。
第十二章:12
危险!危险!添加危险元素

当空间站系统故障时,各种威胁出现。在本章中,你将看到空气开始从空间站泄漏,并且会在一些房间中发现移动的危险,包括流氓机器人、能量球和有毒的水坑。
我把危险放在最后,这样你可以在不担心时间或精力耗尽的情况下先测试游戏到这一点。在本章中,我们将开始漏气并绘制一个计时条,让你知道剩余多少空气。我们还将加入一些可能伤害你的危险,并消耗你的能量。最后,我们将清理游戏并准备好进行游戏!
添加空气倒计时
玩家在游戏中有两种失败方式:空气用尽或能量用尽。在屏幕底部,两个条形图显示玩家剩余的空气和能量(见图 12-1)。

图 12-1:屏幕底部的两个条形图显示剩余的空气和能量。
当你走过有毒的溢出物,或者被移动的危险物体击中时,你会失去能量,空气也会因为空间站墙壁的泄漏而逐渐耗尽。如果你穿上太空服,你可以争取更多的时间,但太空服的气瓶最终也会耗尽。你最艰难的决定之一可能是决定什么时候补充空气和使用食物来恢复能量。
显示空气和能量条
我们将创建一个新的程序部分,称为AIR,你需要将它放在DOORS部分之后,但在程序末尾的START部分之前。将清单 12-1 中的新代码添加到你上一个章节的最终清单中(listing11-7.py)。将文件保存为listing12-1.py。如果你运行程序,它不会做任何新的事情,但这段代码为绘制空气和能量条创建了函数。
listing12-1.py
--snip--
objects[21][0] = frames[airlock_door_frame]
objects[21][1] = shadow_frames[airlock_door_frame]
###############
## AIR ##
###############
def draw_energy_air():
box = Rect((20, 765), (350, 20))
➊ screen.draw.filled_rect(box, BLACK) # Clear air bar.
➋ screen.draw.text("AIR", (20, 766), color=BLUE)
➌ screen.draw.text("ENERGY", (180, 766), color=YELLOW)
➍ if air > 0:
➎ box = Rect((50, 765), (air, 20))
➏ screen.draw.filled_rect(box, BLUE) # Draw new air bar.
➐ if energy > 0:
box = Rect((250, 765), (energy, 20))
screen.draw.filled_rect(box, YELLOW) # Draw new energy bar.
###############
## START ##
###############
--snip--
清单 12-1:绘制空气和能量条
我们通过在屏幕底部的状态区域绘制一个黑色框来清除它,开始新的draw_energy_air()函数 ➊。然后我们添加蓝色的 AIR 标签 ➋,黄色的 ENERGY 标签 ➌。这个函数将使用已经在程序的VARIABLES部分设置为 100 的air和energy变量。
如果玩家还有一些空气(如果变量air大于 0) ➍,将创建一个使用air变量作为宽度的框 ➎。然后,框将被填充为蓝色 ➏。这会绘制 AIR 指示条,它最初宽度为 100 像素,并随着AIR变量的减少而变小。
我们使用类似的指令来绘制能量条 ➐,但条形图的起始位置更靠右(x位置是 250,而不是 50)。
添加空气倒计时功能
我们将创建三个函数来启用空气倒计时。当你没有空气时,end_the_game()函数会运行。它会显示玩家失败任务的原因,播放一些音效,并在游戏窗口中央显示一个大的游戏结束消息。
air_countdown()函数消耗空气供应。我们还会添加一个alarm()函数,在游戏开始后不久运行,警告玩家他们的空气供应不足。
这三个函数位于清单 12-2 中。将此处显示的新代码添加到程序的AIR部分,在你刚刚添加的draw_energy_air()函数之后。将程序保存为listing12-2.py。你可以使用pgzrun listing12-2.py运行此程序,但此时你还看不到任何新内容。
listing12-2.py
--snip--
if energy > 0:
box = Rect((250, 765), (energy, 20))
screen.draw.filled_rect(box, YELLOW) # Draw new energy bar.
➊ def end_the_game(reason):
global game_over
➋ show_text(reason, 1)
➌ game_over = True
sounds.say_mission_fail.play()
sounds.gameover.play()
➍ screen.draw.text("GAME OVER", (120, 400), color = "white",
fontsize = 128, shadow = (1, 1), scolor = "black")
➎ def air_countdown():
global air, game_over
if game_over:
return # Don't sap air when they're already dead.
➏ air -= 1
➐ if air == 20:
sounds.say_air_low.play()
if air == 10:
sounds.say_act_now.play()
➑ draw_energy_air()
➒ if air < 1:
end_the_game("You're out of air!")
➓ def alarm():
show_text("Air is running out, " + PLAYER_NAME
+ "! Get to safety, then radio for help!", 1)
sounds.alarm.play(3)
sounds.say_breach.play()
###############
## START ##
###############
--snip--
清单 12-2:添加空气倒计时
air_countdown()函数 ➎ 每次运行时都会减少air变量的值 1 ➏。如果值等于 20 ➐或 10,会播放一个警告音效,提醒玩家他们的空气已经很低。
你在清单 12-1 中添加的draw_energy_air()函数会更新空气和能量的显示 ➑。如果空气耗尽 ➒,end_the_game()函数会运行并显示字符串"You're out of air!"。
提示
音频文件必须存储在sounds文件夹中,格式应为.wav或.ogg。要播放名为bang.wav的声音,可以使用sounds.bang.play()。与图像一样,你不需要告诉 Pygame Zero 音频文件的扩展名或其存储位置。为什么不尝试为游戏中的不同点录制并添加你自己的音效呢?
在end_the_game()函数 ➊ 中,我们使用变量reason来存储接收到的信息,并将其作为死亡原因显示在屏幕上 ➋。game_over变量被设置为True ➌。其他函数使用这个变量来判断游戏是否结束,从而使一切停止。end_the_game()函数随后在屏幕中央绘制“游戏结束”的大字。文字的位置是x = 120,y = 400,使用白色文字和 128 的字体大小 ➍。我们还为文字添加了一个投影效果,投影偏移了 1 像素,并且是黑色的(见图 12-2)。

图 12-2:哦不!你没氧气了!
本节中的最后一个函数alarm() ➓,播放警报声音并显示一条消息,提醒你呼叫无线电寻求帮助。它在警告中使用玩家的名字,以便个性化。
sounds.alarm.play()命令中括号里的数字表示声音应该播放的次数(在清单 12-2 中,它是三次)。
启动空气倒计时并触发警报
我们还没有设置这三个新函数的运行。为此,我们需要在程序的 START 部分添加一些指令,这部分(也许让人困惑的是)位于程序清单的末尾。添加 清单 12-3 中显示的新指令,并将其保存为 listing12-3.py。
listing12-3.py
--snip--
###############
## START ##
###############
clock.schedule_interval(game_loop, 0.03)
generate_map()
clock.schedule_interval(adjust_wall_transparency, 0.05)
clock.schedule_unique(display_inventory, 1)
clock.schedule_unique(draw_energy_air, 0.5)
clock.schedule_unique(alarm, 10)
# A higher number below gives a longer time limit.
clock.schedule_interval(air_countdown, 5)
清单 12-3:启动空气倒计时
现在游戏有了时间限制。当空气用尽时,游戏结束。使用 pgzrun listing12-3.py 运行程序,你应该能看到空气供应逐渐下降。
如果你发现游戏的最终版本太难,可以通过将 清单 12-3 中最后一行的 5 改为更高的数字,来给自己更多时间。这个数字决定了 air_countdown() 函数消耗空气供应的频率,单位是秒。特别是,如果你使用的是 Raspberry Pi 2,时间限制可能会显得很具挑战性,因为该平台上游戏运行较慢。尽管如此,完成游戏仍然是可能的,但你可以将数字 5 增大一点,给自己更多的、咳咳、呼吸空间。
训练任务 #1
当你的空气供应降到 0 时,你应该会看到 "GAME OVER" 信息,并且发现你无法再控制宇航员。每 5 秒你的能量下降 1%,因此大约 8.5 分钟(500 秒)后空气会耗尽。你能想出如何让空气泄漏得更频繁一点,以便更轻松地测试空气耗尽时会发生什么吗?
完成训练任务后,确保将程序恢复原状:否则你会发现任务完成起来相当困难!
添加移动危险物
游戏中有三种移动的危险物:两种类型的能量球和一架失控的飞行无人机。
图 12-3 显示了这些移动危险物使用的方向编号。
危险物会沿直线移动,直到碰到某物,然后我们会添加一个数字来改变它们的方向。我们添加的数字决定了危险物的移动模式。例如,如果我们向方向编号添加 1,危险物将按顺时针方向移动(上、右、下、左)。如果我们向方向编号添加 -1,危险物将按逆时针方向移动(左、下、右、上)。如果我们添加 2,它将在左右(2 和 4)或上下(1 和 3)之间反弹。看看 图 12-3,确认这是否能理解。每个危险物都可以有自己的移动模式。

图 12-3:移动危险物使用的方向编号按顺时针顺序编号。
如果加法结果超过 4,我们就减去 4。例如,如果一个危险物体按顺时针方向移动,每次碰到物体时我们都会将方向数加 1。如果它是向下移动(方向 3),碰到物体时我们加 1,使得它开始向左移动(方向 4)。下一次碰到物体时,我们加 1,这使得方向数变为 5。因此,我们减去 4,得到方向数 1。如图 12-3 所示,这是 4 之后的下一个方向数,按顺时针方向旋转。
表 12-1 总结了我们可以使用的数字,以获得不同的移动模式。
表 12-1: 当危险物体碰到物体时如何改变方向
| 移动模式 | 要加到方向数上的数字 | |
|---|---|---|
| 顺时针 | ![]() |
1 |
| 逆时针 | ![]() |
-1 |
| 左/右 | ![]() |
2 |
| 上/下 | ![]() |
2 |
红色警报
请小心不要混淆描述移动的两个数字。方向数(见图 12-3)告诉程序危险物体的移动方向。我们加到方向数上的数字(见表 12-1)则告诉程序当危险物体碰到物体时应该如何反弹。
添加危险数据
在AIR和START部分之间,我们将在程序中添加一个名为HAZARDS的新部分。列表 12-4 展示了危险数据。将其添加到程序中,并保存为listing12-4.py。如果运行程序,它暂时不会做任何新操作,但你可以检查命令行窗口中是否没有出现错误信息。
listing12-4.py
--snip--
sounds.alarm.play(3)
sounds.say_breach.play()
###############
## HAZARDS ##
###############
hazard_data = {
# room number: [[y, x, direction, bounce addition to direction]]
➊ 28: [[1, 8, 2, 1], [7, 3, 4, 1]], 32: [[1, 5, 1, 1]],
34: [[5, 1, 1, 1], [5, 5, 1, 2]], 35: [[4, 4, 1, 2], [2, 5, 2, 2]],
36: [[2, 1, 2, 2]], 38: [[1, 4, 3, 2], [5, 8, 1, 2]],
40: [[3, 1, 3, 1], [6, 5, 2, 2], [7, 5, 4, 2]],
41: [[4, 5, 2, 2], [6, 3, 4, 2], [8, 1, 2, 2]],
42: [[2, 1, 2, 2], [4, 3, 2, 2], [6, 5, 2, 2]],
46: [[2, 1, 2, 2]],
48: [[1, 8, 3, 2], [8, 8, 1, 2], [3, 9, 3, 2]]
}
###############
## START ##
###############
--snip--
列表 12-4:添加危险数据
我们创建了一个hazard_data字典,使用房间编号作为字典的键。对于每个房间,都会有一个包含所有危险数据的列表。每个危险的数据以一个包含y位置、x位置、起始方向和碰到物体时要加的数字的列表形式表示。
例如,房间 28 ➊ 存在一个包含列表数据[7, 3, 4, 1]的危险。这意味着该危险从y = 7,x = 3 开始。它开始向左移动(方向 4),当它碰到物体时会顺时针转动,因为我们将 1 加到它的方向数上。
房间 41 包含三个危险(分别在三个列表中),它们从左向右并来回移动。我们知道这一点,因为它们的方向初始为 2 或 4(向右或向左),并在碰到物体时将方向加 2(变为 4 或 6:我们知道 6 减去 4 会变成 2)。
消耗玩家能量
在危害数据之后,我们需要添加一个名为deplete_energy()的函数,当危害击中玩家时会减少玩家的能量。列表 12-5 展示了这个新函数。将它添加到程序的HAZARDS部分中列表 12-4 后面,并将程序保存为listing12-5.py。你可以使用 pgzrun listing12-5.py 运行程序来检查错误,但它不会做任何新的操作。
listing12-5.py
--snip--
46: [[2, 1, 2, 2]],
48: [[1, 8, 3, 2], [8, 8, 1, 2], [3, 9, 3, 2]]
}
➊ def deplete_energy(penalty):
global energy, game_over
if game_over:
return # Don't sap energy when they're already dead.
➋ energy = energy - penalty
draw_energy_air()
if energy < 1:
end_the_game("You're out of energy!")
###############
## START ##
###############
--snip--
列表 12-5:减少玩家的能量
deplete_energy()函数接受一个数字 ➊,并使用这个数字来减少玩家的energy变量 ➋。因此,我们可以用这个函数来处理消耗不同能量值的危害。
启动和停止危害
当玩家进入新房间时,函数hazard_start()会将危害添加到房间中。列表 12-6 展示了这个函数,你需要将它添加到程序中HAZARDS部分的deplete_energy()函数后面。将程序保存为listing12-6.py。如果你使用 pgzrun listing12-6.py 运行它,你应该不会注意到任何变化,因为我们还没有设置这个函数来运行。
listing12-6.py
--snip--
if energy < 1:
end_the_game("You're out of energy!")
def hazard_start():
global current_room_hazards_list, hazard_map
➊ if current_room in hazard_data.keys():
➋ current_room_hazards_list = hazard_data[current_room]
➌ for hazard in current_room_hazards_list:
hazard_y = hazard[0]
hazard_x = hazard[1]
➍ hazard_map[hazard_y][hazard_x] = 49 + (current_room % 3)
➎ clock.schedule_interval(hazard_move, 0.15)
###############
## START ##
###############
--snip--
列表 12-6:将危害添加到当前房间
hazard_start()函数会在每次玩家进入新房间时运行,因此它首先检查当前房间在hazard_data字典中是否有条目 ➊。如果有,这个房间应该有移动的危害,接下来会执行函数的其余部分。我们将房间的危害数据放入一个名为current_room_hazards_list的列表 ➋。然后函数使用一个循环 ➌依次处理列表中的每个危害。
危害使用它们自己的房间地图,称为hazard_map,这样它们可以轻松地飞越地板上的物体,而不会覆盖房间地图中的物品。如果危害使用与道具相同的房间地图,它们就会在飞越道具时将道具抹去,或者我们需要一个复杂的方法来记住危害下方的内容。
这三个危害对象在objects字典中分别对应数字 49、50 和 51。程序使用简单的计算来确定哪个危害对象应该进入特定的房间。正如你之前所见,Python 的%操作符会给出除法后的余数。当你将任何数字除以 3 时,余数将是 0、1 或 2。所以,程序会将房间号除以 3,然后将余数加到 49 上,从而选出一个危害对象的编号 ➍。例如,如果我们在房间 34,程序会计算出34 % 3的余数是 1,然后将 1 加到 49 上,选择编号为 50 的危害对象。
这种选择危害编号的方式确保了玩家进入房间时,危害始终是相同类型的。由于地图宽度为五个房间,它还保证了两个直接相连的房间不能有相同的危害。这为地图增加了多样性,尽管并非所有房间都有危害,因此在实践中,玩家可能会在走过一些空房间后,连续两次遇到相同的危害。
该函数的最后,通过安排每隔 0.15 秒执行一次 hazard_move() 函数来完成 ➎。
要在玩家进入新房间时启动 hazard_start() 函数,在 start_room() 函数中添加一条指令,如 列表 12-7 所示。将你的程序保存为 listing12-7.py。该版本的程序将在你离开起始房间时冻结,因为我们还没有完成添加危害的代码。
listing12-7.py
--snip--
###############
## GAME LOOP ##
###############
def start_room():
global airlock_door_frame
show_text("You are here: " + room_name, 0)
if current_room == 26: # Room with self-shutting airlock door
airlock_door_frame = 0
clock.schedule_interval(door_in_room_26, 0.05)
hazard_start()
--snip--
列表 12-7:玩家进入房间时启动危害
并非所有房间都有危害,因此当玩家离开房间时,我们会停止危害的移动。我们之前在 game_loop() 函数中添加了指令,用于在玩家改变房间时关闭让危害移动的函数。我们将它们注释掉了,因为那时我们还没有准备好使用它们。
现在我们准备好添加它们了!按照以下步骤取消注释这些指令(你在 第八章 中做过类似的操作):
-
在 IDLE 中点击 编辑 ▸ 替换(或按 CTRL-H)以显示替换文本对话框。
-
在“查找”框中输入 #clock.unschedule(hazard_move)。
-
在“替换为”框中输入 clock.unschedule(hazard_move)。
-
点击 全部替换。IDLE 应该会在四个地方替换该指令,并跳转到列表中的最后一项。列表 12-8 显示了过程结束时将被高亮的新行(你无需输入此列表)。在这段代码之前,有三个类似的代码块,现在也会在玩家离开房间时通过出口停止危害的移动。
listing12-8.py
--snip--
if player_y == -1: # through door at TOP
clock.unschedule(hazard_move)
current_room -= MAP_WIDTH
generate_map()
player_y = room_height - 1 # enter at bottom
player_x = int(room_width / 2) # enter at door
player_frame = 0
start_room()
return
--snip--
列表 12-8:当玩家离开房间时停止危害移动
将你的更新程序保存为 listing12-8.py。如果你运行此版本的程序,控制台将显示错误信息,且当你离开房间时,游戏将会冻结。原因是我们还没有添加 hazard_move() 函数。
设置危害地图
我们现在需要确保,当生成房间地图、景物和道具时,也会生成一个空的危害地图。hazard_start() 函数将用房间中的任何危害来填充这个地图。
在程序的 MAKE MAP 部分的 generate_map() 函数末尾,添加 列表 12-9 中显示的新代码。将此新代码放在 GAME LOOP 部分之前,并确保将第一行缩进四个空格,因为它位于函数内部。
保存程序为 listing12-9.py。当你运行它时,程序还不能正常工作,因为它还不完整。
listing12-9.py
--snip--
for tile_number in range(1, image_width_in_tiles):
room_map[prop_y][prop_x + tile_number] = 255
hazard_map = [] # empty list
for y in range(room_height):
hazard_map.append( [0] * room_width )
###############
## GAME LOOP ##
###############
--snip--
清单 12-9:创建空的危险物体地图
这些新指令为危险物体地图创建了一个空列表,并用与房间宽度相同的 0 填充每一行。
让危险物体移动
现在,让我们添加缺失的 hazard_move() 函数,使危险物体能够移动。将其放在程序中 HAZARDS 部分的 hazard_start() 函数之后,正如 清单 12-10 所示。保存程序为 listing12-10.py。
listing12-10.py
--snip--
hazard_map[hazard_y][hazard_x] = 49 + (current_room % 3)
clock.schedule_interval(hazard_move, 0.15)
def hazard_move():
global current_room_hazards_list, hazard_data, hazard_map
global old_player_x, old_player_y
if game_over:
return
for hazard in current_room_hazards_list:
hazard_y = hazard[0]
hazard_x = hazard[1]
hazard_direction = hazard[2]
➊ old_hazard_x = hazard_x
old_hazard_y = hazard_y
hazard_map[old_hazard_y][old_hazard_x] = 0
➋ if hazard_direction == 1: # up
hazard_y -= 1
if hazard_direction == 2: # right
hazard_x += 1
if hazard_direction == 3: # down
hazard_y += 1
if hazard_direction == 4: # left
hazard_x -= 1
hazard_should_bounce = False
➌ if (hazard_y == player_y and hazard_x == player_x) or \
(hazard_y == from_player_y and hazard_x == from_player_x
and player_frame > 0):
sounds.ouch.play()
deplete_energy(10)
hazard_should_bounce = True
➍ # Stop hazard going out of the doors
if hazard_x == room_width:
hazard_should_bounce = True
hazard_x = room_width - 1
if hazard_x == -1:
hazard_should_bounce = True
hazard_x = 0
if hazard_y == room_height:
hazard_should_bounce = True
hazard_y = room_height - 1
if hazard_y == -1:
hazard_should_bounce = True
hazard_y = 0
➎ # Stop when hazard hits scenery or another hazard.
if room_map[hazard_y][hazard_x] not in items_player_may_stand_on \
or hazard_map[hazard_y][hazard_x] != 0:
hazard_should_bounce = True
➏ if hazard_should_bounce:
hazard_y = old_hazard_y # Move back to last valid position.
hazard_x = old_hazard_x
➐ hazard_direction += hazard[3]
➑ if hazard_direction > 4:
hazard_direction -= 4
if hazard_direction < 1:
hazard_direction += 4
➒ hazard[2] = hazard_direction
➓ hazard_map[hazard_y][hazard_x] = 49 + (current_room % 3)
hazard[0] = hazard_y
hazard[1] = hazard_x
###############
## START ##
###############
--snip--
清单 12-10:添加危险物体移动函数
hazard_move() 函数使用了类似于玩家移动的思想。危险物体的位置存储在 old_hazard_x 和 old_hazard_y 变量中 ➊。然后,危险物体被移动 ➋。
然后,我们检查危险物体是否撞到了玩家 ➌、是否出了门 ➍,或者撞到了景物或另一个危险物体 ➎。如果是 ➏,则将其位置重置为原来的值,并通过将数据列表中的最后一个数字加到方向编号上来改变其方向 ➐。如果加上这个数字后,方向编号超过 4 ➑,函数会减去 4,正如我们在本章之前讨论的,因为 4 是最大的有效方向编号。另一方面,如果加上这个数字后,方向编号小于 1,函数则会加上 4。最后,新的方向会保存在危险物体数据中 ➒。
在函数的末尾 ➓,危险物体被放入了危险物体地图。
你可以通过运行 pgzrun listing12-10.py 来启动这个程序。第一个有危险物体的房间是在你起始房间的右侧。当你进入时,尽管你看不见任何危险物体,但你的能量有时会神秘地下降。这是因为我们还没有添加绘制危险物体的代码。
提示
当危险物体碰到你时 ➌,deplete_energy() 函数会减少你 10% 的能量。如果你觉得游戏太难,可以将这个数字改为 5。如果你完成游戏并希望下次有更大的挑战,可以将它改为 20!
在房间中显示危险物体
让我们为房间中的危险物体添加几行代码,以便显示它们,因为不可见的危险似乎不太公平。清单 12-11 显示了要添加到程序 DISPLAY 部分的 draw() 函数中的三行新代码。将这些代码放在函数末尾,绘制玩家代码之前。
由于这些指令在 draw() 函数内(缩进 4 个空格)、在 y 循环内(再缩进 4 个空格)以及在 x 循环内(再缩进 4 个空格),因此总共需要缩进 12 个空格。保存程序为 listing12-11.py。
listing12-11.py
--snip--
# Use shadow across width of object.
for z in range(0, shadow_width):
draw_shadow(shadow_image, y, x+z)
else:
draw_shadow(shadow_image, y, x)
hazard_here = hazard_map[y][x]
if hazard_here != 0: # If there's a hazard at this position
draw_image(objects[hazard_here][0], y, x)
if (player_y == y):
draw_player()
--snip--
清单 12-11:显示移动中的危险物体
这个列表完成了移动的危险物体。使用 pgzrun listing12-11.py 来运行你的程序。然后开始奔命吧!你现在应该能看到移动的危险物体,比如图 12-4 中展示的能量球。

图 12-4:这个能量球沿逆时针方向在房间内弹跳。
训练任务 #2
测试移动的危险物体是否正常工作。进入你起始房间右侧的房间(或者必要时传送到房间 32)。当能量球撞到你时,你的能量是否减少?能量球会弹开吗?你能把能量球弹到两个门口,检查它是否停留在房间内吗?当你的能量用完时,游戏会结束吗?
阻止玩家穿过危险物体
我们还需要添加一行代码来防止玩家走进或穿过危险区域。实际上,危险物体通常会从玩家身上弹开,但如果没有在列表 12-12 中做出修复,有时玩家是可以穿过危险的。
我们已经在game_loop()函数中添加了需要的代码,但我们将其注释掉了。现在是时候取消注释了,通过删除\末尾行前的#符号,以及删除下一行开头的#符号。
我们还需要删除items_player_may_stand_on后面的冒号。一个快速找到正确部分的方式是按 CTRL-F 打开搜索框,然后输入#\。 列表 12-12 展示了需要修改的行。
listing12-12.py
--snip--
# If the player is standing somewhere they shouldn't, move them back.
if room_map[player_y][player_x] not in items_player_may_stand_on \
or hazard_map[player_y][player_x] != 0:
player_x = old_player_x
player_y = old_player_y
player_frame = 0
--snip--
列表 12-12:阻止玩家穿过危险物体
将你的程序保存为listing12-12.py,并通过运行 pgzrun listing12-12.py 来测试它。你能追踪到空间站中所有三种飞行危险吗?
添加有毒泄漏
你可能已经注意到图 12-4 中地板上的绿色污点。这是有毒的泄漏物,当你走在上面时会消耗你的能量。你需要进行战略思考。你应该冲过去以更快到达某个地方吗?还是应该小心绕过它,节省能量以便后续使用,尽管这样可能会让你变慢?
列表 12-13 展示了当你在有毒地面上行走时,如何耗尽你的能量的指令。这些指令需要添加到game_loop()函数中,紧接在你在列表 12-12 中修复的指令之后。
将你的程序保存为listing12-13.py。你可以通过运行 pgzrun listing12-13.py 并在有毒地面上行走来测试它是否有效。有毒地面是对象 48,并作为景物放置在房间中。
listing12-13.py
--snip--
# If the player is standing somewhere they shouldn't, move them back.
if room_map[player_y][player_x] not in items_player_may_stand_on \
or hazard_map[player_y][player_x] != 0:
player_x = old_player_x
player_y = old_player_y
player_frame = 0
if room_map[player_y][player_x] == 48: # toxic floor
deplete_energy(1)
if player_direction == "right" and player_frame > 0:
player_offset_x = -1 + (0.25 * player_frame)
--snip--
列表 12-13:玩家在有毒地面上行走时减少能量
完成最后的修整
游戏现在差不多完成了。在你开始探索空间站之前,我们需要移除一些在构建和测试游戏时使用的指令。
禁用传送器
任务规则禁止在太空站工作开始后使用传送器。找到game_loop()函数中的相关指令,使用鼠标高亮它们,然后点击格式 ▸ 注释掉区域来禁用这些指令。你的代码现在应该像清单 12-14 那样。
listing12-14.py
--snip--
#### Teleporter for testing
#### Remove this section for the real game
## if keyboard.x:
## current_room = int(input("Enter room number:"))
## player_x = 2
## player_y = 2
## generate_map()
## start_room()
## sounds.teleport.play()
#### Teleport section ends
--snip--
清单 12-14:传送器已关闭。
清理数据
在测试游戏时,你可能已经修改了某些变量和列表的内容。游戏开始时应显示像图 12-5 中的样子。如果没有,请查看程序中的VARIABLES部分,确保current_room变量设置为 31。

图 12-5:你的任务开始了
如果你携带的不只是悠悠球,请查看程序中的PROPS部分,并检查这行代码是否正确:
in_my_pockets = [55]
你的冒险开始了
这是一个激动人心的时刻:你的训练已经完成;太空站已经准备就绪;你的火星任务即将开始。让我们设置一个科幻风格的开场音乐,在游戏开始时播放。清单 12-15 展示了你将添加到Escape中的最终指令。
listing12-15.py
--snip--
clock.schedule_unique(alarm, 10)
clock.schedule_interval(air_countdown, 11) # A higher number gives a longer
time limit.
sounds.mission.play() # Intro music
清单 12-15:游戏开始时播放科幻风格的开场音乐。
将最终程序保存为escape.py。现在,你可以使用 pgzrun escape.py 来玩游戏。有关说明,请参见《玩游戏》第 11 页。
恭喜你完成了太空站的建设。你确实已经赢得了在这个任务中的一席之地。现在,是时候开始你在星球表面的工作了!
你的下一次任务:自定义游戏
你成功逃脱《逃脱》游戏了吗?真是惊险万分!在下一次任务中,尝试自定义游戏。使用本书的方式有很多种,所以你可能在构建游戏时已经做了一些自定义。以下是一些修改游戏的建议,从最简单的开始:
-
将游戏中的角色名字改为你朋友的名字。请参阅《清单 4-1》第 63 页和第四章。
-
自定义图像。你可以编辑我们的图像,或者创建自己的图像。游戏中包括一张白板图像,你可以使用自己喜欢的艺术软件进行编辑。如果你制作的图像与我们的尺寸相同、使用相同的文件名,并将其存储在images文件夹中,它们应该可以直接放入游戏世界中而不会有任何问题。
-
重新设计房间布局。第六章解释了如何在房间中布置景物。
-
向游戏中添加你自己的物品。首先创建它们的图像。道具应该是 30 像素见方。景物项可以更大,并且应当接触其瓷砖空间的左右两侧,以便当玩家无法更靠近景物时,不会显得不自然(例如,如果你的图像宽度为 30、60 或 90 像素,并且两侧都接触地面,它应该看起来没问题)。你需要将新物品添加到
objects字典中(请参见第五章)。有关景物定位的帮助,请参见第六章。有关道具定位的建议,请参见第九章。 -
创建你自己的太空站地图(请参见第四章)。
-
使用游戏引擎制作你自己的游戏。你可以替换图像和地图,并编写你自己的谜题,基于 Escape 代码制作新游戏。
USE OBJECTS部分是游戏谜题的编程区域。它详细描述了物品单独使用或与其他物品组合时发生的情况。保存合成物品的代码(配方)并进行更新可能会很有用(请参见第十章);保存标准响应的显示代码(请参见第十章);以及保存开门的代码(请参见第十一章)。
如果你对第 26 房间进行任何更改,你需要禁用其压力垫的代码(请参见第十一章)。
请记住,你所做的任何更改可能会破坏原版 Escape 游戏中的谜题,使其无法完成。例如,可能会变得无法找到重要工具。我建议你将所有更改另存为不同的文件,这样你就可以随时回到原始代码。
分享你的自定义修改
我很想了解你的自定义修改!你可以在 Twitter 上找到我,用户名是 @musicandwords,或者访问我的网站 www.sean.co.uk,该网站包括本书的附加内容。如果你将修改后的 Escape 游戏与他人分享,或者分享你使用其代码、声音或图像构建的游戏,请注明本书及其作者,并明确表示你已修改了代码。谢谢!
你适合飞行吗?
勾选以下选项框以确认你已掌握本章的关键内容。
你可以使用 Pygame Zero 来绘制带有阴影的文本,并可以调整显示的文本大小。
你可以通过在 sounds.sound_name.play() 指令中的括号里写入播放次数来多次播放声音。
移动障碍物的方向从顶部开始编号为 1,按顺时针方向移动。要为障碍物创建一个移动模式,提供你希望在碰到物体时增加的方向编号。
deplete_energy() 函数会减少玩家的能量。
危险物体使用自己的房间地图,称为hazard_map。这使得它们能够更轻松地在地面上的物体之间移动。
在开始游戏之前,请检查起始变量是否正确。

第十三章:A
ESCAPE:完整游戏列表**

本附录展示了Escape游戏的最终列表。你可以将其作为参考,查看各个函数和部分应放置的位置,或者如果你想在一个地方查看完整的列表,也可以通读一遍。这个列表不包括你在构建游戏时写的临时部分,比如EXPLORER部分。它仅包含最终游戏中的代码。
记住,你也可以下载escape.py列表并在 IDLE 中阅读,使用 CTRL-F 进行搜索。
我在这个列表中将PLAYER_NAME更改为“Captain”。在你构建或自定义游戏时,你可以使用你自己的名字(见第 4-1 列表中的➊,位于第 63 页)。
为了测试这个项目,我根据这本书中的说明重建了游戏。这个游戏列表是从已经在 Windows、树莓派 3 Model B+ 和树莓派 2 Model B 上测试完成的游戏代码中复制出来的。
# Escape - A Python Adventure
# by Sean McManus / www.sean.co.uk
# Art by Rafael Pimenta
# Typed in by PUT YOUR NAME HERE
import time, random, math
###############
## VARIABLES ##
###############
WIDTH = 800 #window size
HEIGHT = 800
#PLAYER variables
PLAYER_NAME = "Captain" # change this to your name!
FRIEND1_NAME = "Karen" # change this to a friend's name!
FRIEND2_NAME = "Leo" # change this to another friend's name!
current_room = 31 # start room = 31
top_left_x = 100
top_left_y = 150
DEMO_OBJECTS = [images.floor, images.pillar, images.soil]
LANDER_SECTOR = random.randint(1, 24)
LANDER_X = random.randint(2, 11)
LANDER_Y = random.randint(2, 11)
TILE_SIZE = 30
player_y, player_x = 2, 5
game_over = False
PLAYER = {
"left": [images.spacesuit_left, images.spacesuit_left_1,
images.spacesuit_left_2, images.spacesuit_left_3,
images.spacesuit_left_4
],
"right": [images.spacesuit_right, images.spacesuit_right_1,
images.spacesuit_right_2, images.spacesuit_right_3,
images.spacesuit_right_4
],
"up": [images.spacesuit_back, images.spacesuit_back_1,
images.spacesuit_back_2, images.spacesuit_back_3,
images.spacesuit_back_4
],
"down": [images.spacesuit_front, images.spacesuit_front_1,
images.spacesuit_front_2, images.spacesuit_front_3,
images.spacesuit_front_4
]
}
player_direction = "down"
player_frame = 0
player_image = PLAYER[player_direction][player_frame]
player_offset_x, player_offset_y = 0, 0
PLAYER_SHADOW = {
"left": [images.spacesuit_left_shadow, images.spacesuit_left_1_shadow,
images.spacesuit_left_2_shadow, images.spacesuit_left_3_shadow,
images.spacesuit_left_3_shadow
],
"right": [images.spacesuit_right_shadow, images.spacesuit_right_1_shadow,
images.spacesuit_right_2_shadow,
images.spacesuit_right_3_shadow, images.spacesuit_right_3_shadow
],
"up": [images.spacesuit_back_shadow, images.spacesuit_back_1_shadow,
images.spacesuit_back_2_shadow, images.spacesuit_back_3_shadow,
images.spacesuit_back_3_shadow
],
"down": [images.spacesuit_front_shadow, images.spacesuit_front_1_shadow,
images.spacesuit_front_2_shadow, images.spacesuit_front_3_shadow,
images.spacesuit_front_3_shadow
]
}
player_image_shadow = PLAYER_SHADOW["down"][0]
PILLARS = [
images.pillar, images.pillar_95, images.pillar_80,
images.pillar_60, images.pillar_50
]
wall_transparency_frame = 0
BLACK = (0, 0, 0)
BLUE = (0, 155, 255)
YELLOW = (255, 255, 0)
WHITE = (255, 255, 255)
GREEN = (0, 255, 0)
RED = (128, 0, 0)
air, energy = 100, 100
suit_stitched, air_fixed = False, False
launch_frame = 0
###############
## MAP ##
###############
MAP_WIDTH = 5
MAP_HEIGHT = 10
MAP_SIZE = MAP_WIDTH * MAP_HEIGHT
GAME_MAP = [ ["Room 0 - where unused objects are kept", 0, 0, False, False] ]
outdoor_rooms = range(1, 26)
for planetsectors in range(1, 26): #rooms 1 to 25 are generated here
GAME_MAP.append( ["The dusty planet surface", 13, 13, True, True] )
GAME_MAP += [
#["Room name", height, width, Top exit?, Right exit?]
["The airlock", 13, 5, True, False], # room 26
["The engineering lab", 13, 13, False, False], # room 27
["Poodle Mission Control", 9, 13, False, True], # room 28
["The viewing gallery", 9, 15, False, False], # room 29
["The crew's bathroom", 5, 5, False, False], # room 30
["The airlock entry bay", 7, 11, True, True], # room 31
["Left elbow room", 9, 7, True, False], # room 32
["Right elbow room", 7, 13, True, True], # room 33
["The science lab", 13, 13, False, True], # room 34
["The greenhouse", 13, 13, True, False], # room 35
[PLAYER_NAME + "'s sleeping quarters", 9, 11, False, False], # room 36
["West corridor", 15, 5, True, True], # room 37
["The briefing room", 7, 13, False, True], # room 38
["The crew's community room", 11, 13, True, False], # room 39
["Main Mission Control", 14, 14, False, False], # room 40
["The sick bay", 12, 7, True, False], # room 41
["West corridor", 9, 7, True, False], # room 42
["Utilities control room", 9, 9, False, True], # room 43
["Systems engineering bay", 9, 11, False, False], # room 44
["Security portal to Mission Control", 7, 7, True, False], # room 45
[FRIEND1_NAME + "'s sleeping quarters", 9, 11, True, True], # room 46
[FRIEND2_NAME + "'s sleeping quarters", 9, 11, True, True], # room 47
["The pipeworks", 13, 11, True, False], # room 48
["The chief scientist's office", 9, 7, True, True], # room 49
["The robot workshop", 9, 11, True, False] # room 50
]
#simple sanity check on map above to check data entry
assert len(GAME_MAP)-1 == MAP_SIZE, "Map size and GAME_MAP don't match"
###############
## OBJECTS ##
###############
objects = {
0: [images.floor, None, "The floor is shiny and clean"],
1: [images.pillar, images.full_shadow, "The wall is smooth and cold"],
2: [images.soil, None, "It's like a desert. Or should that be dessert?"],
3: [images.pillar_low, images.half_shadow, "The wall is smooth and cold"],
4: [images.bed, images.half_shadow, "A tidy and comfortable bed"],
5: [images.table, images.half_shadow, "It's made from strong plastic."],
6: [images.chair_left, None, "A chair with a soft cushion"],
7: [images.chair_right, None, "A chair with a soft cushion"],
8: [images.bookcase_tall, images.full_shadow,
"Bookshelves, stacked with reference books"],
9: [images.bookcase_small, images.half_shadow,
"Bookshelves, stacked with reference books"],
10: [images.cabinet, images.half_shadow,
"A small locker, for storing personal items"],
11: [images.desk_computer, images.half_shadow,
"A computer. Use it to run life support diagnostics"],
12: [images.plant, images.plant_shadow, "A spaceberry plant, grown here"],
13: [images.electrical1, images.half_shadow,
"Electrical systems used for powering the space station"],
14: [images.electrical2, images.half_shadow,
"Electrical systems used for powering the space station"],
15: [images.cactus, images.cactus_shadow, "Ouch! Careful on the cactus!"],
16: [images.shrub, images.shrub_shadow,
"A space lettuce. A bit limp, but amazing it's growing here!"],
17: [images.pipes1, images.pipes1_shadow, "Water purification pipes"],
18: [images.pipes2, images.pipes2_shadow,
"Pipes for the life support systems"],
19: [images.pipes3, images.pipes3_shadow,
"Pipes for the life support systems"],
20: [images.door, images.door_shadow, "Safety door. Opens automatically \
for astronauts in functioning spacesuits."],
21: [images.door, images.door_shadow, "The airlock door. \
For safety reasons, it requires two person operation."],
22: [images.door, images.door_shadow, "A locked door. It needs " \
+ PLAYER_NAME + "'s access card"],
23: [images.door, images.door_shadow, "A locked door. It needs " \
+ FRIEND1_NAME + "'s access card"],
24: [images.door, images.door_shadow, "A locked door. It needs " \
+ FRIEND2_NAME + "'s access card"],
25: [images.door, images.door_shadow,
"A locked door. It is opened from Main Mission Control"],
26: [images.door, images.door_shadow,
"A locked door in the engineering bay."],
27: [images.map, images.full_shadow,
"The screen says the crash site was Sector: " \
+ str(LANDER_SECTOR) + " // X: " + str(LANDER_X) + \
" // Y: " + str(LANDER_Y)],
28: [images.rock_large, images.rock_large_shadow,
"A rock. Its coarse surface feels like a whetstone", "the rock"],
29: [images.rock_small, images.rock_small_shadow,
"A small but heavy piece of Martian rock"],
30: [images.crater, None, "A crater in the planet surface"],
31: [images.fence, None,
"A fine gauze fence. It helps protect the station from dust storms"],
32: [images.contraption, images.contraption_shadow,
"One of the scientific experiments. It gently vibrates"],
33: [images.robot_arm, images.robot_arm_shadow,
"A robot arm, used for heavy lifting"],
34: [images.toilet, images.half_shadow, "A sparkling clean toilet"],
35: [images.sink, None, "A sink with running water", "the taps"],
36: [images.globe, images.globe_shadow,
"A giant globe of the planet. It gently glows from inside"],
37: [images.science_lab_table, None,
"A table of experiments, analyzing the planet soil and dust"],
38: [images.vending_machine, images.full_shadow,
"A vending machine. It requires a credit.", "the vending machine"],
39: [images.floor_pad, None,
"A pressure sensor to make sure nobody goes out alone."],
40: [images.rescue_ship, images.rescue_ship_shadow, "A rescue ship!"],
41: [images.mission_control_desk, images.mission_control_desk_shadow, \
"Mission Control stations."],
42: [images.button, images.button_shadow,
"The button for opening the time-locked door in engineering."],
43: [images.whiteboard, images.full_shadow,
"The whiteboard is used in brainstorms and planning meetings."],
44: [images.window, images.full_shadow,
"The window provides a view out onto the planet surface."],
45: [images.robot, images.robot_shadow, "A cleaning robot, turned off."],
46: [images.robot2, images.robot2_shadow,
"A planet surface exploration robot, awaiting set-up."],
47: [images.rocket, images.rocket_shadow, "A 1-person craft in repair."],
48: [images.toxic_floor, None, "Toxic floor - do not walk on!"],
49: [images.drone, None, "A delivery drone"],
50: [images.energy_ball, None, "An energy ball - dangerous!"],
51: [images.energy_ball2, None, "An energy ball - dangerous!"],
52: [images.computer, images.computer_shadow,
"A computer workstation, for managing space station systems."],
53: [images.clipboard, None,
"A clipboard. Someone has doodled on it.", "the clipboard"],
54: [images.bubble_gum, None,
"A piece of sticky bubble gum. Spaceberry flavour.", "bubble gum"],
55: [images.yoyo, None, "A toy made of fine, strong string and plastic. \
Used for antigrav experiments.", PLAYER_NAME + "'s yoyo"],
56: [images.thread, None,
"A piece of fine, strong string", "a piece of string"],
57: [images.needle, None,
"A sharp needle from a cactus plant", "a cactus needle"],
58: [images.threaded_needle, None,
"A cactus needle, spearing a length of string", "needle and string"],
59: [images.canister, None,
"The air canister has a leak.", "a leaky air canister"],
60: [images.canister, None,
"It looks like the seal will hold!", "a sealed air canister"],
61: [images.mirror, None,
"The mirror throws a circle of light on the walls.", "a mirror"],
62: [images.bin_empty, None,
"A rarely used bin, made of light plastic", "a bin"],
63: [images.bin_full, None,
"A heavy bin full of water", "a bin full of water"],
64: [images.rags, None,
"An oily rag. Pick it up by a corner if you must!", "an oily rag"],
65: [images.hammer, None,
"A hammer. Maybe good for cracking things open...", "a hammer"],
66: [images.spoon, None, "A large serving spoon", "a spoon"],
67: [images.food_pouch, None,
"A dehydrated food pouch. It needs water.", "a dry food pack"],
68: [images.food, None,
"A food pouch. Use it to get 100% energy.", "ready-to-eat food"],
69: [images.book, None, "The book has the words 'Don't Panic' on the \
cover in large, friendly letters", "a book"],
70: [images.mp3_player, None,
"An MP3 player, with all the latest tunes", "an MP3 player"],
71: [images.lander, None, "The Poodle, a small space exploration craft. \
Its black box has a radio sealed inside.", "the Poodle lander"],
72: [images.radio, None, "A radio communications system, from the \
Poodle", "a communications radio"],
73: [images.gps_module, None, "A GPS Module", "a GPS module"],
74: [images.positioning_system, None, "Part of a positioning system. \
Needs a GPS module.", "a positioning interface"],
75: [images.positioning_system, None,
"A working positioning system", "a positioning computer"],
76: [images.scissors, None, "Scissors. They're too blunt to cut \
anything. Can you sharpen them?", "blunt scissors"],
77: [images.scissors, None,
"Razor-sharp scissors. Careful!", "sharpened scissors"],
78: [images.credit, None,
"A small coin for the station's vending systems",
"a station credit"],
79: [images.access_card, None,
"This access card belongs to " + PLAYER_NAME, "an access card"],
80: [images.access_card, None,
"This access card belongs to " + FRIEND1_NAME, "an access card"],
81: [images.access_card, None,
"This access card belongs to " + FRIEND2_NAME, "an access card"]
}
items_player_may_carry = list(range(53, 82))
# Numbers below are for floor, pressure pad, soil, toxic floor.
items_player_may_stand_on = items_player_may_carry + [0, 39, 2, 48]
###############
## SCENERY ##
###############
# Scenery describes objects that cannot move between rooms.
# room number: [[object number, y position, x position]...]
scenery = {
26: [[39,8,2]],
27: [[33,5,5], [33,1,1], [33,1,8], [47,5,2],
[47,3,10], [47,9,8], [42,1,6]],
28: [[27,0,3], [41,4,3], [41,4,7]],
29: [[7,2,6], [6,2,8], [12,1,13], [44,0,1],
[36,4,10], [10,1,1], [19,4,2], [17,4,4]],
30: [[34,1,1], [35,1,3]],
31: [[11,1,1], [19,1,8], [46,1,3]],
32: [[48,2,2], [48,2,3], [48,2,4], [48,3,2], [48,3,3],
[48,3,4], [48,4,2], [48,4,3], [48,4,4]],
33: [[13,1,1], [13,1,3], [13,1,8], [13,1,10], [48,2,1],
[48,2,7], [48,3,6], [48,3,3]],
34: [[37,2,2], [32,6,7], [37,10,4], [28,5,3]],
35: [[16,2,9], [16,2,2], [16,3,3], [16,3,8], [16,8,9], [16,8,2], [16,1,8],
[16,1,3], [12,8,6], [12,9,4], [12,9,8],
[15,4,6], [12,7,1], [12,7,11]],
36: [[4,3,1], [9,1,7], [8,1,8], [8,1,9],
[5,5,4], [6,5,7], [10,1,1], [12,1,2]],
37: [[48,3,1], [48,3,2], [48,7,1], [48,5,2], [48,5,3],
[48,7,2], [48,9,2], [48,9,3], [48,11,1], [48,11,2]],
38: [[43,0,2], [6,2,2], [6,3,5], [6,4,7], [6,2,9], [45,1,10]],
39: [[38,1,1], [7,3,4], [7,6,4], [5,3,6], [5,6,6],
[6,3,9], [6,6,9], [45,1,11], [12,1,8], [12,1,4]],
40: [[41,5,3], [41,5,7], [41,9,3], [41,9,7],
[13,1,1], [13,1,3], [42,1,12]],
41: [[4,3,1], [10,3,5], [4,5,1], [10,5,5], [4,7,1],
[10,7,5], [12,1,1], [12,1,5]],
44: [[46,4,3], [46,4,5], [18,1,1], [19,1,3],
[19,1,5], [52,4,7], [14,1,8]],
45: [[48,2,1], [48,2,2], [48,3,3], [48,3,4], [48,1,4], [48,1,1]],
46: [[10,1,1], [4,1,2], [8,1,7], [9,1,8], [8,1,9], [5,4,3], [7,3,2]],
47: [[9,1,1], [9,1,2], [10,1,3], [12,1,7], [5,4,4], [6,4,7], [4,1,8]],
48: [[17,4,1], [17,4,2], [17,4,3], [17,4,4], [17,4,5], [17,4,6], [17,4,7],
[17,8,1], [17,8,2], [17,8,3], [17,8,4],
[17,8,5], [17,8,6], [17,8,7], [14,1,1]],
49: [[14,2,2], [14,2,4], [7,5,1], [5,5,3], [48,3,3], [48,3,4]],
50: [[45,4,8], [11,1,1], [13,1,8], [33,2,1], [46,4,6]]
}
checksum = 0
check_counter = 0
for key, room_scenery_list in scenery.items():
for scenery_item_list in room_scenery_list:
checksum += (scenery_item_list[0] * key
+ scenery_item_list[1] * (key + 1)
+ scenery_item_list[2] * (key + 2))
check_counter += 1
print(check_counter, "scenery items")
assert check_counter == 161, "Expected 161 scenery items"
assert checksum == 200095, "Error in scenery data"
print("Scenery checksum: " + str(checksum))
for room in range(1, 26): # Add random scenery in planet locations.
if room != 13: # Skip room 13.
scenery_item = random.choice([16, 28, 29, 30])
scenery[room] = [[scenery_item, random.randint(2, 10),
random.randint(2, 10)]]
# Use loops to add fences to the planet surface rooms.
for room_coordinate in range(0, 13):
for room_number in [1, 2, 3, 4, 5]: # Add top fence
scenery[room_number] += [[31, 0, room_coordinate]]
for room_number in [1, 6, 11, 16, 21]: # Add left fence
scenery[room_number] += [[31, room_coordinate, 0]]
for room_number in [5, 10, 15, 20, 25]: # Add right fence
scenery[room_number] += [[31, room_coordinate, 12]]
del scenery[21][-1] # Delete last fence panel in Room 21
del scenery[25][-1] # Delete last fence panel in Room 25
###############
## MAKE MAP ##
###############
def get_floor_type():
if current_room in outdoor_rooms:
return 2 # soil
else:
return 0 # tiled floor
def generate_map():
# This function makes the map for the current room,
# using room data, scenery data and prop data.
global room_map, room_width, room_height, room_name, hazard_map
global top_left_x, top_left_y, wall_transparency_frame
room_data = GAME_MAP[current_room]
room_name = room_data[0]
room_height = room_data[1]
room_width = room_data[2]
floor_type = get_floor_type()
if current_room in range(1, 21):
bottom_edge = 2 #soil
side_edge = 2 #soil
if current_room in range(21, 26):
bottom_edge = 1 #wall
side_edge = 2 #soil
if current_room > 25:
bottom_edge = 1 #wall
side_edge = 1 #wall
# Create top line of room map.
room_map=[[side_edge] * room_width]
# Add middle lines of room map (wall, floor to fill width, wall).
for y in range(room_height - 2):
room_map.append([side_edge]
+ [floor_type]*(room_width - 2) + [side_edge])
# Add bottom line of room map.
room_map.append([bottom_edge] * room_width)
# Add doorways.
middle_row = int(room_height / 2)
middle_column = int(room_width / 2)
if room_data[4]: # If exit at right of this room
room_map[middle_row][room_width - 1] = floor_type
room_map[middle_row+1][room_width - 1] = floor_type
room_map[middle_row-1][room_width - 1] = floor_type
if current_room % MAP_WIDTH != 1: # If room is not on left of map
room_to_left = GAME_MAP[current_room - 1]
# If room on the left has a right exit, add left exit in this room
if room_to_left[4]:
room_map[middle_row][0] = floor_type
room_map[middle_row + 1][0] = floor_type
room_map[middle_row - 1][0] = floor_type
if room_data[3]: # If exit at top of this room
room_map[0][middle_column] = floor_type
room_map[0][middle_column + 1] = floor_type
room_map[0][middle_column - 1] = floor_type
if current_room <= MAP_SIZE - MAP_WIDTH: # If room is not on bottom row
room_below = GAME_MAP[current_room+MAP_WIDTH]
# If room below has a top exit, add exit at bottom of this one
if room_below[3]:
room_map[room_height-1][middle_column] = floor_type
room_map[room_height-1][middle_column + 1] = floor_type
room_map[room_height-1][middle_column - 1] = floor_type
if current_room in scenery:
for this_scenery in scenery[current_room]:
scenery_number = this_scenery[0]
scenery_y = this_scenery[1]
scenery_x = this_scenery[2]
room_map[scenery_y][scenery_x] = scenery_number
image_here = objects[scenery_number][0]
image_width = image_here.get_width()
image_width_in_tiles = int(image_width / TILE_SIZE)
for tile_number in range(1, image_width_in_tiles):
room_map[scenery_y][scenery_x + tile_number] = 255
center_y = int(HEIGHT / 2) # Center of game window
center_x = int(WIDTH / 2)
room_pixel_width = room_width * TILE_SIZE # Size of room in pixels
room_pixel_height = room_height * TILE_SIZE
top_left_x = center_x - 0.5 * room_pixel_width
top_left_y = (center_y - 0.5 * room_pixel_height) + 110
for prop_number, prop_info in props.items():
prop_room = prop_info[0]
prop_y = prop_info[1]
prop_x = prop_info[2]
if (prop_room == current_room and
room_map[prop_y][prop_x] in [0, 39, 2]):
room_map[prop_y][prop_x] = prop_number
image_here = objects[prop_number][0]
image_width = image_here.get_width()
image_width_in_tiles = int(image_width / TILE_SIZE)
for tile_number in range(1, image_width_in_tiles):
room_map[prop_y][prop_x + tile_number] = 255
hazard_map = [] # empty list
for y in range(room_height):
hazard_map.append( [0] * room_width )
###############
## GAME LOOP ##
###############
def start_room():
global airlock_door_frame
show_text("You are here: " + room_name, 0)
if current_room == 26: # Room with self-shutting airlock door
airlock_door_frame = 0
clock.schedule_interval(door_in_room_26, 0.05)
hazard_start()
def game_loop():
global player_x, player_y, current_room
global from_player_x, from_player_y
global player_image, player_image_shadow
global selected_item, item_carrying, energy
global player_offset_x, player_offset_y
global player_frame, player_direction
if game_over:
return
if player_frame > 0:
player_frame += 1
time.sleep(0.05)
if player_frame == 5:
player_frame = 0
player_offset_x = 0
player_offset_y = 0
# save player's current position
old_player_x = player_x
old_player_y = player_y
# move if key is pressed
if player_frame == 0:
if keyboard.right:
from_player_x = player_x
from_player_y = player_y
player_x += 1
player_direction = "right"
player_frame = 1
elif keyboard.left: #elif stops player making diagonal movements
from_player_x = player_x
from_player_y = player_y
player_x -= 1
player_direction = "left"
player_frame = 1
elif keyboard.up:
from_player_x = player_x
from_player_y = player_y
player_y -= 1
player_direction = "up"
player_frame = 1
elif keyboard.down:
from_player_x = player_x
from_player_y = player_y
player_y += 1
player_direction = "down"
player_frame = 1
# check for exiting the room
if player_x == room_width: # through door on RIGHT
clock.unschedule(hazard_move)
current_room += 1
generate_map()
player_x = 0 # enter at left
player_y = int(room_height / 2) # enter at door
player_frame = 0
start_room()
return
if player_x == -1: # through door on LEFT
clock.unschedule(hazard_move)
current_room -= 1
generate_map()
player_x = room_width - 1 # enter at right
player_y = int(room_height / 2) # enter at door
player_frame = 0
start_room()
return
if player_y == room_height: # through door at BOTTOM
clock.unschedule(hazard_move)
current_room += MAP_WIDTH
generate_map()
player_y = 0 # enter at top
player_x = int(room_width / 2) # enter at door
player_frame = 0
start_room()
return
if player_y == -1: # through door at TOP
clock.unschedule(hazard_move)
current_room -= MAP_WIDTH
generate_map()
player_y = room_height - 1 # enter at bottom
player_x = int(room_width / 2) # enter at door
player_frame = 0
start_room()
return
if keyboard.g:
pick_up_object()
if keyboard.tab and len(in_my_pockets) > 0:
selected_item += 1
if selected_item > len(in_my_pockets) - 1:
selected_item = 0
item_carrying = in_my_pockets[selected_item]
display_inventory()
if keyboard.d and item_carrying:
drop_object(old_player_y, old_player_x)
if keyboard.space:
examine_object()
if keyboard.u:
use_object()
#### Teleporter for testing
#### Remove this section for the real game
## if keyboard.x:
## current_room = int(input("Enter room number:"))
## player_x = 2
## player_y = 2
## generate_map()
## start_room()
## sounds.teleport.play()
#### Teleport section ends
# If the player is standing somewhere they shouldn't, move them back.
if room_map[player_y][player_x] not in items_player_may_stand_on \
or hazard_map[player_y][player_x] != 0:
player_x = old_player_x
player_y = old_player_y
player_frame = 0
if room_map[player_y][player_x] == 48: # toxic floor
deplete_energy(1)
if player_direction == "right" and player_frame > 0:
player_offset_x = -1 + (0.25 * player_frame)
if player_direction == "left" and player_frame > 0:
player_offset_x = 1 - (0.25 * player_frame)
if player_direction == "up" and player_frame > 0:
player_offset_y = 1 - (0.25 * player_frame)
if player_direction == "down" and player_frame > 0:
player_offset_y = -1 + (0.25 * player_frame)
###############
## DISPLAY ##
###############
def draw_image(image, y, x):
screen.blit(
image,
(top_left_x + (x * TILE_SIZE),
top_left_y + (y * TILE_SIZE) - image.get_height())
)
def draw_shadow(image, y, x):
screen.blit(
image,
(top_left_x + (x * TILE_SIZE),
top_left_y + (y * TILE_SIZE))
)
def draw_player():
player_image = PLAYER[player_direction][player_frame]
draw_image(player_image, player_y + player_offset_y,
player_x + player_offset_x)
player_image_shadow = PLAYER_SHADOW[player_direction][player_frame]
draw_shadow(player_image_shadow, player_y + player_offset_y,
player_x + player_offset_x)
def draw():
if game_over:
return
# Clear the game arena area.
box = Rect((0, 150), (800, 600))
screen.draw.filled_rect(box, RED)
box = Rect ((0, 0), (800, top_left_y + (room_height - 1)*30))
screen.surface.set_clip(box)
floor_type = get_floor_type()
for y in range(room_height): # Lay down floor tiles, then items on floor.
for x in range(room_width):
draw_image(objects[floor_type][0], y, x)
# Next line enables shadows to fall on top of objects on floor
if room_map[y][x] in items_player_may_stand_on:
draw_image(objects[room_map[y][x]][0], y, x)
# Pressure pad in room 26 is added here, so props can go on top of it.
if current_room == 26:
draw_image(objects[39][0], 8, 2)
image_on_pad = room_map[8][2]
if image_on_pad > 0:
draw_image(objects[image_on_pad][0], 8, 2)
for y in range(room_height):
for x in range(room_width):
item_here = room_map[y][x]
# Player cannot walk on 255: it marks spaces used by wide objects.
if item_here not in items_player_may_stand_on + [255]:
image = objects[item_here][0]
if (current_room in outdoor_rooms
and y == room_height - 1
and room_map[y][x] == 1) or \
(current_room not in outdoor_rooms
and y == room_height - 1
and room_map[y][x] == 1
and x > 0
and x < room_width - 1):
# Add transparent wall image in the front row.
image = PILLARS[wall_transparency_frame]
draw_image(image, y, x)
if objects[item_here][1] is not None: # If object has a shadow
shadow_image = objects[item_here][1]
# if shadow might need horizontal tiling
if shadow_image in [images.half_shadow,
images.full_shadow]:
shadow_width = int(image.get_width() / TILE_SIZE)
# Use shadow across width of object.
for z in range(0, shadow_width):
draw_shadow(shadow_image, y, x+z)
else:
draw_shadow(shadow_image, y, x)
hazard_here = hazard_map[y][x]
if hazard_here != 0: # If there's a hazard at this position
draw_image(objects[hazard_here][0], y, x)
if (player_y == y):
draw_player()
screen.surface.set_clip(None)
def adjust_wall_transparency():
global wall_transparency_frame
if (player_y == room_height - 2
and room_map[room_height - 1][player_x] == 1
and wall_transparency_frame < 4):
wall_transparency_frame += 1 # Fade wall out.
if ((player_y < room_height - 2
or room_map[room_height - 1][player_x] != 1)
and wall_transparency_frame > 0):
wall_transparency_frame -= 1 # Fade wall in.
def show_text(text_to_show, line_number):
if game_over:
return
text_lines = [15, 50]
box = Rect((0, text_lines[line_number]), (800, 35))
screen.draw.filled_rect(box, BLACK)
screen.draw.text(text_to_show,
(20, text_lines[line_number]), color=GREEN)
###############
## PROPS ##
###############
# Props are objects that may move between rooms, appear or disappear.
# All props must be set up here. Props not yet in the game go into room 0.
# object number : [room, y, x]
props = {
20: [31, 0, 4], 21: [26, 0, 1], 22: [41, 0, 2], 23: [39, 0, 5],
24: [45, 0, 2],
25: [32, 0, 2], 26: [27, 12, 5], # two sides of same door
40: [0, 8, 6], 53: [45, 1, 5], 54: [0, 0, 0], 55: [0, 0, 0],
56: [0, 0, 0], 57: [35, 4, 6], 58: [0, 0, 0], 59: [31, 1, 7],
60: [0, 0, 0], 61: [36, 1, 1], 62: [36, 1, 6], 63: [0, 0, 0],
64: [27, 8, 3], 65: [50, 1, 7], 66: [39, 5, 6], 67: [46, 1, 1],
68: [0, 0, 0], 69: [30, 3, 3], 70: [47, 1, 3],
71: [0, LANDER_Y, LANDER_X], 72: [0, 0, 0], 73: [27, 4, 6],
74: [28, 1, 11], 75: [0, 0, 0], 76: [41, 3, 5], 77: [0, 0, 0],
78: [35, 9, 11], 79: [26, 3, 2], 80: [41, 7, 5], 81: [29, 1, 1]
}
checksum = 0
for key, prop in props.items():
if key != 71: # 71 is skipped because it's different each game.
checksum += (prop[0] * key
+ prop[1] * (key + 1)
+ prop[2] * (key + 2))
print(len(props), "props")
assert len(props) == 37, "Expected 37 prop items"
print("Prop checksum:", checksum)
assert checksum == 61414, "Error in props data"
in_my_pockets = [55]
selected_item = 0 # the first item
item_carrying = in_my_pockets[selected_item]
RECIPES = [
[62, 35, 63], [76, 28, 77], [78, 38, 54], [73, 74, 75],
[59, 54, 60], [77, 55, 56], [56, 57, 58], [71, 65, 72],
[88, 58, 89], [89, 60, 90], [67, 35, 68]
]
checksum = 0
check_counter = 1
for recipe in RECIPES:
checksum += (recipe[0] * check_counter
+ recipe[1] * (check_counter + 1)
+ recipe[2] * (check_counter + 2))
check_counter += 3
print(len(RECIPES), "recipes")
assert len(RECIPES) == 11, "Expected 11 recipes"
assert checksum == 37296, "Error in recipes data"
print("Recipe checksum:", checksum)
#######################
## PROP INTERACTIONS ##
#######################
def find_object_start_x():
checker_x = player_x
while room_map[player_y][checker_x] == 255:
checker_x -= 1
return checker_x
def get_item_under_player():
item_x = find_object_start_x()
item_player_is_on = room_map[player_y][item_x]
return item_player_is_on
def pick_up_object():
global room_map
# Get object number at player's location.
item_player_is_on = get_item_under_player()
if item_player_is_on in items_player_may_carry:
# Clear the floor space.
room_map[player_y][player_x] = get_floor_type()
add_object(item_player_is_on)
show_text("Now carrying " + objects[item_player_is_on][3], 0)
sounds.pickup.play()
time.sleep(0.5)
else:
show_text("You can't carry that!", 0)
def add_object(item): # Adds item to inventory.
global selected_item, item_carrying
in_my_pockets.append(item)
item_carrying = item
# Minus one because indexes start at 0.
selected_item = len(in_my_pockets) - 1
display_inventory()
props[item][0] = 0 # Carried objects go into room 0 (off the map).
def display_inventory():
box = Rect((0, 45), (800, 105))
screen.draw.filled_rect(box, BLACK)
if len(in_my_pockets) == 0:
return
start_display = (selected_item // 16) * 16
list_to_show = in_my_pockets[start_display : start_display + 16]
selected_marker = selected_item % 16
for item_counter in range(len(list_to_show)):
item_number = list_to_show[item_counter]
image = objects[item_number][0]
screen.blit(image, (25 + (46 * item_counter), 90))
box_left = (selected_marker * 46) - 3
box = Rect((22 + box_left, 85), (40, 40))
screen.draw.rect(box, WHITE)
item_highlighted = in_my_pockets[selected_item]
description = objects[item_highlighted][2]
screen.draw.text(description, (20, 130), color="white")
def drop_object(old_y, old_x):
global room_map, props
if room_map[old_y][old_x] in [0, 2, 39]: # places you can drop things
props[item_carrying][0] = current_room
props[item_carrying][1] = old_y
props[item_carrying][2] = old_x
room_map[old_y][old_x] = item_carrying
show_text("You have dropped " + objects[item_carrying][3], 0)
sounds.drop.play()
remove_object(item_carrying)
time.sleep(0.5)
else: # This only happens if there is already a prop here
show_text("You can't drop that there.", 0)
time.sleep(0.5)
def remove_object(item): # Takes item out of inventory
global selected_item, in_my_pockets, item_carrying
in_my_pockets.remove(item)
selected_item = selected_item - 1
if selected_item < 0:
selected_item = 0
if len(in_my_pockets) == 0: # If they're not carrying anything
item_carrying = False # Set item_carrying to False
else: # Otherwise set it to the new selected item
item_carrying = in_my_pockets[selected_item]
display_inventory()
def examine_object():
item_player_is_on = get_item_under_player()
left_tile_of_item = find_object_start_x()
if item_player_is_on in [0, 2]: # don't describe the floor
return
description = "You see: " + objects[item_player_is_on][2]
for prop_number, details in props.items():
# props = object number: [room number, y, x]
if details[0] == current_room: # if prop is in the room
# If prop is hidden (= at player's location but not on map)
if (details[1] == player_y
and details[2] == left_tile_of_item
and room_map[details[1]][details[2]] != prop_number):
add_object(prop_number)
description = "You found " + objects[prop_number][3]
sounds.combine.play()
show_text(description, 0)
time.sleep(0.5)
#################
## USE OBJECTS ##
#################
def use_object():
global room_map, props, item_carrying, air, selected_item, energy
global in_my_pockets, suit_stitched, air_fixed, game_over
use_message = "You fiddle around with it but don't get anywhere."
standard_responses = {
4: "Air is running out! You can't take this lying down!",
6: "This is no time to sit around!",
7: "This is no time to sit around!",
32: "It shakes and rumbles, but nothing else happens.",
34: "Ah! That's better. Now wash your hands.",
35: "You wash your hands and shake the water off.",
37: "The test tubes smoke slightly as you shake them.",
54: "You chew the gum. It's sticky like glue.",
55: "The yoyo bounces up and down, slightly slower than on Earth",
56: "It's a bit too fiddly. Can you thread it on something?",
59: "You need to fix the leak before you can use the canister",
61: "You try signalling with the mirror, but nobody can see you.",
62: "Don't throw resources away. Things might come in handy...",
67: "To enjoy yummy space food, just add water!",
75: "You are at Sector: " + str(current_room) + " // X: " \
+ str(player_x) + " // Y: " + str(player_y)
}
# Get object number at player's location.
item_player_is_on = get_item_under_player()
for this_item in [item_player_is_on, item_carrying]:
if this_item in standard_responses:
use_message = standard_responses[this_item]
if item_carrying == 70 or item_player_is_on == 70:
use_message = "Banging tunes!"
sounds.steelmusic.play(2)
elif item_player_is_on == 11:
use_message = "AIR: " + str(air) + \
"% / ENERGY " + str(energy) + "% / "
if not suit_stitched:
use_message += "*ALERT* SUIT FABRIC TORN / "
if not air_fixed:
use_message += "*ALERT* SUIT AIR BOTTLE MISSING"
if suit_stitched and air_fixed:
use_message += " SUIT OK"
show_text(use_message, 0)
sounds.say_status_report.play()
time.sleep(0.5)
# If "on" the computer, player intention is clearly status update.
# Return to stop another object use accidentally overriding this.
return
elif item_carrying == 60 or item_player_is_on == 60:
use_message = "You fix " + objects[60][3] + " to the suit"
air_fixed = True
air = 90
air_countdown()
remove_object(60)
elif (item_carrying == 58 or item_player_is_on == 58) \
and not suit_stitched:
use_message = "You use " + objects[56][3] + \
" to repair the suit fabric"
suit_stitched = True
remove_object(58)
elif item_carrying == 72 or item_player_is_on == 72:
use_message = "You radio for help. A rescue ship is coming. \
Rendezvous Sector 13, outside."
props[40][0] = 13
elif (item_carrying == 66 or item_player_is_on == 66) \
and current_room in outdoor_rooms:
use_message = "You dig..."
if (current_room == LANDER_SECTOR
and player_x == LANDER_X
and player_y == LANDER_Y):
add_object(71)
use_message = "You found the Poodle lander!"
elif item_player_is_on == 40:
clock.unschedule(air_countdown)
show_text("Congratulations, "+ PLAYER_NAME +"!", 0)
show_text("Mission success! You have made it to safety.", 1)
game_over = True
sounds.take_off.play()
game_completion_sequence()
elif item_player_is_on == 16:
energy += 1
if energy > 100:
energy = 100
use_message = "You munch the lettuce and get a little energy back"
draw_energy_air()
elif item_player_is_on == 42:
if current_room == 27:
open_door(26)
props[25][0] = 0 # Door from RM32 to engineering bay
props[26][0] = 0 # Door inside engineering bay
clock.schedule_unique(shut_engineering_door, 60)
use_message = "You press the button"
show_text("Door to engineering bay is open for 60 seconds", 1)
sounds.say_doors_open.play()
sounds.doors.play()
elif item_carrying == 68 or item_player_is_on == 68:
energy = 100
use_message = "You use the food to restore your energy"
remove_object(68)
draw_energy_air()
if suit_stitched and air_fixed: # open airlock access
if current_room == 31 and props[20][0] == 31:
open_door(20) # which includes removing the door
sounds.say_airlock_open.play()
show_text("The computer tells you the airlock is now open.", 1)
elif props[20][0] == 31:
props[20][0] = 0 # remove door from map
sounds.say_airlock_open.play()
show_text("The computer tells you the airlock is now open.", 1)
for recipe in RECIPES:
ingredient1 = recipe[0]
ingredient2 = recipe[1]
combination = recipe[2]
if (item_carrying == ingredient1
and item_player_is_on == ingredient2) \
or (item_carrying == ingredient2
and item_player_is_on == ingredient1):
use_message = "You combine " + objects[ingredient1][3] \
+ " and " + objects[ingredient2][3] \
+ " to make " + objects[combination][3]
if item_player_is_on in props.keys():
props[item_player_is_on][0] = 0
room_map[player_y][player_x] = get_floor_type()
in_my_pockets.remove(item_carrying)
add_object(combination)
sounds.combine.play()
# {key object number: door object number}
ACCESS_DICTIONARY = { 79:22, 80:23, 81:24 }
if item_carrying in ACCESS_DICTIONARY:
door_number = ACCESS_DICTIONARY[item_carrying]
if props[door_number][0] == current_room:
use_message = "You unlock the door!"
sounds.say_doors_open.play()
sounds.doors.play()
open_door(door_number)
show_text(use_message, 0)
time.sleep(0.5)
def game_completion_sequence():
global launch_frame #(initial value is 0, set up in VARIABLES section)
box = Rect((0, 150), (800, 600))
screen.draw.filled_rect(box, (128, 0, 0))
box = Rect ((0, top_left_y - 30), (800, 390))
screen.surface.set_clip(box)
for y in range(0, 13):
for x in range(0, 13):
draw_image(images.soil, y, x)
launch_frame += 1
if launch_frame < 9:
draw_image(images.rescue_ship, 8 - launch_frame, 6)
draw_shadow(images.rescue_ship_shadow, 8 + launch_frame, 6)
clock.schedule(game_completion_sequence, 0.25)
else:
screen.surface.set_clip(None)
screen.draw.text("MISSION", (200, 380), color = "white",
fontsize = 128, shadow = (1, 1), scolor = "black")
screen.draw.text("COMPLETE", (145, 480), color = "white",
fontsize = 128, shadow = (1, 1), scolor = "black")
sounds.completion.play()
sounds.say_mission_complete.play()
###############
## DOORS ##
###############
def open_door(opening_door_number):
global door_frames, door_shadow_frames
global door_frame_number, door_object_number
door_frames = [images.door1, images.door2, images.door3,
images.door4, images.floor]
# (Final frame restores shadow ready for when door reappears).
door_shadow_frames = [images.door1_shadow, images.door2_shadow,
images.door3_shadow, images.door4_shadow,
images.door_shadow]
door_frame_number = 0
door_object_number = opening_door_number
do_door_animation()
def close_door(closing_door_number):
global door_frames, door_shadow_frames
global door_frame_number, door_object_number, player_y
door_frames = [images.door4, images.door3, images.door2,
images.door1, images.door]
door_shadow_frames = [images.door4_shadow, images.door3_shadow,
images.door2_shadow, images.door1_shadow,
images.door_shadow]
door_frame_number = 0
door_object_number = closing_door_number
# If player is in same row as a door, they must be in open doorway
if player_y == props[door_object_number][1]:
if player_y == 0: # if in the top doorway
player_y = 1 # move them down
else:
player_y = room_height - 2 # move them up
do_door_animation()
def do_door_animation():
global door_frames, door_frame_number, door_object_number, objects
objects[door_object_number][0] = door_frames[door_frame_number]
objects[door_object_number][1] = door_shadow_frames[door_frame_number]
door_frame_number += 1
if door_frame_number == 5:
if door_frames[-1] == images.floor:
props[door_object_number][0] = 0 # remove door from props list
# Regenerate room map from the props
# to put the door in the room if required.
generate_map()
else:
clock.schedule(do_door_animation, 0.15)
def shut_engineering_door():
global current_room, door_room_number, props
props[25][0] = 32 # Door from room 32 to the engineering bay.
props[26][0] = 27 # Door inside engineering bay.
generate_map() # Add door to room_map for if in affected room.
if current_room == 27:
close_door(26)
if current_room == 32:
close_door(25)
show_text("The computer tells you the doors are closed.", 1)
sounds.say_doors_closed.play()
def door_in_room_26():
global airlock_door_frame, room_map
frames = [images.door, images.door1, images.door2,
images.door3,images.door4, images.floor
]
shadow_frames = [images.door_shadow, images.door1_shadow,
images.door2_shadow, images.door3_shadow,
images.door4_shadow, None]
if current_room != 26:
clock.unschedule(door_in_room_26)
return
# prop 21 is the door in Room 26.
if ((player_y == 8 and player_x == 2) or props[63] == [26, 8, 2]) \
and props[21][0] == 26:
airlock_door_frame += 1
if airlock_door_frame == 5:
props[21][0] = 0 # Remove door from map when fully open.
room_map[0][1] = 0
room_map[0][2] = 0
room_map[0][3] = 0
if ((player_y != 8 or player_x != 2) and props[63] != [26, 8, 2]) \
and airlock_door_frame > 0:
if airlock_door_frame == 5:
# Add door to props and map so animation is shown.
props[21][0] = 26
room_map[0][1] = 21
room_map[0][2] = 255
room_map[0][3] = 255
airlock_door_frame -= 1
objects[21][0] = frames[airlock_door_frame]
objects[21][1] = shadow_frames[airlock_door_frame]
###############
## AIR ##
###############
def draw_energy_air():
box = Rect((20, 765), (350, 20))
screen.draw.filled_rect(box, BLACK)
screen.draw.text("AIR", (20, 766), color=BLUE)
screen.draw.text("ENERGY", (180, 766), color=YELLOW)
if air > 0:
box = Rect((50, 765), (air, 20))
screen.draw.filled_rect(box, BLUE) # Draw new air bar.
if energy > 0:
box = Rect((250, 765), (energy, 20))
screen.draw.filled_rect(box, YELLOW) # Draw new energy bar.
def end_the_game(reason):
global game_over
show_text(reason, 1)
game_over = True
sounds.say_mission_fail.play()
sounds.gameover.play()
screen.draw.text("GAME OVER", (120, 400), color = "white",
fontsize = 128, shadow = (1, 1), scolor = "black")
def air_countdown():
global air, game_over
if game_over:
return # Don't sap air when they're already dead.
air -= 1
if air == 20:
sounds.say_air_low.play()
if air == 10:
sounds.say_act_now.play()
draw_energy_air()
if air < 1:
end_the_game("You're out of air!")
def alarm():
show_text("Air is running out, " + PLAYER_NAME
+ "! Get to safety, then radio for help!", 1)
sounds.alarm.play(3)
sounds.say_breach.play()
###############
## HAZARDS ##
###############
hazard_data = {
# room number: [[y, x, direction, bounce addition to direction]]
28: [[1, 8, 2, 1], [7, 3, 4, 1]], 32: [[1, 5, 4, -1]],
34: [[5, 1, 1, 1], [5, 5, 1, 2]], 35: [[4, 4, 1, 2], [2, 5, 2, 2]],
36: [[2, 1, 2, 2]], 38: [[1, 4, 3, 2], [5, 8, 1, 2]],
40: [[3, 1, 3, -1], [6, 5, 2, 2], [7, 5, 4, 2]],
41: [[4, 5, 2, 2], [6, 3, 4, 2], [8, 1, 2, 2]],
42: [[2, 1, 2, 2], [4, 3, 2, 2], [6, 5, 2, 2]],
46: [[2, 1, 2, 2]],
48: [[1, 8, 3, 2], [8, 8, 1, 2], [3, 9, 3, 2]]
}
def deplete_energy(penalty):
global energy, game_over
if game_over:
return # Don't sap energy when they're already dead.
energy = energy - penalty
draw_energy_air()
if energy < 1:
end_the_game("You're out of energy!")
def hazard_start():
global current_room_hazards_list, hazard_map
if current_room in hazard_data.keys():
current_room_hazards_list = hazard_data[current_room]
for hazard in current_room_hazards_list:
hazard_y = hazard[0]
hazard_x = hazard[1]
hazard_map[hazard_y][hazard_x] = 49 + (current_room % 3)
clock.schedule_interval(hazard_move, 0.15)
def hazard_move():
global current_room_hazards_list, hazard_data, hazard_map
global old_player_x, old_player_y
if game_over:
return
for hazard in current_room_hazards_list:
hazard_y = hazard[0]
hazard_x = hazard[1]
hazard_direction = hazard[2]
old_hazard_x = hazard_x
old_hazard_y = hazard_y
hazard_map[old_hazard_y][old_hazard_x] = 0
if hazard_direction == 1: # up
hazard_y -= 1
if hazard_direction == 2: # right
hazard_x += 1
if hazard_direction == 3: # down
hazard_y += 1
if hazard_direction == 4: # left
hazard_x -= 1
hazard_should_bounce = False
if (hazard_y == player_y and hazard_x == player_x) or \
(hazard_y == from_player_y and hazard_x == from_player_x
and player_frame > 0):
sounds.ouch.play()
deplete_energy(10)
hazard_should_bounce = True
# Stop hazard going out of the doors
if hazard_x == room_width:
hazard_should_bounce = True
hazard_x = room_width - 1
if hazard_x == -1:
hazard_should_bounce = True
hazard_x = 0
if hazard_y == room_height:
hazard_should_bounce = True
hazard_y = room_height - 1
if hazard_y == -1:
hazard_should_bounce = True
hazard_y = 0
# Stop when hazard hits scenery or another hazard.
if room_map[hazard_y][hazard_x] not in items_player_may_stand_on \
or hazard_map[hazard_y][hazard_x] != 0:
hazard_should_bounce = True
if hazard_should_bounce:
hazard_y = old_hazard_y # Move back to last valid position.
hazard_x = old_hazard_x
hazard_direction += hazard[3]
if hazard_direction > 4:
hazard_direction -= 4
if hazard_direction < 1:
hazard_direction += 4
hazard[2] = hazard_direction
hazard_map[hazard_y][hazard_x] = 49 + (current_room % 3)
hazard[0] = hazard_y
hazard[1] = hazard_x
###############
## START ##
###############
clock.schedule_interval(game_loop, 0.03)
generate_map()
clock.schedule_interval(adjust_wall_transparency, 0.05)
clock.schedule_unique(display_inventory, 1)
clock.schedule_unique(draw_energy_air, 0.5)
clock.schedule_unique(alarm, 10)
# A higher number below gives a longer time limit.
clock.schedule_interval(air_countdown, 5)
sounds.mission.play() # Intro music
第十四章:B
变量、列表和字典表

为了帮助你理解 Escape 列表,我提供了下表,其中包含游戏中使用的一些变量、列表和字典。我已包括那些我认为对定制游戏最有用的内容。你还可以使用本书的索引查找特定变量、列表和字典的引用。
如果变量、列表或字典的名称是大写字母,这意味着它的内容在设置之后不应再更改。
| 变量、列表或字典 | 描述 |
|---|---|
ACCESS_DICTIONARY |
将钥匙与门配对的字典。请参阅 “添加访问控制” 在 第 185 页 (第十一章)。 |
air |
玩家剩余的空气量。在VARIABLES部分设置为起始值。 |
air_fixed |
当玩家已将空气罐装配到太空服上时,设置为True。否则,设置为False。 |
checksum |
用于检查输入的数据是否正确,特别是在游戏列表输入时。如果你修改了游戏数据,你需要修改或禁用校验和代码。在assert指令前加上#可以禁用它们。 |
current_room |
玩家当前所在房间的编号。在VARIABLES部分设置为起始房间。 |
energy |
玩家剩余的能量。在VARIABLES部分设置为起始值。 |
FRIEND1_NAME |
一位朋友的名字,用于房间和物品的描述中。 |
FRIEND2_NAME |
另一位朋友的名字,用于房间和物品的描述中。 |
GAME_MAP |
存储房间之间连接方式的地图。请参阅 “创建地图数据” 在 第 60 页 (第四章)。 |
game_over |
当游戏结束时设置为True。否则,它应该是False。 |
hazard_data |
包含移动危险物品的位置信息和运动信息的字典。请参阅 “添加移动危险” 在 第 203 页 (第十二章)。 |
hazard_map |
用于跟踪玩家当前所在房间中移动的危险物品。自动生成,你不需要修改它。 |
HEIGHT |
游戏窗口的高度(像素)。 |
in_my_pockets |
玩家携带的物品的对象编号列表。在PROPS部分设置,包含玩家开始游戏时的物品。 |
item_carrying |
玩家在库存中选择的物品的对象编号。 |
item_player_is_on |
玩家站立的物品的对象编号。 |
items_player_may_carry |
列出玩家可以拾取的物品的对象编号。 |
items_player_may_stand_on |
列出玩家可以走在其上的物品的对象编号。 |
LANDER_SECTOR |
Poodle 着陆器隐藏的房间编号。 |
LANDER_X |
Poodle 着陆器隐藏的 x 坐标。 |
LANDER_Y |
Poodle 着陆器隐藏的 y 坐标。 |
MAP_HEIGHT |
地图的高度,单位为房间数(参见 第四章,图 4-1 在 第 60 页)。 |
MAP_WIDTH |
地图的宽度,单位为房间数(参见 第四章,图 4-1 在 第 60 页)。 |
objects |
包含游戏中所有物品的图像和描述的字典。请参阅 “制作空间站物品字典” 在 第 85 页 (第五章)。 |
outdoor_rooms |
行星表面房间编号的范围(参见 第四章,图 4-1 在 第 60 页)。 |
PILLARS |
包含前墙透明度动画帧的字典。 |
PLAYER |
包含玩家动画帧的字典。 |
player_direction |
玩家面朝的方向。应为left、right、up或down。 |
player_frame |
用于玩家动画帧的字典。 |
PLAYER_NAME |
用于物品描述和玩家消息中的名称。在VARIABLES部分将其设置为你的名字。 |
PLAYER_SHADOW |
包含玩家动画的阴影字典。 |
player_x |
玩家在房间中的x位置,按瓦片计算。在VARIABLES部分将其设置为起始位置。 |
player_y |
玩家在房间中的y位置,按瓦片计算。在VARIABLES部分将其设置为起始位置。 |
props |
包含游戏中所有可移动物品位置的字典。请参阅 “添加道具信息” 在 第 151 页 (第九章)。 |
RECIPES |
包含物品合成新物品方法的列表。请参阅 “物品合成” 在 第 177 页 (第十章)。 |
room_map |
用于记住玩家所在房间中每个位置的内容。自动生成。你不需要修改此项。 |
scenery |
包含房间中固定物品定位数据的字典。请参阅 “理解景观数据字典” 在 第 97 页 (第六章)。 |
standard_responses |
用于当玩家使用没有其他用途的物品时显示的消息字典。 |
suit_stitched |
当玩家修复了西装时,设置为True。否则,设置为False。 |
use_message |
玩家使用或尝试使用物品时显示的文本。 |
WIDTH |
游戏窗口的宽度,单位为像素。 |
第十五章:C
调试你的代码列表

本书中的一些代码列表第一次运行时可能不会起作用。不要气馁!这是编程中常见的情况,即使是经验丰富的程序员也会遇到。忽略一些细节可能会导致程序发生重大差异。修复程序中的错误叫做调试。
为了尽量减少问题,我将代码列表保持得尽可能简短,因此如果某个列表无法正常工作,你不需要检查太多指令。我还在文本中添加了警告,提醒你注意特别棘手的部分。
记住,如果你无法解决程序的问题,可以使用我在书籍资源中提供的该列表的版本(参见 “ZIP 文件中包含的内容” 在第 8 页)。如果你修改了程序,可以尝试将我列表中的新部分复制并粘贴到你的程序中。
在本附录中,我整理了一些技巧,帮助你修复任何无法正常工作的程序。当 Python 发现错误时,通常会显示程序中出现问题的第一行。这并不总是错误实际所在的行:它只是 Python 发现问题时所能执行到的地方。如果显示的行看起来没问题,首先检查前一行,然后检查列表中的其他新指令是否有错误。
缩进
缩进用于告诉 Python 哪些部分的程序是属于同一组的。例如,所有属于某个函数的指令需要在定义该函数的def指令下进行缩进。属于while、for、if或else命令的指令也需要进行缩进。列表 C-1 提供了一个示例,展示了get_floor_type()函数的一部分。
--snip--
➊ def get_floor_type():
➋ if current_room in outdoor_rooms:
➌ return 2 # soil
➍ else:
➎ return 0 # tiled floor
--snip--
列表 C-1:游戏列表的摘录,展示了缩进层级
所有的指令都属于函数get_floor_type() ➊,因此它们都至少缩进四个空格(参见 ➋ 和 ➍)。return指令(➌ 和 ➎)也属于它们上方的if ➋ 和 else ➍命令,因此它们再缩进四个空格,总共缩进八个空格。当你在输入def、if和else指令时,行尾加上冒号后,下一行的缩进会在 IDLE 中自动添加。使用 DELETE 键删除不需要的缩进。
如果某些指令的缩进级别设置错误,程序可能会出现异常行为,或者即使 Python 没有报告错误,也可能运行得更慢。因此,仔细检查你的缩进级别是值得的。
如果 Python 给出一个错误提示,显示它期望一个缩进块,这意味着你没有缩进应该缩进的内容。如果 Python 告诉你有意外的缩进,说明你在指令开始时添加了过多的空格,或者你可能有指令的缩进层级不一致,应该对齐。在本书中,我使用了四个空格作为每个缩进层级。
大小写敏感
Python 是大小写敏感的,这意味着你使用大写字母(ABC...)还是小写字母(abc...)都很重要。大多数情况下,你应该在编写 Python 代码时使用小写字母。以下是一些例外情况:
-
True、False和None这些值的首字母是大写的。当你正确输入它们时,它们会在 IDLE 中显示为橙色。 -
程序中的一些变量、字典和列表名称是大写的,如
TILE_SIZE和PLAYER。如果你的大小写不一致,可能会收到错误信息,提示某个名称未定义。Python 不会把两个大小写不同的名称识别为相同的名称。(也要检查名称的拼写错误。) -
任何在引号中的内容都可以有大小写变化。这是程序用来执行某些操作的文本,通常写得让它在人类阅读时看起来正确。
-
Python 会忽略
#符号后面同一行的内容,所以你可以在这里使用任何你喜欢的大小写字母。
圆括号和方括号
检查你是否使用了正确的括号形状,并且顺序正确,特别是当 Python 告诉你列表或字典中的某个地方出错时:
-
圆括号
()用于元组和为函数提供信息。例如,range()、print()和len()函数使用圆括号。我们的 Escape 游戏中的函数也使用圆括号,如remove_object()和draw_image()。 -
方括号
[]标记列表的开始和结束。有时,你可能会在一个列表中包含另一个列表,这样就会有多个括号对。 -
花括号
{}标记字典的开始和结束。
冒号
当代码行以 for、while、if、else 或 def 开头时,它需要在行末加上冒号(:)。冒号还用于在字典中分隔键和值。Escape 列表不使用分号(;),所以如果你的代码中有分号,改成冒号。
逗号
列表或元组中的项需要用逗号分隔。当向列表中添加新行时,确保在添加新项之前,在最后一项后加上逗号。通过观察数据中的模式,可以帮助你发现涉及逗号的错误。例如,props 字典和 recipes 列表中的每个列表都包含三个数字。
图像和声音
如果 Python 告诉你找不到图像或声音目录,检查你是否已经下载了文件,并且文件保存的位置是否正确。请参见 “下载游戏文件” 第 7 页和第 19 页的 示例 1-1。
拼写
IDLE 的颜色编码可以帮助你发现一些指令中的拼写错误。检查屏幕上的颜色是否与书中的颜色匹配。拼写变量和列表时要小心:任何错误都可能导致程序停止或出现异常行为。

















































浙公网安备 33010602011771号