PygameZero-游戏编程入门手册-一-

236 阅读57分钟

PygameZero 游戏编程入门手册(一)

原文:Beginning Game Programming With Pygame Zero

协议:CC BY-NC-SA 4.0

一、创作电脑游戏

编写电脑游戏是让编程变得有趣的一个好方法,但它也有缺点。主要的缺点是,要制作一个可用的游戏,你需要写很多代码,这需要很多时间。一个完整的工作游戏对于一本初学编程的书来说通常是太多了。不要担心,因为这本书使用了有效的例子,并利用 Python 和 Pygame Zero 的简单性使它尽可能地简单。在本书中,你将创建几个不同的游戏来说明不同的编程技术。

创造一个游戏不仅仅是写代码。这本书涵盖了创建一个电脑游戏以及编程的一些其他方面。

首先你需要一个想法。然后需要发展这个想法,提出一套规则和控制。它可能需要额外的资源,如图像和声音。然后,您需要编写代码来实现它。接下来(现在是有趣的部分),你需要测试它,找出什么有效,如何改进。然后你回到起点重新定义这个想法,并重复编程周期。

在这一章中,你还将了解 Python 和 Pygame Zero,以及使其适合游戏编程的一些原因。

灵感而不是模仿

第一步是想出一个主意。为此,你可以从你玩过的游戏中获得灵感,这些游戏可以是现有的电脑游戏、纸牌游戏、棋盘游戏或游乐场游戏。或者你可以想出一个全新的游戏,也许从现实世界的活动中获得灵感。如果你想基于以前已经做过的东西来创造一个游戏,那么你必须小心不要侵犯他人的知识产权,包括版权、专利和商标。

像许多法律一样,保护游戏和计算机程序的规则很复杂,并且因国家不同而不同。不可能对错综复杂的法律提供真正的指导,但是有一些你应该遵循的一般规则。

版权可以保护文字、图形、代码、音乐等作品的各个方面。然而,版权并不包括游戏的理念或游戏的玩法。作品一旦创作出来就自动享有版权,通常不需要具体的版权声明或注册,尽管这可以提供额外的保护。

专利要复杂得多,可以涵盖想法和概念。专利是为了发明,在游戏编程的情况下,它们可以被授予游戏的特定技术方面。例如,有些专利涉及赛车游戏中显示方向的方式,以及足球游戏中如何识别球员。要知道你正在开发的游戏可能涉及哪些专利是非常困难的。如果你正在创建一个商业游戏,那么你可能需要寻求专利方面的专业建议。

商标是保护名字和标志的一种方式,在电脑游戏中,它们可以包括角色的外貌。这可能会阻止您使用受商标保护的可识别字符。如果你想使用任何受商标保护的角色,那么你需要从商标所有人那里获得许可。

玩游戏

了解什么是好游戏的最好方法就是玩游戏。不要只玩一个游戏,玩很多不同的游戏。玩好游戏和坏游戏,想想是什么让游戏变得好和坏。

你是厌倦了玩这个游戏,还是它让你着迷,以至于你不能把自己从屏幕上拉走?哪些游戏让你想继续玩下去,为什么?

如前所述,你不仅仅需要从电脑游戏中获得灵感。也玩一些棋盘游戏。想想什么效果好,什么不好。想想用实物玩游戏和在电脑屏幕上玩游戏的区别;两者都可能有利有弊。

创造资源

在查看其他资源时,您可能会想到图形和声音效果。您可能还需要其他资源,包括介绍性视频、教程和背景音乐。

对于大多数游戏来说,你会想要包含图形。这些图形的外观和大小可以决定编程。例如,如果你有一个角色需要在屏幕上移动,那么你需要知道这个角色如何移动(它的脚是否移动)以及这个角色移动所需的空间大小。因此,在开始编程之前,至少创建任何图形的轮廓是有意义的。

声音效果有时可以留到项目的后期,尽管它们通常仍然是创建一个完整游戏的重要部分。如果把它们留到以后再添加,那么在设计游戏的时候,想想它们什么时候会被使用,会有什么影响,仍然是一个好主意。

开发周期

与编程相关的主要术语是敏捷。敏捷编程是一种开发软件的方式,它以小增量创建代码,一次实现一个功能,然后返回添加更多的代码。术语敏捷编程通常指的是一种通过定期评审和团队会议(称为 scrums)在团队中开发软件的编程技术,但是当你自己编程时也可以使用类似的技术。

关于使用敏捷风格方法开发代码的一些要点:

  1. 收集需求。与最终用户会面,或者像自己是客户一样与自己一起回顾自己的想法。

  2. 规划发展。将工作分成小块,每次可以执行一点。

  3. 设计代码以完成当前功能。

  4. 写代码。

  5. 测试代码。除了测试独立代码,还要测试它如何与其他部分交互。

  6. 评估代码是否仍然符合需求。

  7. 回到 1。考虑已经创建的代码。这与它试图实现的目标相一致吗?

对你开发的代码的每一部分重复这个循环。一旦实现了所有需要的部分,您就可以得到一个发布版本。当添加更多特性或改进代码时,遵循相同的周期。

使用敏捷编程时一些有用的东西:

  • 在代码的不同部分如何交互之间设计接口。

  • 用增量发布在短代码冲刺中工作。

  • 对上一步已经完成的内容和下一步将要创建的内容进行定期的简短回顾。评审通常在工作环境中每天进行,但如果您在业余时间工作,则有所不同。

  • 通过代码需要通过的特定测试来执行测试驱动的开发。自动化测试在敏捷编程中很流行,但是您也可以手动测试。

  • 定期重构代码;审查代码以提高清晰度/性能。

  • 定期与用户(或者你自己,如果是个人项目)一起检查设计是否符合预期。

  • 使用橡皮鸭调试(参见第十一章)。

本书中的游戏是基于敏捷编程而创建的。书中不会特别列出任何代码评审,但是您会看到代码是如何一次启动一个特性来构建的。

让编程变得有趣

无论你的全职工作是编写电脑游戏,还是你在业余时间做的事情,编程都应该是你喜欢的事情。我从创作自己喜欢玩的东西中找到了极大的满足感。

虽然你可以试着提前想好这些概念,但是你可能不知道你是否喜欢这个游戏,直到你开始玩它。然后,当你开始调整游戏,以确保它是正确的难度,或者是否有你想要添加的功能。这将在第四章中详细讨论,届时你将会看到一些用于改进初始游戏设计的技术。

Python 和 Pygame Zero

Python 是一种流行的编程语言,在教育界和工业界都有使用。它适用于许多不同的计算机操作系统,包括苹果 Mac OS X、微软 Windows 和 Linux。学习 Python 的一些好处是它容易学习,使用的代码更少(与其他一些语言相比),并且可以帮助教授好的编程技术。

Pygame 是一个可以在 Python 中使用的库,可以使图形游戏编程变得更容易。Pygame Zero 是一个使用 Pygame 的库,但通过减少所需的代码量,使图形游戏编程比 Pygame 更容易。使用这些,有可能在屏幕上创建角色,并非常容易地移动它们。

这本书使用了运行在 Linux 上的 Python 版本,这是 Raspberry Pi 上的当前版本。这些游戏应该可以在不同的计算机系统和安装了 Pygame Zero 的最新版本的 Python 上运行。

Python 中有不同的编程风格。在本书中,最初的几个程序主要是使用函数式编程技术编写的,但随后的程序将基于面向对象编程。当开始编程时,函数式编程风格通常被认为更容易学习,但是一旦你开始创建更长的程序,那么当使用面向对象编程时,编写和理解代码通常更容易。

编译与解释

不同的计算机和操作系统以不同的方式工作。如果您正在创建一个为手机或平板电脑设计的游戏(使用触摸屏),那么您可能需要设计不同于为带有游戏控制器的游戏机设计游戏的界面。此外,计算机内部不同的处理器和操作系统不同的工作方式意味着很难编写能在多台计算机上运行的游戏。

编写计算机代码时,通常会使用一种基于文本的编程语言。计算机不能直接运行那个,代码需要转换成计算机能理解的机器码。当使用 C 之类的计算机语言时,必须先将代码转换成机器代码,然后才能运行程序。这就是所谓的编译语言,程序需要被编译成与运行它的计算机体系结构相匹配的机器代码。

Python 的做法不同,它使用解释器将代码转换成机器语言。这是在程序运行时完成的。这样做的好处是,只要您想要运行代码的计算机有解释器,您通常不需要做任何额外的事情来在该计算机上运行它。缺点是解释型语言运行速度较慢,因为它需要在运行时转换这些代码。这种性能对于本书中的任何游戏来说都不是问题,但是如果你在编写一个图形密集型游戏的话,你应该意识到这一点。

还有一种混合形式,代码被编译成中间形式,但仍然需要解释器(或类似的东西)才能在每个特定的计算机体系结构上运行。这就是 Java 如何使用 Java 虚拟机将 Java 字节码转换成计算机可以理解的机器语言。

按照 Python 的解释,它应该能够在各种不同的计算机上运行,而不需要任何改变。不幸的是,在某些平台上安装 Python 解释器和 Pygame Zero 库有时会有点棘手。幸运的是,有一个更简单的解决方案,使用 Mu 编辑器,这是 Pygame Zero 编程初学者的首选编辑器。

选择编程环境

在本书中,游戏是为 Raspberry Pi 设计的,Raspberry Pi 是一种小型、廉价的计算机,专门为学习计算和计算机编程的人设计。树莓 Pi 有不同的变体,包括小巧的树莓 Pi Zero 和功能齐全的树莓 Pi 4。你可以在本书的游戏中使用任何型号的 Raspberry Pi,尽管出于性能原因,我建议使用 Raspberry Pi 2 或更好的版本。如果你也使用 Raspberry Pi 为游戏设计图像(如第五章所述),那么 Raspberry Pi 4 可能更好,但这不是必需的。

Raspberry Pi 是学习 Python 的理想选择,因为你需要的大部分软件都已经预装了。这些程序仍然可以在其他计算机上运行,如果你愿意,你可以在另一个平台上自由开发代码,但是在其他系统上需要一些额外的步骤。

Python 程序是文本文件,因此,您可以在任何文本编辑器中创建它们。如果你以前没有用 Python 编程,那么我建议你从 Mu 编辑器开始。Mu 编辑器不是最强大的编辑器,但是它的简单性使它成为入门的理想选择。它还处理包括 Pygame Zero 在内的大部分设置。

如果使用的是 Raspberry Pi,那么 Raspbian 的最新版本包含 Mu,但是如果它还没有安装,那么您可以从命令 shell 安装 Mu。通过单击屏幕顶部的黑色终端图标启动命令 shell。

然后输入以下命令:

sudo apt update
sudo apt install mu-editor

然后,您可以从 Raspbian 菜单系统运行 Mu。从开始菜单中选择编程菜单,然后点击 Mu,它看起来应该如图 1-1 所示。

img/488486_1_En_1_Fig1_HTML.jpg

图 1-1

管理部门编辑器的屏幕截图

如果您想在其他操作系统上安装 Mu,那么您可以从 https://codewith.mu/ 下载 Mu 编辑器。在 Windows 下安装时,Mu 网站上的建议是“只为该用户安装”。这将使添加以后可能需要的任何模块变得更加容易。

管理部门编辑器有不同的模式,适用于不同的编程环境。这本书使用了 Python 3 和 Pygame Zero 模式。

当你有了更多的经验,你可能会想换一个更强大的编辑器。如果使用 Raspberry Pi,那么你有一个数字可以选择,你可以直接从命令行运行程序。如果您使用不同的环境,那么您可能需要用 Pygame Zero 设置一个本地 Python 环境。

摘要

本章已经讨论了在你开始编程之前应该考虑的一些事情。它给出了一些建议,告诉你可以从哪里获得灵感,并警告你应该避免围绕他人知识产权的一些陷阱。

它解释了 Python 是什么,以及为什么 Pygame Zero 是游戏编程初学者的好选择。

在下一章,你将开始编写代码,并使用 Python 创建一个命令行游戏。

二、Python 入门

为了开始编程 Python,本章从一些基本的命令行编程开始。这将创建一个简单的基于文本的游戏,可以使用键盘玩。这仅仅是开始;从下一章开始,你将能够创建有趣的图形游戏。

使用管理部门编辑器

当您第一次启动编辑器时,它会询问您以哪种模式启动。您将在本书的项目中使用的模式是 Python 3 和 Pygame Zero。如果您之前已经运行过编辑器,那么它将以上次使用的模式启动,在这种情况下,您可以使用编辑器左上角的模式按钮来更改模式。

对于本章,您将创建基本的基于文本的程序,因此您应该选择 Python 3。在以后的章节中,你应该使用 Pygame Zero。

当你第一次启动 Mu 时,应该会有一个空屏幕,上面有一条注释# Write your code here :-).

行首的#表示这是一个注释,将被忽略。注释对于程序员解释程序如何工作真的很有用,但是 Python 就是忽略了它们。现在您可以删除这一行,但是当您编写自己的代码时,我建议您添加注释来解释代码是如何工作的,因为这对将来理解代码是有用的。

首先,你可以创建一个名为“Hello World”的基础程序。这是你能创建的最小的程序之一。这实际上是一行代码,如下所示:

print ("Hello World")

用以下打印语句替换管理部门编辑器中的注释。然后,您需要在运行程序之前保存它;我建议将它保存在默认文件夹(/home/pi/mu_code)中,并将其命名为 helloworld.py。如果您试图在保存之前运行代码,那么系统会提示您首先保存它。

保存文件后,单击 Run,您将在屏幕底部看到程序正在运行。在这种情况下,它将 Hello World 打印到基于文本的屏幕区域。如图 2-1 所示。

img/488486_1_En_2_Fig1_HTML.jpg

图 2-1

在管理部门编辑器中运行的 Hello World 程序

完成后,单击停止图标停止程序运行。

这是从 Mu 运行 Python 程序最常见的方式。另一种方法是从 Raspbian Linux 命令 shell 运行程序。使用保存按钮保存当前程序。在本例中,通过查看编辑器底部的状态消息,您将看到文件保存的位置

/home/pi/mu_code/helloworld.py

要从命令行运行该程序,请从 Raspbian 菜单启动器启动终端程序。终端是一个基于文本的界面,用于与操作系统通信,包括启动其他程序。您可以使用 cd 命令切换到存储程序的文件夹。文件名由目录组成,该目录由直到最后一个“/”字符的所有字符组成(注意,Linux 上的目录分隔符与 Windows 操作系统上使用的文件夹分隔符方向相反)。

在这种情况下,目录路径是/home/pi/mu_code/

cd /home/pi/mu_code/
python3 helloworld.py

您的程序现在将运行并显示与您之前在 Mu 输出屏幕中看到的相同的“Hello World”文本。如图 2-2 所示。

img/488486_1_En_2_Fig2_HTML.jpg

图 2-2

从命令行运行 Hello World 代码

运行 Python 代码的另一种方式是使用 REPL。它代表 read-eval-print 循环(但名字并不重要)。REPL 提供了一种以交互模式运行 Python 代码的方式。在将少量代码包含到您的程序中之前,这对于测试运行这些代码非常有用。

要在 REPL 中运行相同的代码,请在管理部门编辑器菜单栏中单击 REPL。您必须在 Python 3 模式下才能看到该菜单选项。如果未显示 REPL 图标,则使用管理部门菜单栏上的模式图标来更改模式。点击 REPL 图标后,屏幕底部会出现一个交互式外壳。请注意,如果您之前的编程仍在运行,那么它将并排显示程序输出和 REPL,如果是这样,那么单击停止按钮,这将为 REPL 提供编辑器的全部宽度。

您将在 REPL 屏幕上看到一条提示,通常会显示“IN [1]:”。在提示符下输入上一个程序代码

img/488486_1_En_2_Fig3_HTML.jpg

图 2-3

管理部门编辑器中运行 Hello World 代码的 REPL

print ("Hello World")

然后按 Enter 查看运行该指令的效果。如图 2-3 所示。

您也可以通过从命令行运行python3来访问 REPL。在这种情况下,REPL 提示符由三个大于字符“> > >”显示。如果从命令行运行,那么您需要按 Ctrl-D 退出。

Python 编程

创建 Python 程序时,需要遵循一定的结构,这样程序才能正确运行。第一个游戏将涵盖 Python 程序运行需要遵循的一些规则。

这个游戏是一个简单的笑话测验。程序会问玩家一个带有玩笑答案的问题。如果玩家正确回答了问题,那么他们将受到祝贺;否则,他们将会得到笑点。

单击 Mu 中的 New 按钮创建一个新文件,并输入清单 2-1 中的代码。

1 print ("Welcome to the Python joke program")
2 player_guess = input ("Why couldn't the engineer fix the computer?\n")
3 if (player_guess == "too many bits"):
4     print ("Well done!")
5 else:
6    print ("too many bits")

Listing 2-1Joke quiz program

必须完全按照显示的内容输入代码,但左侧的数字除外,这些数字不应输入(默认情况下显示在管理部门编辑器的空白处)。包含这些数字是为了更容易解释代码,或者在代码不能正常工作时帮助解决问题。它们不应该包含在内,因为它们不构成代码的一部分。

Python 代码是区分大小写的,所以就 Python 而言,print、PRiNT 和 Print 是完全不同的。间距也很重要。第 1、2、3 和 5 行应该从编辑器左侧的第一个字符位置开始。第 4 行和第 6 行应该缩进四个空格;编辑器会在看到冒号“:”字符后自动缩进。每当您按 Tab 键时,Mu 还会插入四个空格。

保存然后运行程序。当你第一次点击保存,你需要给它一个文件名。将文件命名为 joke.py 或其他合适的名称。然后,您可以单击播放按钮来运行该程序。

该程序将打印“欢迎来到 Python 笑话”,然后是“为什么工程师不能修好计算机?”。此时,玩家需要猜一猜。输入I don't know。然后,计算机会以“位数太多”作为回应。

在这一点上,我想为这样一个糟糕的笑话道歉。我相信你可以做得更好,所以请随意将引号之间的文字改成你最喜欢的笑话。

如果您第二次运行该程序,那么您已经知道了答案,因此您可以在提示时键入答案,如图 2-4 所示。

img/488486_1_En_2_Fig4_HTML.jpg

图 2-4

joke.py 游戏的输出

为了解释代码是如何工作的,一次看一行是很有用的。

第一行是print ("Welcome to the Python joke program")

这段代码运行一个名为 print 的函数。函数是执行特定功能的代码块。在这种情况下,打印功能包含在 Python 中,并且包含可以将文本打印到屏幕上的代码。因为有了括号,您可以将打印视为一种功能。一些函数需要一个或多个括号内的值,这些值被称为参数,但并不是所有的函数都使用参数。在 print 的例子中,它接受一个文本字符串参数。文本两边的引号表示文本将用作文本字符串而不是变量。

Note

功能和方法。你可以在本书中看到函数和方法的参考。在 Python 中,方法类似于函数,但包含在类中,对对象进行操作。Python 根据上下文使用两者。这些是面向对象的编程术语,在第九章中有解释。

第 2 行使用了input函数,它向用户显示一条消息,然后等待用户输入响应。该参数是一个文本字符串,就像第 1 行中使用的一样。该函数返回一个字符串值,其中包含玩家输入的猜测值。返回值存储在一个名为player_guess的变量中。

参数文本字符串在末尾包含一个特殊序列\n。这是一个将光标移动到下一行的转义序列。这是必需的,因为与打印功能不同,输出功能不会自动添加新行。变量和转义序列将在本章后面讨论变量时解释。

第 3 行比较存储在player_guess中的变量,看它是否匹配文本字符串"too many bits"。如果匹配,那么它运行缩进的代码块,在本例中是第 4 行。第 4 行是print函数,再次给玩家消息"Well done!"

第 5 行是一个else,与第 3 行的 if 相反。如果不满足第 3 行的条件,那么它运行 else 子句后的缩进文本块,即第 6 行。第 6 行使用print函数来print "too many bits"

ifelse子句是条件语句,稍后会有更详细的解释。

还可以添加更多的东西来改进程序,但首先理解数据如何存储在计算机程序中是有用的。

变量

在计算机程序中存储信息的一种常见方式是使用变量。你可以把一个变量想象成一个盒子,你可以在里面存储一些东西,但是变量不是存储物理对象,而是存储信息。

以下示例将创建一个名为my_variable的新变量,并将值 7 存储在该变量中。

my_variable = 7

变量名必须以字母或下划线字符开头。变量名的其余部分可以包括字母、数字和下划线。变量名总是区分大小写,所以名为My_Variable的变量与my_variable不同。

在一些编程语言中,你需要指定你将在变量中存储什么,比如它将是一个数字还是一个字符串。在 Python 中,这是动态的,因此变量可以根据需要改变类型。了解不同的变量类型很重要,因为经常需要在不同的变量类型之间进行转换。

Python 中使用的主要变量类型有

img/488486_1_En_2_Fig5_HTML.jpg

图 2-5

组成单词 PYTHON 的字符串

  • Integers (int) 存储不带任何分数的整数。

  • 整数的例子有 3,3948392 和–237(它们不必是正数)。

  • 浮点数(float) 存储包含分数或小数点的数字。浮点数的例子包括 2.99、3.14159、–345.2 和 1.0。

  • Character (chr) 指文本的单个字符。在 Python 中,字符以 unicode 存储,因此除了能够存储标准文本(如“a”和数字(如“3”)之外,还有许多不同的字符,如希腊字母或带重音符号的字母。请注意,“3”字符与数字 3 不同;当存储为字符时,通常需要将其转换为数字,然后才能对其执行任何算术运算。

  • 字符串(str) 用于保存文本。它们被存储为串在一起的字符的集合。你可以把它想象成一串字母珠子,每个珠子有一个字母组成一个单词(见图 2-5 )。字符串可以是任意长度,从空字符串(零个字符)到整本书(如果您愿意)。

  • 布尔(bool) 可以表示真或假。事实上,它们只能保存这两个值,这意味着它们对于简单的是或否决策非常有用。

每种变量类型(如 int)的括号中的文本是内置函数的名称,用于将不同的变量转换为该变量类型。例如,如果你有一个整数,但希望它是一个字符串,那么你可以使用 str 函数。然后,您可以使用 type 函数来查看存储的是什么类型的变量。

要看到这一点,您可以在 REPL 中输入一些命令。在管理部门中,单击 REPL 按钮,然后在此处显示的>>>字符后面输入文本。响应以粗体显示。

>>> variable1 = 10
>>> print (variable1)
10
>>> type(variable1)
int
>>> variable2 = str(variable1)
>>> print(variable2)
10
>>> type(variable2)
str

如您所见,当使用 print 函数打印时,variable1 和 variable2 似乎显示了相同的值,但是它们被存储为不同的变量类型。

如果您试图使用+运算符连接这两个变量,那么您将得到如下所示的错误:

>>> variable3 = variable1 + variable2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'

需要在变量类型之间进行转换的一个常见原因是,如果您想要打印存储在整型或浮点型变量中的值。这可以通过打印 str 函数的输出来实现。

>>> print ("The value of variable1 is " + str(variable1))
The value of variable1 is 10

当需要对数字执行算术运算时,理解数字何时存储为字符串和整数/浮点数也很重要。通过测试 REPL 的一些操作,您可以亲自看到这一点。

>>> integer1 = 1
>>> integer2 = 2
>>> integer3 = integer1 + integer2
>>> print (integer3)
3
>>>
>>> string1 = "1"
>>> string2 = "2"
>>> string3 = string1 + string2
>>> print (string3)
12

如您所见,当将整数相加时,您会得到算术和,即本例中的 1 + 2 = 3。如果它们被存储为字符串,那么第二个字符串被附加到第一个字符串,给出字符串“12”。

这个例子说明了为什么区分数字和字符串中的数字很重要,但是用浮点数代替整数呢?事实上,为什么我们甚至需要整数作为一个浮点数可以容纳任何整数值,只是在小数点后加零?主要有两个原因。首先是为了效率;存储整数需要更少的空间,而且对计算机来说更容易操作。另一个是关于由于取整数值造成的不准确性。为了存储浮点数,特别是那些由于除法运算而产生的数字,计算机可能需要对数值进行舍入。如果您随后创建一个具有相同数量但使用不同技术的不同变量,那么它可能会被不同地舍入,从而给出一个几乎相同但不完全相同的值。因此,检查浮点值是否等于另一个浮点值通常被认为是不安全的。如果您想要检查某个值的浮点值,您应该总是比较以查看是否在某个范围内,而不是假设它是一个精确的值。如果你只需要整数,那么最好将它们存储为整数。

想要增加或减少一定数量的数字是很常见的。例如,当一个玩家得分时,你可能需要增加分数变量。您可以使用以下代码实现这一点:

score = score + 1

这是可行的,但是有一个快捷方式允许你增加一个现有的变量。在 Python 中,可以使用+=来增加变量,使用-=来减少变量。你可以在 REPL 测试这个来看这个。

>>> score = 0
>>> score += 1
>>> score
1
>>> score += 2
>>> score
3
>>> score -= 1
>>> score
2

+=和-=快捷键被程序员大量使用,你很少会在程序中看到更长的格式。

+=也适用于字符串,它会将新字符串追加到第一个字符串的末尾。这一点如下所示:

>>> var1 = "string 1"
>>> var1 += " string 2"
>>> var1
'string 1 string 2'

字符串和格式

如前所述,字符串是一组字符。字符串不需要局限于普通的文本字符,因为它们也可以使用特殊的字符序列。您已经看到了转义序列\n,它插入了一个换行符;还有其他一些,如\'\",当您想要在字符串中包含引号时使用,当您想要在字符串中包含\字符时使用。

创建一个字符串就像把一些文本放在引号中一样简单(单引号或双引号)。“这是一个字符串”,“这也是一个字符串”。在 Python 中使用单引号和双引号的唯一区别是,如果您想在字符串中使用相同的引号,那么您需要首先使用转义字符。

如前所示,您还可以使用加号+将字符串加在一起。这将两个字符串组合成一个新的单个字符串(其他一些编程语言称之为串联)。如果您想包含一个非字符串变量,那么首先将它转换成一个字符串,如下所示:

>>> string1 = "Your score is "
>>> score = 10
>>> string2 = string1 + str(score) + "points"
>>> print (string2)
Your score is 10 points

有一些替代技术可用于格式化字符串。第一种称为 printf 风格的格式。Python 文档现在不鼓励使用 printf 样式的格式,因为使用它时很容易出错。如果您在其他人的代码中遇到这种格式,识别它是很有用的。如果您遇到右引号后带有%的字符串,那么他们使用的是 printf 样式的格式:

>>> score = 20
>>> "Your score is %d points" % (score)
'Your score is 20 points'

格式化字符串的一种改进方法是使用 str.format()。它使用大括号{}来表示变量应该插入的位置。要创建与前面示例相同的,您应该输入

>>> score = 30
>>> "Your score is {} points".format(score)
'Your score is 30 points'

更好的方法是使用新的 f 弦。这些包括字符串主要部分的变量名,而不是把它加到末尾。

>>> score = 40
>>> f"Your score is {score} points"
'Your score is 40 points'

不幸的是,f 字符串仅在 Python 的最新版本(3.6 版或更高版本)中可用,在旧版本中将会失败。Raspbian 在 Buster image (2019)之前没有兼容版本。现在可以在 Raspberry Pi 上使用 f-strings,但是这种用法将仅限于那些运行 Raspbian 最新版本的用户。其他计算机升级到 Python 的最新版本也需要一些时间,因此您可能更适合使用 str.format 或者使用+字符将字符串连接在一起。本书中的代码主要使用串联或 str.format 方法,这取决于哪种方法更具可读性。

还有许多内置的字符串方法,在操作文本时会有所帮助。例如,如果您希望在比较文本时忽略大小写字母之间的差异,则可以使用 str.lower 方法将字符串转换为小写。这包含在清单 2-2 所示的笑话程序的改进代码中。

print ("Welcome to the Python joke program")
player_guess = input ("Why couldn't the engineer fix the computer?\n")
if (player_guess.lower() == "too many bits"):
    print ("Well done!")
else:
   print ("too many bits")

Listing 2-2Updated joke quiz program

笑话问答程序的更新版本现在将接受答案,不管玩家是否使用任何大写字母。

列表

前面提到的变量对于存储单个信息非常有用,但是当您需要存储更多信息时就有点受限了。为此,Python 提供了列表。

例如,如果您希望在一个问答游戏中有许多不同的问题,那么您可以创建一个名为 questions 1、question2 等的列表,而不是创建不同的变量。下面的两个列表显示了五个小测验的问题和答案。这将用于创建第一个游戏。

answers = ["Tetris", "Picade", "Python", "Sega", "luigi"]

questions = [
    "What Russian tile matching game was popular in the 1980s?",
    "What is the name of the Raspberry Pi arcade machine from Pimoroni?",
    "What programming language has a logo featuring two snakes?",
    "Which company created Sonic The Hedgehog?",
    "What is the name of Mario's twin brother?"
    ]

我把答案放在第一位,因为它们更短,更容易理解。答案列表包含五个字符串。方括号表示这是一个列表,各个条目用逗号分隔。

如果条目长度超过几个单词,那么将每个条目放在单独的一行上通常更容易阅读代码。正如您在问题列表中看到的,这与带有方括号和分隔逗号的答案遵循相同的格式,但是每个条目都放在一个新行上,并且在每行的开头有四个空格字符,以指示这是同一个块的一部分。

可以通过使用列表名称后跟方括号中的索引位置来访问各个条目。与大多数编程语言一样,索引从位置 0 开始。以下示例显示了如何将第一个问题和答案打印到屏幕上:

>>> answers = ["Tetris", "Picade", "Python", "Sega", "luigi"]
>>>
>>> questions = [
...     "What Russian tile matching game was popular in the 1980s?",
...     "What is the name of the Raspberry Pi arcade machine from Pimoroni?",
...     "What programming language has a logo featuring two snakes?"
...     "Which company created Sonic The Hedgehog?",
...     "What is the name of Mario's twin brother?"
...     ]
>>> print (questions[0])
What Russian tile matching game was popular in the 1980s?
>>> print (answers[0])
Tetris

您也可以通过索引引用问题来更新问题。为了纠正没有以大写字母开始 Luigi 的故意错误,我们可以将其更新如下:

answers[4] = "Luigi"

若要向列表中添加问题,请使用 append 方法。

>>> questions.append("What is the name of the giant barrel throwing ape in Nintendo's classic game?")
>>> questions.append("Donkey Kong")

您也可以使用[]创建一个空列表。要存储玩家的猜测,您可以使用

>>> guesses = []

如果您决定要删除一个条目,那么可以使用 del 语句删除一个作为指定索引的条目。例如,要删除第二个问题,请使用

>>> del questions[1]

这将移动列表中的其余条目来填补空白,因此如果您希望保持两个数组的顺序,那么您需要对答案列表执行同样的操作。

您可以对列表做更多的事情,包括在指定位置插入条目,根据条目的值删除条目,甚至对整个列表重新排序。有关更多详细信息,请参见 Python 文档,附录 b 中包含一个链接。

Note

Python 还有一种不同的数据存储对象类型,称为数组。它的工作方式与列表类似,但首先需要导入。数组确实有一些优点,例如,如果您需要对整个数组执行数学运算。数组超出了本书的范围。如果您想了解更多信息,请参见附录 b 中的链接。

字典

当您希望根据索引位置访问数据时,列表是组织数据的一种有用方式,但有时您希望将信息与单词相关联。在这种情况下,您可以使用一个字典,其中每个条目都与一个键而不是一个数字位置相关联。

你可以把它想象成一本传统的字典,用一个词来索引,然后提供一个描述。Python 中的字典可以使用任何字符串作为索引,这就是所谓的键。描述可以是任何种类的变量或对象,称为值。

创建字典的方式与创建列表的方式类似,但是在字典周围使用大括号{},并使用键值对。

>>> dictionary1 = {'key1':'value1', 'key2':'value2'}

然后使用键而不是我们在列表中使用的数字索引来引用各个条目

>>> print (dictionary1['key2'])
value2

一个例子是,你的游戏根据用户选择的语言有不同的欢迎信息。您可以使用用户的语言作为密钥。

>>> welcome_message = {'english':'Welcome', 'french':'Bienvenue', 'german':'Herzlich willkommen'}
>>> language = 'french'
>>> print (welcome_message[language])

Bienvenue

元组

Python 中另一种常用的数据结构是元组。认为元组的最好方式是将其视为一个一旦创建就不能更改的列表(在编程“行话”中,这被称为不可变的)。这些通常在 Python 中作为返回值使用,其中需要返回多个值,或者表示具有多个值的对象,如 x,y 坐标。

要创建一个元组,只需创建一个用括号括起来的值列表。例如,下面可以表示一艘宇宙飞船的位置,其中 x = 10,y = 15。

position1 = (10,15)

当在屏幕上创建一个演员时,你会在第三章中看到使用元组的例子。

条件语句(if、elif、else)

条件语句提供了一种改变代码执行的方法。它们的工作方式是测试特定的条件,如果条件满足,就只运行部分代码。

您已经在清单 2-1 的早期代码中看到了一个 if 语句。这里重复了代码中处理 if 语句的部分:

3 if (player_guess.lower() == "too many bits"):
4     print ("Well done!")
5 else:
6    print ("too many bits")

在这种情况下,第 4 行的代码只有在满足条件时才运行。第 6 行的代码仅在不满足“else”定义的条件时运行。

if 语句计算直到冒号的所有测试或指令。这就是所谓的条件表达式。它确定条件语句的输出是真还是假。如果为 true,则运行 If 后面缩进的文本块。如果它不是真的,那么它将跳过该文本块。

“else”子句和相关的代码块是可选的。如果包含该条件,那么该代码将仅在不满足“if”条件时运行。

文本块的缩进很重要。我建议每个块每缩进四个空格。在 Mu 中,这通常是自动完成的,按 Tab 键会自动用正确的空格数替换它。在其他编辑器中,按 Tab 键可能会生成一个制表符而不是四个空格;这将阻止代码运行。

添加 if 语句时,您需要评估的值不一定是正确或错误的答案,在这种情况下,您可以使用比较运算符将其更改为正确或错误的答案。考虑一个游戏,当玩家在游戏中前进时,你增加不同数量的分数。一枚银币加 1 分,一枚金币加 5 分,一袋硬币加 10 分。如果玩家达到 100 分,他们就可以升级。这很容易在添加分数代码中实现,使用

if (score == 100):
    level += 1
    print ("Level up to "+str(level))

然而,这个代码有一个问题。如果玩家已经达到了 98 分,然后收集了一袋为他们赢得 10 分的硬币,那么他们的分数将增加到 108 分。该比较永远不会是真的,因为分数会增加得太快,并且他们不会满足分数等于 100 的条件。

相反,你需要检查分数是否等于或大于 100。尖括号“< >”可以用来检查某个值是小于还是大于某个值。因此

if (score > 99):

将检查 100 或更高。或者,您可以将它与等号结合起来,与大于或等于进行比较。因此

if (score >= 100):

如果分数等于 100 或大于 100,将起作用。

不同比较的总结如图 2-6 所示。

img/488486_1_En_2_Fig6_HTML.jpg

图 2-6

常见的比较运算符

如果您使用大于号代替相等测试来更改代码,那么您可能还需要更新代码的相关部分。如果在早期的代码中替换了大于或等于,那么每当玩家在 100 分之后得到一分,就会增加等级。因此,代码需要在一定范围内进行检查,而不仅仅是增加级别,比如

if (score >= 100 and score < 200):
    level = 1
    print ("Level up to "+str(level))

这增加了另一个测试,即逻辑and操作符。使用and语句,只有当左侧和右侧都为真时,才满足条件。

这是以的形式使用的

if (condition1 and condition2):

另一个逻辑运算符是or运算符,如果任一条件为真,该运算符将计算为真。图 2-7 对此进行了总结。

img/488486_1_En_2_Fig7_HTML.jpg

图 2-7

逻辑运算符

乍一看,包括真假可能会觉得多余,但有时也可能有用。通常,这些可以用作循环中的条件(循环中的 True 运算符会创建一个永久循环),或者在调试代码时临时使用其中任何一个都很有用。

还有其他方法来评估真或假的条件。这可以通过返回值的函数或直接输入变量来实现。在这些情况下,如果一个值等于假或零,那么它被评估为假。对于任何其他返回值,该值计算为 true。这可能是返回值为正或为负,或者字符串为非空。当试图理解一个值将如何被解释时,这会引起一点混乱。如果有一些不明确的地方,那么我建议将它与一个已知的值进行比较,以使它变得清晰。

简单的问答游戏

在了解了一些基础知识之后,您现在应该可以使用之前创建的问题和答案列表来创建一个简单的测验了。

将清单 2-3 中的代码输入一个新文件。忽略为了使代码更容易解释而包含的行号。

 1 # Simple quiz game
 2
 3 # Score starts at 0 - add one for each correct answer
 4 score = 0
 5
 6 # List of questions
 7 questions = [
 8     "What Russian tile matching game was popular in the 1980s? ",
 9     "What is the name of the Raspberry Pi arcade machine from Pimoroni? ",
10     "What programming language has a logo featuring two snakes? ",
11     "Which company created Sonic The Hedgehog? ",
12     "What is the name of Mario's twin brother? "
13     ]
14
15 # Answers - correspond to each question
16 answers = ["Tetris", "Picade", "Python", "Sega", "Luigi"]
17
18 print ("Welcome to the computer game quiz")
19
20 # Ask the first questions, store response in player_guess
21 player_guess = input (questions[0])
22 if (player_guess.lower() == answers[0].lower()):
23     # If correct say so and add 1 point
24     print ("Correct")
25     score += 1
26 else:
27     print ("Incorrect")
28
29 # Ask the second question
30 player_guess = input (questions[1])
31 if (player_guess.lower() == answers[1].lower()):
32     # If correct say so and add 1 point
33     print ("Correct")
34     score += 1
35 else:
36     print ("Incorrect")
37
38 # Ask the third questions
39 player_guess = input (questions[2])
40 if (player_guess.lower() == answers[2].lower()):
41     # If correct say so and add 1 point
42     print ("Correct")
43     score += 1
44 else:
45     print ("Incorrect")
46
47 print ("You scored {} points".format(score))

Listing 2-3Simple quiz game – quiz0.1.py

这个程序包含在附带的源代码 quiz0.1.py 中。

代码以一些以#字符为前缀的注释开始。

第 4 行创建了 score 变量,并将其初始值设置为 0。

第 6 到 16 行添加了问题和答案,如前所述。

在向玩家(18)发出欢迎消息后,第 21 到 27 行询问第一个问题,并使用 if 语句检查它是否正确。你会在第 22 行看到玩家的答案和正确答案都被转换成小写(。lower function)以便玩家是否使用大写字母输入答案并不重要。

第 29 到 36 行问第二个问题,然后第 38 到 45 行问第三个问题。

最后,第 47 行告诉玩家他们做得有多好。

如果您查看第 21 到 27 行、第 30 到 36 行以及第 39 到 45 行,您会注意到一些代码在代码块之间是重复的。除了问题编号之外,第一个问题的文本块与第二个和第三个问题的文本块相同。仅仅三个问题就浪费了很多代码,但是想象一下如果有更多的问题。如果您必须为每个新问题添加八行额外的代码(包括一个注释),那么这将会增加很多代码。这就是循环有用的地方。

循环–While,For

在条件语句之后,代码需要做的最重要的事情之一就是重复动作。这通常以循环的形式完成。

清单 2-3 中的测试代码展示了重复代码如何增加需要编写的代码量。这也意味着,如果您想要对代码进行更改,那么更改将需要跨多行进行,这不仅浪费时间,还会增加出错的风险。

当涉及到需要保持运行的代码时,循环甚至更加重要。如果你有一台街机,那么当每个人都玩完后,如果需要重启整个机器,那就没什么好处了。对于大多数电脑游戏来说,在“游戏结束”之后,你希望可以选择重新开始游戏,而不需要重启。

在 Python 中创建循环时,本质上有两种不同类型的循环。while循环是最容易构建的,因此将首先讨论。

While 循环

可以通过演示来展示while循环

num_times = 0
while (num_times < 10):
    print ("This is line number "+str(num_times))
    num_times += 1

如果在管理部门 REPL 中输入代码并按 enter 键,则应看到以下内容:

This is line number 0
This is line number 1
This is line number 2
This is line number 3
This is line number 4
This is line number 5
This is line number 6
This is line number 7
This is line number 8
This is line number 9

这将重复该命令十次。

要考虑的主要问题是 while 循环,它将在变量 num_times 小于 10 时运行。要循环运行,num_times 变量必须在每次循环中更新。

在这种情况下,变量在每个循环中递增一次,但有时变量可能会发生不同的变化。当玩家的分数小于某个值时,或者直到达到某个触发器时,可能需要运行循环。在以后的游戏中会有更多循环的例子。

For 循环

另一种方法是 for 循环。通常情况下,for循环通常用于遍历一个列表。当您想给它一个列表并为列表中的每一项运行一些代码时,这很有用。

同样,这很容易通过一个例子来演示

questions = [
     "What Russian tile matching game was popular in the 1980s?",
     "What is the name of the Raspberry Pi arcade machine from Pimoroni?",
     "What programming language has a logo featuring two snakes?"
     "Which company created Sonic The Hedgehog?",
     "What is the name of Mario's twin brother?"
     ]
for this_question in questions:
    print (this_question)

这将依次打印出每个问题,并显示以下输出:

What Russian tile matching game was popular in the 1980s?
What is the name of the Raspberry Pi arcade machine from Pimoroni?
What programming language has a logo featuring two snakes?
Which company created Sonic The Hedgehog?
What is the name of Mario's twin brother?

看看 for 循环中的代码,它所做的是遍历问题列表并将当前值存储在一个名为this_question的临时变量中。然后它打印出this_question的内容。

另一个例子是你想运行一个循环固定的次数。这是之前使用的 while 循环的替代方法:

for x in range(0,10):
    print ("This is line number "+str(x))

这一次,for 循环使用了 range 函数,该函数允许它在一个数字范围内进行迭代。实际上,它就像一个从第一个参数到第二个参数的数字列表(不包括第二个参数)。这将给出一个 0,1,2,3,4,5,6,7,8,9 的列表。还有第三个参数可以用来改变数字之间的步长。

所以range(0,10,2)只会显示 0 到 9 之间的偶数。

该函数的格式为

range(start, stop, step)
start (optional if only one parameter is used) - the first number included
stop (required) - the maximimum value is one less than the stop value
step (optional) - the difference between each value

这些值可以是负数。如果你想递减计数,那么步长可以是-1,每次迭代递减一。

其他一些编程语言有不同的forforeach循环。Python 的for循环就像其他编程语言中的foreach循环,但是通过range函数,它可以像其他编程语言中的for循环一样工作。

永久循环–为真时

while 循环的一个特殊情况是,它可以在条件设置为 True 的情况下运行。这意味着循环将永远运行。

while True:
    print ("Program is still running")

我不建议你运行前面的代码,因为它会一直运行下去。实际上,永远可能是一个夸张的说法(但这是其他一些编程语言中使用的术语);循环实际上一直运行,直到你从外部停止程序,计算机停止,或者世界末日,无论哪一个先出现!

如果您确实运行了程序,那么您可以使用 Mu 中的 Stop 按钮或者 Ctrl-C(如果从命令行运行 Python)来取消。Ctrl-C 将发送一个信号告诉 Python 停止运行,并给出一个键盘中断错误消息。在命令行程序中包含永久循环是很常见的,尽管在 Pygame Zero 中不太常见,因为永久循环是在后台处理的。

您也可以看到使用while 1的其他程序。当 1 的值为真时,情况也是一样的。

改变循环流程-中断并继续

如果想要一个“几乎永远”的循环会怎样?也许你想让程序永远运行下去,除非玩家要求退出。有两个语句可以用来改变循环中的流(这也适用于 for 循环),它们是 break 和 continue。

break 语句将导致程序在该点退出循环,然后在循环外运行代码。continue 语句使代码跳回到循环的开始,重新计算表达式,然后再次运行循环(如果满足条件)或退出循环(如果不满足条件)。

功能

函数是定义代码块的一种方式,这样就可以在程序的其他地方使用它。这些可以内置并包含在库中,或者您可以创建自己的库。

最流行的 Python 函数之一是 print 函数,它已经在本章的许多例子中使用过。在最基本的用途中,print 函数接受一个字符串,该字符串显示在控制台中。

print ("string")

本质上,当你调用一个函数时,当前的程序流会暂停。提供的任何参数都被传递给函数,函数中的代码运行,然后当函数完成时,流程返回到代码中的前一点。

您可以创建自己的函数,如清单 2-4 所示。

1 def ask_question (question, answer):
2    player_guess = input(question)
3    if (player_guess.lower() == answer.lower()):
4        print ("Correct")
5        return 1
6    else:
7        print ("Incorrect")
8        return 0

Listing 2-4Example of a Python function

再次注意,行号不会出现在代码中。

如果您试图运行它,这段代码不会做任何事情,而是应该作为一个更大的程序的一部分。在这种情况下,行号通常不会从 0 开始,因为函数通常不会是可执行文件中的第一个条目。

这与清单 2-3 (第 20 到 27 行)中使用的代码相同,但是使用了一个名为ask_question的函数,并将问题和答案作为参数传递,而不是直接访问列表。

第一行使用了“def”语句,将它标识为一个函数。第 1 行的下一项是函数名ask_question。函数名遵循与变量类似的约定,例如,它不能以数字开头,约定建议使用下划线字符而不是空格。括号用于包含需要传递给函数的任何参数。在这种情况下,有两个论点:问题和答案。第 1 行的最后一个字符是“:”字符,它表示函数的开始,函数的内容需要在下面缩进。

函数中不需要使用参数,但是如果没有参数,括号仍然是必需的。向函数传递参数时需要知道的一件重要事情是,函数会将作为参数传递的数据进行本地复制,因此当函数返回时,对这些变量所做的任何更改都会丢失。

除了 return 语句(稍后将解释)之外,函数体与前面的代码相同,并且函数中不使用列表代码中的条目,而是使用参数中提供的值。这意味着不同的参数可以传递给函数,而不必复制代码。

第 5 行和第 8 行的 return 语句用于结束函数并返回主代码。Return 语句并不总是必要的,因为如果到达函数的末尾,就有一个隐含的 return,但是如果您希望代码在到达函数末尾之前返回,或者如果函数需要传回一个值,则可以添加 return 语句。return 语句后面通常跟有要返回的值或变量,但是如果没有(或者没有 return 语句),则返回一个特殊值“None”。

变量作用域

变量可以在代码的主要部分或函数内部创建;范围定义了变量可以在哪里更新,可以是局部的,也可以是全局的。如果变量是在函数内部创建的,那么它将是一个局部变量,只能在函数内部使用。复制到局部函数中的参数也是如此。这允许多个变量具有相同的名称,这是代码重用的一个重要特性。它还可以防止意外更改另一个函数中的变量。

有时你需要访问在别处创建的变量。例如,如果有一个保存分数的变量,那么它可能需要由任何需要更新分数的函数来更新。为了实现这一点,应该在函数中使用 global 关键字,这样它就可以将变量作为全局变量来访问。

这是通过一个例子最容易理解的。清单 2-5 显示了示例代码来演示局部和全局变量的使用。

variable1 = 1
variable2 = 1

def local_function (variable1):
    variable1 += 1
    variable2 = 5
    print ("variable1 in local_function {}".format(variable1))
    print ("variable2 in local_function {}\n".format(variable2))

def global_function (argument1):
    global variable1, variable2
    variable1 = argument1 + 10
    variable2 = 15
    print ("variable1 in global_function {}".format(variable1))
    print ("variable2 in global_function {}\n".format(variable2))

print ("variable1 in top level-code {}".format(variable1))
print ("variable2 in top level-code {}\n".format(variable2))

local_function (variable1)

print ("variable1 in top level-code {}".format(variable1))
print ("variable2 in top level-code {}\n".format(variable2))

global_function (variable1)

print ("variable1 in top level-code {}".format(variable1))
print ("variable2 in top level-code {}".format(variable2))

Listing 2-5Code demonstrating variable scope

运行时,它将产生清单 2-6 中所示的输出。

variable1 in top-level code 1
variable2 in top-level code 1

variable1 in local_function 2
variable2 in local_function 5

variable1 in top-level code 1
variable2 in top-level code 1

variable1 in global_function 11
variable2 in global_function 15

variable1 in top-level code 11
variable2 in top-level code 15

Listing 2-6Output of code demonstrating variable scope

有两个变量是在代码的顶层创建的(在任何函数之外)。有两个功能;local_function展示了局部变量,而global_function展示了如何改变全局变量。除了清楚地表明所指的是什么之外,命名没有任何意义。任何函数都可以有局部或全局变量的任意组合。

这两个变量都被设置为 1,第一个 print 语句证实了这一点。第一个变量作为参数传递给local_function,它被定义为一个局部变量,只在函数内部可见。该值增加到 2,显示在函数内部,但函数完成后,原始变量不变。另一个名为 variable2 的变量被创建并设置为 5。在local_function内使用时,显示值为 5,但这个变量只存在于函数内,在那个函数外,variable2的值保持为 1。

global_function中,variable1作为一个参数被传递,但被存储为一个名为argument1的局部变量。variable1variable2都通过全局语句被设置为全局变量,当它们在该函数中被更新时,它也会更新全局顶级变量中的值。

还有一件事。如果一个变量是在顶层创建的,然后在一个函数中读取而没有使用全局语句,那么顶层变量的值将被读取。该变量名不能用作局部变量的名称。

应该尽可能避免使用全局变量。这是因为在多个地方更新变量会导致代码难以理解和调试。如果你在代码中看到太多的全局变量,这有时被称为“不好的味道”。在使用 Pygame Zero 时(从下一章开始),你会看到使用了相当多的全局变量。这是 Pygame Zero 的一个特性,因为代码在 Pygame Zero 的不同函数中运行,如果不使用全局变量,很难将变量传递给这些函数。幸运的是,面向对象编程使这变得更容易,但这将在第九章讨论。在接下来的几章中,请接受将会有一些全局变量,但是在本书的后面情况会改变。

重构代码

现在,您已经学习了一些额外的编程技术的理论,您可以通过一个新的改进的测验将它付诸实践。游戏玩家不会注意到这个版本的任何不同,但我喜欢把它看作是“更好的代码”。

这就是所谓的重构代码。重构是对代码结构进行的更改,这种更改通常不会增加任何额外的功能,但会使代码更清晰、更易于理解。它还可以用来简化新特性的添加。

新代码如清单 2-7 所示,我将其命名为 quiz0.2.py。

# Simple quiz game
def ask_question (question, answer):
    player_guess = input(question)
    if (player_guess.lower() == answer.lower()):
        print ("Correct")
        return 1
    else:
        print ("Incorrect")
        return 0

# List of questions
questions = [
    "What Russian tile matching game was popular in the 1980s? ",
    "What is the name of the Raspberry Pi arcade machine from Pimoroni? ",
    "What programming language has a logo featuring two snakes? ",
    "Which company created Sonic The Hedgehog? ",
    "What is the name of Mario's twin brother? "
    ]

# Answers - correspond to each question
answers = ["Tetris", "Picade", "Python", "Sega", "Luigi"]

while True:
    print ("Welcome to the computer game quiz")

    # Score starts at 0 - add one for each correct answer
    score = 0

    for i in range (0,len(questions)):
        if (ask_question (questions[i], answers[i]) == True):
            score += 1

    print ("You scored {} points\n".format(score))

Listing 2-7Refactored version of simple quiz game – quiz0.2.py

这个改进的测验从一个名为ask_question的函数开始。这个函数向玩家提问,检查是否有正确的回答,并根据提供的答案是否正确返回 1 或 0。该函数位于代码的开头,因为它需要在被调用之前被定义。

然后,问题和答案被存储为列表。列表的顺序是这样的,问题和答案有相同的索引。代码的其余部分被包装在 while True 循环中,以便在回答完问题后,测验会返回到开始。

进一步的改进

就像本书中的所有游戏一样,这是一个有效的游戏,但是还有改进的空间。一些改进的想法是添加更多的问题(或者为您感兴趣的主题更改它们),选择随机出现的问题,并根据正确回答的问题数量更改输出以给出不同的短语。

试着添加这些,看看你是否能让游戏更有趣。您需要使用随机模块来随机选择问题;你可以在网上找到细节,或者在下一章解释后回到这里。在提供的源代码中,我包含了一个将所有这些合并为 quiz0.3.py 的示例,尽管我建议您在查看代码之前尝试一下进行自己的更改。

摘要

这一章非常简要地介绍了 Python 编程语言。本书篇幅不够,无法详细解释 Python。从现在开始,本书将假设读者对 Python 编程语言有所了解。如果你需要更多关于 Python 入门的信息,那么我推荐 Apress 出版社出版的 M. L .赫特兰德的《Python 入门》一书。

我还在附录 b 中包含了官方 Python 文档的链接。

下一章将继续使用 Pygame Zero 创建图形游戏。

三、Pygame Zero

到目前为止,这些程序都是基于文本的,但如果我们仅仅停留在基于文本的游戏上,它们就不会像图形游戏那样有吸引力。这本书的其余部分是关于图形游戏,将使用 Pygame Zero 创建。

要了解 Pygame Zero 是什么,首先需要了解 Pygame 是什么。Pygame 是一个 Python 的编程库。它是为更容易地创建多媒体应用(如游戏)而设计的。它还可以在多个平台上运行,使得与其他电脑共享你制作的游戏变得更加容易。

虽然 pygame 使用 Python 创建游戏变得容易多了,但在开始之前,它仍然需要相当数量的标准代码,即样板代码。

Pygame Zero 是一个较新的编程库,它使用 Pygame,但是去掉了许多样板代码,使得创建游戏更加容易。设计用于教育,以帮助教学编程,这是一个伟大的方式开始创造电脑游戏。

Pygame 零开发

在撰写本文时,Pygame Zero 是一个正在进行的项目,定期进行改进。幸运的是,大多数变化都保持了向后兼容性,但一些新功能不会在 Pygame Zero 的所有安装上工作。如果您确信您编写的游戏只能在当前或更高版本上运行,那么您可以将它们包含到您的游戏设计中,但是如果您的游戏能够在更广泛的计算机上运行更重要,那么您可能希望将自己限制在与 Pygame Zero 库的旧版本兼容的功能上。

一个例子是,你现在可以在资源(如图像文件)上包括文件路径和文件扩展名,但这在 Pygame Zero 的旧版本(2018 年夏季之前)上不起作用。本书中的代码已经在 Pygame Zero 的最新版本上进行了测试,但是在我意识到向后兼容性问题的地方,我已经尝试编写代码以便在旧版本中也能工作。

指南针游戏

第一个图形游戏将被称为指南针游戏。指南针游戏的灵感来自于我志愿加入的童子军玩的一个游戏;这又是一个游戏的变种,叫做“船长来了”。该游戏用于帮助教授四个基本方向(指南针的点)。在真实(非电脑)游戏中,童子军大厅的每面墙上都贴有一个标签。幼仔被给予一个方向,他们必须跑向合适的墙。还可以给出额外的指令,比如队长来了,幼崽必须站着不动,敬礼。

在这一章中,你将创建一个电脑版本的游戏,玩家得到一个必须遵守的指令。玩家必须按照指定的方向移动他们的角色。游戏如图 3-1 所示。

img/488486_1_En_3_Fig1_HTML.jpg

图 3-1

指南针游戏截图

这将提供一个学习 Pygame Zero 的机会,以及如何让一个角色在屏幕上走动。这将使用敏捷方法创建,一次添加一个功能来创建游戏。

必需的文件

这个项目需要几个图像文件。这些需要放在游戏源代码正下方的图像目录中。

您将需要第三章目录中源代码的文件,然后是子目录 images。如果您正在使用管理部门编辑器,那么它们应该被复制到目录/home/pi/mu_code/images中。

如果您在提供的源代码的第三章目录中查找,您会看到许多以 compassgame 为前缀的 Python 文件。游戏要经历多次迭代,在游戏开发的不同阶段都会用到这些文件。如果你是按照电脑上的说明来做的,那么你只需要使用一个名为 compassgame.py 的文件来创建游戏,这个文件将贯穿本章。如果您想直接跳到每个阶段的代码,而不是自己键入代码,可以使用源代码中提供的文件。

在 Pygame Zero 模式下运行 Mu

游戏应该在 Mu 中创建为一个新文件。你需要把游戏模式调到零。这可以通过单击编辑器左上角的模式图标来实现。如图 3-2 所示。

img/488486_1_En_3_Fig2_HTML.jpg

图 3-2

在管理部门编辑器中更改为 Pygame Zero 模式

首先将下面两行添加到文件中,然后将其保存为 compassgame.py。

WIDTH = 800
HEIGHT = 600

然后点击 Mu 中的 Play 按钮,你应该会看到一个黑屏,800 像素宽,600 像素高。第一个例子很好地展示了为什么 Pygame Zero 如此易于使用。仅仅定义屏幕的尺寸就足以创建一个游戏窗口。事实上,这可能是使用一个空文件启动的,因为这些是默认值。这比使用 Pygame 的等价代码要少,也更容易理解。

您可以通过单击右上角的 x 或按管理部门菜单栏中的停止来关闭程序。

如果您没有使用 Mu,那么可以在任何其他编辑器中创建该文件,但是应该使用以下命令从命令行运行:

pgzrun compassgame.py

这段代码可以在 compassgame-layout1.py 文件的源代码中找到。

Tip

记住,如果菜单中有 run 菜单项而不是 play,那么你需要切换到 Pygame Zero 模式。单击左上角的模式按钮选择您的模式。

添加背景图像

既然您已经知道了如何创建一个基本的 Pygame Zero 应用,那么是时候添加一些更有趣的东西了。你可以从用一些更有趣的东西替换普通的黑色背景开始。

用清单 3-1 中的代码替换当前代码。

WIDTH = 800
HEIGHT = 600

BACKGROUND_IMG = "compassgame_background_01"

def draw():
    screen.blit(BACKGROUND_IMG, (0,0))

Listing 3-1Simple Pygame Zero program with image background

该代码在提供的源代码中以 compassgame-layout2.py 的形式提供。

单击 Play 按钮,您现在应该会看到和以前一样的屏幕,但是它现在会有一个绿色的背景图像。如果不起作用,请确保您已经将图像复制到正确的目录中。mu_code/images 目录下应该有一个文件 compassgame_background_01.png。

该代码通过创建一个变量 BACKGROUND_IMG 来工作,该变量包含要显示的文件的名称。图像作为文件名输入,没有任何路径信息或。png 后缀。在最新版本的 Pygame Zero 上,如果您愿意,可以使用完整的文件名,但是为了保持与旧版本 Pygame Zero 的兼容性,文件必须位于 image 文件夹中,并且不包含后缀。这对于任何用作演员和背景的图像文件都是一样的。

def draw() :行定义了绘图功能。这是一个 Python 函数,Pygame Zero 每秒大约调用 60 次。它应该用来告诉 Pygame Zero 屏幕上应该显示什么。

该函数调用screen.blit,它在适当的位置显示位图图像(在本例中,从屏幕的左上角开始显示 0,0)。

Note

使用这么长文件名的原因是因为默认情况下,Mu 将所有代码放在同一个目录中。如果创建多个 Python 程序,它们都共享同一个图像目录。像这样命名它们使得哪个文件用于哪个程序变得很明显。

如果你正在使用一个不同的编辑器或者已经将你的游戏组织到一个专用的目录中,那么你可能想要从文件名的开头删除 compassgame_ 前缀。

图像文件名也包括一个数字,这将允许我们改变背景或人的外观。

添加演员

在计算机图形学中,角色和其他物体被称为精灵。在 Pygame Zero 的例子中,它用一个更“友好”的名字来称呼精灵演员。我经常称这些为精灵,因为这是正确的计算术语,但是请记住在 Pygame Zero 中定义它们时要将它们添加为 Actor 对象。

精灵是计算机游戏中使用的图像,通常由位图图像创建。这些通常以人物(人、动物、外星人等)的形式出现。),但它们也可以用于玩家需要与之交互的对象,如障碍物、球或武器发射的子弹。

在这种情况下,您可以从代表玩家角色的单个精灵开始。稍后你可以添加更多精灵作为障碍来增加挑战。

玩家角色的精灵需要几个图像,这样你就可以显示它面向不同的方向,让它看起来在移动。最少需要的是一个人物面向以下每个方向的图像:前、右、左、后。为了使运动更加真实,可以使用额外的图像,使腿在图像之间移动。在第五章中,你将会看到如何设计你自己的精灵角色,但是现在你可以使用源代码中包含的精灵。最简单的方法是从 mu_code 的 image 子目录中的源代码的 image 目录中复制文件。这个游戏的精灵是一个人,但是它可以被一个动物或者一个完全不同的角色代替,比如一辆汽车。

要创建 sprite,请使用带有图像文件的 Actor 对象。

player = Actor('imagefile')

与前面提到的背景图像相同的规则也适用于图像。如果您想要最大程度的兼容性,请使用位于映像目录中的不带扩展名的映像名称。如果使用 Pygame Zero 的最新版本,可以包含扩展名和文件位置的路径。

要将 sprite 定位在屏幕的特定位置,您可以随后将该位置添加为一个元组。下面将在屏幕中央创建一个精灵:

player = Actor('compassgame_person_down_1', (WIDTH/2,HEIGHT/2))

坐标系从屏幕的左上角开始。x 坐标向右增加,y 坐标向下增加。这与图形和地图的工作方式不同。图 3-3 中的图像显示的是游戏画面,其中标注了一些关键坐标。

img/488486_1_En_3_Fig3_HTML.jpg

图 3-3

Pygame 零屏幕坐标

除了创建演员,您还需要包含将它绘制到屏幕上的代码。这是通过将以下条目放入 draw 函数来实现的:

player.draw()

清单 3-2 中显示了演示这一点的代码,它作为 compassgame-player.py 包含在源代码中。

WIDTH = 800
HEIGHT = 600

BACKGROUND_IMG = "compassgame_background_01"

#Player character
player = Actor('compassgame_person_down_1', (WIDTH/2,HEIGHT/2))

def draw():
    screen.blit(BACKGROUND_IMG, (0,0))
    player.draw()

Listing 3-2Simple Pygame Zero program with player actor

在屏幕上移动精灵

现在您已经创建了一个 sprite(演员),您可以从键盘读取按键,并使播放器向按键的方向移动。

为了更容易测试一个键是否被按下,Pygame Zero 为每个键提供了一个属性。要测试向上箭头键是否被按下,您应该检查“keyboard.up”的值。如果值为真,则按下向上键,如果值为假,则不按下。

你不会用这个方法从玩家那里得到文本输入,因为它不会告诉你按键的顺序。然而,它对于游戏编程是有用的,其中只有少量的键可以被按下,并且可以同时按下多个键(例如向上和向右以对角移动)。

当你知道向哪个方向移动玩家时,你可以改变 x 和 y 属性来移动角色一定的像素。

移动角色的代码如清单 3-3 所示。用更新后的代码替换当前代码。

WIDTH = 800
HEIGHT = 600

BACKGROUND_IMG = "compassgame_background_01"

#Player character
player = Actor('compassgame_person_down_1', (WIDTH/2,HEIGHT/2))
# Direction that player is facing
direction = 'down'

def draw():
    screen.blit(BACKGROUND_IMG, (0,0))
    player.draw()

def update():
    # Need to be able to update global variable direction
      global direction

    # Check for direction keys pressed
    # Can have multiple pressed in which case we move in all the directions
    # The last one in the order below is set as the direction to determine the
    # image to use
    new_direction = "
    if (keyboard.up):
        new_direction = 'up'
        move_actor(new_direction)
    if (keyboard.down):
        new_direction = 'down'
        move_actor(new_direction)
    if (keyboard.left) :
        new_direction = 'left'
        move_actor(new_direction)
    if (keyboard.right) :
        new_direction = 'right'
        move_actor(new_direction)
    # If new direction is not "" then we have a move button pressed
    # so set appropriate image
    if (new_direction != ") :
        # Set image based on new_direction
        player.image = "compassgame_person_"+new_direction+"_1"
        direction = new_direction

def move_actor(direction, distance = 5):
    if (direction == 'up'):
        player.y -= distance
    if (direction == 'right'):
        player.x += distance
    if (direction == 'down'):
        player.y += distance
    if (direction == 'left'):
        player.x -= distance

    # Check not moved past the edge of the screen

    if (player.y <= 30):
        player.y = 30
    if (player.x <= 12):
        player.x = 12
    if (player.y >= HEIGHT - 30):
        player.y = HEIGHT - 30
    if (player.x >= WIDTH - 12):
        player.x = WIDTH - 12

Listing 3-3Code to allow the character to move around the screen

这包含在名为 compassgame-movement1.py 的源代码中。

到目前为止,您应该能够理解大部分代码,但是有一些新的东西可能需要解释。

new_direction 变量是更新函数内部的局部变量。它用来保持它检测到的最后一个键被按下的方向(所以如果你向上和向右按,它会保持向右)。这样做是为了当两个键都被按下时,角色不会在向上和向右之间改变,但在稍后让角色的腿移动时也很有用。由于 new_direction 存储为字符串,因此可以使用以下行将其包含在播放器图像中:

player.image = "compassgame_person_"+new_direction+"_1"

如果玩家面向右侧,将显示图像compassgame_person_right_1.png.

添加了一个名为 move_actor 的新函数。顾名思义,这会移动演员在屏幕上的位置。第一个参数是移动的方向。该函数的第二个参数被定义为“distance = 5”。这意味着如果向函数提供了一个值,那么该值将存储在距离变量中,但是如果参数中没有传递任何内容,那么距离变量将被设置为 5。当您想要包含参数的默认值时,这很有用。

移动演员时,代码会检查位置,以确保角色不会超出屏幕边缘。它使用 30 个像素的 y 偏移值和 12 个像素的 x 偏移值,这样整个演员都保留在屏幕上。

使动作更加真实

如果您运行清单 3-3 中的代码,您将看到角色四处移动并面向他们行走的方向,但这看起来并不特别真实。由于腿不动,玩家看起来像是在滑行而不是在行走。为了使运动看起来更真实一点,我们可以改变图像来显示人的腿在运动。

使用的技术类似于制作卡通的方式,每一帧都是通过轻微的移动单独绘制的。然后这些画面一个接一个地显示出来,形成一幅动态图像。在一幅典型的漫画中,他们每秒钟的运动可能会产生大约 20 幅图像。理论上,在每次运行更新功能时都有可能改变图像,大约每秒 60 次;然而,为了简单起见,代码只在第五次调用更新函数时更新。这将给出每秒 12 帧的帧速率。要实现这一点,需要玩家移动的每个方向都有四幅图像。图 3-4 显示了用于右侧方向的四幅图像。

img/488486_1_En_3_Fig4_HTML.jpg

图 3-4

向右走的四个精灵图像

在这个例子中,只有腿在移动,但是你可以让手臂也在移动,这样会更真实一点。

每个方向使用 4 个图像需要 16 个不同的图像。如果你想提高帧速率,那么你可以增加图像的数量。如果你想移动角色的次数增加一倍,你可以将每张图片之间的延迟减半,并将图片的数量增加一倍,达到 32 张。

以前,通过更新 actor 属性来更改图像。为了允许显示不同的图像,可以将此更改为对名为 set_actor_image 的新函数的函数调用,该函数将基于行进方向和序列中的适当图像来确定正确的图像。

要更新前面的代码以显示字符的工作情况,请执行以下步骤。

在代码顶部附近添加一个新的全局变量 player_step_count。它可以放在方向变量的定义之后。

player_step_count = 1

更换管路

player.image = "compassgame_person_"+new_direction+"_1"

随着

set_actor_image (new_direction)

然后将以下代码添加到文件的底部:

# Show image matching new_direction and current step count
def set_actor_image (new_direction):
    global player, player_step_count

    player_step_count += 1
    if player_step_count >= 4:
        player_step_count = 1

    player.image = "compassgame_person_"+new_direction+"_"+str(player_step_count)

更新后的代码以 compassgame-movement2.py 的形式包含在源代码中。如果您现在运行代码,那么您会看到腿在移动,但速度会太快。它仍然需要代码通过每隔 5 帧替换图像来减慢运动。

这是通过允许 player_step_count 一直计数到图像数量的五倍,然后将图像数量除以 5 来实现的。然后,代码将丢弃任何余数,然后加 1(从 1 开始图像编号,而不是从 0 开始)。

This is best illustrated by working through some examples.

With player_step_count set to 0
Divide player_step_count (0) by the delay (5) giving 0.0
Discard anything after the decimal place which gives 0
Add 1 to get image number 1

With player_step_count set to 1
Divide player_step_count (1) by the delay (5) giving 0.2
Discard anything after the decimal place which gives 0
Add 1 to get image number 1

With player_step_count set to 5
Divide player_step_count (5) by the delay (5) giving 1.0
Discard anything after the decimal place which gives 1
Add 1 to get image number 2

With player_step_count set to 19
Divide player_step_count (19) by the delay (5) giving 3.8
Discard anything after the decimal place which gives 3
Add 1 to get image number 4

With player_step_count set to 20, the maximum value has been exceeded so set back to 0 and recalculate the value.

其中大部分使用基本运算,但是要丢弃小数点后的值,您将需要函数 floor(),它包含在 math 模块中。floor 函数定义为返回小于或等于 x 的最大整数值。

数学模块包括几个数学函数,在创建游戏时会很有用。更多详情可查阅 https://docs.python.org/3.5/library/math.html

要导入数学模块,请在代码顶部添加以下行:

import math

然后更新 set_actor_image 函数(它被添加到代码的底部)以匹配以下内容:

# Show image matching new_direction and current step count
def set_actor_image (new_direction):
    global player, player_step_count

    step_delay = 5
    player_step_count += 1

    if player_step_count >= 4 * step_delay:
        player_step_count = 1

    player_step_position = math.floor(player_step_count / step_delay) +1
    player.image = "compassgame_person_"+new_direction+"_"+str(player_step_position)

更新后的文件作为 compassgame-movement3.py 包含在源代码中。

如果您运行更新的代码,您应该会看到腿以更真实的速度移动。

保持游戏状态

编程中的一个重要概念是能够跟踪程序所处的状态。这是程序需要跟踪过去发生的事情以及影响它如何处理未来事件的地方。

如果你从棋盘游戏的角度来考虑,那么最初的状态可能是你把游戏从盒子里拿出来,并把适当的指示物放入每个位置。一旦游戏设置好了,那么可能会有另一个状态来决定谁将首先开始游戏(可能基于骰子的滚动)。

然后,当游戏开始时,每个人之间的状态将依次改变,以便他们滚动设备,移动到下一个位置,并执行任何所需的动作。最后,当一个玩家达到目标时,会有一些胜利的状态。

在电脑游戏中,这是需要使用一个或多个变量来跟踪的东西。如果游戏代码正在显示菜单屏幕,而不是游戏已经在进行中,那么游戏代码可以不同地处理按键。变量可以是任何东西,从具有特定会议的单个数字到具有多个属性的完整类。

对于这个游戏,代码需要跟踪两件事。一个是游戏的状态,让它在游戏结束时不会一直在屏幕上移动角色,另一个是玩家需要往哪个方向移动。这些可以合并成一个变量,但是为了更容易理解,这个例子使用了两个独立的变量,一个叫做 game_state,另一个叫做 target_direction。

第一个变量称为 game_state,它跟踪游戏中的不同阶段。当游戏还没有开始时,这是一个空字符串;当游戏正在进行时,这是一个“正在玩”的字符串;当游戏已经结束时,这是一个“结束”的字符串。

总之:

  • "–游戏未开始

  • “玩”——游戏正在进行中

  • “结束”——游戏结束

对于目标方向,变量可以是不同的基本方向(罗盘上的四个主要方向)。这些是“北”、“东”、“南”和“西”,如图 3-5 所示。

img/488486_1_En_3_Fig5_HTML.jpg

图 3-5

罗盘上的四个点

代码将被更新以生成随机方向。因此,需要通过在文件顶部添加以下条目来导入随机模块:

import random

通过在文件顶部附近添加以下行来添加变量(例如在 BACKGROUND_IMG 条目之后):

game_state = "
target_direction = "

在更新函数顶部附近,用以下内容替换global direction行:

    global direction, game_state, target_direction

    # If state is not running then we give option to start or quit
    if (game_state == " or game_state == 'end'):
        # Display instructions (in draw() rather than here)
        # If space key then start game
        if (keyboard.space):
            game_state = "playing"
            target_direction = get_new_direction()
        # If escape then quit the game
        if (keyboard.escape):
            quit()
        return

在文件的底部,添加以下函数:

def get_new_direction():
    move_choices = ['north', 'east', 'south', 'west']
    return random.choice(move_choices)

这段代码将处理游戏开始时的状态。

如果游戏没有进行,那么它等待玩家按下开始键,在这个例子中是空格键。如果按下该按钮,它会将状态设置为正在播放,并分配一个新的目标方向。

get_new_direction 函数有一个不同方向的列表,并使用 random choice 来随机选择一个方向。

这在源代码中以 compassgame_state1.py 的形式提供。

你现在可以再次运行游戏。记住你现在需要按下空格键,玩家才能被移动。

接下来要添加的是告诉玩家走哪条路的方法。这可以通过使用 screen.draw.text()来完成,它将在屏幕上显示文本。用以下代码替换当前的 draw 函数:

def draw():
    screen.blit(BACKGROUND_IMG, (0,0))
    # If game not running then give instruction
    if (game_state == "):
        # Display message on screen
        screen.draw.text("Press space bar to start", center=(WIDTH/2,HEIGHT/2), fontsize=60, shadow=(1,1), color=(255,255,255), scolor="#202020")
    elif (game_state == 'end'):
        screen.draw.text("Game Over\nPress space bar to start again", center=(WIDTH/2,HEIGHT/2), fontsize=60, shadow=(1,1), color=(255,255,255), scolor="#202020")
    else:
        screen.draw.text(target_direction, center=(WIDTH/2,50), fontsize=60, shadow=(1,1), color=(255,255,255), scolor="#202020")
        player.draw()

新的 draw 功能根据游戏状态显示三个不同的文本块。第一个方块是当game_state = ",这种情况下它指示玩家按空格键开始游戏。第二个块由 elif (else if)控制,它检查游戏是否结束,第三个块是游戏正在进行时。没有必要检查正在玩游戏的状态,因为如果不是前两个状态,那么它一定是在玩状态。

只有在玩游戏的时候才会调用player.draw,否则文本会覆盖玩家。现在还不可能到达游戏的终点。这将是以后要实施的事情。

这段代码有趣的地方在于显示文本的部分。以下是第一个条目的详细信息,但其他条目都以类似的方式工作:

screen.draw.text("Press space bar to start", center=(WIDTH/2,HEIGHT/2), fontsize=60, shadow=(1,1), color=(255,255,255), scolor="#202020")

text 方法接受一个要显示的字符串和一个位置;其余的参数是可选的。只需将一个元组作为第二个参数,例如(10,10),就可以输入位置。在这种情况下,文本在屏幕中央看起来更好,因此元组被传递给 center 参数。它使用宽度和高度值的一半来确定位置。

这里使用的其他可选参数有

  • fontsize–用于设置字体的大小;默认值为 24。

  • 阴影–给文本添加阴影;这些值是阴影位置的 x 和 y 偏移。

  • 颜色–文本的颜色。

  • 颜色

如您所见,代码使用不同的方式输入颜色。您可以使用一些不同的颜色格式,例如(r,g,b),其中(255,255,255)是白色,或者 html 颜色字符串,其中“#202020”是浅灰色。有关如何创建颜色的更多详细信息,请参见第六章。

Note

由随机模块创建的随机数是伪随机的。计算机很难创建真正的随机数,因此它们有办法生成对最终用户来说看起来是随机的数字。根据操作系统和硬件的不同,它可能包括不太确定的来源,如时间和鼠标移动,使其不太可预测。这对于游戏来说通常已经足够了,但是如果将它用于加密目的,您可能需要查看其他随机来源。

检测碰撞

如果到目前为止你已经遵循了代码(或者运行了 compassgame-movement3.py ),那么你现在应该有一个可以在屏幕上工作的角色了。下一步是检测玩家何时移动到屏幕的正确一侧。在这种情况下,角色靠近那一边就足够了,而不是在最边上,因为这比必须停在那一边更自然一些。实现的方法之一是创建代码来查看字符的位置,并检查它是否达到某个阈值。虽然这是一种有效的方法,但是如果你改变了字符的大小(可能是不同的高宽比),你可能需要更新代码来处理这个问题,这就有点不灵活了。相反,Pygame Zero 提供了一个很好的功能,允许它检查冲突。

不幸的是,Pygame Zero 文档没有提供太多关于检测碰撞的信息。Pygame Zero 使用标准的 Pygame 方法,这些方法在 Pygame 文档中有很好的记录(参见附录 B 中的链接)。

碰撞检测通常用于检测两个精灵(演员)是否碰撞。要理解这一点,你需要知道 Pygame 中的所有精灵都有一个 Rect 属性。这是当你通过 Pygame Zero 创建一个演员时自动创建的。Rect 是一个你看不见的虚拟矩形。它是完全包含图像大小的最小矩形。如图 3-6 所示,在演员周围添加了一个边框。

img/488486_1_En_3_Fig6_HTML.jpg

图 3-6

带边框的演员

collideRect 方法可用于确定一个矩形是否与另一个矩形重叠。例如,考虑一个汽车游戏,其中两辆汽车在游戏区域内移动,您想知道它们是否会相撞。如果您有名为 car1 和 car2 的演员,您可以使用

if car1.collideRect(car2):
    print ("Car 1 and Car 2 have collided")

回到游戏,目前在做《罗盘游戏》;对于这个特殊的检测,我们实际上不需要创建一个演员来碰撞,我们只需要知道他们何时接近屏幕的边缘。相反,我们可以创建一个具有适当尺寸的简单 Rect 对象。然后,如果演员与这些矩形中的一个碰撞,我们知道它们在那个区域。图 3-7 中的图像显示了游戏的布局,矩形显示在游戏网格上。在这张图片中,矩形被夸大了,以便更容易看到。

img/488486_1_En_3_Fig7_HTML.jpg

图 3-7

检测玩家接近屏幕边缘的碰撞矩形

你会看到矩形重叠。这不是这个游戏的问题,因为我们只是检查玩家是否达到了其中之一,但这是你在创建其他游戏时需要注意的。

以下代码用于创建矩形,可在 draw 函数之前添加:

#Rectangles for compass points for collision detection to ensure player is in correct position
box_size = 50
north_box = Rect((0, 0), (WIDTH, box_size))
east_box = Rect((WIDTH-box_size, 0), (WIDTH, HEIGHT))
south_box = Rect((0, HEIGHT-box_size), (WIDTH, HEIGHT))
west_box = Rect((0, 0), (box_size, HEIGHT))

前面的矩形是不可见的,这就是我们想要的。暂时显示矩形是一个好主意,因为它可以帮助显示任何矩形是否在错误的位置。为此,您可以将以下内容添加到 draw 函数中(在 screen.blit 之后)。这还包括一个围绕玩家的盒子,并为每个矩形使用不同的颜色。

    screen.draw.rect(north_box, (255,0,0))
    screen.draw.rect(east_box, (0,255,0))
    screen.draw.rect(south_box, (0,0,255))
    screen.draw.rect(west_box, (255,255,255))
    screen.draw.rect(Rect(player.topleft, player.size), (0,0,0))

源代码以 compass game-collidel 1 . py 的形式包含在内。对该版本做一点小小的改动是一个好主意,这样您就可以看到当您在屏幕上移动时演员周围的矩形也在移动。每当玩家周围的方框与其他矩形重叠时,就可以检测到冲突。

要检测冲突,您可以将以下代码添加到更新函数的底部:

    if (player.colliderect(north_box)):
        print ("Collided with North")
    if (player.colliderect(south_box)):
        print ("Collided with South")
    if (player.colliderect(east_box)):
        print ("Collided with East")
    if (player.colliderect(west_box)):
        print ("Collided with West")

这作为 compass game-collider 2 . py 包含在源代码中。现在,如果你运行这个程序,如果你在 Mu 的控制台上观看(或者在终端上观看,如果你从那里启动的话),每当玩家进入一个矩形时,你会看到几条打印消息。

这有利于测试,但是您现在应该在下一阶段之前用 colliderect 和 draw.rect 语句删除代码块。

使用矩形是一种执行碰撞检测的便捷方式,对于这个游戏来说足够好了。如果使用其他游戏,你可能需要考虑精灵如何互动,特别是当他们周围有很多“空白”的时候。如果你有一个没有填满矩形的形状,那么如果一个玩家实际上没有接触到另一个物体,但是矩形重叠了,这可能会让玩家感到沮丧。相反,可以使用碰撞点方法在特定点上进行测试,或者在您自己的代码中实现更精确的碰撞检测。

方向的改变

现在你可以添加代码来处理玩家到达目标时的情况。一旦他们到达要求的区域,玩家需要被告知他们下一步需要去哪里。玩家应该在被告知下一个目标之前移动到新的位置,以此类推。

删除打印出冲突通知的代码后,在更新函数底部的位置添加以下内容:

    if (reach_target(target_direction)):
        target_direction = get_new_direction()

此外,在更新功能后添加以下内容:

def reach_target(target_direction):
    if (target_direction == 'north'):
        if (player.colliderect(north_box)):
            return True
        else:
            return False
    elif (target_direction == 'south'):
        if (player.colliderect(south_box)):
            return True
        else:
            return False
    elif (target_direction == 'east'):
        if (player.colliderect(east_box)):
            return True
        else:
            return False
    elif (target_direction == 'west'):
        if (player.colliderect(west_box)):
            return True
        else:
            return False

更新函数中的额外代码将使用函数 reach_target 检查玩家是否到达了他们的目标目的地。

如果玩家与当前目标方向相关的盒子发生碰撞,则 reach_target 函数返回 true。如果不是,则返回 false。

这段代码可以从 compassgame-collide3.py 获得。

如果你运行这个游戏,你应该在屏幕的顶部看到指令,如果你去指定它们的一边,那么你会得到一个新的指令。

记分

要添加评分机制,只需要有一个变量,每次达到目标时都会更新。要实现这一点,需要创建一个新的全局变量来保存当前的分数。

# Current score for this game
score = 0

这需要是一个全局变量,所以在 update 函数中,将全局行更新为

global direction, game_state, target_direction, score

每次游戏开始都需要重置分数,所以在 game_state 设置为“正在玩”的文本块中添加score = 0

要增加分数,在更新函数的底部,在 if 语句中添加 score += 1,该语句检查是否达到目标。

因此

    if (reach_target(target_direction)):
        target_direction = get_new_direction()

成为

    if (reach_target(target_direction)):
        target_direction = get_new_direction()
        score += 1

这将记录分数。要在屏幕上显示它,您可以更新 draw 函数来显示分数。首先将它作为一个全局变量添加到抽奖开始处,然后在最后的 else 文本块中添加以下内容,以便在游戏进行过程中显示分数。您可以将它放在调用 player.draw()之前。

        screen.draw.text('Score '+str(score), fontsize=60, center=(WIDTH-130,50), shadow=(1,1), color=(255,255,255), scolor="#202020")

你也可以在游戏结束部分添加最终得分。

        screen.draw.text("Game Over score "+str(score)+"\nPress space to start", fontsize=60, center=(WIDTH/2,HEIGHT/2), shadow=(1,1), color=(255,255,255), scolor="#202020")

draw 函数将如下所示:

def draw():
    global score
    screen.blit(BACKGROUND_IMG, (0,0))

    # If game not running then give instruction
    if (game_state == "):
        # Display message on screen
        screen.draw.text("Press space bar to start", center=(WIDTH/2,HEIGHT/2), fontsize=60, shadow=(1,1), color=(255,255,255), scolor="#202020")
    elif (game_state == 'end'):
        screen.draw.text("Game Over "+str(score+"\nPress space bar to start again", center=(WIDTH/2,HEIGHT/2), fontsize=60, shadow=(1,1), color=(255,255,255), scolor="#202020")
    else:
        screen.draw.text(target_direction, center=(WIDTH/2,50), fontsize=60, shadow=(1,1), color=(255,255,255), scolor="#202020")
        screen.draw.text('Score '+str(score), fontsize=60, center=(WIDTH-130,50), shadow=(1,1), color=(255,255,255), scolor="#202020")
        player.draw()

这作为 compassgame-score.py 包含在源代码中。

如果您运行更新的代码,您将看到分数随着您达到每个目标而增加。

添加倒计时器

最后,需要有一些东西使它成为一个挑战。否则,你可以永远在两边之间徘徊。如果没有任何形式的挑战,我敢肯定大多数人会很快厌倦。

为了增加挑战,将会有一个计时器,这样玩家需要在设定的时间内在屏幕上移动。计时器将在固定时间启动,例如 10 秒,给玩家时间到达目标。如果他们成功了,那么计时器将被重置,但会稍微递减,使其变得更难。如果他们不能在规定时间内完成,那么游戏就结束了。

计算时间的一种粗略方法是考虑更新功能的运行频率。在 Pygame Zero 中,更新函数通常每秒被调用 60 次,大约 0.016 秒,所以通过计算函数被调用的次数,你可以计算出玩家完成任务需要多长时间。这样做的问题是循环的频率没有保证;如果计算机很忙,那么它可能需要更长的更新时间,给玩家一个不公平的优势。相反,代码应该跟踪自上次调用更新函数以来已经过了多长时间。这可以通过向 update()方法添加一个参数来确定自上次运行以来有多长时间。为此,用update(time_interval)替换 update()。time_interval 变量将设置为自上次运行更新以来的秒数(大约为 0.016)。

要实现这一点,请添加以下全局变量:

# Number of seconds to play when the timer starts
  timer_start = 10.9
# number of seconds to decrement the timer each time we score a point
timer_decrement = 0.2
# This is the actual timer set to the initial start value
timer = timer_start

在 update 方法中将 timer 变量添加到全局变量列表中(不需要添加其他新变量,因为我们不需要更改它们)。

在处理游戏开始时按下 keyboard.space 键的代码块中,添加

        timer = timer_start

就在方向键被按下之前,减少计时器并检查我们没有低于 0.9。

    # Update timer with difference from previous
    timer -= time_interval
    # Check to see if timer has run out
    if (timer < 0.9):
        game_state = 'end'
        return

然后在分数增加之后(每次达到目标),需要重置计时器(但是包括基于当前分数的递减)。

        # Update timer - subtracting timer decrement for each point scored
        timer = timer_start - (score ∗ timer_decrement)

最后,要在屏幕上看到 timer,将 timer 作为全局变量添加到 draw 函数中,并在屏幕上显示分数的同时添加以下内容。

        screen.draw.text('Time: '+str(math.floor(timer)), fontsize=60, center=(100,50), shadow=(1,1), color=(255,255,255), scolor="#202020")

你可能想知道为什么 10 秒倒计时的计时器设置为 10.9 秒。

这是因为打印使用 floor 函数来去除任何分数,并以整秒显示计时器。如果我们测试计时器在零以上,玩家将期望计时器显示达到零时游戏立即结束,而不是继续计算下一秒。此外,玩家也希望计时器停留在 10 秒,而不是 9 秒,一旦我们减去第一个时间间隔。从 10.9 秒开始计时,到不到 1 秒结束,几乎正好是 10 秒,用户将看到从 10 到 0 的值。

指南针游戏版本 0.1 的最终代码

你现在将有一个完整的游戏可以玩。当你到达终点时,它会告诉你你的分数。然后你可以按空格键来试试这个游戏,看看你是否能打破这个分数。游戏的完整列表,到目前为止,包含在列表 3-4 中。这也作为 compassgame-v0.1.py 包含在源代码中。

import random
import math

WIDTH = 800
HEIGHT = 600

BACKGROUND_IMG = "compassgame_background_01"

game_state = "
target_direction = "

#Player character
player = Actor('compassgame_person_down_1', (WIDTH/2,HEIGHT/2))
# Which image is being displayed
player_step_count = 1
# Direction that player is facing
direction = 'down'

# Number of seconds to play when the timer starts
timer_start = 10.9
# number of seconds to decrement the timer each time we score a point
timer_decrement = 0.2
# This is the actual timer set to the initial start value
timer = timer_start

#Rectangles for compass points for collision detection to ensure player is in correct position
box_size = 50
north_box = Rect((0, 0), (WIDTH, box_size))
east_box = Rect((WIDTH-box_size, 0), (WIDTH, HEIGHT))
south_box = Rect((0, HEIGHT-box_size), (WIDTH, HEIGHT))
west_box = Rect((0, 0), (box_size, HEIGHT))

# Current score for this game
score = 0

def draw():
    global score, timer
    screen.blit(BACKGROUND_IMG, (0,0))

    # If game not running then give instruction
    if (game_state == "):
        # Display message on screen
        screen.draw.text("Press space bar to start", center=(WIDTH/2,HEIGHT/2), fontsize=60, shadow=(1,1), color=(255,255,255), scolor="#202020")
    elif (game_state == 'end'):
        screen.draw.text("Game Over "+str(score)+"\nPress space bar to start again", center=(WIDTH/2,HEIGHT/2), fontsize=60, shadow=(1,1), color=(255,255,255), scolor="#202020")
    else:
        screen.draw.text(target_direction, center=(WIDTH/2,50), fontsize=60, shadow=(1,1), color=(255,255,255), scolor="#202020")
        screen.draw.text('Score '+str(score), fontsize=60, center=(WIDTH-130,50), shadow=(1,1), color=(255,255,255), scolor="#202020")
        screen.draw.text('Time: '+str(math.floor(timer)), fontsize=60, center=(100,50), shadow=(1,1), color=(255,255,255), scolor="#202020")
        player.draw()

def update(time_interval):
    # Need to be able to update global variable direction
    global direction, game_state, target_direction, score, timer_start, timer_decrement, timer

    # If state is not running then we give option to start or quit
    if (game_state == " or game_state == 'end'):
        # Display instructions (in draw() rather than here)
        # If space key then start game
        if (keyboard.space):
            game_state = "playing"
            timer = timer_start
            target_direction = get_new_direction()
        # If escape then quit the game
        if (keyboard.escape):
            quit()
        return

    # Update timer with difference from previous

    timer -= time_interval
    # Check to see if timer has run out
    if (timer < 0.9):
        game_state = 'end'
        return

    # Check for direction keys pressed

    # Can have multiple pressed in which case we move in all the directions
    # The last one in the order below is set as the direction to determine the
    # image to use
    new_direction = "
    if (keyboard.up):
        new_direction = 'up'
        move_actor(new_direction)
    if (keyboard.down):
        new_direction = 'down'
        move_actor(new_direction)
    if (keyboard.left) :
        new_direction = 'left'
        move_actor(new_direction)
    if (keyboard.right) :
        new_direction = 'right'
        move_actor(new_direction)
    # If new direction is not "" then we have a move button pressed
    # so set appropriate image
    if (new_direction != ") :
        # Set image based on new_direction
        set_actor_image (new_direction)
        direction = new_direction

    if (reach_target(target_direction)):
        target_direction = get_new_direction()
        score += 1
        # Update timer - subtracting timer decrement for each point scored

        timer = timer_start - (score ∗ timer_decrement)

def reach_target(target_direction):
    if (target_direction == 'north'):
        if (player.colliderect(north_box)):
            return True
        else:
            return False
    elif (target_direction == 'south'):
        if (player.colliderect(south_box)):
            return True
        else:
            return False
    elif (target_direction == 'east'):
        if (player.colliderect(east_box)):
            return True
        else:
            return False
    elif (target_direction == 'west'):
        if (player.colliderect(west_box)):
            return True
        else:
            return False

def move_actor(direction, distance = 5):
    if (direction == 'up'):
        player.y -= distance
    if (direction == 'right'):
        player.x += distance
    if (direction == 'down'):
        player.y += distance
    if (direction == 'left'):
        player.x -= distance

    # Check not moved past the edge of the screen

    if (player.y <= 30):
        player.y = 30
    if (player.x <= 12):
        player.x = 12
    if (player.y >= HEIGHT - 30):
        player.y = HEIGHT - 30
    if (player.x >= WIDTH - 12):
        player.x = WIDTH - 12

# Show image matching new_direction and current step count
def set_actor_image (new_direction):
    global player, player_step_count

    step_delay = 5
    player_step_count += 1

    if player_step_count >= 4 ∗ step_delay:
        player_step_count = 1

    player_step_position = math.floor(player_step_count / step_delay) +1
    player.image = "compassgame_person_"+new_direction+"_"+str(player_step_position)

def get_new_direction():
    move_choices = ['north', 'east', 'south', 'west']
    return random.choice(move_choices)

Listing 3-4Compass game. A simple Pygame Zero program with image background

完整的游戏大概 170 行代码,包括注释和空行。这听起来可能很多,但比在许多其他编程语言中要少得多。

摘要

本章介绍了 Pygame Zero 以及创建第一个图形游戏。代码相当长,这反映了创建一个游戏所付出的努力,但它比许多其他编程语言中所需的同等代码要短得多。

这个游戏目前还很基础,将在下一章游戏设计中进一步开发。