Python-PyGame-和树莓派游戏开发教程-全-
Python、PyGame 和树莓派游戏开发教程(全)
一、什么是编程语言?
计算机程序是计算机为了完成一项任务而必须执行的一系列语句,这项任务通常是一项重复的任务,需要人类花费很长时间来计算。计算机语言描述这些语句的排列或语法。有各种各样的计算机语言,每一种都适合一个或多个任务。
每种语言都有自己独特的语法和命令集,但它们都有执行大致相同类型操作的结构:
-
投入
-
输出
-
分支(基于数据做出决策)
-
环
命令或关键字是一种特殊的短语,该语言使用它来执行一个动作,无论是从用户那里获得输入还是在屏幕上显示文本。这些命令是保留字,不能在程序中用于任何其他目的。我们将在本书后面更深入地研究它们,但是 Python 中的关键字示例有
-
为
-
如果
-
及格
计算机程序是做什么的?
计算机程序一遍又一遍地执行一系列任务,操纵用户的输入,并在反馈回路中传递输出。当您移动鼠标时(输入),屏幕上的箭头会随之移动(输出)。
计算机程序的旧定义是一个基本的数学公式:
程序=算法+数据
算法是处理数据的一步一步的过程。该算法利用提供给它的数据来解决问题。什么样的问题?它可以是任何东西,从计算一个矩形的面积或一个房间的体积,根据操纵杆的输入将玩家的化身移动到哪里,或者决定敌人应该如何应对刚刚获得电源的玩家。
所有的计算机程序都是以同样的方式编写的吗?对于一个给定的问题,有标准的解决方法吗?嗯,不。不是真的。在计算机编程中有很多方法可以达到同样的效果!没有解决问题的正确方法。只要你的程序做了它应该做的,那就很好!你可能想稍后“调整”你的代码来加速它,但是一旦你有了正确的算法,任何优化都会发生。您的程序必须按预期运行。这是至关重要的。
结论
计算机程序用于对用户输入的一系列数据元素执行繁重的任务。对于游戏来说,这意味着在向玩家显示游戏世界的同时,更新玩家化身的位置并维护游戏世界。
坚持使用一种语言是不明智的,而是要尽可能多的体验多种语言。这将使你,程序员,决定哪种语言最适合给定的情况。你的第一语言是一个很好的选择;Python 是一种非常强大的语言,可以用于各种目的,非常适合第一次编程的人。
二、Python 是什么?
Python 是一种支持面向对象、函数式和命令式编程风格的现代编程语言。因为它的可读性和易用性,它是初学者的理想选择。所有这一切的好处是,与同等的 C/C++或 Java 程序相比,您可以用更少的代码行编写程序。
我刚才到底说了什么?让我们把最后一段分解一下,使它更易读一些。
编程风格
Python 适合以下风格的编程:
-
必要的
-
面向对象
-
功能的
命令式编程在很长一段时间里是编写计算机代码最常见的方式。它一步一步地描述了如何以非常线性的方式对数据进行更改。
例如,我们有以下项目:
-
茶叶袋
-
牛奶
-
杯子
-
匙
-
烧水用水壶
-
水
这些是我们在‘程序’中使用和操纵的东西;这是我们的数据。我们希望将该数据更改为不同的状态。什么州?嗯,我们想要一杯奶茶。我们如何做到这一点?我们规定了一系列操作,将这些数据转换成其他数据,如下所示:
-
将茶包放入杯中
-
将水倒入水壶
-
烧开水壶
-
当水壶烧开的时候,看电视
-
把水壶里的水倒到杯子里
-
把牛奶倒进杯子里
-
用勺子搅拌茶
-
服务
在代码中(不特别是 Python 代码),这可以写成
addTo(cup, tea_bag)
addTo(kettle, water)
boil(kettle)
while isBoiling(kettle):
watchTV()
addTo(cup, getWaterFrom(kettle))
addTo(cup, milk)
stir(cup)
serve(cup)
这些是改变我们的初始数据(我们的输入)并将其转换为我们的输出的规定步骤(过程)。见图 2-1 。
图 2-1。
输入、过程、输出框图
面向对象
命令式程序将功能(算法)与数据分开。面向对象语言保留了数据的功能。对象在一个地方包含数据和用于操作该数据的指令。
这是有好处的;它存储的算法处理你的数据。让我们以一支铅笔为例。它有一些描述它的属性:
-
颜色
-
困难
-
笔尖尺寸
-
长度
它还具有可以应用于它的某些操作或方法:
-
写
-
抹去
-
尖锐
这些方法改变对象的状态;记住状态是由数据决定的。例如,当你用铅笔写字时,笔尖的长度会越来越短。当您削尖铅笔时,它的总长度会变短,但笔尖大小会重置为最大值。
功能的
函数式编程并不新鲜,最早是在 20 世纪 30 年代开发的。它源于λ微积分。函数式编程使用数学函数来执行计算。这些计算中没有数据发生变化;而是计算新值。这意味着函数式程序没有状态。
函数式编程倾向于用于递归(从自身调用同一个函数)和项目迭代。
在 Python 中,可以用下面一行代码计算斐波那契数:
fib = lambda n: n if n < 2 else fib(n-1) + fib(n-2)
这摘自一篇关于 StackOverflow ( http://bit.ly/FibonacciPython
)的讨论。
要计算一个值,程序员只需传入一个整数值:
fib(5)
Pygame 是什么?
Pygame 是由 Pete Shinners 作为一个简单的 DirectMedia 库(SDL)的包装器开始的。它自 2000 年以来一直由社区维护,并在 GNU 宽松通用公共许可证下发布。这意味着如果你愿意,你可以随意查看源代码。
Pygame 的创建是为了允许在不使用 C 或 C++等编程语言的情况下开发游戏。
Pygame 可以用来编写复古风格的快节奏 2D 游戏,或者现代休闲和超休闲游戏。它处理加载图像、显示精灵、播放声音等困难。,给你的。
更多关于 Pygame 的细节,请访问他们的网站: www.pygame.org/news
。
结论
Python 是一种现代的多参数编程语言。它可用于命令式、面向对象和函数式编程。
此外,Pygame 是一个允许你在 2D 创建快节奏动作游戏的框架。
所以,现在我们知道了 Python 的能力,是时候看看语言本身了。
三、Python 简介
本章我们将介绍 Python 语言。在这个阶段,我们只对理解 Python 语言及其关键字的格式或语法感兴趣。Python 是一种解释型语言,这意味着它需要另一个叫做解释器的程序来运行我们编写的任何代码。
Python 解释器程序叫做 Python,是一个可执行程序。当您从命令行单独运行 Python 时,您将看到以下内容:
pi@raspberrypi ∼ $ python
Python 2.7.9 (default, Jan 13 2013, 11:20:46)
[GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more info
>>>
这是 Python 解释器,将在您键入命令时运行每个命令块。
终端窗口
对于我们的前几个 Python 实验,我们将使用 Raspbian 中的终端窗口。要打开终端窗口,请点按屏幕左上角看起来有点像> _ 的图标。这将打开一个带有如下文本的窗口:
pi@raspberrypi:~ $
这是一个非常友好的提示,因为计算机正在告诉一些重要的信息。它显示您作为(pi@raspberrypi)登录,以及您在目录结构中的位置。在这种情况下,它是~它是您的主目录的简写。
该文本的右侧是光标。这是您键入的文本将出现的地方。
运行 Python 解释器
要启动 Python 解释器,请在终端窗口中键入以下内容:
$ python
Python 中的命令块是至少一行长的命令列表。现在让我们试一试:
print 'Hello, world!'
这将指示 Python 显示短语“Hello,world!”屏幕上。注意 Python 没有显示引号:
Hello, world!
这是因为‘你好,世界!’是字符串文字。字符串是包含字母数字或符号字符的任何短语,括在“和”或“和”之间。你不能混淆和匹配报价。能够同时使用这两者有时会变得非常方便。
让我们试试这个:
print "It's going to rain on Saturday."
使用双引号来标记字符串文字的开始和结束位置,我们可以使用单引号作为撇号:
It's going to rain on Saturday.
如果我们使用单引号,我们将不得不在行中添加一个特殊的转义字符:
print 'It\'s going to rain on Saturday.'
我们稍后会对字符进行转义,但是如果只是想在句子中放一个撇号,那就有点麻烦了!
让我们来分解一下我们刚刚使用的打印语句。print 是 Python 用来将信息输出到屏幕的关键字。第二部分,字符串文字,是 print 命令的参数。参数也称为自变量。
Python 被解读
Python 的每一行都有解释。这意味着计算机接受您键入的每一行代码,并一次一行地将其转换为计算机可以理解的代码。另一种类型的语言是编译语言。当一种语言需要编译来把你的源代码翻译成计算机能理解的语言时,这个过程是由另一个叫做编译器的程序来完成的。这是一个单独的程序,在你写完所有代码后运行。
因为 Python 语言是解释型的,所以运行它只需要一个程序:Python。当我们在交互式 Python shell 中时,我们输入的任何内容都会立即被程序解释,如果有结果的话,结果会显示在屏幕上。
作为计算器的 Python
假设我们想把两个数加在一起,为了便于讨论,2 和 2。在 Python 解释器中键入以下内容,然后按 return 键:
2+2
您将在屏幕上看到的是您(希望)期望看到的内容:
4
我们将在后面看到所有的算术运算(加、减、乘、除)都是可用的,还有一些你以前可能没见过的。他们会在你阅读课文时被介绍。
示例:
5 * 4
10 / 2
7 + 2
9 - 4
更复杂的东西呢,比如
2 + 2 * 6
你期望看到什么?24?为什么是 14?那是因为算术运算符是按优先顺序工作的,或者换句话说,有些运算符比其他运算符更重要。乘法运算符“*”和除法运算符“/”比分别用于加法和减法的+和–更重要。
如果你想保证操作的顺序,你可以像这样使用括号'('和')':
(2 + 2) * 6
其现在将给出 24,因为将首先执行 2 和 2 的相加,然后其乘积将乘以 6。注意你的括号!确保它们匹配。如果不这样做,您将得到一个延续标记'…'),如下所示:
>>> (2 + 2 * 6
...
假设您想以米为单位计算地板的面积(宽×长),并将该值转换为平方英尺。假设这个房间长 2 米,宽 4 米。您可以使用类似于
(2 * 4) * (3.28 * 3.28)
这是因为一米有 3.28 英尺;为了得到一平方米的英尺数,我们将 3.28 英尺乘以它本身,得到 10.7584。乘以 2 * 4 得到
86.0672
或者大约 86 平方英尺。
我们稍后将深入讨论这一点,但是现在我们应该花点时间来讨论到目前为止已经输入了什么。
您输入的数值称为常数。他们永远不会改变。1 永远是 1,24.234 永远是 24.234。为了安全起见,我们可以将常量存储在内存中,以便以后在程序中引用它们。计算机内存中的这些槽叫做变量。之所以这么叫,是因为我们存储的值会随着程序的进行而变化。假设我们想要存储常数 10.76。我们必须给它起个名字。此操作称为变量赋值,如下所示:
squareFeet = 10.76
你可以理解为“给平方英尺赋值 10.76”或者“给平方英尺赋值 10.76”,或者(我喜欢这样称呼)“平方英尺等于 10.76。”这更像是一种“说出你所看到的心态”!
任何时候我们想要使用这个变量,我们使用它的方式和使用一个常量是一样的。计算那个 2 × 4 米的房间的面积
(2 * 4) * squareFeet
Python 是区分大小写的!请注意,变量的名称是“平方英尺”,而不是“平方英尺”或“平方英尺”
关键词
Python 内置的关键字非常少,总共 31 个。从这些我们可以制作任何你想制作的程序,从简单的球棒和球游戏到电子表格应用,如果你想制作其中一个的话。Python 的关键字如下:
-
和
-
如同
-
维护
-
破裂
-
班级
-
继续
-
极好的
-
是吗
-
艾列弗
-
其他
-
除...之外
-
高级管理人员
-
最后
-
为
-
从
-
全球的
-
如果
-
进口
-
在
-
存在
-
希腊字母的第 11 个
-
不
-
或者
-
及格
-
打印
-
上升
-
返回
-
尝试
-
正在…
-
随着
-
产量
这些是语言的组成部分,如果你喜欢的话,是乐高积木。从所有这些词你可以创造任何东西,从简单的计算到游戏到应用软件。有时,大部分真正困难的工作已经为您完成,并以 Python 模块的形式提供。这是一个命令、例程和对象的库,它们打包在一起提供一个通用的功能。PyGame 是一个模块集合的例子。PyGame 中的每个模块都提供了预先编写的代码,可以在屏幕上绘制图像,从玩家那里获得输入,或者播放背景音乐,从而使程序员制作游戏变得更加容易。
印刷
我们已经看到了如何在屏幕上显示简单的结果,但是您可以对这些消息的格式(它们看起来是什么样子)有更多的了解。例如,您可以使用转义序列添加空白字符,如制表符,并使用 print 命令返回到文本。例如:
print("these\nare\non\nseparate\nlines")
print("tab over\tto here")
反斜杠字符“\”用于为下一个字符生成“转义”代码。转义字符或控制序列可以追溯到电传时代,用于控制我们正在打印的设备的输出:在这种情况下是屏幕。
有各种控制序列,这些序列及其描述在表 3-1 中列出。
表 3-1。
控制序列
|换码顺序
|
描述
|
| --- | --- |
| \ | 输出反斜杠 |
| ' | 输出单引号(') |
| " | 输出双引号(") |
| \a | 铃 |
| \b | 执行退格 |
| \f | 执行换页 |
| \n | 执行换行 |
| \N(姓名) | UNICODE 数据库中的字符命名名称 |
| \r | 执行回车 |
| \t | 执行水平定位 |
| \ uxxx | 具有 16 位十六进制值 xxxx 的字符 |
| \Uxxxxxxxx | 具有 32 位十六进制值 xxxxxxxx 的字符 |
| \v | 执行垂直制表符 |
| \ooo | 八进制值为 ooo 的字符 |
| \xhh | 十六进制值为 hh 的字符 |
从这些转义字符中,您可以创建复杂的输出。这可用于显示表格信息,例如:
print("Team\t\tWon\tLost\nLeafs\t\t1\t1\nSabres\t\t0\t2")
将显示下表:
Team Won Lost
Leafs 1 1
Sabres 0 2
这很好,但是如果我们想做得更好呢?假设我们想将数字右对齐而不是左对齐?这意味着将数字移动到与“赢”和“输”的最后一个字符相同的列这就是字符串格式发挥作用的地方。
字符串格式
字符串格式允许您决定信息如何以文本形式显示给用户。我们已经看到了如何通过决定文本放置的位置来操纵文本的可视部分;我们现在将研究数据如何呈现给用户。我们不需要改变数据:我们只是改变用户查看数据的方式。
通过在文本中为要插入的信息使用占位符来实现格式化。这些如表 3-2 所示。
表 3-2。
字符串格式占位符
|占位符
|
描述
|
| --- | --- |
| %s | 线 |
| %d | 整数 |
| %f | 浮点数 |
| %r | 原始值 |
原始值对于程序的最终用户来说不是特别有用,但是当您调试代码试图找出错误时,它会很方便。在调试章节的后面会有更多的内容。
如果我们想要显示三个数字,例如,一个对象的 x、y 和 z 坐标,那么我们可以使用类似
print("{%d, %d, %d}" % (1, 2, 3))
字符串文字中的“%”表示下面的项是占位符,并且是整数的“d”类型。字符串文字外的“%”用来表示“用'填充那些占位符”,然后括号中的最后一位'('和')'称为元组。这些值按照出现的顺序放在字符串中。输入该行时出现的文本是
{1, 2, 3}
让我们再试一次,但这次是玩家的名字、分数和完成百分比:
print("%s scored %d and completed %f of the quest" % ('Sloan', 15, 55))
这将输出
Sloan scored 15 and completed 55.000000 of the quest
您会注意到输出有点过了;浮点数显示了很多零。我们可以通过指定小数点右边应该有多少个点来最小化这个问题。
让我们将该行改为只显示两个小数点:
print("%s scored %d and completed %.2f of the quest" % ('Sloan', 15, 55))
现在该语句的输出是
Sloan scored 15 and completed 55.00 of the quest
我们还可以使用“%”符号后的数字来分隔值。例如:
print("%20s%20d" % ('Sloan', 15))
这将在宽度为 20 的列中显示值“Sloan”和“15 ”:
Sloan 15
这些值与它们的位置右对齐,它们都占据 20 列。如果我们想让玩家的名字靠左对齐呢?我们将使用负值:
print("%-20s%20d" % ('Sloan', 15))
通过使用负值,您指定需要 20 个空格,但文本必须靠左对齐:
Sloan 15
回到曲棍球队的例子,我们现在可以使用这些信息来更好地放置文本和数据。我们可以让表格使用固定值,而不是依赖于根据文本大小而变化的制表符,如下所示:
print("%s%s%s\n%s%d%d\n%s%d%d" % ('Team', 'Won', 'Lost', 'Leafs',1,1,'Sabres',0,2))
这显示了占位符前面没有值时的显示方式。现在每个项目的列宽为:
print("%-10s%10s%10s\n%-10s%10d%10d\n%-10s%10d%10d" % ('Team', 'Won', 'Lost', 'Leafs',1,1,'Sabres',0,2))
这么多“%”符号,不是吗!这是一个相当严重的案例,而且不是一个特别常见的案例。事实上,你可以用一种叫做变量赋值的东西来使它更容易阅读。我们现在将详细讨论这一点。
变量
当我们处理数据时,变量被用来在内存中存储数据。我们使用名字来访问数据。每个变量都可以赋值。这个值代表我们想要处理的数据。假设我们想存储玩家的名字,以便以后可以检索。
这叫做变量赋值。当我们给一个名字赋值时,我们说这个名字现在包含了赋值:
>>> player = 'Sloan'
>>> print(player)
Sloan
>>>
我们的变量赋值是
player='Sloan'
等号(=)的左边是名称,等号的右边是值。该值可以是字符串、整数或浮点数,也可以是复杂的数学公式。
命名变量
你可以随意称呼变量,但是我建议它们反映你期望存储在其中的数据。对于可以用作名字的字符,有几个注意事项。它们可以是字母数字字符,可以包含下划线字符(_),但名称不能以数字开头。您还应该小心以下划线字符开头的名称,因为这有时用于 Python 本身使用的内部名称,以及稍后将讨论的其他特殊情况。
这些是有效的变量名:
playerName
player1
numOfLives
_arg1
this_is_a_long_name_hope_its_worth__it__555
这些是无效的变量名。给出的理由显示在“#”的右边。“#”在 Python 中用作注释字符。一行中“#”后面的所有内容都被忽略:
123Fred # starts with a number
Fr*d # contains an illegal character '*'
player$ # contains an illegal character '$'
the Player # contains a space. Spaces are not allowed
Python 变量名区分大小写!小心:
thisVariable
不等同于
Thisvariable
你被警告了!小心你的箱子!
PYTHON 是区分大小写的!
Python 作为计算器,第二部分
请记住,计算机程序从用户那里获取信息,对其进行处理,并作为输出反馈给用户。在本节中,我们将把 Python 变成一个计算器。假设我们将一罐汽水的价格设定为 55 美分,让我们记住将值 55 放入变量中:
canOfSoda = .55
我们可以用这个变量名来回忆一罐汽水的价格。现在,假设我们被告知购买 12 罐汽水,让我们在另一个变量中也记住这一点:
numCans = 12
我们现在可以用一个简单的公式打印出 12 罐汽水的价值:
canOfSoda * numCans
但是等等!这是什么?!你不会得到 6.6 分,你会得到这个:
6.6000000000000005
这似乎有点奇怪,不是吗?为什么会发生这种情况?这都与精度有关。当计算机计算分数时,它们必须使用二进制数(以 2、0 或 1 为基数)来计算这些分数。当把它们转换回十进制值时,就不太对了。我们可以使用字符串格式自己整理它:
"%2.2f" % (canOfSoda * numCans)
那更好!我们可以进一步整理它,以显示美元(或当地货币符号)金额:
"$%2.2f" % (canOfSoda * numCans)
这将显示
'$6.60'
请注意,我们的值周围有' ';这是因为我们对 Python 终端进行原始输出,所以我们输入的任何内容都会立即得到处理和输出。如果我们想打印出不带引号的字符串,我们需要添加 print 命令:
print("$%2.2f" % (canOfSoda * numCans))
产出:
$6.60
然而坏消息是,一罐汽水的价格已经涨到了 70 美分。不过没问题,因为我们可以告诉 Python 记住新值:
canOfSoda = .7
现在当我们计算 12 罐汽水时,我们会得到一个新的值。
以下进程的输出显示以前的值、赋值和新值:
>>> canOfSoda
0.55
>>> canOfSoda = .7
>>> canOfSoda
0.7
>>>
如果我们想知道一打罐头的价格,我们使用和以前一样的行:
print("$%2.2f" % (canOfSoda * numCans))
您知道吗,您可以使用键盘上的上下光标(箭头)键在交互式 Python 程序中输入的 Python 语句的历史中向前或向后移动。当您单步执行语句时,您可以使用左右光标键沿行移动,使用 delete/backspace 键删除不需要的字符。可以节省你一些打字时间!
算术运算符
算术运算符是用于对数字执行算术运算的缩写符号。你在学校里会用到它们中的大多数;Python 使用了一些与学校使用的符号不同的符号(表 3-3 )。
表 3-3。
Python 算术运算符
|操作员
|
描述
|
例子
|
| --- | --- | --- |
| + | 加法;运算符两边的两个值相加在一起 | 4 + 5 等于 9 |
| - | 减法;运算符右侧的值从左侧的值中减去 | 5–4 等于 1 |
| ***** | 乘法;运算符两边的两个值相乘 | 2 × 3 等于 6 |
| / | 司;将运算符左侧的值除以右侧的值 | 10 / 5 等于 2 |
| % | 模数;将运算符左侧的值除以右侧的值,得到余数 | 5 / 2 等于 1 |
| ****** | 指数;通过右侧提供的电源提高左侧的值 | 2 ** 4 会给 16。这在数学上写为 24 或 2 * 2 * 2 * 2 |
| / | 楼层划分;将运算符左侧的值除以右侧的值,得到整数的下限值 | 5 / 2 将给出 2.0 |
数据类型
Python 使用了一种叫做鸭子类型的东西。Duck typing 确保了只要特定命名值(变量)的方法或功能存在,Python 就会对其执行该操作。诗人詹姆斯·惠特科姆·莱利想出了这个短语来描述归纳推理:
如果它看起来像鸭子,游泳像鸭子,叫声像鸭子,那么它很可能就是一只鸭子。
Python 也有特定的数据类型,这些数据类型用于描述变量的内容。Python 具有以下内置数据类型:
-
数字
-
顺序
-
绘图
-
文件
-
班级
-
例子
-
例外
数字类型
Python 中的数字可以用整数或分数来表示。整数被称为整数,它们没有分数部分,如-256,–5,1,5,9,17,2048。分数有一个小数点和小数点后的一些值,例如 0.5、0.333、–0.1。
整数由两种数据类型表示:“int”是 integer 的缩写,“long”是 long。分数用“float”表示还有一种叫做“复数”的数字,我们在游戏中不会用到,但是 Python 可以处理它。
数字数据类型在表 3-4 中有详细描述。
表 3-4。
数字数据类型
|数字类型
|
描述
|
| --- | --- |
| int | 整数的大小至少为 32 位(4 字节),这意味着您可以存储任何整数,最大为 4,294,967,295,包括 4,294,967,295。但是,这通常是一个有符号的值,这意味着值的范围实际上是从–2,147,483,648 到+2,147,483,647。 |
| 浮动 | 浮点数是具有小数部分的数字,如 2.4 或 1.49387。 |
| 龙 | 长整数的精度没有限制,因此可以用来存储它们的位数没有上限。 |
| 复杂 | 复数有实部和虚部。这些部分是浮点数。 |
此外,表 3-5 中所示的运算符可能会派上用场。这些在日常算术中并不使用,但是你可能想求一个值的反,或者把它从整数转换成浮点,反之亦然。
表 3-5。
附加运算符
|操作员
|
描述
|
例子
|
| --- | --- | --- |
| –x | 对值“x”求反 | –2 给–2 |
| +x | 保持值“x”不变 | +2 等于 2 |
| abs(x) | “x”的绝对值 | ABS(–5)给出 5 |
| int(x) | 将“x”转换为整数 | int(5.44)得出 5 |
| long(x) | 将“x”转换为整数 | long(5)给出 5 |
| 浮动(x) | 将“x”转换为整数 | float(5)给出 5.0 |
| 复数(实数,虚数) | 用实部“real”和虚部“imaginary”创建一个复数 | 复数(1,5)给出(1+5j) |
例如,要计算长 5 米、高 10 米的建筑物侧面的面积:
width = 5
height = 10
area = width * height
要显示该值,请键入
area
这将显示 5 × 10 的答案:
50
再次字符串格式化
让我们回到曲棍球比分表:
print("%-10s%10s%10s\n%-10s%10d%10d\n%-10s%10d%10d" % ('Team', 'Won', 'Lost', 'Leafs',1,1,'Sabres',0,2))
我们可以将它分解成更小、更易读的数据块。不要害怕这样做;让你的程序可读性比速度更好。你应该努力使代码正确,而不是快速。优化可以以后再来。
让你的代码可读!当你以后再看它的时候,你仍然能够理解它!
表格中使用了通用格式;每个队都有十个字符作为他们的名字和输赢号码。每一行的末尾都有一个新的字符。我们可以设置一个变量来记住这种格式:
formatter="%-10s%10s%10s\n"
然后,只需为每个标题和团队分配使用这种格式的变量:
header=formatter % ("Team", "Won", "Lost")
leafs=formatter % ("Leafs", 1, 1)
sabres=formatter % ("Sabres", 0, 2)
现在我们已经将标题和团队数据存储在变量中,我们可以将它们组合在一行中来绘制我们的表格:
print("%s%s%s" % (header, leafs, sabres))
如果我们愿意,我们可以把它赋给一个变量,然后打印出来。我们的变量赋值如下所示:
table = "%s%s%s" % (header, leafs, sabres)
如果我们只是在 Python 解释器程序中输入表格,我们会得到如下显示:
'Team Won Lost\nLeafs 1 1\nSabres 0 2\n'
这是 name 表内容的原始输出。这向我们显示了名称表包含的内容,但不是它将如何显示。要正确显示表格
print(table)
将显示
Team Won Lost
Leafs 1 1
Sabres 0 2
结论
通过在终端窗口中输入 python,可以通过 Python 解释器交互式地使用 Python。虽然这对于一次性计算和简单的文本输出很方便,但我们现在将通过使用文本编辑器之类的工具创建实际的程序来更深入地研究 Python 的世界。
四、从解释器挣脱出来
到目前为止,我们一直使用解释器来编写代码。当输入每一行时,Python 程序会对其进行解释,处理后的行输出会显示在屏幕上。从现在起我们将使用闲置。
要退出解释器,如果您还没有这样做,请按 Ctrl+D 或输入 quit()。保持终端窗口打开!我们很快就会需要它。
什么是闲?
在本书中,我们将使用集成开发环境(IDE ),它包含在 Raspbian IDLE 中,是集成开发和学习环境的缩写。
起动怠速
要启动 IDLE,点击 Raspberry Pi logo,打开“编程”入口,点击“Python 3 (IDLE)”。Python shell 将在一个新窗口中打开,如图 4-1 所示。
图 4-1。
启动空闲 IDE
开始一个新文件
要创建新文件,请单击文件➤新建文件或按键盘上的 Ctrl+N(图 4-2 )。
图 4-2。
从“文件”菜单中选择“新建文件”,创建一个新的编辑器窗口
这将打开一个新的文本编辑器窗口,我们可以输入组成我们程序的代码(图 4-3 )。
图 4-3。
将用于编写 Python 程序的空白编辑器窗口
这是一个好主意,组织你的工作,并知道在哪里可以很容易地找到它。这里将展示一些基本的项目管理。我们将首先在当前用户的主目录(通常是/home/pi)中创建一个文件夹,并将这个新文件夹命名为“pygamebook”(不带引号)。我们会把我们写的所有程序放在这个文件夹里。我们可以为每个项目创建子文件夹,但是“pygamebook”是我们的主文件夹。
在终端窗口/命令提示符下,输入以下命令,在每一行后按 enter 键创建 pygamebook 文件夹:
$ cd
$ mkdir pygamebook
第一行将确保在您的主目录(~)中创建“pygamebook”文件夹。第二行创建了一个名为“pygamebook”的目录(mkdir 是“make directory”的缩写)。使用此文件夹将您从本书创建的所有文件放在一起。
你好,世界!
大多数人编写的第一个计算机程序是显示“你好,世界!”信息的程序在屏幕上。这本书也不会有什么不同!在下面的空白窗口中键入代码,每一行都是我们进行的描述。
任何 Python 脚本文件的第一行都是 Python 解释器的位置。这被称为 hash-bang,看起来像这样:
#!/usr/bin/python
所有程序都由外壳运行。这是计算机操作系统的一部分,控制程序对内存、磁盘驱动器等资源的访问。
因为源文件只是文本文件,所以这个 hash-bang 让 shell 知道它是一个应该由位于/usr/bin/python 的 Python 解释器运行的脚本。
现在我们已经准备好了,我们可以开始我们的程序了。在这种情况下,它非常简单;我们标准的“你好,世界!”程序:
print("Hello, World!")
现在,编辑器窗口中应该有以下几行:
#!/usr/bin/python
print("Hello, World!")
通过单击文件➤保存或按键盘上的 Ctrl+S 来保存文件。出现提示时,将文件另存为“hello.py”(不带引号),保存到我们之前创建的“pygamebook”文件夹中。
从命令行运行
如果您想从命令提示符运行您的程序,您将不得不再执行一个步骤。默认情况下,Raspbian 不使文件可执行;我们必须这么做。在终端窗口中,移动到“pygamebook”文件夹,并使用 chmod 命令使程序可执行。以下命令序列将完成此操作:
$ cd
$ cd pygamebook
$ chmod +x hello.py
这将可执行标志添加到文件的属性中。没有这个属性,操作系统将无法运行我们的程序。要在终端窗口中运行程序,请键入
每个脚本只需添加一次可执行标志属性!
$ ./hello.py
为什么我们要加上。/'?这是因为在 Raspbian 中,可执行文件是通过一系列路径来搜索的。我们的新文件夹不是系统路径的一部分,所以我们必须告诉它它在哪里。幸运的是,这有一个捷径;当前目录名为“.”
如果您愿意,可以省略这一步;事实上,只有当您像前面显示的那样单独运行程序时,才需要 hash-bang 行。如果省略该行,Raspbian shell 不知道使用什么程序来运行脚本。在这种情况下,您可以使用
$ python hello.py
这将启动 python 并运行“hello.py”脚本(图 4-4 )。
图 4-4。
添加可执行属性并从命令行运行 hello.py
从内部空闲运行
要在空闲状态下运行程序,按键盘上的 F5 键或点击菜单中的运行➤运行模块(图 4-5 )。
图 4-5。
通过从运行菜单中选择运行模块或按键盘上的 F5 来运行程序
当程序运行时,你应该看到“你好,世界!”显示在窗口中(图 4-6 )。
图 4-6。
在空闲的内部运行 hello.py
从现在开始,在本文中,这本书将专注于为我们的 Python 程序编写脚本文件,而不是像我们在前几章那样使用 Python 解释器。
当创建一个将从命令行运行的 PYTHON 脚本文件时,总是将解释器的路径作为第一行放在 HASH-BANG: #!/usr/bin/python
在很大程度上,我将从示例程序中省略这一行,并假设我们将从 IDLE 中运行或用 python 启动我们的程序。
结论
Raspbian 包括一个名为 IDLE 的 Python IDE,可以用来编辑和运行 Python 程序,而不需要使用终端窗口。您仍然可以在终端窗口中运行使用 IDLE 创建的 Python 脚本,只需确保添加了 hash-bang 行来显示在执行该脚本时应该运行什么 Python 解释器程序。
在整篇文章中,我将交替使用脚本和程序。脚本是一个文本文件,由程序解释以执行其中的指令。程序是类似的,但它通常(但不总是)被编译成机器代码。由于这些相似性,我不打算在本文中吹毛求疵地谈论 Python 源文件是被称为程序还是脚本。
五、做决策
到目前为止,我们已经看到了非常线性的程序。这些程序从一个语句到下一个语句,从不偏离。它们只是一个线性购物清单。你先吃蔬菜,然后是面包,然后是蔬菜罐头,最后是猫粮。为什么呢?因为这是超市里这些商品通常出现的顺序。
但是如果你想做简单的改变呢?如果你的程序可以根据输入决定做什么,那会怎么样?
在计算机科学中,这被称为分支。本质上,决策是基于给定的数据做出的,一个代码块在另一个代码块上执行。让我们来看一下图 5-1 中的一张图。
图 5-1。
显示简单“if”语句的流程图
这被称为流程图,它显示了根据我们的输入做出的一系列决策所采用的路线(流程),我们使用这些输入来生成我们的输出。在图中,我们在问一个问题:“下雨了吗?”我们的数据要么是“是”,要么是“否”。如果问题的答案是“是”,那么我们就带一把雨伞。否则呢?我们什么都不做。
计算机在这些类型的决策方面非常出色;要么是,要么不是;开或关;真或假。
事实上,计算机只真正理解这些二元决策。
二进制意味着某物不是开就是关,是真还是假。
在 Python 中,我们没有“是”或“否”的值,但我们确实有类似的值;“真”和“假”这分别是“开”和“关”的值。那么我们如何用 Python 写这个呢?我们使用“如果”关键字。在英语中,我们会说,“如果下雨,我会带雨伞”;在 Python 中,写为
isRaining = True
if isRaining:
print("I will take my umbrella to work today")
第一行将常量“True”赋给“isRaining”True 是一个特殊的关键字(和 False 一起),在 Python 中用来表示布尔测试的结果。
第二行检查包含在“isRaining”中的值,如果它被设置为 True(确实如此),它将打印出文本字符串。请注意,您必须在 print 语句开始时按 tab 键。这是因为它构成了“isRaining”为真时将执行的语句列表。在这种情况下,我们有一个语句,但是如果我们有更多的语句要执行,如果' isRaining '为真,它们都将使用 tab 键缩进。
如果条件总是等于两个值之一:真或假。
我们也可以将“如果”语句写成
if isRaining == True:
这要清楚得多,但不是首选的用法。当你有一个以“is”或“has”开头的变量时,假设它包含一个布尔值。永远!永远!–在使用变量之前,请检查情况是否如此。
“if”语句的格式是
if condition: {statement}
或者
if condition: {block}
第二种方法是首选,因为您可能希望返回并向要在“if”块中执行的代码添加更多行。并不是说对于块中的每一行,每次都要缩进相同的量。
关于块的一个注记
代码块是一行或多行 Python 代码。例如,当包含在控制语句(如“if”、“for”或“while”)中时,组成块的语句必须移动一个制表符。这是因为 Python 不使用语法糖或额外的字符来表示块的开始和结束。基于 C 语言的语言使用“{”和“}”来表示块。Python 没有。例如见图 5-2;这是 Python 版本旁边的 C 语言风格的等价物。
图 5-2。
展示了显式块字符和 Python 的隐式缩进方法之间的区别
我们还可以在“:”后放置多行,如下例所示:
isSunny = True
if isSunny:
print("It is sunny outside")
print("I won't need my umbrella")
只有当“isSunny”为“True”时,才会执行“if”块中的两行如果我们想展示一些东西,如果训练不是真的呢?我们能这样做吗:
isRaining = True
if isRaining:
print("I will take my umbrella to work today")
print("It is nice and sunny")
该程序运行时会显示以下输出:
I will take my umbrella to work today.
It is nice and sunny
这不是理想的情况,因为我们只希望输出一行。第二行总是会被执行,因为正如我们所知,程序盲目地一步一步运行,直到程序结束,没有其他行需要处理。我们需要做的是:
isRaining = True
if isRaining:
print("I will take my umbrella to work today")
else:
print("It is nice and sunny")
请注意额外的关键字“else”。这使我们能够更好地控制如果“正在训练”被证明是错误的,我们期望做什么。你不必为每个“如果”加上一个“否则”有时没有其他选择,您只想针对特定条件运行一组特定的语句。
相等性测试
Python 允许程序员测试相等性——在我们测试特定变量等于 true 时,我们已经看到了这一点。我们知道,如果条件必须等于两个值之一:真或假,那么我们如何测试(不)相等呢?我们使用以下范围运算符之一:
-
等于(==)
-
小于(
-
大于(>)
-
小于或等于(< =)
-
大于或等于(> =)
-
不等于(!=)
这些是数学符号。对于那些不熟悉它们的人来说,尤其是小于和大于符号,小尖点表示较小的值。不能对包含布尔值 True 或 False 的变量使用这些运算符;相等运算符只能处理数字或字符串。
下面的程序提示用户输入两个字符串值,然后检查哪个字符串更大。我们将在一秒钟内讨论更好的细节,但是这个程序确实有一些缺点。你能看出它们是什么吗?
打印“这个程序将接受两个字符串,并决定哪一个更大”
first = input("First string: ")
second = input("Second string: ")
if first > second:
tup = (first, second)
else:
tup = (second, first)
print("%s is greater than %s" % tup)
第一行显示一条消息,指示程序将做什么。接下来的两行提示用户输入两个单独的字符串值,并将它们放在“第一”和“第二”变量中。“if”语句的条件是
if first > second:
这将检查第一个字符串是否大于第二个字符串。如果是,则创建一个称为“tup”的元组,并存储第一个和第二个元组。注意顺序;第一是在第二之前。我们将在后面详细讨论元组,但现在让我们假设它们是一个或多个值的集合。
如果第二个字符串大于第一个字符串,那么也创建 tup 变量,但是顺序相反;“第二个”出现在“第一个”之前
键入前面的程序并运行它。在表 5-1 中输入数值。
表 5-1。
两个字符串程序的值
|程序运行次数
|
第一
|
第二
|
| --- | --- | --- |
| 1 | 小写 a | 大写 A |
| 2 | 美国汽车协会 | 打鼾声 |
| 3 | nine | One hundred |
你对结果有什么注意?你料到了吗?
我们这个小例子的问题是,除非‘first’绝对大于‘second’,否则执行‘else’块。我们可以通过将程序改为
print("This program will take two strings and decide which one is greater")
tup = None
first = input("First string: ")
second = input("Second string: ")
if first > second:
tup = (first, second)
elif second > first:
tup = (second, first)
if tup != None:
print("%s is greater than %s" % tup)
else:
print("The strings were equal")
关键字“None”用于初始为“tup”赋值无意味着没有给变量赋值。我们仍然希望有一个名为' tup '的变量,并在以后给它赋值。因此,在这种情况下,我们最初将“tup”设置为等于“None ”,因为在程序的逻辑中可能根本不会设置它。如果我们不设置它,那么试图访问它将导致“未定义”错误。
如果你看到一个“name 'variable name' not defined”的错误,这通常意味着你在使用它之前没有给它赋值,或者你拼错了变量名!
更改前面的程序,在第二个“if”中使用等号(==)您需要更改“打印”声明的文本吗?如果是,你会把它们换成什么?
比文本相等更常见的是数字相等。数字相等测试用于碰撞检测,决定玩家或敌人是否死亡,还有多少燃料,等等。
比方说,我们想要检查玩家的角色是否在屏幕上的某个边界内。这包括检查玩家的 x 和 y 坐标。我们可以使用布尔逻辑在一个语句中组合我们的条件。
在这个例子中,我们测试玩家的 x 和 y 坐标,以确定他们是否在一个矩形区域内,该区域宽 100 个单位,高 225 个单位,位置在(0,25)处,如图 5-3 所示:
图 5-3。
玩家在矩形区域内的位置
从图中我们可以清楚地看到,玩家在矩形内。我们如何让计算机检查玩家是否在矩形内,并做出相应的响应?这是在 2D 空间——二维空间;玩家位置的水平和垂直分量,即他们的 x 和 y 坐标。最简单的方法是把它分成两个独立的 1D 检查,然后把两个检查的结果结合起来。在英语中:
如果玩家的 x 坐标介于 0 和 100 之间,y 坐标介于 25 和 250 之间,则他们在该区域内。
在代码中,这看起来像
x = 50
y = 50
if x >= 0 and x <= 100 and y >= 25 and y <= 250:
print("Player is inside the area. Sound the alarm!")
else:
print("Player is outside the area. Do nothing")
使用布尔逻辑
正如我们在前一章中看到的,计算机使用布尔逻辑:任何问题,只要它能得出正确或错误的答案。下列布尔关键字可用于使 If 条件更复杂:
-
和
-
或者
-
不
和
只有当两个条件都为真时,if 语句中的 And 才等于 true:
isRaining = True
isSunny = True
if isRaining and isSunny:
print("Sun showers")
在游戏的上下文中,你可能有一个条件来测试玩家是否有一把钥匙,他或她撞上了一扇门,然后打开了门:
if playerHasKey and playerHitDoor:
OpenTheDoor()
RemoveKeyFromInventory()
OpenTheDoor()和 RemoveKeyFromInventory()这两个方法是程序员做的;它们不是 Python 的一部分。我们将在后面的章节中学习如何创建用户自定义函数。
在布尔逻辑中,真值表用于显示运算的结果(“与”、“或”、“非”)。通常,这显示了称为“A”和“B”的两个输入的值和一个结果。
表 5-2 中所示的‘和’的真值表如下。
表 5-2。
和“真值表”
|A
|
B
|
结果
|
| --- | --- | --- |
| 假 | 错误的 | 错误的 |
| 假 | 真实的 | 错误的 |
| 真 | 错误的 | 错误的 |
| 真 | 真实的 | 真实的 |
这说明对于' and ',只有当' A '和' B '都为真时,' A '和' B '的组合结果才能为真。
或者
或者在“if”语句中,如果两个条件中的一个为真,则“if”将等于“true ”:
isRaining = True
isSunny = False
if isRaining or isSunny:
print("Some kind of weather out there")
else:
print("No weather! How unusual for this time of year")
“或”的真值表如表 5-3 所示:
表 5-3。
或“真值表”
|A
|
B
|
结果
|
| --- | --- | --- |
| 假 | 错误的 | 错误的 |
| 假 | 真实的 | 真实的 |
| 真 | 错误的 | 真实的 |
| 真 | 真实的 | 真实的 |
这说明只有当' A '和' B '都为假时' or '才为假。
不
Not 用来否定一个条件:把它从真变为假,反之亦然。这是一元运算符,仅在一种情况下有效:
isRaining = True
isSunny = False
if isRaining and not isSunny:
print("It's raining and not sunny")
else:
print("Sun showers")
“not”的真值表(表 5-4 )的不同之处在于,它只有一个输入,因为它是一元运算符。因此,真值表只有“A”输入。
表 5-4。
“非”真值表
|A
|
结果
|
| --- | --- |
| 假 | 真实的 |
| 真 | 错误的 |
您可以看到,无论输入是什么,not 关键字都会将其否定。
嵌套 if
当我们需要根据大量事实做出复杂的决定时,我们可以做所谓的“嵌套”这意味着将一个“if”代码块放在另一个“if”代码块中,例如:
isRaining = True
isCloudy = True
if isRaining:
print("I will take my umbrella to work today")
elif isCloudy:
print("It looks like it will rain. I'll take my umbrella")
else:
print("It is sunny. I'll not bother with the brolly")
表 5-5 中显示了这方面的真值表,以使前面的示例更加清晰。
表 5-5。
“if”块真值表
|下雨了
|
多云
|
输出
|
| --- | --- | --- |
| 真 | 真实的 | 我今天要带伞去上班 |
| 真 | 错误的 | 我今天要带伞去上班 |
| 假 | 真实的 | 看起来要下雨了,我要带着雨伞以防万一 |
| 假 | 错误的 | 天气晴朗。我不会为这个麻烦的 |
因此,IF 语句的格式是
if condition:
Action(s)
[else:
Action(s)]
[elif condition:
Action(s)]
关于开关的一个注记
对于其他语言的用户,你应该注意到 Python 中没有“switch”语句。它是为语言而提出的,但最终被否决。在像 Python 这样的 OO(面向对象)语言中,“switch”可以用多态来代替(我们后面会讲到!)来电。Stack Overflow(一个很棒的网站,也是您应该收藏的网站)有一篇关于如何避开“切换”的文章
详见 http://stackoverflow.com/questions/126409/ways-to-eliminate-switch-in-code
。switch 关键字可以很容易地使用 ifs 实现,如下所示:
character = input("Enter command (help, list): ")
if character == "help":
print("The help screen goes here")
elif character == "list":
print("List the items here")
else:
print("Invalid command!")
结论
计算机非常擅长快速做出简单的决定。使用比较和范围运算符,可以确定两个值是否相等,或者它们是否在一个范围内(例如,在 1 和 10 之间)。
可以使用布尔逻辑操作符(如 And、Or、Not 和 If 关键字)来组合这些决策,以生成分支代码;如果为真,运行一些代码,如果为假,运行一些其他代码。我们将在后面的章节中看到这些小积木如何构建复杂的系统。
六、让树莓派重复
电子游戏重复这个动作,直到所有玩家的生命都消失了,或者游戏结束了。到目前为止,我们只编写了运行一系列命令然后终止的程序。通过使用某些 Python 关键字,我们可以让计算机在需要时重复一段代码,或者使用条件,或者重复设定的次数。
for 循环
Python 中的“for 循环”接受一个列表,并对列表中的每一项执行一系列操作。这些操作包含在“:”字符后面的代码块中,并向右移动一个制表符。图 6-1 中的流程图显示了“for”循环内部发生的情况。
图 6-1。
显示 for 循环的流程图
例如,下面的程序将打印数字 1 到 5。我们稍后将讨论 range()函数的一些奇怪之处。
不要忘了脚本顶部的 hash-bang!请记住,您需要使用 hash-bang 来从命令提示符运行脚本文件。您还需要更改文件模式(chmod)并添加可执行文件标志。如果你不太记得怎么做,请参见第四章(“从解释器中挣脱”)。
for i in range(1, 6):
print(i)
这里的“I”变量有特殊的含义。它充当回路的控制。事实上,我们给任何控制流量的变量命名为控制变量。同样,这只是我给变量的一个名字。我可以叫 n 或者 j 或者 fred。控制变量往往有简短的名字。我选择这个是因为我们要遍历整数或整数,而‘I’似乎适合这个任务。
“for”循环的格式为
for condition:
Action(s)
其中“条件”是生成列表的任何语句。
range()函数
range()函数由 Python 提供,因此被称为内部函数。它生成一个从起始值到 1-n 的数字列表,其中 n 是范围内的最后一个值。以下示例摘自 Python 解释器中输入的语句:
>>> range(1,6)
[1, 2, 3, 4, 5]
>>> range(2,4)
[2, 3]
您还可以指定第三个参数。此参数指示在“for”循环的每次迭代后添加到每个数字的计数。默认值为 1(一),这就是为什么您不需要提供它:
>>> range(10, 255, 10)
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240, 250]
>>> range (10, 0, -1)
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
没错,你甚至可以用负值从一个较大的数迭代到一个较小的数。
我们将在下一章讨论列表和字典,但是我们已经知道‘for’循环遍历列表。所以,如果我们没有数字呢?如果我们有电视上角色的名字会怎么样:
names = ['John', 'Anne', 'Fred', 'Beth']
for name in names:
print(name)
“names”变量被赋予一个人名列表的值。“for”循环遍历每个名字,并将它们打印在屏幕上。本例中的控制变量是“名称”在循环的每次迭代中,列表中的下一个名字将被取出并处理。
While 循环
For 循环非常适合遍历一个固定的列表,但是如果我们不知道我们有多少项呢?例如,读取文件内容或从网站获取数据。这就是“while”关键字出现的地方。
尽管无论使用哪种循环,while 循环都具有相同的格式,但是 while 循环有三种类型:
-
计算
-
哨兵
-
有条件的
计算
计数 while 循环基本上只是“for”关键字的替代品。当然,你可以用它来代替' for '关键字,但是它看起来不那么优雅。例如:
#!/usr/bin/python
i = 1
while i < 6:
print(i)
i = i + 1
在这个例子中,我保留了 hash-bang。
在“for”循环中,控制变量包含在“for”语句中,而在“while”循环中,控制变量是在循环外定义的。程序员还必须每次手动更新所需步骤的控制变量。
不要忘记更新控制变量!如果你不这样做,你将会陷入一个无限循环,你将不得不使用 Ctrl + C 退出你的程序!
请注意,“while”语句的格式是
while condition:
Action(s)
“where 条件”是一个等同于“真”或“假”的语句:一个布尔值。这类似于“if”关键字,因为它也采用布尔条件作为其参数。该“while”循环的流程图如图 6-2 所示。
图 6-2。
示例“while”循环的流程图
从流程图中我们可以看到,只有当布尔条件为“真”时,才会执行“while”块中的语句
这意味着“while”循环的条件充当了块内各行的看门人。如果条件不成立,就不要执行里面的行。将“I”的初始化更改为 6:
#!/usr/bin/python
i = 6
while i < 6:
print(i)
i = i + 1
运行脚本。会发生什么?没什么?完全没有输出。这是因为 i< 6,当 i = 6 时返回‘False’。
哨兵
sentinel while 循环是一种不断循环直到达到某个值的循环。
让我们回到上一章的菜单例子。我们有三个命令:列表、帮助和退出。当用户选择退出时,程序结束。
我们不知道用户在整个程序会话中会使用多少个命令,也不知道他们会使用多少次相同的命令。这是“while”循环的理想用例,因为它可用于在条件未满足时保持程序运行:
cmd = input("Command: ")
while cmd != 'quit':
if cmd == 'help':
print("Put help here")
elif cmd == 'list':
print("Put list here")
else:
print("Invalid command!")
cmd = input("Command: ")
当我们开始进入多个标签时,你必须保持正确的间距。在您的编辑器中,您应该会看到如图 6-3 所示的程序。
图 6-3。
显示缩进代码行的菜单程序
如果 input()函数对您不起作用,请尝试 raw_input(),因为您可能正在运行 Python 2.7
以下是运行该程序的典型输出:
Command: help
Put help here
Command: list
Put list here
Command: badcommand
Invalid command!
Command: quit
有条件的
这些是前两者的结合:计数和哨兵。这是当你想计数一个序列,但你不知道你是什么序列计数。假设你想写一个程序把所有的数字加起来达到某个值。您可能会这样写:
#!/usr/bin/python
topNum = int(input("Sum of numbers to? "))
count = 0
while topNum > 0:
count += topNum
topNum = topNum - 1
print("Sum of all numbers is %d" % count)
要获得数字形式的输入,我们必须使用 int()函数将其转换为整数。
input()函数总是返回一个字符串!
结论
计算机程序中经常使用循环。根据具体情况,您必须选择使用哪个循环:for 循环还是 while 循环。
如果范围是已知的,或者您想要循环遍历一个值列表(就像本章中的名称示例),那么 for 循环非常适合您。
while 循环有三种类型:计数循环、标记循环和条件循环。计数版本很少使用,大多数人更喜欢“for”循环。然而,sentinel 和 conditional 被广泛用于保持循环,直到满足某个未知条件(通常在编写时)。
七、容器
到目前为止,我们主要是在一个变量中存储一个值,如下所示:
playerName = 'Sloan'
Python 允许在同一个变量中存储多个值。在这种情况下,您将为容器对象指定一个名称。有三种不同类型的容器:
-
元组
-
目录
-
词典
在有些情况下,您会使用其中一种而不是另一种,我们将讨论每种用法的利弊。
容器术语
容器子部分称为元素。每个元素都可以使用“[”和“]”字符进行索引,并指定一个介于 0 和 n-1 之间的数字,其中“n”是它们之间的容器中的项目数。我们将在元组部分看到这是如何工作的。
元组
元组是不可变的对象集合。集合中的元素和容器本身都不能修改。这意味着不能在元组中添加和删除元素。
对于视频游戏,你可能想把玩家的位置——他们的 x 和 y 坐标——存储在一个元组中。
元组是对象的列表,可以是文字或变量名,用逗号分隔,并用括号字符“(”和“)”括起来。例如:
('sloan', 'robert')
若要访问元组中的元素,请使用“[”和“]”字符,并将索引值放在它们之间。假设您需要数组中的第一个元素,那么您应该像这样指定“0”:
('sloan', 'robert')[0]
这将显示
'sloan'
从元组中移除元素
如果你想从一个元组中删除项目(你不能),但有一个办法。创建新的元组!假设您有一个包含五个元素的元组,但是您不想要第三个元素:
numbers = (1, 2, 3, 4, 5)
tuple(x for x in numbers if x != 3)
哇哦!这看起来不必要的复杂!这是因为元组是不可变的,这意味着它们不能改变。让我们稍微分解一下复杂性。tuple()函数从给定的序列中返回一个元组。例如:
>>> tuple('abcdef')
('a', 'b', 'c', 'd', 'e', 'f')
>>> tuple(['sloan', 'robert'])
('sloan', 'robert')
在前一个例子中,字符串被分割成单独的字符。后者包含一个名称列表,并根据该列表创建一个元组。
完成所有魔法的部分叫做列表理解。它用于基于现有列表创建列表。
x for x in range(0, 5) if x != 3
列表理解接受一个列表,处理列表中的每个元素,并生成另一个列表。正如我们已经看到的,tuple()关键字从任何序列中生成一个元组。字符串是字符序列,元组是(不可变的)对象序列,列表也是对象序列。
不要太拘泥于那种陈述的格式;这仅仅是如何从元组中移除项目的一个例子。
更改元素值
元组的另一个‘不能做’。但是,也有一种方法可以解决这个问题。元组是不可变的,这意味着它们不能被改变,它们的元素也是如此。唯一的选择是创建另一个元组。
在这个例子中,我们有一个表示玩家的 x 和 y 坐标的元组(“playPos”)和另一个表示玩家的速度(“speed”)的元组。为了得到玩家的下一个位置,我们把速度加到当前位置。记住,我们不能改变元组或它的元素。我们必须创建一个新元组,并将该值赋给“playPos”:
playPos = (5, 4)
print("playPos = %d, %d" % playPos)
speed = (2, 1)
playPos = (playPos[0] + speed[0], playPos[1] + speed[1])
print("playPos = %d, %d" % playPos)
该行:
playPos = (playPos[0] + speed[0], playPos[1] + speed[1])
在等号的右边,创建了新的元组。这就被命名为“playPos”分配给“playPos”的元组被这个新值覆盖。这相当于以下简单的赋值:
>>> num = 5
>>> print(num)
5
>>> num = 10
>>> print(num)
10
在本例中,值“num”最初被赋值为 5。然后给它赋值 10。这将覆盖最初存储在“num”中的值
印刷中的元组
我们以前在显示格式化字符串时使用过元组。例如:
>>> numbers = (1, 2, 3, 4, 5)
>>> print("%d %d %d %d %d" % numbers)
1 2 3 4 5
解构元组
另一个常见的动作是将元组解构为其组成部分,并将它们分配给单独的变量。这是通过把要赋值的变量放在等号的左边,把元组放在另一边来实现的。在下面的例子中,我们有一个包含两个元素的向量“vec ”:一个用于 x 坐标,另一个用于 y 坐标:
>>> vec = (2, 3)
>>> x, y = vec
>>> x
2
>>> y
3
>>>
类似地,也可以通过指定逗号分隔的值来构造元组。这个我不推荐;我更喜欢使用带括号的显式语法,但这也同样有效:
>>> vec2 = x, y
>>> vec2
(2, 3)
>>>
列表
列表是可变的容器。这意味着元素和列表本身都可以改变。对于列表来说,这意味着我们可以在创建列表后添加和删除元素。使用 append()方法将项目添加到列表中,通过 remove()方法移除项目。方法是对象可以执行的操作。我们将在本文的面向对象部分看到更多的方法。
列表在电子游戏中被大量使用。你的物品清单是一个物品列表,屏幕上的精灵(图像)存储在一个列表中,组成游戏的关卡集合也可以存储在一个列表中。
列表创建
您可以创建一个空白列表,也可以创建一个最初填充了值的列表:
blankList = []
populatedList = [1, 2, 3, 4, 5]
如果我们在 Python 解释器中运行这些命令,其输出将是
>>> blankList = []
>>> populatedList = [1, 2, 3, 4, 5]
>>> blankList
[]
>>> populatedList
[1, 2, 3, 4, 5]
>>>
向列表中添加值
如果我们想向“空白列表”添加值,我们只需使用 append()方法,并将我们想要添加的内容放在括号内:
>>> blankList.append("Python")
>>> blankList
['Python']
>>>
添加另一个计算机语言名称(这次是 Lua)将意味着我们的空白列表将包含
>>> blankList.append("Lua")
>>> blankList
['Python', 'Lua']
>>>
从列表中删除值
要从列表中删除一个项目,remove()方法的用法如下:
>>> populatedList = [1, 2, 3, 4, 5]
>>> populatedList.remove(3)
>>> populatedList
[1, 2, 4, 5]
>>>
您还可以通过索引值从列表中移除项目。在列表中没有内置的方法来做到这一点;相反,我们使用“del”关键字。例如,要删除第一个元素,即索引 0(零),我们可以使用
>>> populatedList = [1, 2, 4, 5]
>>> del populatedList[0]
>>> populatedList
[2, 4, 5]
>>>
这意味着我们也可以删除多个项目;假设我们想从列表中删除所有项目。我们会这样做:
>>> populatedList = [2, 4, 5]
>>> del populatedList[:]
>>> populatedList
[]
>>>
并再次填充列表:
populatedList = [1, 2, 3, 4, 5]
假设我们想从列表中删除 2 和 3。我们可以发出这一行两次:
del populatedList[1]
为什么两次?列表的索引 1 是“2”元素。当我们删除列表中的某个内容时,之后的所有内容都会上移一个位置。因此,该数组现在包含
[1, 3, 4, 5]
这意味着索引 1 现在包含“3”
当我们可以一次完成所有任务时,两次键入相同的命令有点浪费。我们可以使用冒号(':')来指定要删除的值的范围。现在,要同时删除 2 和 3,我们可以使用
del populatedList[1:3]
冒号前的数字是删除的起始索引。列后的数字是 1 加上要删除的元素数。如果您想删除从第一个元素开始的所有内容,您可以使用
del populatedList[1:]
医生候诊室计划
我创建了一个简单的程序,以医生候诊室为例演示列表。用户可以添加患者,在呼叫时将他们从列表中删除,以及退出程序。所有的动作都是通过一个菜单完成的。
#!/usr/bin/python3
names = [] # an empty list of names
我们每天早上从一张空白清单开始。
cmd = ""
while cmd != '4':
有四个命令:1–列出姓名,2–添加姓名,3–呼叫下一位患者,4–退出。用户的命令将存储在“cmd”变量中。请注意,我们有一个“while”循环来保持用户在程序中,直到他们选择退出。
print("1\. List names")
print("2\. Add name")
print("3\. Call next patient")
print("\n4\. Quit")
显示菜单是为了让用户知道他们可以选择的选项。
cmd = input("\rCommand : ")
现在会提示用户输入命令。我们现在将使用一系列嵌套的 if 来执行用户选择的命令。
if cmd == '1':
for name in names:
print (name)
print ("\n")
如果用户输入‘1’,那么我们使用‘for’循环遍历‘names’列表中的所有名字。在每次迭代中,我们打印病人的名字。最后,我们用一个换行符(' \n ')结束它,在屏幕上给我们一些空白。
elif cmd == '2':
newName = input("Name : ")
names.append(newName)
如果用户输入“2 ”,我们会提示他们输入新到达的患者的姓名。然后,我们使用 append()方法将新名称添加到名称列表中。
elif cmd == '3':
if len(names) == 0:
print ("There are no more patients!")
else:
nextPatient = names[0]
names.remove(nextPatient)
print ("Calling %s" % nextPatient)
对于第三个也是最后一个命令,但不是我们程序的结尾,用户选择呼叫下一个要看的病人。这种做法提供了严格的先到先得的政策。这意味着列表中的第一项被删除。但是,如果列表中没有项目,则会显示一条警告消息。您可以使用“len”关键字来确定项目列表的长度。
elif cmd != '4':
print ("Invalid command!")
程序的最后几行用来让用户知道他们输入了无效的命令:不是 1、2、3 或 4。
将程序保存为“patients.py”(不带引号),并且不要忘记更改程序的属性以允许它被执行。记住!您只需更改一次:
$ chmod +x patients.py
要运行该程序:
$ ./patients.py
当你和程序在同一个目录的时候。
字典
字典是一组键/值对。这意味着您可以使用用户定义的密钥来访问信息,而不是索引号。
电子游戏中可以使用字典来查找相关数据。例如,如果你想查看一把特定的剑造成的伤害,你可以使用字典来存储游戏中每种武器的数据。您可以对列表做同样的事情,但是遍历列表需要时间,一次一个元素来查找数据。当你想快速找到某样东西时,使用字典。
例如,我们将定义一个包含不同人的电话号码的字典。一个人的电话号码可以通过姓名获得:
>>> numbers = {'Sloan':'416-555-1234', 'Kevin':'212-555-4321'}
>>> numbers['Sloan']
'416-555-1234'
>>>
第一行定义了包含两个条目的字典“numbers”。每个条目都是一个单独的键/值对。每个键/值用冒号“:”分隔,每对用逗号“,”分隔。
遍历字典
我们可以使用字典的 iteritems()方法遍历每个条目:
>>> for k,v in numbers.iteritems():
... print ("%s = %s" % (k ,v ))
...
Sloan = 416-555-1234
Kevin = 212-555-4321
>>>
向词典中添加新项目
字典有一个更简单的添加新条目的方法:如果一个键不存在,该值被添加到字典中。如果一个键已经存在,那么新值被分配给该键。
>>> numbers['Frank'] = '216-555-1234'
>>> numbers
{'Sloan': '416-555-1234', 'Frank': '216-555-1234', 'Kevin': '2
>>>
从字典中删除条目
为了从字典中删除一个词条,我们使用我们的老朋友“del”从“数字”字典中去掉“斯隆”
>>> del numbers['Sloan']
>>> numbers
{'Frank': '216-555-1234', 'Kevin': '212-555-4321'}
>>>
结论
我们已经看到 Python 为我们提供了三种不同类型的容器,为我们的程序提供了选项。元组可以用来像不可变(不能被改变)的项目一样分组在一起。用一个元组定义一个结构线空间中的一个点。空间中一个点的属性是它的 x 和 y 坐标。这两个元素不会改变,你很少迭代(循环)它们。列表容器可用于存储可添加和移除的项目集合。最后,字典允许添加、删除和修改条目。
我们现在要从 Python 语言中休息一下,看看如何着手设计一个游戏,并跳跃到 Raspberry Pi 使用的名为 LXDE 的窗口系统。这是因为我们将在接下来的几章中开始研究 PyGame。
八、放在一起:井字棋
在我们开始研究 PyGame 以及如何创建街机风格的游戏之前,我们应该后退一步,将前几章中介绍的内容放入一个简单的 ASCII 控制台游戏井字棋中,这是一个两人游戏。
规则
对于那些以前没有玩过井字棋的人,这里有一些规则:
在一张纸上画一个有九个正方形的棋盘,人们通常是这样做的,先画两条互相平行的水平线,再画两条互相平行但垂直于水平线的垂直线,就像一个散列符号:#(图 8-1 )。
图 8-1。
井字棋棋盘的布局
第一个玩家使用令牌 X,第二个玩家使用令牌 o。每个玩家从 X 开始,将他们的令牌放在棋盘上的一个盒子中。一个槽只能接受一个令牌!如图 8-2 所示,当玩家放置的代币形成水平、垂直或对角线三排时,游戏结束。
图 8-2。
井字棋棋盘上的获胜线
程序布局
该计划将分为以下几个部分:
-
变量声明和初始化–创建变量并赋予它们初始值
-
显示一条欢迎信息——简单的文字说明程序做什么以及如何玩游戏
-
展示公告板
-
听取玩家的意见——他们希望将棋子放在棋盘的什么位置
-
测试输入的有效性——不断询问玩家输入是否无效
-
将棋子放在棋盘上
-
检查玩家是否赢了——如果他们赢了,显示祝贺信息并结束游戏
-
如果还有空位可以放代币,跳回“显示牌”
我们将在这个程序中使用 while 循环和 if 语句。while 循环将在仍然有空位或者没有人赢的时候继续玩游戏。if 语句将用于确定我们是否有赢家或者玩家的输入是否有效。
变量
我们需要一个地方来存储程序运行时的数据,并需要决定我们将使用什么变量。表 8-1 显示了哪些变量将被声明以及它们将如何被使用。
表 8-1。
声明的变量
|可变的
|
使用
|
| --- | --- |
| 板 | 最初包含字符 1 到 9,这将包含 X 和 O 标记在棋盘上的位置,并将用于在屏幕上绘制棋盘 |
| 电流令牌 | 当前令牌,即当前玩家,将包含 X 或 O |
| winnington | 其中一个控制变量将用于在所有玩家回合中保持游戏进行。当设置为 X 或 O 时,程序将退出 |
| 槽已填充 | 有可能没有人会赢得这场比赛——这有时被称为猫的游戏。在这种情况下,我们需要一种方法来退出 while 循环,如果没有其他移动可以进行的话。每当玩家移动一步,第二个控制变量就会增加 |
游戏
在“pygamebook”文件夹中创建一个名为“ch8”的新文件夹。在“ch8”中创建一个名为“tictactoe.py”的新文件在 Python IDLE 中打开文件,并输入以下文本。我将在进行过程中添加注释来帮助说明代码在做什么。
#!/usr/bin/python
#
# Program: Tic-Tac-Toe Example
# Author: Sloan Kelly
头信息很有用,因为它可以快速识别这个程序或脚本的目的是什么,以及是谁写的。
board = ['1', '2', '3', '4', '5', '6', '7', '8', '9']
currentToken = 'X'
winningToken = "
slotsFilled = 0
程序使用的变量被声明和初始化。“board”变量包含一个字符串数组,其中包含符号 1–9。这些将用于两个原因:向玩家显示他们可以输入什么数字,其次允许程序确定某个位置是否被令牌占用。
将“currentToken”设置为第一个玩家的令牌,将“winningToken”和“slotsFilled”分别设置为空字符串(“”)和 0 的默认值。后两个变量用于控制游戏,并确保游戏在没有赢家和棋盘上有空位的情况下继续进行。
print ("Tic-Tac-Toe by Sloan Kelly")
print ("Match three lines vertically, horizontally or diagonally")
print ("X goes first, then O")
将向玩家显示关于该程序的一些基本信息。它让他们知道程序的名字,谁创作的,以及一些基本的游戏规则。
while winningToken == " and slotsFilled < 9:
一个 sentinel while 循环的例子,当没有人赢并且有空位要填补时,保持游戏运行。
print("\n")
print("%s|%s|%s" % (board[0], board[1], board[2]))
print("-+-+-")
print("%s|%s|%s" % (board[3], board[4], board[5]))
print("-+-+-")
print("%s|%s|%s" % (board[6], board[7], board[8]))
向玩家展示棋盘。随着时间的推移,棋盘上的条目将填满 X 和 O,但在第一轮,棋盘包含符号 1 到 9。然后,播放器将输入这个数字,我们必须将它向下转换一位,因为在 Python 中,数组索引从 0 开始,而不是从 1 开始。
此外,不要忘记你的缩进!
pos = -1
while (pos == -1):
当玩家选择了一个无效的位置值时,这个 while 循环会将玩家留在循环中。
pos = int(input("\n%s's turn. Where to? : " % currentToken))
if pos < 1 or pos > 9:
pos = -1
print ("Invalid choice! 1-9 only.")
提示玩家输入,然后通过确保输入介于 1 和 9 之间来验证它。否则,显示一条错误消息,并将“pos”变量设置回–1(无效输入),这将使播放器保持在 while 循环中,直到它们输入正确的值。
pos = pos – 1
移动“位置”,使其位于“电路板”阵列的 0–8 范围内。
if board[pos] == 'X' or board[pos] == 'O':
pos = -1
print("That spot has already been taken by %s! Try again" % board[pos])
检查棋盘上位置“pos”处的数值是否被玩家获取,如果是,显示警告。
board[pos] = currentToken
slotsFilled = slotsFilled + 1
否则,将索引“pos”处的板设置为当前令牌,并增加“slotsFilled”变量。请注意,这两行位于 while 循环之外,因为此时已经验证了“pos”变量。
row1 = board[0] == currentToken and board[1] == currentToken and board[2] == currentToken
row2 = board[3] == currentToken and board[4] == currentToken and board[5] == currentToken
row3 = board[6] == currentToken and board[7] == currentToken and board[8] == currentToken
为了使这个程序更整洁,我将棋盘、列和对角线检查分成了多行代码。第一组决定行的状态。
col1 = board[0] == currentToken and board[3] == currentToken and board[6] == currentToken
col2 = board[1] == currentToken and board[4] == currentToken and board[7] == currentToken
col3 = board[2] == currentToken and board[5] == currentToken and board[8] == currentToken
第二组决定列的状态。
diag1 = board[0] == currentToken and board[4] == currentToken and board[8] == currentToken
diag2 = board[2] == currentToken and board[4] == currentToken and board[6] == currentToken
最后一组决定了对角线的状态。
row = row1 or row2 or row3
col = col1 or col2 or col3
diag = diag1 or diag2
这些组被组合成单个变量,以使 if 检查更容易。
if (row or col or diag):
如果玩家获得了一行或一列或一条对角线,他们就赢了,游戏进入结束游戏状态。
print("\n")
print("%s|%s|%s" % (board[0], board[1], board[2]))
print("-+-+-")
print("%s|%s|%s" % (board[3], board[4], board[5]))
print("-+-+-")
print("%s|%s|%s" % (board[6], board[7], board[8]))
再次显示棋盘,显示获胜的玩家。
print("Congratulations %s! You won!!" % currentToken)
winningToken = currentToken
显示“祝贺您!”消息并设置获胜令牌。记住——这是主(顶部)while 循环使用的标记控制变量之一。如果将其设置为非空值,也就是说,我们将其设置为“currentToken”的内容,则主循环结束。
if currentToken == 'X':
currentToken = 'O'
else:
currentToken = 'X'
如果游戏还在玩,当前的代币需要换成相反的。如果当前令牌是 X,我们交换 O,反之亦然。
if slotsFilled == 9 and winningToken == ":
print("No one won :( Better luck next time, players!")
我们最后的 if-check 在主循环之外,如果两个玩家都没有赢,它会显示一条消息。
保存并运行
保存并运行程序。如果您想从命令行运行该程序,您需要在终端中找到该文件夹,例如:
$ cd ~
$ cd pygamebook
$ cd ch8
然后输入 chmod 命令以确保程序可以执行:
$ chmod +x tictactoe.py
最后,输入以下内容来运行游戏:
$ ./tictactoe.py
如果你想在空闲状态下运行游戏,按键盘上的 F5 键或者从“运行”菜单中选择“运行模块”。
结论
这不是我们的第一个 2D 图形游戏,但它是我们的第一个游戏!用 Python 写游戏的温和介绍。我们使用了本书前几章提到的结构来构建这个游戏。尽管它们很简单,但是这些小的构建块——变量、循环、条件和容器——可以帮助我们构建复杂的软件。
九、PyGame 基础介绍
PyGame 是一个免费的 Python 框架,它提供了用于编写视频游戏的模块。它建立在简单的 DirectMedia 层库(SDL)之上,该层库提供了对声音和视觉元素的简单访问。
在这一节中,我们将看到如何设置 PyGame,以及在我们未来的程序中会用到的一些元素。Python 语言不包含 PyGame,因此在使用之前必须导入框架。
导入 PyGame 框架
在 Python 中,通过“import”关键字导入模块。要导入 PyGame,您需要在脚本的顶部添加下面一行,在 hash-bang 之后:
import pygame, os, sys
from pygame.locals import *
第一行导入 PyGame 模块及其对象,以及 OS 和系统模块。import 关键字不直接在当前符号表中输入 pygame、os 和 sys 中定义的对象的名称。它只输入模块名。要访问每个模块的元素,我们必须使用模块名,这就是为什么我们必须编写 pygame.locals。第二行说明我们将从 pygame 框架导入常量,就好像它们是在本地定义的一样。在这种情况下,我们不需要给每个常量加上前缀“pygame”“from”关键字是 import 关键字的变体,它允许我们导入模块元素,就好像它们是在我们的(本地)代码库中定义的一样。
正在初始化 PyGame
在使用框架中的任何对象之前,您必须首先初始化它。我们还希望将更新限制在每秒 30 帧,因此我们添加了一个 fpsClock 变量,并将其初始化为每秒 30 帧。
pygame.init()
fpsClock = pygame.time.Clock()
surface = pygame.display.set_mode((800, 600))
第一行初始化 PyGame。第二行创建一个对象的实例,并将该值存储在“fpsClock”中。对象是一个类的实例。我们将在面向对象部分详细讨论这一点。Python 中的一切都是对象,这也是这门语言的魅力所在;但是现在,我们只能说你可以创建你自己的数据类型。这些用户定义的数据类型称为“类”
第三行创建了一个表面,我们可以在上面绘制图像(背景和精灵)。set_mode()方法采用两个参数,以像素为单位指定表面的宽度和高度。在本例中,我们创建了一个 800 × 600 像素的表面。
在我们在屏幕上画画之前清理屏幕是一个好习惯。因此,我们将创建一个包含背景的红色、绿色和蓝色成分的元组,而不是凭空提取数字。屏幕上的像素由红色、绿色和蓝色组合而成。这些比率决定了显示什么颜色。例如,( 0,0,0)是黑色,( 255,255,255)是白色。元组按顺序表示组成颜色的红色、绿色和蓝色组合。所以,(255,0,0)是红色的,(0,0,255)是蓝色的。
background = pygame.Color(100, 149, 237) # cornflower blue
在这个例子中,我选择了矢车菊蓝,因为它不是你经常看到的颜色,所以当窗口出现时,你就知道程序已经工作了。
主循环
一些程序,特别是那些从命令行运行的程序,倾向于执行一系列任务并退出。对于大多数窗口环境程序和游戏来说,这是不正确的。这些程序保持活动状态,直到用户明确退出。在执行过程中,它们执行所谓的主循环。这包含一系列重复执行直到程序结束的语句。主循环是
while True:
这将程序保留在内存中,因为它在条件为“真”时执行循环。因为条件实际上为“真”,所以循环将总是执行。
surface.fill(background)
在屏幕上画任何东西之前,我们首先清理表面。这将删除之前的内容,让我们重新开始。
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
PyGame 为我们提供了来自窗口管理器的事件:按键、按钮点击和窗口关闭请求。当我们收到窗口关闭请求(“退出”)时,我们将停止 PyGame 并退出应用。在循环过程中会发生许多事件,这些事件保存在一个我们可以迭代的列表中。因此,我们必须检查每个事件,看看它是什么类型,然后采取行动。在我们的基本框架中,我们只检查“退出”事件。
pygame.display.update()
fpsClock.tick(30)
pygame.display.update()方法重绘屏幕。当你在屏幕上放置对象时,它会被拉到一个叫做后台缓冲区的内存区域。当调用 update 时,这个后台缓冲区变得可见,并且当前显示数据的缓冲区(前台缓冲区)成为后台缓冲区。这允许平滑移动并减少闪烁。
在“pygamebook”文件夹中创建一个名为“ch9”的文件夹。将本章中已经介绍过的代码保存到一个名为“firstwindow.py”的新文件中。当运行程序时,你应该会看到一个矢车菊蓝色的窗口出现(图 9-1 )。
图 9-1。
显示矢车菊蓝色背景的 PyGame 窗口
单击右上角的“X”按钮关闭窗口。
大多数现代视频卡有两个内存区域;两者都用于向用户显示图像,但是一次只显示一个。这种技术被称为双缓冲,如图 9-2 所示。
图 9-2。
现代图形适配器中的双缓冲
从用户的角度来看,他们看到监视器上可见的项目。但是在幕后,程序正在绘制到后台缓冲区。随着电子手指的轻弹,用户看到了后台缓冲区中的图像。图 9-3 显示了发生的情况。
图 9-3。
前台和后台缓冲区的内容
剧院已经使用这种技术来隐藏场景的变化。当演员们在舞台上时,幕布后面一个新的场景正在上演。当演员的场景结束时,幕布打开,新的场景展现出来。
最后,我们希望将更新限制在每秒 30 帧。为此,我们调用 fpsClock.tick(30)。这确保了我们在游戏中获得一致的时间。这是时钟可以运行的最大值,但不是最小值。您可能会在游戏中执行复杂的计算,这可能会降低帧速率。当你开始编写比本文中介绍的更复杂的游戏时,你需要意识到这一点。
图像和表面
PyGame 使用表面将图像绘制到屏幕上。它使用图像模块从磁盘加载图像文件。然后将它们转换为内部格式并存储在曲面对象中,以备后用。您将为主屏幕创建至少一个曲面对象。这将是你要在其上绘制精灵和其他图像的对象。
我们执行主绘图的表面是后台缓冲区。然后,我们通过调用 update()方法将这个后台缓冲区呈现给屏幕。
创建图像
在大多数情况下,您会希望在第三方产品中创建图像,例如位于 www.gimp.org
的开源 GIMP (GNU 图像处理程序)。GIMP 是与 Photoshop 齐名的专业级图形程序。如果你像我一样,在职业生涯的大部分时间里都在使用 Photoshop,你可能会发现一开始使用 GIMP 有点令人沮丧——这不是应用的错!只要放松,你就能像在 Photoshop 中一样创建图像!任何允许您生成 BMP、PNG 和 JPG 图像的图像创建程序都可以。附录中有一个清单。如果你被图片困扰,这本书的网站( http://sloankelly.net
)上有一些(糟糕的)图片可以帮助你。有些图片是通过 GPL (GNU 公共许可证)成为 SpriteLib 的一部分;这意味着这些图像可以免费用于商业和非商业用途。
加载图像
Python 使用表面在屏幕上绘制图像。当你把一幅图像装入内存时,它被放在一个特殊的表面上。例如,加载一个名为“car.png”的图像:
image = pygame.image.load('car.png')
这将把图像加载到内存中,并在“图像”中放置一个对新加载对象的引用
绘制图像
图像绘制在 PyGame 表面上。记得在我们的骨骼游戏中,我们创建了一个用于在屏幕上绘制图像的表面。要绘制图像:
surface.blit(image, (0, 0))
其中“surface”是表面实例,“image”是您要在屏幕上绘制的图像。第二个参数是您希望在屏幕上绘制图像的位置。
屏幕坐标和分辨率
屏幕或监视器是计算机系统的主要输出设备。有两种不同类型的屏幕:阴极射线管(CRT)和液晶显示器(LCD)。后者越来越便宜,因此更受欢迎,还是因为它受欢迎而更便宜?计算机以给定的分辨率向监视器输出图像。分辨率意味着“多少像素?往下多少像素?”物理屏幕分辨率以像素为单位。像素这个词是像素的简称。您的电脑有多种分辨率可供选择,从 320×240 像素到 2560×1600 像素甚至更高。
计算机内部的图形卡与 CPU 一起在显示器上产生图像。对于较新的图形卡,图形处理器单元(GPU)放置在卡上,以提高系统的 3D 功能-通过提供更高的分辨率、特殊效果和更好的帧速率,使游戏更加逼真。
分辨率定义了图像在屏幕上的显示细节。列数(水平轴)和行数(垂直轴)定义了应用可用的像素数。在下面的例子中,显示了 1920×1080 分辨率的屏幕地图。不管你的显示器运行的分辨率是多少,原点在左上角,它的坐标总是(0,0)。参见图 9-4 。
图 9-4。
1920×1080 显示器的屏幕坐标
精灵表
Sprite sheets 通常用于将角色动画的所有帧保存在一个图像上。这个名字来自于一个精灵,用计算机术语来说,是一个在游戏中用作化身的小图像。图 9-5 显示了一个子画面示例。
图 9-5。
四图像子画面
这个 sprite 表包含四个图像:两个空间入侵者角色的两帧动画。当我们想要在屏幕上绘制字符时,我们选择使用 sprite 工作表的哪个单元格。单元格由精灵的高度和宽度决定。在这种情况下,我们有 32×32 像素的精灵,这意味着我们的精灵表是 64×64 像素,因为我们有 2×2 个精灵。
PyGame 允许我们显示我们想要显示的图像的一部分。因此,举例来说,如果我们只想显示第一个入侵者的第二帧(图像的右上角),我们可以使用这样的行:
surface.blit(image, (0, 0), (32, 0, 32, 32))
第三个参数,即包含四个值的元组,是我们希望在(0,0)处显示的图像区域。元组表示要显示的图像的(x,y,width,height)。
完整列表
本章中程序的完整列表如下所示:
import pygame, os, sys
from pygame.locals import *
pygame.init()
fpsClock = pygame.time.Clock()
surface = pygame.display.set_mode((800, 600))
background = pygame.Color(100, 149, 237) # cornflower blue
while True:
surface.fill(background)
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
pygame.display.update()
fpsClock.tick(30)
结论
本章介绍了我们将在每个游戏中使用的基本循环,以及如何初始化 PyGame。调用 pygame.display 对象上的 set_mode()并返回一个将被用作后台缓冲区的表面,我们的所有图像都将显示在该表面上。
使用 image.load()方法将图像加载到内存中,并使用其 blit()方法在图面上绘制图像。图像可以包含多个形状,这些形状被称为 sprite sheets。通过指定要绘制的框架的矩形,可以绘制 sprite 工作表的单个框架。
十、设计你的游戏
在我们开始编写我们的第一个游戏之前,我们要放慢一点速度。在开始任何一个项目之前,无论是家装、旅行、还是游戏编程,你都应该坐下来规划你想做的事情。
这通常包括采取以下步骤:
-
初始概念
-
功能规格
-
程序设计
-
编码
-
试验
-
循环
编码和测试往往携手并进;您将编写一些代码,然后测试它。从编程的角度来看,这个循环占据了你游戏开发的大部分时间。
初始概念
我们关心的是这里的小项目。在一个更正式的环境中,这需要走到所有相关人员(利益相关者)身边,问他们想从计划中得到什么。在我们的例子中,它是一个电子游戏。你可能会和两三个人一起工作,这部分倾向于集思广益:
-
这将是一场赛车游戏
-
带着武器
-
还有陷阱!可以设陷阱!
这些想法都存储在一个文档中;Google Drive 非常适合这类工作,因为它支持开发人员之间的协作。
一旦你有了所有的需求,你就可以进入功能需求了。请记住,所有这些文档都是“活的”,因为它们可以更改。后续文档/代码需要更新以反映这些更改。
最初的概念不断重复,这些文档形成了所谓的游戏设计文档或简称为 GDD。
样机研究
作为游戏设计初始阶段的一部分,你作为程序员可能会被要求做一些概念验证工作,称为原型。这是一个粗略的草图,描述了游戏的一部分可能会是什么样子。例如,在一个纸牌游戏中,它可能是一个弃牌动画,或者是玩家死亡时的屏幕抖动。
作为原型阶段的一部分而生成的代码并不一定会进入生产阶段,也就是说,你的游戏已经上市了。这种情况有时确实会发生,所以你应该总是尽量让你的代码尽可能的干净。
功能规格
功能规范采用第一阶段收集的需求,并删除所有围绕它们的“无用”语言。他们制定了一系列关于游戏的规则,这些规则可以传递给编码人员去执行。例如,我们的赛车游戏可以发射武器,所以我们的功能需求可能有一个“武器”部分和一个“陷阱”部分。
这些部分进一步将需求分解成程序员可以带走和实现的小块。连同程序设计,这形成了所谓的技术设计文件 (TDD)。请参见以下示例。
武器发射
玩家可以用机枪向另一个玩家 开火。每个玩家每秒钟最多可以射击十次。如果按住枪超过 2 秒,枪就会开始发热。这将启动一个“热量”计数器。在热量计数器达到 5 秒后,枪将不再可以发射。一旦玩家释放了开火按钮,这把枪还需要 5 秒钟才能冷却下来。
这也给了艺术家一些线索;他们必须展示枪加热和冷却的过程。
程序设计
如您所见,每一步都提炼了前一步的信息。程序设计采用功能需求,并将它们分解成程序员可以理解和实现的模块。程序员可能会采用这些模块,并进一步细化它们,制作更小的模块。
这里的总体目标是把一个问题分解,直到你有很多更小、更容易解决的问题。这听起来违反直觉:把一个问题变成多个问题。“泡杯茶”是一个更大的问题。这可以被分解成这样的小问题:
-
煮水壶
-
将茶包放入杯中
-
将开水放入杯中
-
等等。等等。
从编程的角度来看,你将需求(游戏的基本思想)通过功能需求(玩家如何与游戏互动——游戏环境如何工作)带到程序设计中,在程序设计中,你将这些功能需求和从编程的角度来看需要做的事情联系起来。
现在,这有点像一个第 22 条军规。你需要有经验,知道如何获取这些需求,并弄清楚它们如何成为程序设计。
编码
有时被称为过程中有趣的部分。这是想法开始形成的地方;添加图形,并使用代码在屏幕上移动它们。如果你还记得开篇的章节,程序本身是这样的:
程序=数据+算法
这些数据被称为模型,由算法处理。用于操纵数据的算法称为控制器,用于将项目呈现给显示器的算法是视图的一部分。在面向对象编程中,这种模式被称为模型视图控制器。
在本文中,我们将尽量保持模型、视图和控制器的分离,通过控制器进行通信,如图 10-1 所示。
图 10-1。
模型视图控制器模式
MVC 模式非常符合我们的“程序=数据+算法”的说法。控制器用代码操纵模型。反过来,视图读取模型的数据来呈现数据。可以有许多不同的视图来呈现不同的数据。
在图 10-2 所示的例子中,我们看到游戏的主视图以全尺寸显示玩家和敌人的精灵,而较小的雷达视图显示玩家和敌人相对于整个游戏世界的大致位置。有一个主视图控制器和一个雷达视图控制器。两个控制器都可以访问相同的数据:玩家和敌人的位置。
图 10-2。
一种显示游戏中相同物体的两种视图的游戏
在主游戏区显示外星人和玩家飞船的代码与它们在雷达视图中的显示方式不同。尽管他们有一个共同点:他们使用相同的数据。玩家的模型也用于显示(在另一个视图中)剩余的生命数、分数以及玩家可以使用的智能炸弹的数量。
虽然在面向对象的章节之前,我们不会被正式介绍 MVC 模式,但是我们将在前面的游戏(砖块和蛇)中使用这种模式的精神。
测试
在开发过程中,您将不断地测试您的代码。每次你实现(编码)一个新的例程,你都要测试它,以确保它做了你为它设定的事情。你怎么知道它在做正确的事情?您拥有“需求”和“功能规范”形式的文档,以确保您期望发生的事情确实发生了。
从编程的角度来看,在编码级别有两种类型的测试:白盒测试和黑盒测试。前者依次检查每个代码步骤,并确保它们按预期执行。后者将每个独立的模块视为一个黑盒。数据进去,结果出来。
循环
正如我之前提到的,游戏设计文档或 GDD 是一个“活的”文档。开发游戏的人会在游戏被创造出来的时候继续玩游戏。这叫做玩测试。这种游戏测试会导致反馈循环,可能会改变原始设计的元素。你会发现让游戏变得‘有趣’的东西变得很累。通过在开发过程中迭代设计,你可以做一些小的改变来改善你最初的概念。
结论
尽管您不会总是为需求和功能规范创建单独的文档,但是将您的想法写下来仍然是一个好主意。即使它只是提醒你什么需要编程,什么艺术需要创造。如果你仍然不热衷于写作,不要忘记一幅画胜过千言万语。
说到编程,在你把手放在键盘上开始打字之前要三思。你必须问自己的最大问题是,“我希望用我将要写的代码实现什么?”在你开始打字之前,你应该对你的目标有一个清晰的想法。
最后,但肯定不是最不重要的,是测试。永远,永远,永远测试你的代码!
十一、游戏项目:打砖块
在这一章我们将回顾砖块,我们的第一个游戏项目。对于没玩过这个游戏的人来说,你在屏幕下方控制一个球棒(图 11-1 )。你的上方有一堆砖块,你必须用球击碎所有的砖块。
听起来很简单,但是在这个项目中,我们将学习
图 11-1。
主砖块播放屏幕
-
球员运动
-
自动(非玩家)移动
-
冲突检出
-
显示图像
主要框架
我们将在这一节打下主要框架,让你对整个游戏的结构有一个大致的了解。为了让我们的第一个游戏简单,不会有任何中间画面,比如闪屏、菜单、暂停屏等等。
框架中将有占位符注释,指示在整个项目过程中将添加新行的点。
#!/usr/bin/python
import pygame, os, sys
from pygame.locals import *
pygame.init()
fpsClock = pygame.time.Clock()
mainSurface = pygame.display.set_mode((800, 600))
pygame.display.set_caption('Bricks')
black = pygame.Color(0, 0, 0)
# bat init
# ball init
# brick init
while True:
mainSurface.fill(black)
# brick draw
# bat and ball draw
# events
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
# main game logic
# collision detection
pygame.display.update()
fpsClock.tick(30)
在“pygamebook”文件夹中创建一个名为“bricks”的新文件夹。将文件保存在那里,并将其命名为“bricks.py”。
形象
游戏中使用了三幅图像,都可以从该书网站的参考资料部分下载( http://sloankelly.net
)。如果您不想使用这些图像,您可以创建自己的图像。然而,该游戏为每个图像假定了以下尺寸。参见图 11-2 至 11-4 。
图 11-4。
Brick.png 31×16 像素
图 11-3。
Bat.png 55×11 像素
图 11-2。
Ball.png 8×8 像素
移动球棒
用户使用他们的鼠标控制球棒。我们通过忽略鼠标上 y 轴的变化,将移动限制在 x 轴上。球棒也被限制只能在屏幕范围内移动。在比赛过程中,球棒必须留在比赛场地(屏幕)内。
Bat 初始化
在框架中找到以下代码行:
# bat init
在那一行下面,添加几个空行给你一些空间。键入以下内容:
bat = pygame.image.load('bat.png')
我们的 bat 作为一个名为‘bat’的曲面加载到内存中。不需要叫这个,但是把你的变量叫做有意义的东西是有意义的。例如,你也可以称它为“batImage”或“batSprite”。
playerY = 540
我们的玩家的移动被限制在 x 轴上,所以他们在屏幕上总是在 540 像素的高度。这相当接近底部;请记住,随着 y 轴值的增加,您会在屏幕上移动得更远。
batRect = bat.get_rect()
蝙蝠的矩形将在我们以后的碰撞检测计算中使用。
mousex, mousey = (0, playerY)
我们给鼠标坐标一个默认值。注意我们在这里使用了一个元组。我们也可以这样写这一行:
mousex = 0
mousey = playerY
这占了两行,并没有暗示我们的价值观是什么;它们在屏幕上代表蝙蝠在 2D 空间的坐标。
画蝙蝠
每次执行主循环时,我们在一行中清除主表面,它已经包含在主循环中:
mainSurface.fill(black)
这用黑色填充了主表面,这样我们就可以在上面画其他的东西了!向下滚动到这一行:
# bat and ball draw
并在其后添加下面一行:
mainSurface.blit(bat, batRect)
保存并运行游戏。你看到了什么?球棒应该在屏幕的左上方。但是为什么会这样呢?答案就在“batRect”再看一下“batRect”的初始化:
batRect = bat.get_rect()
这将包含球棒的尺寸:
(0,0,55,11)
这意味着图像将在(0,0)处绘制。是时候移动球棒了。
移动球棒
移动球棒分两步完成:
-
捕获鼠标输入
-
在新位置绘制蝙蝠图像
向下滚动到标有
# events
将下面的代码改为:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
elif event.type == MOUSEMOTION:
mousex, mousey = event.pos
if (mousex < 800 - 55):
batRect.topleft = (mousex, playerY)
else:
batRect.topleft = (800 - 55, playerY)
那是许多标签!注意制表符的位置,否则你的代码将不起作用。
事件
事件是由 Windows 管理器生成的,无论是在 Microsoft Windows、Mac OS 下,还是在 Linux 操作系统下的 X-Windows 管理器,比如在 Raspberry Pi 上运行的那个。应用于当前活动窗口的事件由系统传递给它进行处理。您只需要检查想要对其执行操作的事件。在这个游戏中,我们只对检查这个感兴趣:
-
用户关闭窗口
-
移动鼠标的用户
-
用户点击鼠标按钮(稍后)
退出事件
根据需要,每个事件都作为带有附加参数的事件类型传递。对于 QUIT 事件,没有附加参数。QUIT 只是给应用一个关闭的信号,我们通过退出 PyGame 和程序本身来完成。
鼠标移动事件
当用户移动鼠标时,信息从硬件(鼠标、物理接口、一些控制器芯片)通过一些低级操作系统驱动程序传递到当前活动的应用。在这种情况下,我们的游戏。随之而来的是鼠标的位置以及任何被按下的按钮。与所有事件一样,只有当事件发生时(在这种情况下,鼠标被移动),才会传递此消息。
鼠标移动的事件类型是“MOUSEMOTION ”,有一个名为“pos”的参数,其中包含鼠标的位置。“pos”是一个包含鼠标位置的 x 和 y 坐标的元组。
新的 x 坐标被限制在屏幕的范围内,然后赋给“batRect”变量的“topleft”属性。
保存并运行程序。蝙蝠现在会随着鼠标的移动而移动。如果没有,或者出现错误,请检查您的代码。这可能是一个迷路或丢失的标签。
移动球
移动球完全是用代码完成的,不需要用户输入,除了第一次点击鼠标按钮让事情滚动,如果你原谅这个双关语的话。
球初始化
球的初始化看起来非常类似于球棒的初始化。在代码中找到这一行:
# ball init
在下面添加以下几行:
ball = pygame.image.load('ball.png')
ballRect = ball.get_rect()
ballStartY = 200
ballSpeed = 3
ballServed = False
bx, by = (24, ballStartY)
sx, sy = (ballSpeed, ballSpeed)
ballRect.topleft = (bx, by)
前两行加载图像并捕获它的矩形。接下来的两行设置了起始 y 坐标和速度的默认值。在后面的代码中,变量“ballServed”用于确定球是否被发球。剩下的几行设置了球的初始位置和速度。
向下滚动代码到
# bat and ball draw
添加以下行在屏幕上绘制球:
mainSurface.blit(ball, ballRect)
保存并运行游戏。现在你会在屏幕的左上角看到球。如果您不知道,请对照上面写的代码行检查您的代码。打字错误或错别字是很常见的,即使是经验丰富的程序员也不例外!
球运动
球的移动是通过将球的速度加到当前位置来实现的。这是物理方程式:
速度=距离/时间
我们如何用代码做到这一点?向下滚动到这样一行
# main game logic
计算距离的公式是
距离=速度×时间
因为我们的速率固定为每秒 30 帧,所以我们将每 1/30 秒向当前位置添加一次速度。这意味着 1 秒钟后,我们的球将会运行
30 × 3 = 90 像素
所以,球的实际速度是每秒 90 像素。
就在“主游戏逻辑”注释行之后,添加以下代码并运行游戏:
bx += sx
by += sy
ballRect.topleft = (bx, by)
这里引入了一个新的符号。+=运算符用于将运算符左侧的值与右侧的值相加,并将总和放入运算符左侧的变量中。是 bx = bx + sx 的简称。还有其他简短形式的操作符,如–=(减)、×=(乘)和/=(除),它们遵循我们为+=概述的规则。球现在将缓慢地从屏幕的左上角沿对角线移动到右下角。如果它击中了球棒会怎么样?当它到达屏幕的末端时会发生什么?没什么;球正好穿过球棒,掠过屏幕边缘。
让我们补救这种情况。首先,我们将把球夹在屏幕区域的范围内。我们的屏幕尺寸是 800×600 像素。我们的球的大小是 8×8 像素。我们将使用一些布尔逻辑来确定,从球的位置,如果它击中了边缘。如果是这样,我们将逆转速度。这意味着在下一个循环中,球将向相反的方向移动,如图 11-5 所示。
图 11-5。
球击中侧壁,显示沿 x 轴方向反转
图 11-5 显示了碰撞的两个阶段:检测和响应。检测——两个物体相撞了吗?反应——我们该怎么办?在这种情况下,我们检测球是否接触到屏幕的外部边缘,我们的反应是将球反射回它来的方向。
检测确定两个物体是否接触过
反应是两个物体碰撞时执行的动作
在焊球位置更新代码后添加一到两行空行,并添加以下内容:
if (by <= 0):
by = 0
sy *= -1
球的 y 坐标与 0 进行比较,0 是显示屏上最上面的一行像素。记住屏幕左上方是(0,0),最下方是最大尺寸;在我们的例子中,就是(800,600)。这段代码将确保屏幕最顶端的边界反射球。球只在 y 轴上反射,因为我们碰到了屏幕的垂直边界,在这种情况下是顶部边缘。
对屏幕底部做同样的操作。在这种情况下,我们必须从最底部的数字中减去球的大小。我们的球是 8×8 像素,所以这意味着我们必须减去 8。请记住,当我们在屏幕上绘制图像时,我们是从图像的左上角开始绘制的:
if (by >= 600 - 8):
by = 600 - 8
sy *= -1
屏幕的侧面将反映在 x 轴上,而不是 y 轴上:
if (bx <= 0):
bx = 0
sx *= -1
这将在左侧边缘反射球(当 x 为 0 时)。最后,当我们在右边时(当 x 是 800–8 或 792 时),我们将进行反思:
if (bx >=800 - 8):
bx = 800 - 8
sx *= -1
保存并运行游戏。现在你会看到球在屏幕上反弹。但它仍然会穿过蝙蝠。我们需要在游戏中添加更多的代码,让它与球棒发生碰撞,这样它就会在屏幕上弹起。
球棒和球的碰撞
球棒和球碰撞的工作方式类似于检查屏幕底部的碰撞。我们将使用 rect 类的 colliderect 方法来确定是否发生了冲突。
在您键入的最后一个代码后添加几个空行,并添加
if ballRect.colliderect(batRect):
by = playerY - 8
sy *= -1
colliderect 接受单个参数,该参数表示我们要对其进行碰撞检查的矩形。colliderect 方法根据矩形是否相交返回布尔值“真”或“假”。见图 11-6 。
图 11-6。
显示接触、不接触和相交的碰撞矩形
左上角的图像显示,当两个矩形接触时,colliderect 将返回“True”。右上方的图像显示,当两个矩形不接触时,colliderect 将返回“False”。
下面两张图片显示了球棒和球相交时发生的情况。Colliderect 将返回“True ”,因为两个矩形接触,但在代码中,我们必须向上移动球的位置,使它们不接触。这阻止了任何异常现象的发生;如果你从侧面击球,球会进入球棒内部!通过替换球来接触球棒的顶部,我们绕过了这个问题,这条线:
by = playerY - 8
是解决问题的方法。保存并运行代码,你就可以用球棒把球击回屏幕。
发球
到目前为止,我们只是在比赛开始时发球。我们希望将发球限制在用户点击鼠标左键的时候。首先,如果球没有被发球,我们将停止球的运动。找到线:
# main game logic
您应该会看到下面的这几行:
bx += sx
by += sy
ballRect.topleft = (bx, by)
将这几行改为
if ballServed:
bx += sx
by += sy
ballRect.topleft = (bx, by)
保存并运行游戏将显示球停留在左上角。
要让它动起来,我们必须将“ballServed”改为“True”为了做到这一点,我们必须响应玩家点击鼠标左键。这在代码的事件部分。向上滚动到事件部分,在最后一个“elif”块后添加以下行:
elif event.type == MOUSEBUTTONUP and not ballServed:
ballServed = True
MOUSEBUTTONUP 测试鼠标上的任何按钮是否被“按下”。所以,真的,右键也可以。我们还测试了 ballServed 已经为“真”的情况。如果球已经发了,我们不需要再发一次。
砖墙
我们快到了!这个谜题的最后一块是玩家必须摧毁的砖墙。如本节开头的屏幕截图所示,我们将在屏幕中央排列砖块。
找到代码中的以下行:
# brick init
添加以下行,列与上一行的井号(#)对齐:
brick = pygame.image.load('brick.png')
bricks = []
再一次,我们加载一个图像,我们将使用它作为砖块。然后,我们创建一个空列表,在其中存储每块砖的位置。
for y in range(5):
brickY = (y * 24) + 100
for x in range(10):
brickX = (x * 31) + 245
width = brick.get_width()
height = brick.get_height()
rect = Rect(brickX, brickY, width, height)
bricks.append(rect)
我们的砖块排成五排,每排十块。我们将砖块位置存储在“砖块”列表中。我们的砖块位置存储为 Rect 实例,因为这将使以后的碰撞检测更容易。
向下滚动找到这行代码:
# brick draw
在后面添加以下几行:
for b in bricks:
mainSurface.blit(brick, b)
保存并运行游戏。你现在会看到砖墙。再一次,你会注意到碰撞不起作用,所以球只是穿过墙壁。我们将在最后一节中解决这个问题。
砖块和球碰撞
我们的球棒和球在移动,我们的砖墙在展示。我们在这个项目中的倒数第二个任务是在球击中砖块时摧毁它们。这类似于球击中球棒,除了我们将移走被击中的砖块。幸运的是,PyGame 在 Rect 类上提供了一个名为 collidelist()的方法。
向下滚动源代码并找到
# collision detection
你会记得我们的砖块只是一列长方形。collidelist()方法获取矩形列表,并返回命中的两个矩形的索引。我们将使用球的矩形作为测试的左侧,并将砖块变量作为函数的参数:
brickHitIndex = ballRect.collidelist(bricks)
if brickHitIndex >= 0:
hb = bricks[brickHitIndex]
捕获与 ballRect 矩形相交的砖块中包含的砖块矩形的索引。通俗地说,就是找出球碰到了哪块砖。如果没有击中砖块,该方法返回–1。所以,我们只对大于或等于零的值感兴趣。请记住,在 Python 中,列表从元素零(0)开始,而不是从元素 1 开始。
mx = bx + 4
my = by + 4
if mx > hb.x + hb.width or mx < hb.x:
sx *= -1
else:
sy *= -1
然后我们计算球的矩形的中点,因为球是一个 8×8 的图像,所以它向内 4 个像素,向下 4 个像素。然后我们用被撞砖块的宽度来测试。如果球在宽度之外,那么球是从侧面被击中的。否则,球会击中砖块的顶部或底部。我们通过改变球的速度来相应地偏转球。
del (bricks[brickHitIndex])
因为我们击中了砖块,所以我们将它从列表中移除。
保存并运行游戏。当球击中砖块时,砖块将被移除,球将因击中而反弹。那么,点击屏幕底部呢?
出界
当球击中屏幕底部时,它应该被标记为出界。目前,我们还没有做到这一点,球只是从底部反弹。
向下滚动源代码,找到这样一行
# main game logic
您将看到这段代码:
if (by >= 600 - 8):
by = 600 - 8
sy *= -1
替换为
if (by >= 600 - 8):
ballServed = False
bx, by = (24, ballStartY)
ballSpeed = 3
sx, sy = (ballSpeed, ballSpeed)
ballRect.topleft = (bx, by)
当球击中屏幕底部时,“发球”标志被重置为“假”,表示球没有被发球。因为球还没发,就不更新了。该代码还将球的位置和速度重置为初始值。
保存并运行完整的游戏,点击任何鼠标按钮发球,并使用鼠标移动。
结论
你已经写了你的第一个游戏!这个游戏真正展示了 Python 和 PyGame 的威力,因为像这样的游戏包含以下内容:
-
鼠标移动
-
自动球运动
-
冲突
-
砖块毁坏
-
边界检查
这都可以在大约 120 行代码中实现。
现在我们已经有了第一个游戏,我们将花一些时间来学习更多关于 Python 语言的知识。
十二、用户定义的函数
用户定义的函数允许你打包和命名几行代码,并在整个程序中重用这些代码行。你所要做的就是调用你赋予你的函数的名字。
什么是函数?
Python 中的一个函数可以用来执行一个简单的任务,因此它只是一个助记符或给一组行的特殊名称。您还可以选择将值作为参数发送到函数中,或者从函数中返回值。一个函数只能返回一个值,但是这个值可以是一个元组。
函数的格式
下面的简单函数在被调用时显示“Hello world ”:
def sayHello():
print("Hello, world!")
sayHello()
使用 def 关键字定义函数。该函数由它的名称和括号“(”和“)”内的可选参数组成。
因为它是一个 Python 块,所以第一行以冒号结尾,组成块的行缩进一个制表符。
作为一个卑微的任务/记忆装置
简单来说,函数可以用作记忆或替换你将反复使用的多行代码。例如,如果您想要显示一个框,您可能想要使用如下内容:
def drawBox():
print("+--------+")
print("| |")
print("+--------+")
drawBox()
print("Between two boxes")
drawBox()
这段代码的输出是
+--------+
| |
+--------+
Between two boxes
+--------+
| |
+--------+
当我们想画一个盒子时,我们现在有了一致性。当我们调用 drawBox()时,每个盒子看起来都和其他盒子一样。
函数允许你重复使用代码
这就是函数的力量:它们允许所谓的代码重用。代码重用意味着您可以在应用中多次使用同一个代码块。如果您出于任何原因需要更改该函数,任何调用它的代码都将获得更改后的版本。
函数的另一个目标是让你调用的地方更容易阅读。一个代码块应该执行一个任务,而不是多个任务。写程序的时候,要考虑这些断点应该出现在哪里。这些应该是你的功能。
例如,您被要求从键盘读入温度,并将它们写入文件,计算平均值、最大值和最小值,并将它们存储在单独的文件中。您可以编写名为
-
getTemperatures()
-
写入温度()
-
calcAverage()
-
calcMinimum()
-
calcMaximum()
-
writeStats()
然后这些将从主程序中以正确的顺序被调用。
发送参数
拥有一个可以在多个地方重复执行的代码块当然很好,但是有一点限制。如果每次调用它的时候都想改变一些值呢?参数(或自变量)可用于为您的函数提供更多信息。例如,您要绘制的框的宽度和高度。考虑以下函数:
def drawBox(width, height):
drawBox()方法有两个参数:一个名为 width,另一个名为 height。这些参数从调用行传递到函数中(稍后会看到)。这些只是我们使用的名字,以便我们可以在函数体中以有意义的方式引用参数。
if width < 0:
width = 3
这些框是在基于字符的显示器上绘制的,因此,我们可以拥有的最小宽度是三个字符;这是因为我们在每个角上使用“+”字符,并用“–”表示水平线。
if height < 3:
height = 3
我们对身高也有类似的限制。我们的最小高度是三,因为我们必须有两条水平线和至少一条包含“|”、一些空格和“|”的线来表示盒子的垂直线。
width = width - 2
不管我们的宽度是多少,它都长了两个字符!这是因为每行都以“|”开始和结束。因此,字符数为宽度–2(两个“|”字符)。
print("+" + "-" * width + "+")
我们的顶行是固定的,因为它包含由“+”表示的角块。我们还使用 Python 方便的字符串算法来生成字符串行;“+”用于将两个字符串连接(相加)在一起,而“*”用于将一个字符串与一个数字相乘,以将一个字符重复一定的次数。
for y in range(3, height + 1):
print("|" + " " * width + "|")
for 循环遍历从“3”到高度加 1 的每个值。请记住,范围是从起始值到比您想要的数字小一的值。同样,我们使用字符串算法来生成我们的行。
print("+" + "-" * width + "+")
我们通过画出盒子的底部来结束这个功能。
要调用该函数,您可以使用函数的名称,然后传入我们想要使用的参数:
drawBox(5, 4)
您必须知道每个参数的用途,这就是为什么将参数命名为易于识别的名称是个好主意。在这个例子中,如果盒子的宽度是 5,高度是 4,那么它的输出将是
+---+
| |
| |
+---+
默认参数值
可以指定每个参数的默认值。这意味着如果用户不想指定一个参数的值,他们不必这样做。假设我们想要默认宽度和高度为 3。将函数定义更改为
def drawBox(width = 3, height = 3):
如果我们只是想要一个 3×3 的盒子,我们可以这样做:
drawBox()
这会将默认值分配给宽度和高度。假设我们想要指定宽度而不指定高度。让我们创建一个 5×3 的矩形:
drawBox(5)
默认值必须是传递给函数的最右边的参数。下列函数签名是有效的,因为第一个带有默认值的参数后面的所有参数也被赋予默认值:
def drawBox(width, height = 10)
def drawSprite(sprite, width = 32, height = 32, transparency = 1)
以下函数签名无效:
def drawBox(width = 5, height)
def drawSprite(sprite = None, width, height, transparency = 1)
带有默认值的参数后面的所有参数也必须有默认值!
命名参数
如果我们只是想要一个默认的宽度,但是我们想要指定一个高度呢?这很简单;只需传入您想要为其指定值的参数的名称:
drawBox(height = 10)
这会画出一个 3×10 的盒子。宽度将默认为 3,因为它没有被赋值。这种技术称为命名参数,允许您通过名称指定参数。在其他语言中,可选参数(具有默认值的参数)必须放在参数列表的末尾。在 Python 中,可以使用命名参数来指定全部或部分可选参数。
返回值
函数的主要用途之一是从提供的参数生成新值。让我们先来看一个微不足道的例子,将两个数字相加:
def add(first, second):
return first + second
print(add(10, 5))
该函数通常用“def”关键字和函数名来定义。该函数有两个参数“第一”和“第二”
组成函数体的唯一一行是
return num1 + num2
“return”关键字获取其右侧的任何值,并将其传递回调用行。在我们的示例中,调用行是以下打印语句:
print(add(10, 5))
“第一个”被赋值为 10,“第二个”被赋值为 5。两者相加后返回。然后“print”关键字显示返回值。因为它是一个整数值,所以这是一个微不足道的任务,它只显示结果:
15
但是我们可以添加的不仅仅是整数值:
print(add('sloan ', 'kelly'))
print(add(3.14, 1.61))
print(add((1,2,3), (4,5,6)))
我们能加在一起的任何东西都可以使用这个功能。我们已经看到,Python 将从函数中返回我们想要的任何东西,这可能取决于传递的参数值是如何确定的。
返回元组
元组可以作为完整的元组返回,也可以作为单独的元素值返回。在下面的示例中,元组被返回并打印到屏幕上:
def getPlayerPosition():
return (10, 5)
print (getPlayerPosition())
输出是
(10, 5)
我们还可以在调用函数时将元组分解成单独的变量,例如:
def getPlayerPosition():
return (10, 5)
x, y = getPlayerPosition()
print ("Player x is", x)
print ("Player y is", y)
这将显示
Player x is 10
Player y is 5
访问全局变量
全局变量通常被认为是糟糕的编程实践。
它们会导致代码中的错误或 bug,因为跟踪每个全局变量何时被访问(值被读取)以及每次被更改(值被写入)都需要时间。
函数可以毫无问题地读取全局变量,如下例所示:
num = 5
def printNum():
print(num)
printNum()
如果我们改变函数内部的值呢?然后会发生什么?
num = 5
def changeNum():
num = 10
print(num)
changeNum()
print(num)
现在,输出是
5
5
为什么会这样呢?嗯,为了防止不好的事情在你的程序中发生,Python 有一个防故障技术来防止全局值被写入,除非你明确地说它们可以被写入。要在函数中将变量标记为“可写”,请添加 global 和全局变量的名称,如下所示:
num = 5
def changeNum():
global num
num = 10
print(num)
changeNum()
print(num)
添加了 global 关键字和全局变量的名称后,对 printNum 中“num”全局变量的任何更改都将被应用。该程序的输出现在将是
5
10
函数的真实示例
函数可以包含自己的变量。这些变量被称为函数的局部变量。它们不能被功能之外的任何东西看到或操纵。变量的这种隐藏被称为变量作用域。我们已经看到,全局变量可以在任何地方访问。对于局部变量,它们只对函数可见,并且只在函数执行时存在。
我们可以为砖块游戏重写一些代码来使用函数。我将把它作为一个练习,让读者将代码的其他部分转换成函数。我们将创建一个函数来加载砖块图像并设置砖块位置。
打开包含“砖块”游戏代码的 Python 文件。现在,您的代码应该有一个类似这样的区域:
# brick init
brick = pygame.image.load('brick.png')
bricks = []
for y in range(5):
brickY = (y * 24) + 100
for x in range(10):
brickX = (x * 31) + 245
width = brick.get_width()
height = brick.get_height()
rect = Rect(brickX, brickY, width, height)
bricks.append(rect)
拆下管路
brick = pygame.image.load('brick.png')
并替换为
brick = None
将剩余的几行改为
def createBricks(pathToImg, rows, cols):
global brick
该函数将接受三个参数。第一个是我们将用来画砖的图像文件的路径。第二个和第三个参数是我们想要的砖块的行数和列数。我们的砖块位置存储在名为“砖块”的列表中,图像名为“砖块”我们将在名为 brick 的文件顶部创建一个全局变量。这保持了我们对砖块的印象。
brick = pygame.image.load(pathToImg)
bricks = []
for y in range(rows):
brickY = (y * 24) + 100
for x in range(cols):
brickX = (x * 31) + 245
width = brick.get_width()
height = brick.get_height()
rect = Rect(brickX, brickY, width, height)
bricks.append(rect)
return bricks
现在,向下滚动到主循环开始处的这一行之前:
现在将这一行添加到“while True”的正上方:
bricks = createBricks('brick.png', 5, 10)
我们将砖块数据列表直接返回到我们的“砖块”变量中。这意味着我们不需要更早地创建一个变量,也不需要在函数中添加一个全局行。
谨慎使用全局变量!
全局变量可以而且应该避免,我们将在本书中看到如何避免。与其现在教那些技术,把水搅浑,不如让这种违规溜走,享受我们的第一场比赛!
保存并运行游戏。它应该像以前一样工作,但酷的是现在你可以很容易地改变砖的行数和列数,只需改变传递给“createBricks”的参数
结论
在本章中,我们探索了代码重用的第一个 Python 例子:函数。函数允许我们编写执行单一任务的宏程序,例如,显示精灵、保存游戏或设置游戏屏幕。
通过给函数名后括号中列出的形式变量(参数)赋值,可以向函数传递附加信息。每个参数都应该有一个名称来描述它们的用途,例如,“playerData”、“width”、“enemySprite”等。
有时函数并不需要所有的参数,您可以为每个参数添加默认值。如果有多个默认值,而您只想指定一个或两个值,那么在调用函数时也可以指定一个命名参数。
十三、文件输入和输出
能够从磁盘上保存和加载文件是游戏开发的一个重要部分。等级、玩家精灵等资产。,是从存储在磁盘上的文件中加载的。进度会保存到磁盘上,以便玩家可以从上次玩的地方继续游戏。
在这一节中,我们将了解文件输入和输出的基础知识,并介绍一种存储有序数据的方法,如我们在第七章中介绍的字典容器。
要保存和加载数据,您的脚本必须导入“os”(操作系统的缩写)模块来访问磁盘上的文件。
从磁盘读取文件
该程序从磁盘读取程序的源代码,并将内容显示在屏幕上:
import os
f = open('readmyself.py', 'r')
for line in f:
print(line)
f.close() # ALWAYS close a file that you open
open 关键字的第一个参数是我们想要访问的文件。第二个参数是我们希望访问文件的模式:
-
r '–读取文件的内容
-
w '–将数据写入文件
-
' a '–将数据追加(添加到现有文件的末尾)到文件中
对于 read,缺省值是“r ”,因此我们可以在这个实例中省略这个参数。最后,这仅适用于文本模式。这意味着如果我们传递一个' \n ',它将被转换为平台特定的行尾。在 UNIX 和 Raspbian 上是' \n ',但在 Windows 上是' \r\n '。
您可以将“b”添加到访问模式参数中(例如,“rb”或“wb”),以指定二进制模式。这种模式通常用于图像或复杂的保存数据。
open 关键字返回一个文件对象。我们可以用它从文件中读取信息,或者写出数据,这取决于我们想做什么。
不要忘记在你打开的任何文件上调用 close()。
在“pygamebook”文件夹中的“ch13”文件夹中,将程序保存为“readmyself.py ”,然后运行它。程序将显示内容,但它会在每行代码之间添加空行:
import os
f = open('readmyself.py', 'r')
for line in f:
print(line)
f.close()
它们不在文件中,那么它们来自哪里呢?嗯,在磁盘上,每一行都以一个换行符' \n '结束,print 关键字添加了自己的换行符,使得这些空行。
要解决这个问题,您可以添加。rstrip('\n ')到每个打印,就像这样:
print(line.rstrip('\n'))
函数的作用是:返回一个字符串的副本,其中所有指定的字符都已从字符串的末尾删除。默认情况下,这都是空白字符,但在这种情况下,我们只想去掉'换行'字符。
将数据写入文件
将文本写入文件使用 file 对象的 write 方法。下一个程序获取一个高分列表,并将其写入一个文本文件。
players = ['Anna,10000', 'Barney,9000', 'Jane,8000', 'Fred,7000']
该列表包含以逗号分隔的运动员姓名及其分数。
f = open('highscores.txt', 'w')
该文件以“写入”模式打开,因为我们正在向该文件发送数据。该文件的名称可以是您想要的任何名称,但它应该是有意义的名称。甚至不一定要以. txt 结尾。
for p in players:
f.write(p + '\n')
列表中的所有值都被循环,并且 File 对象的 write 方法被调用,列表项后跟一个“\n”。如果我们不包括这一点,文件将把所有的名字和分数混杂在一行中。
f.close()
你一定要记得在用完文件后把它关上。当我写一个读/写文件时,我总是先写开始和结束行,然后写我想对文件做什么。这意味着我永远不会忘记关闭文件。
在磁盘上找到“highscores.txt”文件,并输入以下命令:
$ more highscores.txt
您应该会看到以下输出:
Anna,10000
Barney,9000
Jane,8000
Fred,7000
虽然这是我们想要的,但是数据的内部结构是错误的。我们通常不会将玩家的名字和他们的分数存储为一个字符串。相反,我们使用某种容器。
向文件中读写容器
有两种方法可以将复杂数据读写到文件中。将举例说明的第一种方法是手动编写自己的格式。第二个是使用 JSON 格式来组织我们的数据,以便在文件中维护结构。
将内存中的数据写入文件称为序列化,将数据从文件读回内存称为反序列化。将数据写入磁盘的代码称为序列化程序,从磁盘读取数据的代码称为反序列化程序。我们将研究如何编写我们自己的序列化器和反序列化器,然后使用 Python 提供的 JSON 库来简化复杂数据的读写。
将数据从内存写入文件称为串行化
从文件中读取数据到内存被称为反序列化
通常,当您拥有专有的数据结构或格式时,或者如果您想要混淆(扰乱和混淆)您正在存储的内容以掩饰您正在做的事情,防止潜在的黑客攻击您的游戏时,您将编写自己的序列化方法。
编写自己的序列化程序
玩家和他们的分数是相关的,但是不应该一起存储在一个字符串中。相反,高分表将是一个字典,包含玩家的姓名(键)和他们的分数(值):
players = { 'Anna': 10000, 'Barney': 9000, 'Jane': 8000, 'Fred': 7000 }
我们可以使用“for”关键字遍历字典中的值,并依次获得每个元素的键。有了密钥,我们可以像这样解锁值:
for p in players:
print(p, players[p])
这将显示以下(几乎熟悉的)输出:
Anna 10000
Barney 9000
Jane 8000
Fred 7000
创建一个名为“serializer.py”的新程序,并输入以下代码:
def serialize(fileName, players):
f = open(fileName, 'w')
for p in players:
f.write(p + ',' + str(players[p]) + '\n')
f.close()
序列化方法有两个参数。第一个是高分表将被写入的文件的名称,第二个是包含球员姓名和分数的字典。将分数包装在 str()函数中会将值转换为字符串,这样我们就可以使用字符串串联(将两个或更多的字符串加在一起)。
players = { 'Anna': 10000, 'Barney': 9000, 'Jane': 8000, 'Fred': 7000 }
serialize('highscores.txt', players)
“玩家”字典是在调用 serialize 函数的上面创建的——也不需要在函数中添加“global ”,因为代码不会改变“玩家”字典,我们将它作为参数传递。
这为我们提供了之前的格式,因为相同的信息被写入文件:
Anna,10000
Barney,9000
Jane,8000
Fred,7000
现在,我们如何从文件中将数据读回内存呢?
编写自己的反序列化程序
反序列化有一个转折,因为数据是字符串格式的——我们毕竟是在写入字符串文件——并且名称和分数由逗号(,)分隔。逗号分隔的值非常常见,有一个名为“split()”的函数可以更容易地分隔字符串值。拆分字符串会返回字符串数组:
‘我的,弦,这里’会分裂成[‘我的’,‘弦’,‘这里’]
为了确保我们的分数存储在正确的数据类型中,使用了“int()”函数。将所有这些放在一起,我们的反序列化函数如下所示:
def deserialize(fileName, players):
f = open(fileName, 'r')
for entry in f:
split = entry.split(',')
name = split[0]
score = int(split[1])
players[name] = score
该函数有两个参数;第一个是包含高分数据的文件名,第二个是玩家的字典。
从文件中读入每一行,并使用逗号(,)作为分隔符调用 split()函数。这将把数值分成球员姓名和分数。向字典中添加一个条目,其中名称是键,整数版本的分数是值。
players = { }
deserialize('highscores.txt', players)
print(players)
“玩家”变量被设置为空白字典。调用函数并显示内容:
{'Anna': 10000, 'Barney': 9000, 'Jane': 8000, 'Fred': 7000}
数据
JSON 代表 JavaScript 对象符号,是系统序列化和反序列化数据以便存储或通过网络传输的常用方法。JSON 对象的格式非常类似于 Python 字典的格式。事实上,它们几乎完全相同。这是格式化为 JSON 字符串的高分表:
{"Anna": 10000, "Barney": 9000, "Jane": 8000, "Fred": 7000}
怪异,对吧!?
Python 提供了“json”模块,通过“json”对象的“dump()”和“load()”方法使读取和写入 JSON 对象变得更加容易。
要使用 JSON,您必须将下面一行添加到程序的顶部,同时导入其余的内容:
import json
JSON 序列化
JSON 序列化在一行中完成。回顾一下之前的 high score 序列化程序,我们可以重写“serialize()”函数:
import json
def serialize(fileName, players):
f = open(fileName, 'w')
json.dump(players, f)
f.close()
我们不必写出自己的格式,而是让“json”对象来完成繁重的工作。“dump()”方法将对象作为 JSON 格式的字符串写出到文件“f”中,不管它是什么。
players = { 'Anna': 10000, 'Barney': 9000, 'Jane': 8000, 'Fred': 7000 }
serialize('jsonhiscore.txt', players)
调用“serialize()”方法的部分不会更改;它仍然传入两个值,但是这次我改变了文件的位置。方便的东西功能!
要查看文件的内容:
$ more jsonhiscore.txt
这将显示以下内容:
{"Anna": 10000, "Barney": 9000, "Jane": 8000, "Fred": 7000}
JSON 反串行化器
' deserialize()'函数将略有变化,因为我们将返回' player '字典,所以我们不需要将它作为参数传入。“deserialize()”方法程序如下所示:
import json
def deserialize(fileName):
f = open(fileName, 'r')
players = json.load(f)
f.close()
return players
“json”对象上的“load()”方法在文件句柄中被调用。该函数获取文件的字符串内容,并构建适当的 Python 数据结构。这个函数的输出存储在变量“players”中,并返回给调用者。
players = deserialize('jsonhiscore.txt')
print (players)
在函数调用站点,我们可以看到“deserialize()”方法丢失了一个参数,但获得了一个返回值。返回值是一个字典,由“print()”的输出来演示:
{'Anna': 10000, 'Barney': 9000, 'Jane': 8000, 'Fred': 7000}
处理错误
文件访问有时可能是一个棘手的行为,因为文件可能会被系统锁定(病毒检查程序),或者您期望的文件可能不存在。为了解决这个问题,你可以使用结构化错误处理(简称 SEH)。你的程序不会崩溃,但是你应该优雅地处理这个事件。
在“ch13”文件夹中创建一个名为“filenotfound.py”的新程序。它演示了一个可以用来确定文件是否存在的函数。该函数尝试读取文件。如果成功,函数返回 True,否则返回 False:
import os
def fileExists(fileName):
try:
f = open(fileName, 'r')
f.close()
return True
except IOError:
return False
我们想要“尝试”执行的代码被放在“try”块中。如果出现问题,就会运行“except”中的代码。“try”块中的代码一旦遇到问题就会停止,因此,如果您有大量的处理,其中一些代码可能无法执行,因此最好使“try”块尽可能短。
print (fileExists('filenotfound.py'))
print (fileExists('this-does-not-exist.txt'))
这个程序的输出是
True
False
结论
现在,您应该了解如何读取和写入文件。完成后记得关闭文件。不要让文件打开超过必要的时间;打开它,做你需要做的,然后尽快关闭它。
序列化是将内存中变量的内容写入磁盘上文件的过程。写入磁盘的代码被称为串行化器。反序列化是读取磁盘上文件的内容并从中构造内存中对象的过程。从磁盘读取数据的代码被称为解串器。
您可以编写自己的序列化/反序列化方法,但是使用像 JSON 这样的预定格式来执行这些操作通常更容易。
磁盘访问有时容易出错,因为您调用的是操作系统。有时文件可能正在使用中,您将无法访问它。确保使用结构化错误处理或简称 SEH 来安全地访问文件。
十四、面向对象编程简介
到目前为止,我们一直将 Python 作为结构化语言使用。每行一个接一个地执行。如果我们想重用代码,我们就创建函数。还有一种编程方式叫做面向对象编程。在面向对象编程中,我们创建小对象,这些小对象不仅保存我们的数据,还将操作(我们想用这些数据做的事情)与数据本身组合在一起。面向对象编程(简称 OOP)的主要特点是
-
包装
-
抽象
-
遗产
-
多态性
接下来的两章将涵盖 OOP 的基础知识,以及如何在你的游戏中使用它。在这一章中,我们将使用许多新术语。这是对主题的简要概述,所以不要觉得必须快速浏览一遍,请慢慢来。
类别和对象
“类”是一个抽象事物的定义。“类”定义了可以对“实例”的数据(属性)采取的方法(动作)类定义可以和 Python 游戏的其他部分写在同一个文件中。然而,更常见的是将类放在它们自己的文件中。
存储在文件中的函数和类定义被称为模块。我们之前已经使用模块将额外的功能导入到我们的游戏中,例如 pygame、os 和 sys。
一个类的“实例”被称为“对象”用户定义类的“实例”很像“5”是一个整数的实例,或者“Hello,World”是一个字符串的实例。“integer”和“string”都是抽象概念,“5”和“Hello,World”分别是它们的实例。
OOP 允许你把你的程序分割成独立的包,就像我们对函数所做的那样,但是所有与类相关的数据和代码都存储在一起。
包装
封装是关于数据隐私的。类的内容——它的状态——是私有的,只有类内部的代码可以访问。
包含在类中的数据被称为私有字段。字段是变量,只能由拥有它们的类直接更改和读取。
字段也可以被暴露,尽管在 Java、C#和 C++这样的语言中,这通常是不被允许的。相反,内部字段隐藏在名为getter(用于获取数据)和setter的方法后面,用于给字段赋值。在任一情况下,字段也被称为属性。
向其他人公开的函数称为公共方法。这些允许外部代码与类交互。
抽象
除了封装,你还想让你的类尽可能简单。你不希望使用它的人不得不做一些复杂的步骤,或者太了解你的类的内部工作来使用它。
这就是抽象的由来。要打开游戏控制台并开始玩游戏,请按下电源按钮。这是一个简单的界面——按钮——它执行许多步骤:执行一个称为 POST(开机自检)的自检,从 BIOS 加载代码,然后启动操作系统。你所要做的就是按一个按钮。
遗产
有时你会开始编写一个类,并意识到它从另一个类复制了相当多的代码。事实上,大部分代码与另一个类相同。如果有办法共享代码就好了。有!这叫做继承,它允许一个类从另一个类派生。这样,您只需编写从基类更改而来的特定代码。说到这里,一个父类被称为基类,一个使用另一个作为基础的类被称为子类或派生类。
多态性
多态来自希腊语,意思是多种形状。在 OOP 中,有时需要改变子类。多态性可以与继承携手并进。例如,我们可能有一个形状类,圆形、正方形和三角形都是从它派生出来的。Shape 类有一个 draw() 方法,其他类实现在屏幕上绘制不同的形状。
为什么要用 OOP?
OOP 允许我们创建代码
-
数据隐藏
-
可重复使用的
-
更容易分别编码和测试
数据隐藏
信息存储在类中,而不是在程序中传递数据,或者更糟的是拥有全局数据。这些类中保存的数据只能通过该类公开的方法来访问。这些方法组成了接口,也就是你的游戏中其他代码是如何访问这个类的。
可重复使用的
就像函数一样,类可以被多个游戏重用。经过多年的编程,你可以建立一个相当大的类库。这些类中的每一个都可以在后续项目中使用。
更容易分别编码和测试
在一个较大的项目中,工作量可以在开发人员之间分配。随着工作量的划分,程序员可以编写类,并独立于游戏的其他部分测试它们。通过分别编写和测试这些类,您增加了可重用性的机会,因为这些类不相互依赖,可以独立工作。
球课
让我们以一个我们以前见过的物体为例:一个球。球可以用它的大小、形状和颜色来描述。这些是它的属性。在游戏世界中,我们不能对球做太多事情,但我们能做的是更新它的位置,检查碰撞,并在屏幕上绘制它。这些动作被称为方法。
在“pygamebook”中创建一个名为“ch14”的新文件夹将“砖块”项目中的“ball.png”图像复制到该文件夹中。在文件夹内创建一个名为“BallClass.py”的新文件。将以下几行添加到文件的顶部,告诉 shell 在哪里可以找到 Python 可执行文件,以及我们将需要哪些模块:
#!/usr/bin/python
import pygame, os, sys
from pygame.locals import *
在 Python 中,我们会这样描述 ball 类:
class Ball:
使用 class 关键字定义一个类。你必须给你的类一个名字。简短而有意义的东西是完美的,但要避免复数。如果你有一个项目的集合(比如球),使用 BallCollection 而不是 balls 作为类的名称。
x = 0
y = 200
speed = (4, 4)
img = pygame.image.load('ball.png')
这些变量被称为“成员字段”,它们是基于每个对象存储的。这意味着每个对象为每个字段获得一个单独的内存位。在我们的 Ball 类中,我们有四个这样的成员字段:一个用于 x 和 y 平面上的坐标、球的速度,一个用于球的图像。
def update(self, gameTime):
pass
方法被定义为带有 def 关键字、方法/函数名和参数列表的函数。主要区别是使用“self”关键字作为参数列表的第一个条目。
前面我提到过成员字段是针对每个对象的。使用“self”关键字是因为 Python 传入了对用于该操作的对象的引用。尽管每个对象的数据不同,但代码却是一样的。它由该类的所有实例共享。这意味着 ball 类的所有实例都使用更新 Ball 的同一段代码。
即使没有其他参数,也必须始终将“self”关键字作为方法参数列表中的第一个参数。
类方法的参数列表中的第一个参数总是“self”
这里有一个新的关键字,这不是 OOP 的一部分,但在这个例子中是至关重要的。我们制作了一个有效的存根。这意味着我们班做的不多。这些方法都不执行任何合理的操作,但是因为 Python 不能有空块,所以我们必须使用' pass '关键字。这在 C 风格的语言中相当于“{ }”。
def hasHitBrick(self, bricks):
return False
如果球击中了砖块,这个方法将返回 true。在我们的存根代码中,我们总是返回 False。
def hasHitBat(self, bat):
return False
我们测试球是否击中球棒的存根方法:
def draw(self, gameTime, surface):
surface.blit(self.img, (self.x, self. y))
这不是一个存根,因为我们确切地知道这将如何实现。我们使用主表面将图像传送到屏幕上正确的 x 和 y 坐标上。要访问对象的成员字段,我们必须使用“self”关键字。属于当前对象的属性和方法通过“self”后跟一个点(“.”来访问)后跟属性或方法。当调用方法时,不要传入“self”,Python 会为您处理。“self”只放在方法声明的参数列表中。
if __name__ == '__main__':
Python 知道每个模块的名称——记住,包含函数和/或类定义的 Python 文件是一个模块——它正在运行,因为它是不带'的文件名。py '扩展名。
使用以下方法之一执行 Python 脚本时:
$ ./myprogram.py
$ python3 myprogram.py
入口文件有一个特殊的名称,因此入口点文件的名称不是“myprogram”,而是“main”。我们可以利用这一点,因为这意味着我们可以将类放在单独的文件中;根据需要导入它们;更重要的是,单独测试它们。
这就是 OOP 的美妙之处:你可以用小对象,孤立地测试它们,然后将它们组合成一个更大的程序。
简单地说,这个“if”语句检查这是否是我们程序的主入口点,如果是,它将运行下面的代码块。如果不是,下面的代码块将被忽略。当我们在其他程序中使用' Ball '类时,我们不必删除这段代码,因为它会被忽略。
pygame.init()
fpsClock = pygame.time.Clock()
surface = pygame.display.set_mode((800, 600))
创建类的实例
这是我们几乎标准的 PyGame 初始化代码。我们初始化 PyGame 并创建一个时钟来将我们的游戏固定在每秒 30 帧。我们创建一个 800×600 像素的表面。
ball = Ball()
要创建一个类的实例,这就是所需要的:将该类的一个新实例分配给一个名称,就像将一个数字分配给一个名称一样。主要的区别在于赋值语句末尾的括号。这允许将参数传递给一个称为构造函数的特殊方法。稍后我们会看到 Python 中的构造函数是什么样子的。
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
我们使用了与 Bricks 程序中相同的代码来确保我们监听系统事件,尤其是当那些事件告诉我们关闭窗口时。
通过调用 ball 对象的“update()”方法来更新球的位置。此方法的实现将编码如下:请记住,目前它只包含“通过”:
ball.update(fpsClock)
我们的显示更新从这一行开始:
surface.fill((0, 0, 0))
清除屏幕以便绘图。我们在这里不用费心创建颜色,只要传入一个表示红色、绿色和蓝色组件的元组(全零是黑色)对于我们的测试代码就足够了。
ball.draw(fpsClock, surface)
在这一行中,我们在前面几行创建的球对象上调用 draw()方法。尽管方法签名有三个参数(self、gameTime 和 surface),但我们没有显式地传入“self”这是作为 ball 类的“Ball”实例在我的 Python 中传递的。
pygame.display.update()
fpsClock.tick(30)
最后,我们更新显示,将后台缓冲区翻转到前台缓冲区,反之亦然。我们也滴答时钟以确保稳定的每秒 30 帧。
Ball update()方法
当我们运行程序时,它不会做太多;事实上,它只是将球画在游戏屏幕的左上角。返回到球的 update()方法,并将其更改为如下所示:
def update(self, gameTime):
sx = self.speed[0]
sy = self.speed[1]
我们不能直接给元组赋值,所以我们将把值复制到局部变量中;它也节省了我们打字的时间。我们可以稍后重新分配元组。
self.x += sx
self.y += sy
if (self.y <= 0):
self.y = 0
sy = sy * -1
if (self.y >= 600 - 8):
self.y = 600 - 8
sy = sy * -1
if (self.x <= 0):
self.x = 0
sx = sx * -1
if (self.x >=800 - 8):
self.x = 800 - 8
sx = sx * -1
self.speed = (sx, sy)
对“sx”和“sy”的任何更改都将被重新分配到“速度”成员字段。
保存并运行程序。你应该看到球在屏幕上弹跳。
构造器
构造函数是一种特殊的方法,在实例化对象时调用。该方法不是使用带有对象、点和方法名的传统调用方法来调用的。当你创建球的时候,你实际上一直在调用构造函数:
ball = Ball()
虽然您没有显式创建构造函数,但是 Python 会为您创建一个。它不包含任何代码,看起来像这样(永远不要这样做,不值得;让 Python 在幕后为您创建一个即可):
def __init__(self):
pass
名称前后的双下划线,如 init,是 Python 使用的特殊方法名称。当你想做一些不同于默认行为的事情时,你可以用你自己的方法覆盖默认方法。Python 将这些名称描述为“神奇的”,因此你永远不应该发明自己的名称,而应该只在文档中使用它们。比如当我们想创建自己的构造函数时。
在 Python 中,构造函数方法被称为 init。它至少接受一个参数,即“self”关键字。在我们的球类中,我们将创建自己的构造函数。从类中删除所有这些行:
x = 0
y = 24
speed = (4, 4)
img = pygame.image.load('ball.png')
替换为
def __init__(self, x, y, speed, imgPath):
self.x = x
self.y = y
self.speed = speed
self.img = pygame.image.load(imgPath)
请注意,我们必须添加“自我”当我们读取或写入值到成员字段时。当我们在构造函数中时也是如此。向下滚动源代码到球初始化行,并将其更改为
ball = Ball(0, 200, (4, 4), 'ball.png')
这将把用于球图形的起始坐标、速度和图像文件传递给所创建的球实例。与函数一样,将值传递给构造函数的能力非常强大,并且允许在许多情况下使用您的对象。
固体
这一切意味着什么?在面向对象语言中,我们创建了一个类来表示我们的球。我们不关心这个类内部发生了什么,只要它做我们期望它做的事情。虽然我们将自己编写本书中的类,但我们可以将这项工作外包给其他开发人员,给他们一个规范或接口来编写代码。例如,所有动作对象都必须有一个接受 FPS 时钟的 update()方法。
类描述了分别描述和执行抽象数据结构的动作的属性和方法。有一个首字母缩略词描述了对象设计的五个原则。对于我们的游戏,我们将努力坚持这些原则:
-
单一责任
-
开闭原理
-
利斯科夫替代
-
界面分离
-
依赖性倒置
这些的首字母拼成实心的。虽然在你所有的游戏中使用这些技术并不重要,但是你应该努力使你的类在某种程度上遵守下面几节列出的原则。如果你愿意,你可以跳过这一步,直接进入结论部分。
单一责任
每个类都应该有一个单独的职责,并且这个职责应该包含在类中。换句话说,您有一个球类,它的功能应该包装在该类中。你不应该在同一个类中实现额外的功能,比如 Bat。为每个项目创建一个单独的类。如果你有很多空间入侵者,你只需要创建一个入侵者类,但是你可以创建一个 InvaderCollection 类来包含所有的入侵者。
开闭原理
你的类应该被彻底的测试(提示:name ==' main '),并且应该被关闭以防止进一步的扩展。进去修复错误是可以的,但是你现有的类不应该增加额外的功能,因为那会引入新的错误。您可以通过两种方式之一实现这一点:扩展或组合。
使用扩展,您可以扩展基类并更改方法的现有功能。通过组合,您可以将旧类封装在新类中,并使用相同的接口来改变调用者与内部类的交互方式。类接口就是可以在类上执行的方法(动作)的列表。
利斯科夫替代
这是迄今为止所有坚实的原则中最棘手的。这个原则背后的思想是,当扩展一个类时,子类的行为应该和它所扩展的类没有什么不同。这也称为类的可替代性。
界面分离
接口分离意味着您应该针对接口而不是实现进行编码。在其他 OOP 语言中有其他方法可以实现这一点,但是 Python 使用了一种叫做 Duck Typing 的东西。
在某些编程语言中,如 Java、C#和 C++,对象的类型用于确定它是否合适。然而,在 Python 中,适用性是由方法或属性的存在而不是对象的类型决定的。
如果它走路像鸭子,叫声像鸭子,那它就是鸭子
Python 会尝试调用具有相同名称和参数的对象上的方法,即使它们不是同一个对象。以这个示例程序为例。我们创建两个类:Duck 和 Person。每个类都有一个名为 Quack()的方法。观察 makeItQuack()函数中发生的情况。传递的参数调用它的 Quack()方法
class Duck:
def Quack(self):
print ("Duck quack!")
class Person:
def Quack(self):
print ("Person quack!")
def makeItQuack(duck):
duck.Quack()
duck = Duck()
person = Person()
makeItQuack(duck)
makeItQuack(person)
当我们创建 add()函数来把两个东西加在一起时,我们以前见过鸭子打字;整数、实数、字符串和元组都可以使用,因为它们都可以使用加号('+')运算符相加。
依赖性倒置
最后是依赖倒置。依赖倒置是一种解耦形式,其中较高级别的模块(类)不应该依赖于较低级别的模块(类)。相反,它们都应该依赖于抽象。第二,抽象不应该依赖于细节。细节应该依赖于抽象。让我们创建一个例子来更好地说明这一点。
class Alien(object):
def __init__(self):
self.x = 0
self.y = 0
def update(self):
self.x = self.x + 5
def draw(self):
print("%d, %d" % (self.x, self.y))
alien1 = Alien()
alien1.update()
alien1.draw()
Alien 类打破了开放/封闭原则,因为它对扩展是封闭的;如果我们想要一个对角线移动的外星人,我们必须创造一个新的职业。我们需要的是另一个类来计算外星人的新位置,就像这样:
class Strafe(object):
def update(self, obj):
obj.x = obj.x + 5
我们有一个单独的类来表示游戏中的每个外星人是如何在屏幕上移动的。这些类可以在创建外来对象时传递给它。假设我们想对角移动一个外星人:
class Diagonal(object):
def update(self, alien):
obj.x = obj.x + 5
obj.y = obj.y + 5
移动类钢鞭和对角线不需要知道他们正在移动什么,只要他们有称为“x”和“y”的字段。类似地,外星人类不需要知道钢鞭和对角线类做什么,只要他们有 update()方法。
class Alien(object):
def __init__(self, movement):
self.x = 0
self.y = 0
self.movement = movement
def update(self):
self.movement.update(self)
def draw(self):
print("%d, %d" % (self.x, self.y))
class Strafe(object):
def update(self, obj):
obj.x = obj.x + 5
class Diagonal(object):
def update(self, obj):
obj.x = obj.x + 5
obj.y = obj.y + 5
alien1 = Alien(Strafe())
alien2 = Alien(Diagonal())
alien1.update()
alien1.update()
alien2.update()
alien2.update()
alien1.draw()
alien2.draw()
为每个移动方法创建单独的类似乎有点过分,但这确实意味着在这个例子中,您不必为每个移动方法创建一个新的 alien 类。例如,如果你想添加一个垂直移动,只需添加几行代码就可以了。事实上,移动职业可以从世界另一端的玩家那里获取信息,而外星人职业永远不需要知道。
结论
这是对 OOP 的简短介绍。至此,您应该了解以下内容:
-
属性是成员字段,包含描述类的数据。
-
方法是属于一个类的函数,对该类执行操作。
-
自我是用来指代的。
-
创建对象实例时,可以使用构造函数来初始化成员字段。
-
Python 使用鸭式打字;当你看到一只像鸭子一样走路、像鸭子一样游泳、像鸭子一样嘎嘎叫的鸟……那就是鸭子。
作为练习,创建一个名为 BatClass 的新空白文件,并实现一个名为 Bat 的类。您可以使用砖块游戏中的代码作为起点。
十五、继承、组合和聚合
当大多数人学习面向对象编程时,他们会学到三样东西:
-
对象具有包含对象状态的属性(数据)。
-
控制访问(更改或查看)对象状态的方法。
-
可以使用一种叫做继承的技术来扩展对象。
还有其他的,但是这些是人们第一次接触面向对象编程时记住的三件主要事情。
大多数人关注最后一个:通过继承进行对象扩展。在很多情况下确实如此,但是有很多方法可以使用称为组合和聚合的技术来扩展对象。本节将介绍对象扩展的三种方法。
遗产
继承发生在 Python 语言的最底层。当你创建一个新的类时,你是在扩展一个叫做“object”的基类这个简单的物体
class Foo:
def bar(self):
print("bar")
foo = Foo()
foo.bar()
可以显式重写为
class Foo(object):
def bar(self):
print("bar")
foo = Foo()
foo.bar()
事实上,如果您正在使用较新的 Python 语法,我们鼓励您使用这种语法。在这篇文章的后面,你会看到它被用在“入侵者”游戏中。有关新旧方式的更多信息,请访问 https://wiki.python.org/moin/NewClassVsClassicClass
。
定义类时使用新的 MyClass(object)语法。
更进一步,让我们创建两个类。第一个是基类。
基类包含执行一组给定操作所需的基本功能级别。它可以包含方法,这些方法是将由子类实现的操作的占位符。
子类是从另一个类派生的任何类。实际上,你创建的每一个类都是 Python 基类“object”的子类。
基类和子类
在“pygamebook”中创建一个名为“ch15”的新文件夹,并在这个新文件夹中创建一个名为“baseclass.py”的文件,然后输入以下代码:
class MyBaseClass(object):
def methodOne(self):
print ("MyBaseClass::methodOne()")
当一个类从另一个类派生时,请记住将基类的名称放在新类名称后面的括号中:
class MyChildClass(MyBaseClass):
def methodOne(self):
print ("MyChildClass::methodOne()")
我们将创建一个函数来调用每个类的 methodOne()方法:
def callMethodOne(obj):
obj.methodOne()
此方法接受一个参数“obj ”,并调用该对象的 methodOne()方法。
instanceOne = MyBaseClass()
instanceTwo = MyChildClass()
然后,它创建“MyBaseClass”和“MyChildClass”类的实例。
callMethodOne(instanceOne)
callMethodOne(instanceTwo)
使用函数,我们传入基类和子类的实例。保存并运行程序。你应该看看
MyBaseClass::methodOne()
MyChildClass::methodOne()
调用该函数,然后它接受参数并调用它接收的对象的 methodOne()方法。在最后一个 callMethodOne()行之后添加另一行:
callMethodOne(5)
运行程序。您应该会看到类似如下的输出
MyBaseClass::methodOne()MyChildClass::methodOne()
Traceback (most recent call last):
File "baseclass.py", line 26, in <module>
callMethodOne(5)
File "baseclass.py", line 17, in callMethodOne
obj.methodOne()
AttributeError: 'int' object has no attribute 'methodOne'
这是因为 Python 内置的“int”对象不包含名为“methodOne”的方法。
Python 使用了一种叫做 duck typing 的技术。
当我看到一只像鸭子一样走路、像鸭子一样游泳、像鸭子一样嘎嘎叫的鸟时,我就把那只鸟叫做鸭子。
这意味着当 Python 看到一个对象上的方法调用时,它会假设该消息可以传递给它。这种技术的好处是继承几乎已经被一种叫做接口编程的技术所取代。
对接口编程意味着你不需要担心对象的内部工作;你只需要知道有哪些方法就可以了。
尽管如此,继承仍然有其适用的用途。例如,您可能有一个提供许多所需功能的基类。子类然后会实现它们特定的方法。
界面编程
让我们看另一个例子。我们将对两个不同的对象使用相同的方法,而不是使用继承:
class Dog(object):
def makeNoise(self):
print ("Bark!")
class Duck(object):
def makeNoise(self):
print ("Quack!")
animals = [ Dog(), Duck() ]
for a in animals:
a.makeNoise()
我们的两个类——Dog 和 Duck——都包含一个名为 makeNoise()的方法。创建一个包含 Dog 和 Duck 类实例的动物列表。然后遍历列表来调用每个对象的 makeNoise()方法。
关于构造函数和基类的注释
为了完善继承,我们需要提到调用对象构造函数的基类的推荐步骤。以下面两个类为例:
class Foo(object):
x = 0
def __init__(self):
print ("Foo constructor")
self.x = 10
def printNumber(self):
print (self.x)
class Bar(Foo):
def __init__(self):
print ("Bar constructor")
b = Bar()
b.printNumber()
当您运行此代码时,您将获得以下输出:
Bar constructor
0
即使“Bar”扩展了“Foo”,它也没有初始化“x”字段,因为没有调用父类的 init()方法。要正确地做到这一点,请将“Bar”的构造函数改为
def __init__(self):
super(Bar, self).__init__()
print ("Bar constructor")
这是怎么回事?super()方法允许我们引用基类;但是,基类需要知道两件事:派生类类型和实例。我们通过传入派生类的类型来实现这一点——在本例中是“Bar”和“self”然后我们可以调用方法 init()来正确设置我们的字段。当您运行该程序时,您应该看到
Foo constructor
Bar constructor
10
在派生类的构造函数中编写任何其他代码之前,必须始终调用基类的构造函数。如果您正在创建一个具有大量功能的基类并从它继承,这一点尤其正确。一定要调用基类的构造函数!
作文
组合是一个或多个对象包含在另一个对象中。对于复合,包含对象的创建和销毁由容器对象控制。容器对象通常充当所包含对象的控制器。例如:
class Alien:
def __init__(self, x, y):
self.x = x
self.y = y
def draw(self):
pass
“Alien”类只包含 x 和 y 坐标,用于在屏幕上的特定点显示外星人。你可能想添加的其他属性是外星人的类型或者它的护盾强度。
class AlienSwarm:
def __init__(self, numAliens):
self.swarm = []
y = 0
x = 24
for n in range(numAliens):
alien = Alien(x, y)
self.swarm.append(alien)
x += 24
if x > 640:
x = 0
y += 24
init()方法采用单个参数来表示群体中外星人的数量。该方法中的逻辑确保群体均匀地分布在屏幕上。每个外星人被 24 个像素宽和 24 个像素低分开。
def debugPrint(self):
for a in self.swarm:
print ("x=%d, y=%d" % (a.x, a.y))
def isHit(self, x, y):
alienToRemove = None
for a in self.swarm:
print ("Checking Alien at (%d, %d)" % (a.x, a.y))
if x>=a.x and x <= a.x + 24 and y >= a.y and y <= a.y + 24:
print (" It's a hit! Alien is going down!")
alienToRemove = a
break
if alienToRemove != None:
self.swarm.remove(alienToRemove)
return True
return False
swarm = AlienSwarm(5)
swarm.debugPrint()
“break”关键字用于退出封闭循环。当' break '关键字被执行时,程序的控制跳转到循环语句之后的那一行。一个相关的关键词是“继续”Continue 停止处理当前循环迭代中的剩余语句,并将控制权移回循环顶部。“中断”和“继续”都适用于任何循环结构。
Alien 类从不在 AlienSwarm 之外调用。它是由 AlienSwarm 类创建的,任何与外界的交互也是通过这个类完成的。
聚合
从概念上讲,聚合很像合成。容器对象有一个到其他对象的链接,它通过一个或多个方法以某种形式操纵它们。然而,最大的区别是对象的创建和销毁是在类之外的其他地方处理的。对于聚合,容器类不能删除它使用的对象。
假设我们有一个碰撞类,我们想检查玩家的子弹是否击中了外星人,我们可以实现类似这样的东西——假设 alien 和 AlienSwarm 保持不变:
class Bullet:
def __init__(self, x, y):
self.x = x
self.y = y
class Player:
def __init__(self):
self.bullets = [Bullet(24, 8)]
self.score = 0
def getBullets(self):
return self.bullets
def removeBullet(self, bullet):
self.bullets.remove(bullet)
class Collision:
def __init__(self, player, swarm):
self.player = player
self.swarm = swarm
def checkCollisions(self):
bulletKill = []
for b in player.getBullets():
if swarm.isHit(b.x, b.y):
bulletKill.append(b)
continue
for b in bulletKill:
self.player.score += 10
print ("Score: %d" % self.player.score)
self.player.removeBullet(b)
swarm = AlienSwarm(5)
player = Player()
collision = Collision(player, swarm)
collision.checkCollisions()
Collision 类是一个聚合,也就是说,它包含对另外两个类的引用:Player 和 AlienSwarm。它不控制这些类的创建和删除。
这符合我们坚实的原则;每个类应该有一个单一的目的,并且应该相互独立。在这种情况下,我们的玩家类不需要了解外星人,同样,AlienSwarm 类也不需要了解玩家。我们可以使用我们的接口创建一个介于两者之间的类,以允许我们(程序员)确定是否发生了冲突。
结论
Python 支持标准的 OOP 技术,但也提供了自己独特的一面:duck typing。通过对接口编程,我们可以确保我们的类可以彼此独立地编写。
对界面编程,保持你的类小而灵活
十六、游戏项目:贪食蛇
在我们的第二个游戏中,我们将重现经典的贪食蛇游戏。自 20 世纪 70 年代末以来,蛇就一直伴随着我们,如果你有一部诺基亚手机,你可能会在上面安装这个游戏的一个版本。你控制一条蛇,用光标键在屏幕上移动。你必须吃水果才能成长。你不允许触摸外墙或你自己。我说过你在成长吗?见图 16-1 。
图 16-1
贪食蛇游戏
在这个游戏中,我们将介绍以下内容:
-
类声明和实例(对象)
-
文件输入
-
基于单元的碰撞检测
-
功能
-
源文本
Snake 将使用比面向对象技术更多的函数。在很大程度上,我们在这个游戏中的对象只是为了组织的目的。将很少涉及 OOP。
功能
定义了以下功能:
-
drawData
-
drawegameover
-
拉丝克
-
牵引墙
-
headHitBody
-
headHitWall
-
loadImages
-
loadMapFile
-
爱情生活
-
定位浆果
-
更新游戏
我们可以创建一个结构图,如图 16-2 所示,展示这些功能如何协同工作。
图 16-2
贪食蛇游戏的结构图
结构图显示了每个功能如何相互作用。括号中的函数不存在。它们被用来像函数一样组合在一起。例如,绘制游戏调用三个独立的函数。我们可以创建另一个功能——我将让读者自行判断。
蛇形框架
贪食蛇游戏的基本轮廓如下所示。在您的工作文件夹中创建一个新文件,并将其命名为 snake.py。别忘了一边走一边看评论,帮助你理解发生了什么,作者(我)的意图是什么。我们将在本节的后面用代码替换一些注释。在键入代码时,您应该在自己的清单中包含注释。这将作为后面代码的占位符。
#!/usr/bin/python
import pygame, os, sys
import random
from pygame.locals import *
现在你应该知道我们节目的熟悉开头了!hash-bang 和导入我们需要的 Python 模块:PyGame、OS 和 System。我们也为这个游戏引入了一个新的:随机。这个模块将允许我们为浆果生成一个随机的起始位置。
pygame.init()
fpsClock = pygame.time.Clock()
surface = pygame.display.set_mode((640, 480))
font = pygame.font.Font(None, 32)
为了缩小地图尺寸,游戏将在 640×480 的窗口中运行。我们马上会看到如何创建地图。我们的 PyGame 初始化和保持每秒 30 帧的时钟也在这里初始化。我们的最后一点初始化是使用默认字体创建一个字体对象,大小为 32 像素。
class Position:
def __init__(self, x, y):
self.x = x
self.y = y
我们的第一堂课很简单:位置。这保持了地图块的位置。我们使用构造函数(在 Python 中,这是 init()方法)来传递 x 和 y 坐标。
class GameData:
def __init__(self):
self.lives = 3
self.isDead = False
self.blocks = []
self.tick = 250
self.speed = 250
self.level = 1
self.berrycount = 0
self.segments = 1
self.frame = 0
bx = random.randint(1, 38)
by = random.randint(1, 28)
self.berry = Position(bx, by)
self.blocks.append(Position(20,15))
self.blocks.append(Position(19,15))
self.direction = 0
游戏数据包含了我们需要存储的关于游戏的所有东西。这些数据的大部分是玩家的蛇。
-
生命——玩家剩余的生命数量。
-
Is dead–当蛇的头部接触到尾巴的一部分或墙壁时,设置为 true。
-
blocks–组成蛇尾的块的列表。
-
滴答——用于向下计数到下一个动画帧的累计总数。以毫秒计。
-
速度–默认的节拍速度。同样以毫秒为单位。
-
等级–当前的难度等级。
-
浆果数量——蛇在这一关吃掉的浆果数量。
-
细分市场——消费浆果时增加的细分市场数量。该值会改变每个级别。
-
帧–用于绘制蛇头的当前动画帧。这条蛇有两帧动画,与吃豆人没有什么不同。
-
方向——蛇当前行进的方向。0 是右,1 是左,2 是上,3 是下。这条蛇只能向四个方向之一移动。他们也不能逆转方向。例如,如果蛇向右移动,玩家不能向左移动。他们可以向上或向下移动,或者继续向右移动。
snake 从两个块开始,这两个块由“Position”类的两个实例表示;这意味着它有一个头部和一个尾部。每吃一颗浆果,碎片的数量就会增加。
浆果位置 bx 和 by 用于将浆果定位在游戏屏幕上的某个位置。这些存储在 GameData 类的“berry”属性中。
def loseLife(gamedata):
pass
def positionBerry(gamedata):
pass
def loadMapFile(fileName):
return None
def headHitBody(gamedata):
return False
def headHitWall(map, gamedata):
return False
def drawData(surface, gamedata):
pass
def drawGameOver(surface):
pass
def drawWalls(surface, img, map):
pass
def drawSnake(surface, img, gamedata):
pass
def updateGame(gamedata, gameTime):
pass
def loadImages():
return {}
这些都是绘制在结构图上的功能。
当我们开始实现游戏的功能时,我们会详细讨论它们。
images = loadImages()
images['berry'].set_colorkey((255, 0, 255))
我们的图像是使用 loadImages()函数加载的。图像存储在字典中。该键是一个字符串值,给出的示例显示我们将“浆果”图像的颜色键设置为紫色(红色= 255,绿色= 0,蓝色= 255)。PyGame 不会绘制任何与提供的颜色匹配的图像像素。这意味着你的图像中可以有透明的像素。这对于窗口或复杂形状(如浆果)非常方便。
snakemap = loadMapFile('map.txt')
data = GameData()
quitGame = False
isPlaying = False
这些局部(到主游戏循环)变量用于存储地图,创建 GameData 类的一个实例,一个确定用户是否退出游戏的控制变量,最后一个确定用户是否正在玩游戏。默认值为“False ”,因为我们希望以“游戏结束”模式开始游戏,以允许用户选择是玩游戏还是退出应用。
while not quitGame:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
在一个真实的游戏中,如果用户关闭了窗口,你可能不想退出游戏。或者,至少您会希望提示他们确认该操作。然而,在这个简单的游戏中,我们只是关闭游戏并退出到桌面。
if isPlaying:
x = random.randint(1, 38)
y = random.randint(1, 28)
我们的屏幕尺寸是 40 个街区长,30 个街区宽。对于 640×480 的屏幕,这意味着我们的块大小为 16×16 像素。这里产生的随机值将被用来放置玩家控制的蛇将要吃掉的浆果。
我们的随机值介于 1 和 38 之间,因为我们希望生成一个介于 1 和 38 之间的值。我们的地图将是一个构成游戏区域边界的实心块。我们将在下一节详细讨论这一点。
rrect = images['berry'].get_rect()
rrect.left = data.berry.x * 16
rrect.top = data.berry.y * 16
现在我们已经有了 x 和 y 坐标的随机值,我们将把它们分配给 berry 图像矩形的左和顶场。
坐标乘以 16,因为每个单元的大小为 16×16。
# Do update stuff here
我们的更新程序将放在这里。这只是一个占位符注释。这种类型的评论将贯穿全书。如果您将注释视为“键入”代码的一部分,请将其包含在您自己的源代码中。我们将在课文的后面回到这一点,如果你没有它,它会导致混乱。
isPlaying = (data.lives > 0)
如果玩家已经没有生命了,这是一个将 isPlaying 变量设置为 false 的好方法。你可以很容易地把它改写成一个“如果”语句。你会怎么做?
if (isPlaying):
isPlaying 的值可能在上一行之后发生了变化。这就是我们在这里对这个变量做另一个 if 检查的原因。
surface.fill((0, 0, 0))
# Do drawing stuff here
else:
如果游戏没有在玩,那么它就处于“游戏结束”模式。小心这个“else ”,因为它与前面的“if”语句成对出现。“游戏结束”模式向用户显示消息。如果他们想再次玩游戏,用户必须按键盘上的“空格”。
keys = pygame.key.get_pressed()
if (keys[K_SPACE]):
isPlaying = True
data = None
data = GameData()
如果用户按空格键,我们将 isPlaying 标志设置为 true,并将数据重置为 GameData 的一个新实例。当您处理完一个对象后,将指向它的变量设置为“None”是一个很好的做法。
drawGameOver(surface)
“游戏结束”屏幕是通过调用 drawGameOver()函数绘制的。
pygame.display.update()
fpsClock.tick(30)
我们的最后几行翻转屏幕(双缓冲显示),将帧速率限制在每秒 30 帧的最大值。保存程序。程序现在不会运行;我们需要先加载图像和地图数据,然后才能在屏幕上看到任何内容。
形象
游戏需要以下图片:
-
berry.png——蛇吃的浆果
-
snake.png–一个多帧图像,包含蛇使用的所有图像
-
wall.png——蛇无法穿越的障碍
我们的图像是 16×16,除了 snake.png 是 144×16 像素。这样做的原因是我们想要的蛇的所有图像都包含在同一个文件中。见图 16-3 。
图 16-3
蛇的骨架
这些图片和本书中的所有例子一样,可以从 http://sloankelly.net
下载。
加载图像
复制或制作图像,并将它们放在与 snake.py 文件相同的目录中。找到 loadImages()函数,并将其更改为如下所示:
def loadImages():
wall = pygame.image.load('wall.png')
raspberry = pygame.image.load('berry.png')
snake = pygame.image.load('snake.png')
这些图像是单独加载的,但我们将把它们放在一个字典中,以便将所有图像放在一起。
return {'wall':wall, 'berry':raspberry, 'snake':snake}
下一步是创建和加载组成游戏屏幕的地图。
游戏地图
游戏的地图保存在一个名为 map.txt 的文本文件中。在“snake.py”所在的文件夹中创建一个名为“map.txt”的新文件。在该文件中输入以下文本:
1111111111111111111111111111111111111111
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1000000000000000000000000000000000000001
1111111111111111111111111111111111111111
那是 30 行文本。顶线和底线是
1111111111111111111111111111111111111111
剩下的几行是
1000000000000000000000000000000000000001
如果你愿意,你可以尝试不同的 0 和 1 的模式。每个“0”代表一个蛇可以穿过的开放空间。每个“1”代表一堵墙,如果碰到它,蛇就会被杀死。保存该文件并打开 snake.py。找到 loadMapFile()函数并将其更改为
def loadMapFile(fileName):
f = open(fileName, 'r')
content = f.readlines()
f.close()
return content
readlines()方法将文件中的每一行文本读入一个列表。保存“snake.py”文件。
绘制“游戏结束”屏幕
如果我们现在运行游戏,我们将什么也看不到,因为我们没有实现任何绘制方法。让我们从显示“游戏结束”屏幕开始。找到 drawGameOver()函数,并将其更改为
def drawGameOver(surface):
text1 = font.render("Game Over", 1, (255, 255, 255))
text2 = font.render("Space to play or close the window", 1, (255, 255, 255))
Font 的 render()方法创建一个完全适合文本的 PyGame 表面。render()方法采用的参数是要显示的字符串、抗锯齿级别和颜色。
抗锯齿意味着文本不会出现锯齿状边缘。在图 16-4 中,你可以看到反走样和无走样的效果。
图 16-4
该字体的抗锯齿版本显示在图像的下半部分
图像被从中间分割开,在红线的左边显示反锯齿文本,在右边显示锯齿版本。
cx = surface.get_width() / 2
cy = surface.get_height() / 2
textpos1 = text1.get_rect(centerx=cx, top=cy - 48)
textpos2 = text2.get_rect(centerx=cx, top=cy)
我们在这里使用命名参数,因为我们不需要为文本位置指定所有的值。这两行创建了用于在屏幕中间放置文本的矩形。
surface.blit(text1, textpos1)
surface.blit(text2, textpos2)
传递给函数的表面实例的 blit()方法用于在表面上绘制文本。保存并运行游戏。当你运行游戏时,你应该会看到下面的屏幕(如图 16-5 所示):
图 16-5
游戏开始时出现的“游戏结束”屏幕
当你完成的时候关上窗户。如果你按“空格”,屏幕会变成空白,什么也不会发生,因为我们还没有添加更新或绘制屏幕的功能。现在让我们添加绘图功能。
画游戏
蛇的绘制、游戏区域和游戏数据(玩家的生活、分数和关卡文本)由三个函数执行:
-
牵引墙
-
拉丝克
-
drawData
在“snake.py”中,在源代码中向下滚动到这样一行
# Do drawing stuff here
在该注释下添加以下几行。确保每行有正确数量的制表符。每行的左栏应直接从注释的' # '下开始:
drawWalls(surface, images['wall'], snakemap)
surface.blit(images['berry'], rrect)
drawSnake(surface, images['snake'], data)
drawData(surface, data)
画浆果没有具体的例程,我们就直接调用主面的 blit()方法。我们在屏幕上画图是有顺序的。在其他图像之后绘制在屏幕上的图像将出现在顶部。所以,墙壁出现在蛇的后面,蛇出现在生命/分数显示的后面。
画墙壁
墙壁是在 drawWalls()函数中绘制的。在源代码中找到这个函数,并将其改为
def drawWalls(surface, img, map):
该函数有三个参数。第一个参数是我们将在其上绘制墙块的主表面。第二个是我们将用来表示墙中一块砖的图像,最后第三个参数是地图数据。这是我们之前从文件中加载的数据。
row = 0
for line in map:
col = 0
for char in line:
if ( char == '1'):
对于行中的每个字符,我们都要检查它。如果字符是“1 ”,我们就放一块积木。因为我们记录了行(变量“row”)和列(变量“col”)的数量,所以计算屏幕上的位置只需要将它们分别乘以 16。为什么呢?因为我们的块图像是 16×16 像素,而我们的文件不是按像素映射的。相反,映射文件中的每个字符代表一个 16×16 的块。
这是一个从零到我们给定的最大行列数排列的字符数组。在这个游戏中,最大值是 40 块乘 30 块。对于一个 640×480 的屏幕来说,就是每块 16×16 像素。
imgRect = img.get_rect()
imgRect.left = col * 16
imgRect.top = row * 16
surface.blit(img, imgRect)
每次绘制块时,都会更改图像矩形的左值和上值,以确保图像绘制到表面的正确位置。
col += 1
row += 1
保存并运行游戏。当你按空格键开始游戏时,你会看到操场周围的墙和浆果。当你准备好了,关闭游戏,让我们开始添加生命,级别和分数显示。见图 16-6 。
图 16-6
游戏运行时显示的墙和浆果与代码到目前为止
绘制玩家数据
玩家需要一些关于他们表现如何的反馈。这通常是他们得分、剩余生命数量以及当前水平的指标。我们将在 drawData()函数中添加代码,以给出玩家的反馈。在代码中找到 drawData()函数,并将其更改为:
def drawData(surface, gamedata):
该函数接受两个参数。第一个是我们将在其上绘制数据的表面。第二是实际的游戏数据本身。引入了一个新的字符串函数,称为 format。它类似于 print()方法使用的方法,但是结果可以存储在一个变量中。使用占位符代替数字和字符串的%d 和%s。第一个变量是{0},第二个变量是{1},依此类推:
text = "Lives = {0}, Level = {1}"
info = text.format(gamedata.lives, gamedata.level)
text = font.render(info, 0, (255, 255, 255))
textpos = text.get_rect(centerx=surface.get_width()/2, top=32)
surface.blit(text, textpos)
使用元组将数据注入字符串,将数据呈现为文本。这被称为字符串格式化,我们在前面的章节中看到了这种类型的代码。
此时保存程序。如果你愿意,你可以运行它。这一次,当游戏开始时,你会在屏幕上方看到玩家的生活和当前水平。
画蛇
画蛇比我们之前的绘图功能要复杂一点——这就是为什么我把它留在最后!我们的蛇形象(实际。png 文件)是 144 像素乘 16,这意味着它包含九个 16×16 的图像。我们需要以某种方式将它们分割成单独的图像。
在代码中找到 drawSnake()函数,并将其改为
def drawSnake(surface, img, gamedata):
该函数接受三个参数。第一个是要在其上绘制蛇的表面。第二个是蛇的图像,最后第三个参数是 GameData 实例。这包含了我们的蛇的所有信息。具体来说,GameData 类的 blocks 属性包含一个范围为 0 的坐标列表..39 为列,0 为..一排 29 个。坐标存储为“位置”类的实例。
这些坐标是位置类的实例。blocks 属性是一个列表,随着 snake 的增长,列表中的条目数量也会增长。
first = True
这被设置为 true,因为绘制的第一个块是特殊的。这是蛇头。我们在这里绘制的图像依赖于
-
蛇面对的方向
-
不管它的嘴是否张开
再看图 16-7 中的蛇图像。
图 16-7
蛇图像包含蛇的头部和尾部的编码数据
子图像实际上有一个模式。最后一个单元格是正常的尾块。剩下的八块代表蛇头。它们在数组中的位置对应于蛇的方向:
-
0–右
-
1–左侧
-
2 页以上
-
3-向下
如果我们将存储在 GameData 的 direction 属性中的方向号乘以 2,就可以得到我们想要的图像的起始单元格号。蛇头也是有动画的——它会开合。我们所要做的就是添加当前帧(GameData 的 frame 属性)来获得所需的图像。
for b in gamedata.blocks:
dest = (b.x * 16, b.y * 16, 16, 16)
我们循环遍历列表中蛇的所有块(位置)。目标是一个简单的计算:位置乘以单个单元格的尺寸(16×16 像素)得到屏幕坐标。
if first:
first = False
src = (((gamedata.direction * 2) + gamedata.frame) * 16, 0, 16, 16)
如果我们在列表的最前面,我们画蛇的头。请记住,我们可以通过指定一个元组来绘制图像的一部分,该元组表示子图像的起始 x 和 y 像素及其宽度和高度。
对于我们的蛇,我们的子图像的 x 坐标是使用以下公式计算的:
((方向 2) +动画 _ 帧)* 16*
我们的图像取自 sprite 工作表的顶部,顶部是 y 坐标为 0(零)的地方。我们的宽度和高度尺寸也固定为 16×16 像素。
else:
src = (8 * 16, 0, 16, 16)
对于普通块,我们只想绘制 snake.png 文件中的最后一幅图像。这是最右边的 16×16 正方形,恰好是图像的第八帧。我们可以硬连接该值,但在这种情况下,8 * 16 会产生更具描述性的代码。
surface.blit(img, dest, src)
保存并运行游戏,你会看到蛇、墙和玩家数据,如图 16-8 所示。
图 16-8
蛇、浆果和墙
更新游戏
静态屏幕虽然有趣,但不能代替实际玩游戏!然而,我们还没有实现任何例程来获取玩家输入,检查碰撞,或者更新玩家的数据。找到以下行:
# Do update stuff here
就在该行之后,添加以下代码:
updateGame(data, fpsClock.get_time())
大部分更新代码驻留在 updateGame()函数中。我们稍后会详细讨论这个问题。
crashed = headHitWall(snakemap, data) or headHitBody(data)
if (crashed):
loseLife(data)
positionBerry(data)
我们现在测试蛇的头部是撞到了一面墙(headHitWall()函数)还是自己的身体(headHitBody()函数)。如果是这种情况,玩家失去一条生命,浆果被重新定位。
updateGame()方法
这是游戏中最大的方法,也是最有效的方法。其目的是
-
更新蛇的头部和尾部
-
获取玩家的输入
-
查看蛇头是否击中了浆果
浏览到如下所示的代码部分:
def updateGame(gamedata, gameTime):
pass
将此功能更改为
def updateGame(gamedata, gameTime):
gamedata.tick -= gameTime
head = gamedata.blocks[0]
游戏的每个部分都可以以不同的速度更新。例如,您可能只想每秒更新一次游戏的某些部分,而其他部分您可能想每秒更新 30 次。这可以通过读取系统时钟并确定自上次调用代码以来的毫秒数来实现。在这个方法中,我们将自上次调用以来的差异(以毫秒为单位)作为“游戏时间”传递
游戏数据的滴答随着当前游戏时间而减少。当这个计数器达到零时,我们更新蛇的头部以显示它是关闭的(如果它当前是打开的)或打开的(如果它当前是关闭的)。我们也注意到蛇头的当前位置。这总是“游戏数据”的 blocks 属性的第零个元素
if (gamedata.tick < 0):
gamedata.tick += gamedata.speed
gamedata.frame += 1
gamedata.frame %= 2
如果 tick 属性小于零,我们就给它加上速度,重新启动计时器。然后,我们将当前帧数加 1。我们使用模计算将值固定为 0 或 1,因为我们只有两帧动画。在其他语言中,有一个“switch”或“case”语句。在 Python 中情况并非如此(抱歉),但是使用嵌套的 if/elif 语句很容易实现。
if (gamedata.direction == 0):
move = (1, 0)
elif (gamedata.direction == 1):
move = (-1, 0)
elif (gamedata.direction == 2):
move = (0, -1)
else:
move = (0, 1)
在蛇的游戏中,蛇总是在动;玩家只控制方向。基于玩家想要蛇移动的方向,创建适当的元组。
newpos = Position(head.x + move[0], head.y + move[1])
然后,这个元组用于生成和存储蛇头的新位置。
first = True
for b in gamedata.blocks:
temp = Position(b.x, b.y)
b.x = newpos.x
b.y = newpos.y
newpos = Position(temp.x, temp.y)
蛇的尾巴随着头部向上移动。这当然只是一种错觉;我们实际上做的是将蛇的各段移动到前一段的位置。
蛇形运动
与 updateGame()函数保持一致;蛇的运动被限制在四个方向之一:左、右、上、下。玩家只能真正建议运动:蛇自己在自己的蒸汽下运动。玩家通过按键盘上的一个箭头键来选择蛇的方向。
为了获得键盘输入,我们获取当前被按下的键的列表:
keys = pygame.key.get_pressed()
get_pressed()方法返回一个布尔值字典。现在我们已经按下了键,我们可以测试每个箭头键,看看玩家是否按下了它。我们还必须确保他们不会试图走相反的方向。如果玩家已经向左转,他就不能向右转;如果他已经向下,他就不能向上转,等等。
if (keys[K_RIGHT] and gamedata.direction != 1):
gamedata.direction = 0
elif (keys[K_LEFT] and gamedata.direction != 0):
gamedata.direction = 1
elif(keys[K_UP] and gamedata.direction != 3):
gamedata.direction = 2
elif(keys[K_DOWN] and gamedata.direction != 2):
gamedata.direction = 3
我们将当前方向存储在“gamedata”实例的方向字段中。
触摸浆果
updateGame()函数的最后一部分是处理我们对蛇头触摸浆果的反应。要继续游戏,玩家必须让蛇“吃掉”出现在游戏场上的浆果。为了“吃”浆果,玩家必须将蛇头转向浆果出现的细胞。一旦浆果被“吞噬”,一个新的浆果会被放置在屏幕上的另一个随机位置,蛇会增长一定数量的部分。分段的数量取决于玩家的级别。级别越高,添加到蛇的分段就越多。
if (head.x == gamedata.berry.x and head.y == gamedata.berry.y):
lastIdx = len(gamedata.blocks) - 1
for i in range(gamedata.segments):
blockX = gamedata.blocks[lastIdx].x
blockY = gamedata.blocks[lastIdx].y
gamedata.blocks.append(Position(blockX, blockY))
如果蛇的头部和浆果在同一个细胞中,那么我们在蛇的尾部附加适当数量的片段。我们加到最后的段数取决于游戏中的等级。级别越高,添加的分段越多。这使得游戏在后面的关卡中变得更加困难,因为每吃掉一个浆果,蛇就会有更多的部分。
bx = random.randint(1, 38)
by = random.randint(1, 28)
gamedata.berry = Position(bx, by)
gamedata.berrycount += 1
接下来,我们生成一个新位置,并将其设置为浆果的位置。我们还增加了一个计数器,记录我们的蛇已经吃掉的浆果的数量。
如果我们的蛇吃掉了十个浆果,我们就进入下一关。这有增加蛇的速度的附加效果(增加一点额外的刺激!),以及蛇每吃一颗浆果给玩家增加的段数。
我们将段的数量固定为 64,更新速度(毫秒)固定为 100:
if (gamedata.berrycount == 10):
gamedata.berrycount = 0
gamedata.speed -= 25
gamedata.level += 1
gamedata.segments *= 2
if (gamedata.segments > 64):
gamedata.segments = 64
if (gamedata.speed < 100):
gamedata.speed = 100
冲突检出
正如我们所看到的,这个游戏中的碰撞检测是基于每个单元而不是每个像素进行的。在某些方面,这使我们的工作更容易,因为我们需要做的就是确定一个块何时与另一个块重叠,换句话说,它们占用同一个单元。
助手功能
有四个函数我们还没有填充,但是没有它们我们将无法检测玩家是否撞到了墙或者蛇是否碰到了自己。我们缺少的实现是针对
-
迷失人生()
-
浆果位置()
-
headHitBody()
-
headHitWall()
失去一条生命
当蛇头撞到自己的尾巴或墙壁时,玩家失去一条生命。当这种情况发生时,我们删除了所有构成蛇尾巴的当前块,并从生命数中减去 1。然后我们给蛇添加两个方块,让玩家重新开始。在代码中找到“loseLife ”,并将其更改为如下所示:
def loseLife(gamedata):
gamedata.lives -= 1
gamedata.direction = 0
将生命数减一,并将方向重新设置为向右。
gamedata.blocks[:] = []
这一行删除列表中的所有项目。
gamedata.blocks.append(Position(20,15))
gamedata.blocks.append(Position(19,15))
在默认位置向蛇添加两个新块。
重新定位浆果
当玩家死了,我们必须为浆果找到一个新的位置。在代码中找到“positionBerry”函数,并将其更改为如下所示:
def positionBerry(gamedata):
bx = random.randint(1, 38)
by = random.randint(1, 28)
found = True
首先,我们在游戏中生成一个随机数。然后,我们循环遍历所有游戏块,以确保我们不会在蛇本身中随机生成一个位置:
while (found):
found = False
for b in gamedata.blocks:
if (b.x == bx and b.y == by):
found = True
检查浆果是否占据了与蛇块相同的位置是很容易的。我们只需要检查两个值是否相等:浆果和每个方块的 x 和 y 坐标。
if (found):
bx = random.randint(1, 38)
by = random.randint(1, 28)
如果浆果在包含方块的单元格上,则“found”设置为 True。如果发生这种情况,我们为“bx”和“by”变量分配新值,然后重试。
gamedata.berry = Position(bx, by)
一旦我们找到一个不包含一条蛇的块,我们就将位置分配给游戏数据的 berry 字段。
测试蛇身命中
蛇的头不能“触摸”它的身体。每次我们更新蛇的时候,我们还必须检查头部是否接触到了身体。我们基于单元的碰撞检测使这变得容易。我们只需要对照组成蛇身体的其余部分的 x 和 y 坐标来检查蛇头的 x 和 y 坐标。找到“headHitBody”函数,并将其更改为如下所示:
def headHitBody(gamedata):
head = gamedata.blocks[0]
创建一个变量来保存对组成 snake 的块列表中第一个块的引用。这是蛇头。
for b in gamedata.blocks:
一次检查一个模块。
if (b != head):
如果块不是头部,检查头部是否与当前块在同一个单元中。
if(b.x == head.x and b.y == head.y):
return True
如果头部与蛇身体中的块在同一位置,则向函数的调用方返回 True。
return False
否则,返回 False,向调用者表明没有冲突。
测试点击量
我们需要填写的最后一个函数是测试蛇头是否撞墙。找到“headHitWall”功能,并将其更改为:
def headHitWall(map, gamedata):
row = 0
for line in map:
col = 0
for char in line:
if ( char == '1'):
对于行中的每个字符,我们检查它是否是一个墙字符。我们的地图文件包含 0 和 1;任何“1”代表游戏区域中的一面墙。我们的控制变量“col”和“row”是根据块的第零个元素的当前位置来检查的。这是蛇头。
if (gamedata.blocks[0].x == col and gamedata.blocks[0].y == row):
return True
col += 1
row += 1
return False
结论
保存游戏并运行它。你应该可以开始玩蛇了。如果你有任何错误,对照书中的文本检查代码,确保你没有调换任何字母。记住空白很重要:那些“制表符”需要在正确的位置!作为最后一种选择,从 http://sloankelly.net
下载代码,然后对照那里的代码检查你的代码。
作为一个练习,改变生活/水平指标,以显示收集浆果的数量。如果每个浆果值 5 分,移动到另一个级别会给玩家额外的 100 分呢?你需要在游戏数据中加入哪些变量?*
十七、模型视图控制器
在“设计你的游戏”一节中提到过模型视图控制器,它描述了不同对象之间的交互如何被用来简化问题:将一个大问题分解成更小的更容易管理的块。见图 17-1 。
图 17-1
模型视图控制器设计模式
模型
模型表示与对象相关联的数据或属性。例如,一个玩家在空间中有一个位置,生命,护盾强度和得分。该模型通常只有很少的方法,可能与将数据保存或序列化到磁盘驱动器等廉价存储有关。这将用于保存游戏数据。然而,更有可能的是,您将拥有一个保存控制器,它将从模型中读取数据并存储它们。
视角
视图是游戏中每个模型的可视化表示。有些模型在游戏中没有直接的视觉表现。例如,与 RPG(角色扮演游戏)中的 NPC(非玩家角色)相关联的数据
控制器
控制器是连接模型和视图的粘合剂。玩家与视图交互(点击按钮,移动玩家),这调用控制器上的一个方法。接着,控制器更新模型以表示新的状态。
在计算术语中,状态是对象或值的当前值。例如,在一种状态下,玩家可能在跳跃,而在另一种状态下,他们可能在奔跑。在每个状态中,内部变量(对象的字段)被设置为特定的值。
为什么要用 MVC?
MVC 允许你,程序员,把对象的功能从它的视觉表现和数据中分离出来。由于每个职责由不同的类处理,所以很容易更换不同的控制器和视图。
作为 MVC 的一个说明性例子,让我们创建一个小游戏,用光标键在屏幕上移动机器人。我们将添加第二个视图,它包含雷达视图中的一个小光点。我们首先将这些类分离到不同的文件中,然后使用另一个文件作为“粘合代码”将它们合并到一个游戏中,这是游戏的主要循环。见图 17-2 。
图 17-2
机器人“游戏”显示机器人在中间。雷达在左上方
上层社会
我们将要创建的类是
-
雷达视图
-
机器人控制器
-
机器人发电机
-
机器人模型
-
机器人视图
您不需要为您创建的每个类添加“模型”、“视图”和“控制器”,但是在这个例子中它清楚地向我们展示了什么类执行什么目的。
雷达视图
雷达视图在窗口左上角的一个小屏幕上显示一个代表机器人的小光点。
机器人控制器
机器人控制器根据玩家的输入改变模型的状态。
机器人发电机
机器人生成器在指定的时间段后在屏幕上的随机位置生成一个机器人。也可以设置机器人的最大数量。
机器人模型
机器人模型保存机器人的状态。它根本没有方法,只有数据。
机器人视图
机器人视图在屏幕上显示机器人。它不改变机器人模型;它只是从模型中读取数据,并根据模型的状态决定显示什么。
文件夹
在“pygamebook”文件夹中创建一个名为“ch17”的新目录我们将在这个目录中创建所有文件。
机器人模型
名为 RobotModel 的模型类只包含机器人的数据。这个类的每个实例的更新将使用 RobotController 来完成,这个类将在后面定义。
创建一个名为“robotmodel.py”的新文件,并键入以下代码:
class RobotModel(object):
类是用一个名字定义的。在我们的例子中,我们将根据预期的目的对每个类进行后置处理。您可能不想这样做,或者这样做可能没有意义。对自己的类名进行判断。
def __init__(self, x, y, frame, speed):
init 方法(类定义中的函数称为方法)是一种特殊的方法,称为构造函数。它有四个参数,分别是机器人的起始位置、当前动画帧及其更新速度。第一个参数“self”是 Python 所必需的,指的是正在创建的对象。
self.x = x
self.y = y
self.frame = frame
self.speed = speed
self.timer = 0
我们将使用“定时器”成员字段来控制机器人的当前帧;它有一个“行走”动画。RobotModel 类的其余部分是访问和更改模型数据的方法:
def setPosition(self, newPosition):
self.x, self.y = newPosition
def getPosition(self):
return (self.x, self.y)
def getFrame(self):
return self.frame
def nextFrame(self):
self.timer = 0
self.frame += 1
self.frame %= 4
RobotController 调用 nextFrame()方法将机器人移动到下一帧。它将帧计数加 1,然后使用模运算符(%)将 self.frame 字段箝位在 0 和 3 之间。
def getTimer(self):
return self.timer
def getSpeed(self):
return self.speed
def setSpeed(self, speed):
self.speed = speed
这些 getter 和 setter 方法将由 RobotGenerator 和 RobotController 类使用。
Getters 和 Setters 之所以被称为 Getters 和 Setters,是因为它们以“get”或“set”开头,用于访问包含在类实例中的数据
机器人视角
RobotView 类在机器人模型中的位置显示机器人的大图形。机器人使用的图形包含四帧,每帧为 32×32 像素。见图 17-3 。
图 17-3
机器人,一个 128×32 像素的图像,有四个 32×32 帧
当前帧是在 RobotController 类中计算的,我们一会儿就会看到。同时,创建一个名为 robotview.py 的新文件,并输入以下文本:
import pygame
from pygame.locals import *
我们需要 Rect 类的这个导入。
from robotmodel import RobotModel
我们的 RobotView 类使用 RobotModel,所以我们需要导入那个文件。
class RobotView(object):
def __init__(self, imgPath):
self.img = pygame.image.load(imgPath)
def draw(self, surface, models):
for model in models:
rect = Rect(model.getFrame() * 32, 0, 32, 32)
surface.blit(self.img, model.getPosition(), rect)
draw()方法接受要在其上绘制机器人的表面以及模型列表。for 循环遍历“模型”中的每个机器人实例,并在表面上绘制它们。
因为我们只想显示一小部分 32×32 的图像。使用模型的框架计算要复制到屏幕的源区域。该模型有四个帧:0、1、2 和 3。如果这个值乘以 32,可能的矩形是(0,0,32,32),(32,0,32,32),(64,0,32,32),(96,0,32,32),如图 17-4 所示。
图 17-4
机器人动画的每一帧的开始坐标
雷达图
雷达视图在雷达屏幕上显示一个微小的光点(3×3 像素,白色)。雷达屏幕是一个 66×50 像素的图像,具有 1 像素的边框。见图 17-5 。
图 17-5
66×50 雷达图像
雷达的面积为 64×48 像素,但图形略大,以适应外部周围 1 像素的边框。雷达的比例尺是主播放区的 1:10,主播放区为 640×480 像素。这也是为什么光点是 3×3 像素,因为它非常接近机器人的 32×32 像素的实际大小。
创建名为 radarview.py 的新文件,并输入以下文本:
import pygame
from robotmodel import RobotModel
class RadarView(object):
def __init__(self, blipImagePath, borderImagePath):
self.blipImage = pygame.image.load(blipImagePath)
self.borderImage = pygame.image.load(borderImagePath)
构造函数有两个参数:一个是光标图像路径,另一个是边框图像路径。图像被加载并放置到字段中,供 draw()方法稍后使用。
def draw(self, surface, robots):
for robot in robots:
draw 方法接受机器人将被绘制到的表面和机器人列表。
x, y = robot.getPosition()
x /= 10
y /= 10
x += 1
y += 1
surface.blit(self.blipImage, (x, y))
代表机器人的‘信号’需要我们做一些数学计算。我们需要将坐标转换成介于 0..x 轴上的 639 和 0..479 设置为介于 0..雷达 x 轴上的 63 和 0..雷达的 y 轴是 47 度。这意味着我们必须将机器人的位置除以 10,再加上 1,因为记住我们的 1 像素雷达边界不算数。
surface.blit(self.borderImage, (0, 0))
最后,绘制边界,完成雷达视图。
机器人控制器
机器人控制器是将模型和视图结合在一起的粘合剂;它使用时钟来更新当前帧,并轮询键盘来读取玩家的输入。它使用这个输入根据机器人的速度(每秒像素)更新玩家的位置。
创建名为 robotcontroller.py 的新文件,并键入以下代码:
from robotmodel import RobotModel
机器人的模型 RobotModel 是从 robotmodel.py 文件导入的,因为控制器类读取和写入机器人模型的值。
这意味着控制器改变游戏中每个机器人的状态。
class RobotController(object):
def __init__(self, robots):
self.robots = robots
RobotController 的构造函数接受一个机器人列表,它将每帧更新一次。不是在每个对象上调用一个更新,而是调用一个更新方法 RobotController 的 update()方法一次,它更新每个模型。这是处理大量类似项目的一种非常有效的方式。
def update(self, deltaTime):
for robot in self.robots:
robot.timer += deltaTime
if robot.getTimer() >= 0.125:
robot.nextFrame()
每个机器人在一个循环中被处理。使用为每个机器人存储的数据,代码确定是更新下一帧还是通过改变其位置来移动对象(参见下文)。
与上次调用此方法的时间差,此时间被添加到模型的“timer”字段中。如果“计时器”大于或等于 0.125 秒,我们告诉模型移动到下一帧。
speed = self.multiply(robot.getSpeed(), deltaTime)
pos = robot.getPosition()
x, y = self.add(pos, speed)
sx, sy = robot.getSpeed()
模型的位置会以每秒像素数乘以上次调用该方法的时间差的方式递增。这详细解释如下:
if x < 0:
x = 0
sx *= -1
elif x > 607:
x = 607
sx *= -1
if y < 0:
y = 0
sy *= -1
elif y > 447:
y = 447
sy *= -1
robot.setPosition((x, y))
robot.setSpeed((sx, sy))
在这一系列 if 语句中,x 轴和 y 轴上的值被固定在屏幕上。然后在当前机器人模型上设置新的位置和速度。
def multiply(self, speed, deltaTime):
x = speed[0] * deltaTime
y = speed[1] * deltaTime
return (x, y)
def add(self, position, speed):
x = position[0] + speed[0]
y = position[1] + speed[1]
return (x, y)
两个帮助函数使元组的工作变得更容易。元组是不可变的,这意味着我们不能改变任何元素的值。我们可以创造新的元组,但不能改变现有的元组。这两个辅助方法使得元组的乘法和加法变得稍微容易一些。
机器人发电机
最后一个类不是 MVC 模式的一部分,但是我需要一种方法来以随机的位置和速度生成机器人。为此,我创建了 RobotGenerator 类。创建一个名为“robotgenerator.py”的新文件,并输入以下代码:
import random
from robotmodel import RobotModel
class RobotGenerator(object):
def __init__(self, generationTime = 1, maxRobots = 10):
self.robots = []
self.generationTime = generationTime
self.maxRobots = maxRobots
self.counter = 0
RobotGenerator 的构造函数允许调用者(创建类实例的代码部分)指定创建机器人和机器人最大数量之间的秒数。“self.counter”字段以秒为单位存储当前时间。如果“self.counter”大于或等于“self.generationTime”,则创建一个机器人(参见以下更新)。
def getRobots(self):
return self.robots
获取机器人列表。该方法有两种访问方式;它作为参数传递给 RobotController 构造函数,并作为参数传递给 RadarView 和 RobotView draw()方法。
def update(self, deltaTime):
self.counter += deltaTime
计时器增加增量时间,增量时间本身是一秒的几分之一。
if self.counter >= self.generationTime and len(self.robots) < self.maxRobots:
self.counter = 0
x = random.randint(36, 600)
y = random.randint(36, 440)
frame = random.randint(0, 3)
sx = -50 + random.random() * 100
sy = -50 + random.random() * 100
newRobot = RobotModel(x, y, frame, (sx, sy))
self.robots.append(newRobot)
如果计数器达到某个时间(generationTime)并且机器人的数量小于机器人的最大数量,我们就在场景中添加一个新的机器人。生成的机器人的位置和速度是随机的。
确保恒定速度
我们希望确保物体运动时速度恒定。有时其他例程需要更长的时间来运行,我们无法确保这一点。例如,如果我们决定我们的机器人应该以每秒 200 像素的速度移动。如果我们假设我们的例程每秒调用 30 帧,那么我们应该每帧增加 6.25 帧。正确不对!
我们的机器人的位置应该每秒改变 200 个像素。如果玩家按住右光标键,机器人应该在 1 秒后向右移动 200 像素。如果 update 方法每秒只被调用 15 次会发生什么?这意味着我们的机器人在 1 秒钟内只会移动 15 × 6.25 = 93.75 个像素。
还记得在“Snake”中,当我们想要更新代码时,我们使用时钟的毫秒刻度来更新部分代码。我们可以使用这个时间增量来计算我们在游戏的单个“滴答”中需要行进的距离。游戏每循环一次,就是一个滴答。
这意味着即使有可变的帧速率,你仍然会有一个恒定的速度,因为增量时间将确保你的速度保持不变。
使用 delta time,您每秒 15 次的更新仍然会在按住右光标键 1 秒后产生 200 像素的位移。为什么呢?因为每一次更新,我们都要将期望的速度乘以上次呼叫后的几分之一秒。十五分之一秒,也就是 66 毫秒。
0.066 × 200 =每次更新 13.2 像素
13.2 像素× 15 次更新=每秒 198 像素
这大概是我们想要的速度。如果我们的帧速率增加到每秒 60 帧:
每秒 60 帧是 16.67 毫秒
0.01667 × 200 =每次更新 3.333 像素
3.333 像素× 60 次更新=每秒 200.00 像素
你可以看到每秒 60 帧,我们得到的速度比每秒 15 帧要精确得多。不过,对于我们的目的来说,每秒 30 帧已经足够满足我们的需求了。
主机器人程序
主机器人程序将所有这些单独的类组合成一个“游戏”例子。创建一个名为 robot.py 的新文件。在这个新文件中,添加以下代码:
import pygame, sys
from pygame.locals import *
我们非常熟悉的导入来访问 PyGame 的例程和类库,以及 Python 提供的操作系统和系统库。
from robotview import RobotView
from robotcontroller import RobotController
from robotgenerator import RobotGenerator
from radarview import RadarView
这些从各自的文件中导入机器人模型、机器人视图、雷达视图、机器人生成器和机器人控制器。我们使用“from”关键字来最小化所需的输入量。有了“from”,我们只需要输入类名,而不是“robotview”。机器人视图。
pygame.init()
fpsClock = pygame.time.Clock()
surface = pygame.display.set_mode((640, 480))
接下来,我们初始化 PyGame 并设置一个时钟,这样我们可以将帧速率限制在每秒 30 帧。我们的测试游戏将是 640×480 像素大小。
lastMillis = 0
“lastMillis”保持帧之间的最后毫秒数。该值由“fpsClock.tick()”返回。
generator = RobotGenerator()
view = RobotView('robot.png')
radar = RadarView('blip.png', 'radarview.png')
controller = RobotController(generator.getRobots())
这是我们创建类的实例的地方。构造函数参数被传递。在这个例子中,我们只是使用硬编码的值,但是如果您愿意,您可以很容易地从文本文件中读取这些数据。
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
我们的主循环有我们以前见过的越狱;当用户关闭窗口时,我们退出 PyGame,并向操作系统发出信号,表示我们正在退出应用。
deltaTime = lastMillis / 1000
generator.update(deltaTime)
controller.update(deltaTime)
通常,您希望在绘制它们的可视化表示之前更新您的类。生成器和控制器都需要更新调用,以便生成新的机器人,并更新已生成的机器人。记住,所有的控制器代码都在一个类中,如果我们改变了那个控制器类中的任何东西,我们所有的机器人都会受到影响。这个真的很厉害!
surface.fill((0, 0, 0))
view.draw(surface, generator.getRobots())
radar.draw(surface, generator.getRobots())
接下来,用黑色清除屏幕,fill()方法的元组用于颜色的红色、绿色和蓝色分量,黑色表示没有所有颜色,因此所有值都为零。首先绘制主视图,因此这将使用当前动画帧在其位置绘制所有机器人。接下来雷达被画在上面。
这就是所谓的抽签顺序。首先被绘制到屏幕上的图像被绘制在稍后绘制的图像之后。把它想象成放在桌子上的照片。放在最前面的会被放在最上面的遮住。
pygame.display.update()
lastMillis = fpsClock.tick(30)
我们在主游戏循环中的最后动作是将前端缓冲区翻转到后端,反之亦然,并将帧速率固定在每秒 30 帧。“lastMillis”被存储,这将为我们提供生成最后一帧所花费的大致时间。这将用于确定每个机器人的位置和动画帧。
保存并运行游戏。大约一秒钟后,一个机器人会出现,然后一个接着一个,直到屏幕上出现十个。请注意,“雷达”视图会随着每个机器人的相对位置而更新。
结论
模型视图控制器设计模式可用于从功能上将一个对象分成三个独立的类。这使程序员能够决定以后如何组合这些类。例如,如果您只想在开发之初提供键盘支持,那么可以在稍后阶段轻松添加一个支持游戏杆的新控制器。这个新增加的内容不会影响视图或模型类。
如果你同时有很多 NPC 在屏幕上,MVC 是理想的。您可以使用一个类来存储它们的位置/帧数据(模型),一个类来执行更新(控制器),另一个类来显示它们(视图)。事实上,根据 NPC 的类型,你可以有不同的视图,例如,BlacksmithView 只画铁匠,ChefView 只画厨师。这减少了内存中的数据量,因为只有一个类(BlacksmithView)有铁匠图像的实例,只有一个类(ChefView)有厨师图像的实例。在一个更传统的 OOP 环境中,你可能会将位置和形状数据放在一起,这意味着你可能会在内存中存储成千上万的图像。
十八、音频
音频是制作游戏的重要组成部分。你可以拥有世界上最好的视觉效果,最好的机械,但是缺少了一些东西——那就是音频!在这一章中,我们来看看如何播放一次性的声音,如爆炸或效果以及音乐。
声音是使用 PyGame 内置的混音器对象播放的。像 PyGame,在使用之前必须先初始化混音器。
pygame.mixer.init()
同样,当您停止使用混音器时,您应该通过调用 quit 方法来优雅地关闭它:
pygame.mixer.quit()
您可以通过调用“get_busy()”方法来检查混音器是否正在播放声音:
pygame.mixer.get_busy()
这将返回一个布尔值 True 或 False,表明混音器仍在做一些事情。我们将在两个示例程序中使用它来保持程序运行。
Sound 类的 init()方法采用单个参数,该参数通常只是磁盘上声音文件的路径。你也可以传入缓冲区和其他东西,但是我们只会传入声音文件的路径。
shootSound = pygame.mixer.Sound('playershoot.wav')
像所有其他类一样,调用构造函数 init()方法——会传回该类的一个实例。声音类只能加载 Ogg Vorbis 或 WAV 文件。Ogg 文件是压缩的,因此更适合对空间要求严格的机器。
播放声音
在“pygamebook”中创建一个名为“ch18”的新文件夹在该文件夹中创建一个名为“testsound.py”的新 Python 脚本文件。输入下面的代码并运行它来播放声音。playershoot.wav 文件可以在参考资料部分的网站( http://sloankelly.net
)上找到。如果你不想下载那个文件,你可以提供你自己的。
import pygame, os, sys
from pygame.locals import *
导入常用模块。
pygame.mixer.init()
初始化混音器。
shootSound = pygame.mixer.Sound('playershoot.wav')
加载 playershoot.wav 声音文件,并将其命名为 shootSound。
shootSound.play()
通过调用 Play()方法播放声音。
while pygame.mixer.get_busy():
pass
这是一个伪 while 语句,用于在声音播放时保持程序忙碌。还记得 pass 关键字吗?这就像一个在 Python 中什么都不做的空白语句。您可以使用它来创建函数的存根代码,或者像在本例中一样,创建空白 while 循环。
pygame.mixer.quit()
当您完成音频时,请始终退出混音器。保存并运行程序,你会听到一声“呸!”关门前的噪音。这是一次性音效的一个例子。这是游戏音频故事的一部分。第二个是音乐,我们将在接下来讨论。
播放、暂停和更改音量
sound 对象允许您更改播放音乐的音量。混音器也可以进行很好的淡出。下面的程序将开始播放一段音乐,并允许播放器控制音量,还可以播放/暂停音乐。完成后,音乐将淡出,程序将停止。
在本节中,我们将介绍
-
pygame.mixer.fadeout()
-
pygame.mixer.pause()
-
pygame.mixer.unpause()
-
Sound.set_volume()
在“ch18”中创建一个名为“playsong.py”的新 Python 脚本,并添加以下代码。像往常一样,我会边走边解释:
import pygame
from pygame.locals import *
PyGame 运行所需的导入。
class Print:
def __init__(self):
self.font = pygame.font.Font(None, 32)
def draw(self, surface, msg, position):
obj = self.font.render(msg, True, (255, 255, 255))
surface.blit(obj, position)
这是一个小的助手类,它将使在主代码中打印文本变得更加容易。它创建一个字体实例,然后“draw()”方法将给定的文本呈现到一个表面上,然后该文本又被传送到给定的表面上。
pygame.init()
pygame.mixer.init()
PyGame 和混音器的初始化。
fpsClock = pygame.time.Clock()
surface = pygame.display.set_mode((640, 480))
out = Print()
创建时钟、主绘图表面和“Print”对象的实例。显示器是 640×480 像素,因为我们不需要为这个项目显示太多信息。
song = pygame.mixer.Sound('bensound-theelevatorbossanova.ogg')
song.play(-1)
将歌曲载入内存并立即播放。请注意,“play()”方法被传递了一个参数–1,这意味着它将一直重复,直到被告知停止为止。
running = True
paused = False
fading = False
volume = 1
从上到下,这些控制变量是:
-
要在音乐播放时保持程序运行
-
音乐暂停了吗
-
音乐逐渐消失了吗
-
音乐音量
进入主循环:
while running:
for event in pygame.event.get():
if event.type == QUIT:
pygame.mixer.fadeout(1000)
通过检查“running”变量,主循环保持运行。如果该变量包含“True”,程序将继续循环并执行循环体。第一部分是“for”循环,它决定了游戏应该处于什么状态。第一个检查(如上)是看玩家是否已经退出游戏(例如,他们点击了窗口上的 X 按钮)。如果是这样,我们指示调音台将音乐淡出 1000 毫秒或 1 秒。
elif event.type == KEYDOWN:
下一个检查是看玩家是否按了一个键。如果他们有,我们想对它作出反应,空格键用于暂停/取消暂停音乐,和键分别用于降低和增加音量,退出键(ESC)用于退出游戏。
if event.key == pygame.K_SPACE:
paused = not paused
if paused:
pygame.mixer.pause()
else:
pygame.mixer.unpause()
如果玩家按下空格键,“暂停”变量将被设置为与当前值相反的值。这意味着如果它是“真”,它将被设置为“假”,反之亦然。在混音器对象上调用适当的方法来暂停/取消暂停音乐。
elif event.key == pygame.K_ESCAPE and not fading:
fading = True
pygame.mixer.fadeout(1000)
如果玩家按下了 escape 键,并且游戏没有让音乐渐隐,那么将“fading”设置为 True,这样玩家就不能让音乐一直渐隐,并通知 PyGame mixer 音乐应该在 1 秒(1000 毫秒)内从最大音量渐隐到零。fadeout()方法采用以毫秒为单位的数值。
elif event.key == pygame.K_LEFTBRACKET:
volume = volume - 0.1
if volume < 0:
volume = 0
song.set_volume(volume)
音量介于 0 和 1 之间。0 表示关闭(静音),1 表示最大音量。如果玩家按下左[括号,音量应该会降低。为此,我们从当前体积中减去 0.1。然后进行检查以确保它保持在范围 0 内..1,然后在“song”对象上调用“set_volume()”以应用此新卷。“song”对象是。我们之前加载的 ogg 文件。
elif event.key == pygame.K_RIGHTBRACKET:
volume = volume + 0.1
if volume > 1:
volume = 1
song.set_volume(volume)
如果播放器按右]括号,音量应该会增加。为此,我们在当前体积中添加 0.1。然后进行检查以确保它保持在范围 0 内..1,然后在“song”对象上调用“set_volume()”以应用此新卷。
现在事件已经处理好了,最后的更新步骤是检查我们是否还在播放音频,如果不是,我们应该退出循环:
if not pygame.mixer.get_busy():
running = False
如果“运行”为假,游戏退出。
surface.fill((0, 0, 0))
out.draw(surface, "Press <SPACE> to pause / unpause the music", (4, 4))
out.draw(surface, "Press <ESC> to fade out and close program", (4, 36))
out.draw(surface, "Press [ and ] to alter the volume", (4, 68))
out.draw(surface, "Current volume: {0:1.2f}".format(volume), (4, 100))
pygame.display.update()
fpsClock.tick(30)
在屏幕上绘制文本,让玩家知道要按的键。
pygame.mixer.quit()
pygame.quit()
当游戏结束时,确保退出混合器和 PyGame。
保存并运行程序;您应该会看到如图 18-1 所示的输出。你还会听到正在播放的歌曲。
图 18-1。
“playsong.py”脚本的输出
结论
这是一个小的介绍,你可以用 PyGame 混音器实现什么。在你的游戏中加入音频非常重要,因为它可以增强乐趣感,并真正有助于传达(例如)物体的重量或受到了多少伤害。
记住当你的游戏结束时,一定要退出混合器!
十九、有限状态机
状态可以被描述为程序或实体的条件。有限定义了程序或实体只能由一定数量的状态来定义。实体由一系列规则控制,这些规则决定程序或实体的下一个状态是什么。
有限状态机被用在视频游戏中,用于人工智能(AI)以及菜单系统和整体游戏状态。
游戏状态
游戏是一种计算机程序,具有独特的、离散的、分隔的状态,例如,闪屏、玩游戏、游戏结束、主菜单和选项菜单。每个部分都可以被视为一个独立的状态。
菜单系统
用于控制游戏各个方面的菜单系统也可以划分成不同的状态,例如,主菜单、显示选项、控制选项和声音选项。这些都是独立的州。
非玩家人工智能
这是有限状态机(FSM)最常见的用法,也是大多数人将它与 FSM 联系在一起的原因。基本上,玩家遇到的每个敌人都有一个有限状态机。所谓附加,我的意思是它引用了一个成员变量形式的有限状态机,例如“self.fsm”。
敌人的 FSM 可以彼此独立运行,或者可以有一个支配性的“群体 AI”来控制整个系列的敌人。例如,你可能有十个敌人,但是“背包 AI”会控制有多少敌人被用来攻击玩家,有多少敌人会“逃跑”,等等。
在具体情况下,我们举一个警卫的例子。他可能有两种状态:巡逻和攻击。守卫停留在巡逻状态,直到一个敌人(玩家)进入范围,比如说 50 个单位,然后他们进入攻击状态。
FSM 通常用图表来描述。每个方块代表状态,每个箭头显示规则和转换方向。也就是说,如果符合该规则,箭头将指向实体应该使用的状态。见图 19-1 。
图 19-1。
有限状态机显示了一个简单的双态巡逻/攻击敌人的人工智能
如果守卫处于巡逻状态,玩家进入近战范围,守卫会移动到攻击状态。这无疑会包含攻击玩家的代码。同样,如果守卫处于攻击状态,而玩家移动到近战范围之外,它将转换回巡逻状态。
有限状态机示例
本例显示了一个三态 FSM。每个状态都有以下方法:
-
输入()
-
退出()
-
更新()
有一个 FSM 管理器控制程序的当前状态。这个管理器有两种方法:
-
changeState()
-
更新()
changeState()方法将实体从一种状态转换到另一种状态,update()方法调用当前状态的 update()方法。
在下一节中,我们将创建一个示例有限状态机(FSM)。在“pygamebook”中创建一个名为“ch19”的新文件夹在“ch19”文件夹中,创建一个名为“fsm.py”的新 Python 文件。完成后,您将看到以下输出:
Entering State One
Hello from StateOne!
Hello from StateOne!
Hello from StateOne!
Hello from StateOne!
Hello from StateOne!
Exiting State One
Entering State Two
Hello from StateTwo!
Hello from StateTwo!
Hello from StateTwo!
Hello from StateTwo!
Hello from StateTwo!
Exiting State Two
Entering Quit
Quitting...
如果没有,请重新检查您的代码。
有限状态机管理器
有限机器管理器类定义如下。记得输入它(和剩下的代码!)明确。您可以在以后更改任何您想要的内容,但是首先要完全按照看到的内容键入代码。
FSM 管理器控制实体的当前状态。在我们的例子中,我们有三个状态。前两种状态显示“hello”消息,后者退出应用。转换规则如下图 19-2 所示。
图 19-2。
显示转换的 FSM 示例状态机
当计数达到零时,状态一转换到状态二。当计数达到零时,状态二转换到状态退出。StateQuit 调用 Python 的 exit()方法退出应用。
class FsmManager:
def __init__(self):
self.currentState = None
当前状态设置为“无”。我们将在下面程序的主要部分中显式调用 changeState()方法。
def update(self):
if (self.currentState != None):
self.currentState.update()
update()方法检查我们是否有当前状态,如果有,我们调用它的 update()方法。注意,我们在这里使用 Python 的 duck 类型。
def changeState(self, newState):
if (self.currentState != None):
self.currentState.exit()
当我们改变状态时,我们希望在转换到新状态之前给当前状态一个“关闭”或“清理”的机会。exit()方法就是这样做的,或者至少由实现状态的开发人员将他们想要的代码放入 exit()方法中。
self.currentState = newState
self.currentState.enter()
同样,当我们进入一个新的状态时,我们需要让状态知道这个事件已经发生。每个州的开发人员如果想要对该事件采取行动,将在 enter()方法中放置代码。
class StateOne:
一般来说,除了屏幕上显示的文本信息,状态一和状态二之间几乎没有区别。
class StateOne:
def __init__(self, fsm):
self.count = 5
self.fsm = fsm
self.nextState = None
我们将在程序的主要部分设置 nextState 字段。这是当前状态将转换到的下一个状态。还有更复杂的 FSM 系统,它们将规则应用于各种状态,从而形成一个更加灵活的系统。这是一个简单的例子,在每个状态中烘焙规则。
def enter(self):
print("Entering State One")
enter()方法用于为当前状态设置各种值。在这个例子中,我们只是在屏幕上写一条消息。
def exit(self):
print("Exiting State One")
exit()方法可以用来在当前状态转换到新状态之前清理它。在本例中,我们展示了一条简单的消息。
def update(self):
print("Hello from StateOne!")
self.count -= 1
if (self.count == 0):
fsm.changeState(self.nextState)
FSM 管理器调用 update()方法。在我们的例子中,我们一直倒数到零,然后转换到下一个状态。
class StateTwo:
def __init__(self, fsm):
self.count = 5
self.fsm = fsm
self.nextState = None
def enter(self):
print("Entering State Two")
def exit(self):
print("Exiting State Two")
def update(self):
print("Hello from StateTwo!")
self.count -= 1
if (self.count == 0):
fsm.changeState(self.nextState)
状态一和状态二没有太大区别。退出状态也很简单;它只是退出应用。
class StateQuit:
def __init__(self, fsm):
self.fsm = fsm
def enter(self):
print("Entering Quit")
def exit(self):
print("Exiting Quit")
def update(self):
print("Quitting...")
exit()
我们不需要更新任何变量;我们只是在这个时候退出应用。
fsm = FsmManager()
stateOne = StateOne(fsm)
stateTwo = StateTwo(fsm)
stateQuit = StateQuit(fsm)
在这里,我们创建我们的 FSM 管理器和状态。每个状态都将 FSM 管理器作为构造函数中的一个参数。
stateOne.nextState = stateTwo
stateTwo.nextState = stateQuit
分配状态一和状态二的下一个状态。状态一的下一个状态是状态二,状态二的下一个状态是状态退出。
fsm.changeState(stateOne)
我们将 FSM 管理器的初始状态设置为 StateOne。
while True:
fsm.update()
我们的 while 循环非常简单;只需调用 FSM 管理器的 update()方法。就是这样。我们的州从那里处理程序流。
保存并运行该文件,您应该会看到我们在本章开始时显示的输出。
结论
任何面向对象模式的目标都是使类和主程序尽可能小。这减少了您必须为特定类阅读的代码量,使其更容易理解。每个类都应该有一个单一的目的。我们的 FSM 管理器类只有一个目的:运行当前选择的状态。每个状态也只有一个目的:执行特定的动作,直到规则改变,然后转换到一个新的状态。
FSM 非常适合人工智能(AI ),因为你可以根据已知的标准设计非常复杂的交互:用户在武器射程之内吗?我能开枪吗?玩家能看到我吗?等等。等等。
您还可以使用 FSM 来控制程序状态。让我们以一个典型的游戏应用的流程为例。见图 19-3 。
图 19-3。
游戏的有限状态机
进入状态是 SplashScreen,该屏幕在 3 秒钟后转换到主菜单。主菜单给用户两个选择:玩游戏或退出操作系统。如果用户在玩游戏时死亡,游戏将转换到游戏结束状态。它保持这种状态 3 秒钟,之后,游戏转换到主菜单状态。
我们的下一个项目“入侵者”将我们的模型-视图-控制器(MVC)和有限状态机(FSM)知识联系在一起。
二十、游戏项目:入侵者
我们最后一个街机风格的游戏项目是《入侵者》,它汇集了我们到目前为止所做的一切。我们将声音、动画、MVC 和 FSM 都打包在一个游戏中。见图 20-1 。
图 20-1。
入侵者行动游戏
在我们开始之前,在“pygamebook”中创建一个名为“projects”的新文件夹,如果还没有的话。在“项目”中,创建另一个名为“入侵者”的文件夹这是我们为这个项目创建的所有文件的存储位置。我们将在这个项目中使用几个文件,它们是
-
bitmapfont . py–包含位图字体的 sprite 表
-
Bullet . py–项目符号类
-
Collision . py–碰撞类别
-
Interstitial . py–间隙屏幕,即“准备就绪”和“游戏结束”屏幕
-
invaders . py——真正可运行的游戏;这是“主”程序,它创建框架并实例化所有对象
-
invaders game . py–实际的“玩游戏”状态类
-
menu . py“类”菜单
-
Player . py–播放器类
-
raspi game . py——你可以用来为你自己的游戏扩展的基类
-
swarm . py–外来虫群类
我们的音效有三个 WAV 文件:
-
爱丽丝梦游仙境
-
播放器. wav
-
playershoot.wav
我们还有几个 PNG 文件,包含所有入侵者的动画帧、播放器、显示字体(我们使用位图字体)和项目符号:
-
alienbullet.png 档案
-
bullet.png 格式
-
爆炸. png
-
fast tracker 2-style _ 12x 12 . png
-
invaders.png
-
船.png
整个源代码和所有资源(图像和声音文件)都可以从 sloankelly 下载。net 在参考资料部分。
上层社会
以下类别将被定义为该项目的一部分:
-
BitmapFont–允许在 PyGame 表面绘制位图字体。
-
BulletController、BulletModel、bullet view——项目符号实体的 MVC 类。子弹可以被一群外星人或玩家“拥有”。
-
collision controller–处理游戏的碰撞检测。这包括玩家/子弹和外星人/子弹以及玩家/外星人碰撞检测。
-
爆炸控制器、爆炸模型、爆炸模型列表、爆炸视图–爆炸实体的 MVC 类。当一个外来入侵者或玩家死亡时,会在他们的位置显示一个爆炸。
-
GameState–所有游戏状态的基类。
-
间隙状态——间隙屏幕在视频游戏中用来显示“游戏结束”或“准备好”的信息。这是程序的“存在状态”;因此,InterstitialState 是从名为“GameState”的 State 基类派生的。
-
入侵者模型,蜂群控制器,入侵者视图——外来入侵者蜂群的 MVC 类。每个外星人没有单独的控制者;取而代之的是“蜂群控制器”更新每个外星人的位置,并决定哪一个向玩家开火。
-
玩游戏状态-玩游戏状态。
-
主菜单状态–主菜单状态。
-
PlayerController、PlayerLivesView、PlayerModel、player view——“播放器”实体的 MVC 类。
-
RaspberryPiGame–包含我们在之前的程序中看到的主要更新循环。这实际上是有限状态管理器。
有限状态机
使用有限状态机(FSM)来控制游戏。图 20-2 中的图表显示了不同的状态以及游戏如何在它们之间转换。
图 20-2。
“入侵者”游戏有限状态机
游戏以主菜单状态开始,以“退出”游戏状态结束。正如你将会看到的,“退出”游戏状态并不是真正的状态;这实际上是国家的缺失。我们将游戏的当前状态设置为‘None ’,代码通过干净利落地退出程序来处理这个问题。在我们的实现中,每个状态的基类被定义为“GameState”
MVC 和“入侵者”
每个实体(玩家、外星人群、外星人)都有相应的模型、视图和控制器类。对于外星入侵者,控制者处理不止一个外星实体。
框架
基本状态类和状态机管理器在一个名为“raspigame.py”的文件中定义。创建此文件并键入以下代码:
import pygame, os, sys
from pygame.locals import *
class GameState(object):
游戏状态类定义了一个由 RaspberryPiGame 类使用的接口。每个状态管理游戏的特定功能。例如:主菜单,实际游戏,和间隙屏幕。GameState 类使用新的类定义格式。每个使用新格式的类都必须从对象扩展。在 Python 中,扩展类意味着将基类的名称放在类名后面的括号中。
def __init__(self, game):
self.game = game
初始化游戏状态类。每个子类型都必须调用此方法。拿一个参数来说,就是游戏实例。
def onEnter(self, previousState):
pass
基类“GameState”不包含 onEnter()的任何代码。扩展“GameState”的类应该提供自己的定义。这个方法是游戏在第一次进入状态时调用的。
def onExit(self):
pass
基类“GameState”不包含 onExit()的任何代码。扩展“GameState”的类应该提供自己的定义。这个方法在离开状态时被游戏调用。
def update(self, gameTime):
pass
基类“GameState”不包含任何用于 update()的代码。扩展“GameState”的类应该提供自己的定义。这个方法由游戏调用,允许状态自我更新。游戏时间(以毫秒为单位)是自上次调用该方法以来的时间。
def draw(self, surface):
pass
基类“GameState”不包含 draw()的任何代码。扩展“GameState”的类应该提供自己的定义。这个方法被游戏调用,允许状态自己画。传递的图面是当前的绘图图面。
class RaspberryPiGame(object):
基本的游戏面向对象的框架为树莓 Pi。用户创建“状态”,在任何特定时间改变屏幕上显示/更新的内容。这实际上只是一个美化了的国家经理。
def __init__(self, gameName, width, height):
pygame.init()
pygame.display.set_caption(gameName);
self.fpsClock = pygame.time.Clock()
self.mainwindow = pygame.display.set_mode((width, height))
self.background = pygame.Color(0, 0, 0)
self.currentState = None
类构造函数接受游戏的名字,这个名字将被用来改变窗口的标题栏。构造函数创建游戏的主窗口、FPS 时钟和默认背景色。当前状态最初设置为“无”
def changeState(self, newState):
if self.currentState != None:
self.currentState.onExit()
if newState == None:
pygame.quit()
sys.exit()
oldState = self.currentState
self.currentState = newState
newState.onEnter(oldState)
此方法从一种状态转换到另一种状态。如果有一个已存在的状态,则调用该状态的 onExit()方法。这将清除当前状态,并在退出时执行该状态需要执行的任何任务。除非 new state 为' None ',否则调用新状态的 onEnter 方法。如果新状态是“无”,那么游戏将终止。
def run(self, initialState):
self.changeState( initialState )
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
gameTime = self.fpsClock.get_time()
if ( self.currentState != None ):
self.currentState.update( gameTime )
self.mainwindow.fill(self.background)
if ( self.currentState != None ):
self.currentState.draw ( self.mainwindow )
pygame.display.update()
self.fpsClock.tick(30)
我们的主游戏循环,我们之前已经见过几次了,已经被移到 run()方法中。这处理所有的事件管理、状态更新和显示。
保存文件。
位图字体
在我们测试玩家的坦克和子弹之前,我们必须首先定义位图字体类。普通字体包含每个字符的数学表示。位图字体提供了一个 sprite 表,其中包含组成字体的所有单个字符。然后,我们使用 PyGame 的内置功能将 sprite sheet“切割”成单独的角色。见图 20-3 。
图 20-3。
摘自 https://opengameart.org/content/8x8-ascii-bitmap-font-with-c-source
的位图字体示例
感谢 OpenGameArt 上的用户‘dark rose’(一个很棒的资源!)作为本例中使用的样本位图字体。从上图中可以看出,字母表中的每个字母和符号都显示在一个网格中。它们按照在 ASCII(美国信息交换标准代码)字符集中出现的顺序排列。第一个可打印的字符是 space,讽刺的是它打印的是一个空格。空格是 ASCII 字符集中的第 33 个字符,因为我们从零开始编号,所以空格是 ASCII 32。
切割图像
为了访问空格旁边的感叹号,ASCII 33,我们使用一些模和除法技巧来计算字符的行和列。
行的计算方法是:取字符的 ASCII 值(在本例中为 33)并除以列数:
33 / 16 = 2
通过获取字符的 ASCII 值并用列数对其进行修改来计算列数:
33 mod 16 = 1
所以,我们的性格(!)位于第 2 行第 1 列。然后,我们将这些值乘以每个单元中的像素数。我们的字符是从一个 8×8 的网格中生成的,所以我们将每个值乘以 8:
2 * 8 = 16
1 * 8 = 8
组成感叹号的 8×8 网格起点的 x 坐标和 y 坐标为(8,16),如图 20-4 所示。
图 20-4。
位图字体的特写,显示感叹号字符的 8×8 网格的像素起始位置
在“入侵者”游戏中,位图字体显示由 bitmap font 类处理。我们现在来定义这个类。创建一个新文件,并将其命名为“bitmapfont.py”。输入下面的代码并保存文件。
不过,这里有一个小小的转折。“入侵者”项目包含的字体没有第一个不可打印的 32 个字符。它以空格字符开始。这不是一个真正的问题,但它增加了一个额外的步骤,将字符下移 32 个位置。请注意 toIndex()方法。
import pygame, os, sys
from pygame.locals import *
class BitmapFont(object):
def __init__(self, fontFile, width, height):
self.image = pygame.image.load(fontFile)
self.cellWidth = width
self.cellHeight = height
width = self.image.get_rect().width
height = self.image.get_rect().height
self.cols = width / self.cellWidth
self.rows = height / self.cellHeight
构造函数加载文件,并根据每个字符的宽度和高度,计算字符表的列数和行数。
def draw(self, surface, msg, x, y):
for c in msg:
ch = self.toIndex(c)
ox = ( ch % self.cols ) * self.cellWidth
oy = ( ch / self.cols ) * self.cellHeight
这是计算消息中当前字符在位图中的 x 和 y 偏移量的代码部分。
cw = self.cellWidth
ch = self.cellHeight
sourceRect = (ox, oy, cw, ch)
surface.blit(self.image, (x, y, cw, ch), sourceRect)
x += self.cellWidth
最后,将部分图像用位图传送到表面。
def centre(self, surface, msg, y):
width = len(msg) * self.cellWidth
halfWidth = surface.get_rect().width
x = (halfWidth - width) / 2
self.draw(surface, msg, x, y)
centre()方法计算消息的总宽度,并使其居中。
def toIndex(self, char):
return ord(char) - ord(' ')
我们用于“入侵者”的位图字体从空格开始(ASCII 32)。我们使用 Python 提供的 order()函数来获取字符的 ASCII 值。减去空间的 ASCII 值得到位图字体的索引值。
间隙筛
间隙屏幕是显示在不同级别之间的图像(“准备好!”)当显示暂停屏幕或玩家死亡时,即出现“游戏结束”屏幕。创建一个名为“interstitial.py”的新文件,并键入以下代码:
import pygame, os, sys
from pygame.locals import *
from bitmapfont import *
from raspigame import *
class InterstitialState(GameState):
我们的 InterstitialState 类扩展了 GameState。记住:如果我们从一个类扩展,我们把父类(或基类)的名字放在类名后面的括号里。
def __init__(self, game, msg, waitTimeMs, nextState):
super(InterstitialState, self).__init__(game)
必须调用基类的构造函数。在 Python 下,子类名称和子类实例‘self’必须传递给 super()方法。Python 3.0 通过“语法糖”的方式“解决”了这个问题,只允许你调用 super()。但与 Raspberry Pi 一起发布的 Python 版本并非如此。
我们还必须直接调用构造函数;这就是为什么调用 init()方法的原因。基类的构造函数需要 RaspiGame 的一个实例,因此它被适时地传递给基类的构造函数。
self.nextState = nextState
self.font = BitmapFont('fasttracker2-style_12x12.png', 12, 12)
self.message = msg
self.waitTimer = waitTimeMs
间隙状态的字段被初始化。
def update(self, gameTime):
self.waitTimer -= gameTime
if ( self.waitTimer < 0 ):
self.game.changeState(self.nextState)
更新方法会一直等到计时器停止计时。当计时器到达零时,游戏被告知进入下一个状态。
def draw(self, surface):
self.font.centre(surface, self.message, surface.get_rect().height / 2)
保存文件。
主菜单
主菜单包含两个项目:
-
开始游戏
-
放弃
和间隙屏幕一样,主菜单也是 GameState 的子类。创建一个名为“menu.py”的新文件,并输入以下代码:
import pygame, os, sys
from pygame.locals import *
from raspigame import *
from bitmapfont import *
我们的主菜单状态使用 bitmap 字体类在屏幕上绘制文本,并且导入 raspigame 文件,因为 main menu state 是 GameState 的子类。GameState 在 raspigame.py 文件中定义。
class MainMenuState(GameState):
def __init__(self, game):
super(MainMenuState, self).__init__(game)
self.playGameState = None
self.font = BitmapFont('fasttracker2-style_12x12.png', 12, 12)
self.index = 0
self.inputTick = 0
self.menuItems = ['Start Game', 'Quit']
当前选定的项目存储在“索引”中,菜单项包含在“菜单项”列表中。
def setPlayState(self, state):
self.playGameState = state
当前播放状态设置为“状态”
def update(self, gameTime):
keys = pygame.key.get_pressed()
if ( (keys[K_UP] or keys[K_DOWN]) and self.inputTick == 0):
self.inputTick = 250
if ( keys[K_UP] ):
self.index -= 1
if (self.index < 0):
self.index = len(self.menuItems) -1
elif ( keys[K_DOWN] ):
self.index += 1
if (self.index == len(self.menuItems)):
self.index = 0
用户按向上和向下箭头键来选择菜单项。为了防止菜单选择失控,更新被限制在每秒四次(250 毫秒)。
elif ( self.inputTick >0 ):
self.inputTick -= gameTime
if ( self.inputTick < 0 ):
self.inputTick = 0
通过更新 inputTick 控制变量来防止选择旋转。一旦达到零,就允许再次输入。
if ( keys[K_SPACE] ):
if (self.index == 1):
self.game.changeState(None) # exit the game
elif (self.index == 0):
self.game.changeState(self.playGameState)
当用户按空格键时,将测试当前选定的索引。如果用户选择了第零个元素,游戏将变为 playGameState。如果用户选择第一个元素,游戏退出。
def draw(self, surface):
self.font.centre(surface, "Invaders! From Space!", 48)
count = 0
y = surface.get_rect().height - len(self.menuItems) * 160
for item in self.menuItems:
itemText = " "
if ( count == self.index ):
itemText = "> "
itemText += item
self.font.draw(surface, itemText, 25, y)
y += 24
count += 1
每个菜单项都绘制在屏幕上。选定的菜单项以“>”字符为前缀,以向玩家指示该项已被选定。
保存文件。
玩家和子弹
bullet 类处理已经发射的子弹的位置和集合。像这个游戏中的所有实体一样,项目符号被分成单独的模型、视图和控制器类。MVC 在这场比赛中起了很大的作用!
子弹类
在入侵者文件夹中创建一个新的 Python 文件,并将其命名为‘bullet . py’。输入以下文本:
import pygame, os, sys
from pygame.locals import *
class BulletModel(object):
我们的子弹模型超级简单。这是一个包含 x 和 y 坐标的类,表示子弹在 2D 空间中的位置。它有一个方法,并且只有一个名为 update()的方法,该方法采用单个增量值。这将添加到项目符号位置的 y 坐标中。
def __init__(self, x, y):
self.x = x
self.y = y
将项目符号的位置设定为屏幕上的(x,y)。
def update(self, delta):
self.y = self.y + delta
更新项目符号的 y 坐标。
class BulletController(object):
项目符号控制器包含一个项目符号列表。每次调用 update()方法时,都会更新每个项目符号。
def __init__(self, speed):
self.countdown = 0
self.bullets = []
self.speed = speed
该构造函数创建一个空的子弹对象数组,并将每个子弹的速度设置为“speed”。倒计时变量被用作玩家的冷却时间。他们每 1000 毫秒只能发射一颗子弹。
def clear(self):
self.bullets[:] = []
清除项目列表。
def canFire(self):
return self.countdown == 0 and len(self.bullets) < 3
玩家只能在倒计时结束且子弹少于三发的情况下开火。
def addBullet(self, x, y):
self.bullets.append(BulletModel(x, y))
self.countdown = 1000
系统中会添加一个项目符号,倒计时会重置为 1 秒(1000 毫秒)。当倒计时到零时,玩家可以再次开火。倒计时字段在 update()方法中更新。
def removeBullet(self, bullet):
self.bullets.remove(bullet)
当子弹杀死一个外星人或者从屏幕上方弹出时,它们会从列表中移除。
def update(self, gameTime):
killList = []
killList 包含将在本次更新中删除的项目符号。从屏幕顶部弹出的项目符号将从列表中移除。
if (self.countdown > 0):
self.countdown = self.countdown - gameTime
else:
self.countdown = 0
游戏时间(以毫秒为单位)从倒计时字段中减去。
当倒计时场达到零时,玩家可以再次开火。
for b in self.bullets:
b.update( self.speed * ( gameTime / 1000.0 ) )
if (b.y < 0):
killList.append(b)
每个项目符号都会更新。如果他们的 y 坐标小于零(子弹从屏幕顶部弹出),那么它被标记为删除。
for b in killList:
self.removeBullet(b)
我们最后的子弹类是视图。这将从项目符号控制器获取所有数据,并在屏幕上显示每个项目符号。
class BulletView(object):
def __init__(self, bulletController, imgpath):
self.BulletController = bulletController
self.image = pygame.image.load(imgpath)
用项目符号控制器和项目符号图像的路径初始化项目符号视图。
def render(self, surface):
for b in self.BulletController.bullets:
surface.blit(self.image, (b.x, b.y, 8, 8))
保存文件。
玩家等级
创建一个名为“player.py”的新文件,并输入以下代码。玩家实体的 MVC 组件包含在这个文件中。
import pygame, os, sys
from pygame.locals import *
from bullet import *
from bitmapfont import *
class PlayerModel(object):
def __init__(self, x, y):
self.x = x
self.y = y
self.lives = 3
self.score = 0
self.speed = 100 # pixels per second
玩家模型包含玩家实体的所有数据:它在屏幕上的位置(以 x 和 y 坐标的形式)、生命的数量、玩家的分数以及他们的移动速度(以像素/秒为单位)。请记住:通过使用每秒像素,我们可以确保无论机器的速度如何,我们都可以获得一致的移动速度。
class PlayerController(object):
def __init__(self, x, y):
self.model = PlayerModel(x, y)
self.isPaused = False
self.bullets = BulletController(-200) # pixels per sec
self.shootSound = pygame.mixer.Sound('playershoot.wav')
构造函数创建一个玩家模型和一个 BulletController 的实例。项目符号控制器接受一个参数,该参数以每秒像素为单位表示移动速度。这是一个负值,因为我们在屏幕上向上移动,屏幕趋向于零。为什么呢?请记住,在计算中,屏幕的左上角是位置(0,0),右下角是 x 轴和 y 轴上的最大值。
def pause(self, isPaused):
self.isPaused = isPaused
阻止玩家移动坦克。
def update(self, gameTime):
self.bullets.update(gameTime)
if ( self.isPaused ):
return
keys = pygame.key.get_pressed()
if (keys[K_RIGHT] and self.model.x < 800 - 32):
self.model.x += ( gameTime/1000.0 ) * self.model.speed
elif (keys[K_LEFT] and self.model.x > 0):
self.model.x -= ( gameTime/1000.0 ) * self.model.speed
玩家可以使用键盘上的光标(箭头)键左右移动。位置根据游戏时间按移动速度的百分比更新。无论 CPU 的速度或帧速率如何,这都可以让我们流畅地移动。
if (keys[K_SPACE] and self.bullets.canFire()):
x = self.model.x + 9 # bullet is 8 pixels
y = self.model.y - 16
self.bullets.addBullet(x, y)
self.shootSound.play()
当玩家点击空格键时,一颗子弹被添加到当前的子弹列表中,我们播放子弹射击的声音。激发受到“BulletController”类的 canFire()方法的限制。
def hit(self, x, y, width, height):
return (x >= self.model.x and y >= self.model.y and x + width <= self.model.x + 32 and y + height <= self.model.y + 32)
这种方法让我们可以测试与任何其他物体的碰撞,方法是将物体浓缩到最纯粹的形式:它在空间中的位置、宽度和高度。
玩家有两个视图类:PlayerView 在屏幕底部显示玩家的坦克,PlayerLivesView 显示玩家剩余的生命数。
class PlayerView(object):
def __init__(self, player, imgpath):
self.player = player
self.image = pygame.image.load(imgpath)
def render(self, surface):
surface.blit(self.image, (self.player.model.x, self.player.model.y, 32, 32))
PlayerView 类有一个名为“render”的主要方法。这会在玩家的位置显示坦克。玩家模型被传递到视图中。
class PlayerLivesView(object):
def __init__(self, player, imgpath):
self.player = player
self.image = pygame.image.load(imgpath)
self.font = BitmapFont('fasttracker2-style_12x12.png', 12, 12)
该构造函数接受两个参数:播放器模型和一个表示位图字体的图像路径的字符串。
def render(self, surface):
x = 8
for life in range(0, self.player.model.lives):
surface.blit(self.image, (x, 8, 32, 32))
x += 40
self.font.draw(surface, '1UP SCORE: ' + str(self.player.model.score), 160, 12)
render 方法绘制船只图像“生命”的次数,然后将玩家的分数显示为“1UP SCORE: 00000”
测试播放器
我们可以通过向 player.py 文件添加以下代码来测试播放器类。这一部分是可选的,但是它给出了一个清晰的例子,可以在独立于主程序的情况下测试类。如果您不想添加此内容,您可以保存文件并转到下一部分。
if ( __name__ == '__main__'):
每个 Python 文件在运行时都有一个名字。如果这是主文件,也就是说,这是正在运行的文件,它被赋予一个特殊的名称“main”如果是这种情况,我们将初始化 PyGame 并创建代码来测试我们的类。
pygame.init()
fpsClock = pygame.time.Clock()
surface = pygame.display.set_mode((800, 600))
pygame.display.set_caption('Player Test')
black = pygame.Color(0, 0, 0)
player = PlayerController(0, 400)
playerView = PlayerView(player, 'ship.png')
playerLivesView = PlayerLivesView(player, 'ship.png')
为我们的玩家分别创建一个控制器、视图和生命视图。
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
player.update(fpsClock.get_time())
surface.fill(black)
playerView.render(surface)
playerLivesView.render(surface)
pygame.display.update()
fpsClock.tick(30)
我们的主循环检查用户是否选择了“QUIT ”(即,他们关闭了窗口),如果没有,则调用 update()方法并呈现每个显示。保存“player.py”文件。
外星虫群类
创建一个名为“swarm.py”的新 Python 文件。我们将在该文件中实现以下类:
-
入侵者模型
-
群集控制器
-
入侵者 w
import pygame, os, sys
from pygame.locals import *
from bullet import *
需要引用 PyGame 库来进行图像操作。alien swarm 也使用子弹,所以我们也需要导入‘bullet . py’文件。我们的入侵者模型包含最少的代码;在 AlienView 中,大部分只是用来描述外星人的数据。
每种异形都有两帧动画,也有两种异形。
class InvaderModel(object):
def __init__(self, x, y, alientype):
self.x = x
self.y = y
self.alientype = alientype
self.animframe = 0
构造函数有三个参数,不包括“self”前两个是异形的起始坐标,最后一个是异形类型。有两种外星人类型:一种红色,一种绿色。他们击中时得分不同,这就是为什么我们需要存储这个模型代表什么类型的外星人。
def flipframe(self):
if self.animframe == 0:
self.animframe = 1
else:
self.animframe = 0
flipframe()方法将动画的当前帧从 0 切换到 1,然后再回到 0。外星人只有两帧动画。
def hit(self, x, y, width, height):
return (x >= self.x and y >= self.y and x + width <= self.x + 32 and y + height <= self.y + 32)
hit()方法中的最后一行都在一行上。碰撞类使用 hit()方法来确定是否发生了碰撞。
SwarmController 类实际上是多个外星人的控制器。它使用合成,因为每个单独的外星人都是由虫群类创造和毁灭的。
class SwarmController(object):
def __init__(self, scrwidth, offsety, initialframeticks):
self.currentframecount = initialframeticks
self.framecount = initialframeticks
当前动画帧从这里控制。这确保了每个外星人与其他外星人在时间上同步行进。
self.invaders = []
self.sx = -8
self.movedown = False
self.alienslanded = False
当前外来方向设置为负(左)方向。当外星人撞到一边时,必须在屏幕上向下移动,这时会设置“向下移动”标志。最后一个标志“alienslanded”意味着当这个标志为真时,玩家的游戏结束。
self.bullets = BulletController(200) # pixels per sec
BulletController 类也是 SwarmController 的一部分。子弹速度的每秒像素值是正的,因为我们在屏幕上往下走。请记住,对于玩家来说,它是负的,因为玩家的子弹在屏幕上。
self.alienShooter = 3 # each 3rd alien (to start with) fires
self.bulletDropTime = 2500
self.shootTimer = self.bulletDropTime # each bullet is fired in this ms interval
self.currentShooter = 0 # current shooting alien
for y in range(7):
for x in range(10):
invader = InvaderModel(160 + (x * 48) + 8, (y * 32) + offsety, y % 2)
self.invaders.append(invader)
嵌套的 for 循环用于生成异类群体。每个 swarm 成员都是 InvaderModel 类的一个实例。
def reset(self, offsety, ticks):
self.currentframecount = ticks
self.framecount = ticks
for y in range(7):
for x in range(10):
invader = InvaderModel(160 + (x * 48) + 8, (y * 32) + offsety, y % 2)
self.invaders.append(invader)
“重置”方法是用来重置下次攻击的外星生物群,加速他们的下降。
def update(self, gameTime):
self.bullets.update(gameTime)
self.framecount -= gameTime
movesideways = True
“framecount”成员字段用作计时器。游戏时间从“帧计数”中的当前时间中减去,当它达到零时,我们“滴答”蜂群。这就是我们如何控制对象的更新速度。我们可以指定不同的“滴答”时间。“帧计数”越小,更新就越快,因为我们必须减去更少的时间。
if self.framecount < 0:
if self.movedown:
self.movedown = False
movesideways = False
self.sx *= -1
self.bulletDropTime -= 250
if ( self.bulletDropTime < 1000 ):
self.bulletDropTime = 1000
self.currentframecount -= 100
if self.currentframecount < 200: #clamp the speed of the aliens to 200ms
self.currentframecount = 200
for i in self.invaders:
i.y += 32
如果我们必须向下移动,在“if self.movedown”下的代码部分提供了将外星人群体向下移动到屏幕上所需的步骤。当群体向屏幕下方移动时,“当前帧计数”会更新。这是因为外星人每次向玩家下落的时候都会加速。
self.framecount = self.currentframecount + self.framecount
for i in self.invaders:
i.flipframe()
if movesideways:
for i in self.invaders:
i.x += self.sx
x, y, width, height = self.getarea()
if ( x <= 0 and self.sx < 0) or ( x + width >= 800 and self.sx > 0 ):
self.movedown = True
getarea()方法决定了游戏场上所有外星人使用的区域。然后,我们使用此信息来确定该区域是否“击中”了侧面。如果这个区域碰到了边上,我们标记蜂群在下一个滴答向下移动。
self.shootTimer -= gameTime
if ( self.shootTimer <= 0):
self.shootTimer += self.bulletDropTime # reset the timer
self.currentShooter += self.alienShooter
self.currentShooter = self.currentShooter % len(self.invaders)
shooter = self.invaders[self.currentShooter]
x = shooter.x + 9 # bullet is 8 pixels
y = shooter.y + 16
self.bullets.addBullet(x, y)
拍摄计时器的工作时间与帧更新时间不同。当定时器到零时,当前射手递增‘alien shooter’;因此它不是主要虫群蜱的一部分。
“当前射手”字段被固定在我们剩下的外星人数量上。这确保了我们不会试图访问我们列表之外的外星人。然后引用当前的射手,我们在射手的位置添加一颗子弹。我选择 3(三)作为增量手,因为它给了射击一种伪随机的感觉。
def getarea(self):
leftmost = 2000
rightmost = -2000
topmost = -2000
bottommost = 2000
设置最大和最小边界。
for i in self.invaders:
if i.x < leftmost:
leftmost = i.x
if i.x > rightmost:
rightmost = i.x
if i.y < bottommost:
bottommost = i.y
if i.y > topmost:
topmost = i.y
使用一些简单的范围检查,我们计算所有外星人最左边、最右边、最上面和最下面的点。
width = ( rightmost - leftmost ) + 32
height = ( topmost - bottommost ) + 32
return (leftmost, bottommost, width, height)
我们最后的入侵者类是视图类。它使用聚合,因为它引用了 SwarmController 类。
class InvaderView:
def __init__(self, swarm, imgpath):
self.image = pygame.image.load(imgpath)
self.swarm = swarm
构造函数接受两个参数。第一个是 SwarmController 实例,第二个是代表我们的外星精灵的图像文件的路径。
def render(self, surface):
for i in self.swarm.invaders:
surface.blit(self.image, (i.x, i.y, 32, 32), (i.animframe * 32, 32 * i.alientype, 32, 32))
“render”方法循环遍历 SwarmController 的“swarm”域中的所有入侵者,并将其显示在屏幕上。入侵者模型的“animframe”字段用于控制精灵片的切片向左移动多远。“alientype”字段是切片向上的距离。
保存文件。我们需要这个文件和其他文件来进行碰撞检测。
冲突检出
我们的碰撞检测类存储在“collision.py”文件中。创建一个新的空白文件,并将其命名为“collision.py”。这将包含以下类:
-
爆炸模型
-
爆炸模型列表
-
分解视图
-
爆炸控制器
-
碰撞控制器
我们将按照它们在文件中出现的顺序来检查它们。
爆炸
动作游戏需要巨大的噪音和爆炸。我们的游戏没有什么不同!四个爆炸类——explosion model、ExplosionModelList、ExplosionView 和 explosion controller——由 CollisionController 用来创建和更新游戏中发生的各种爆炸。每个爆炸都是使用由一系列动画帧组成的 sprite sheet 在屏幕上绘制的。
我们的文件以熟悉的方式从一系列导入开始:
import pygame, os, sys
from pygame.locals import *
from player import *
from bullet import *
from swarm import *
from interstitial import *
我们自己的球员,子弹,蜂群和间质类是必需的。
class ExplosionModel(object):
def __init__(self, x, y, maxFrames, speed, nextState = None):
self.x = x
self.y = y
self.maxFrames = maxFrames
self.speed = speed
self.initialSpeed = speed
self.frame = 0
self.nextState = nextState
“ExplosionModel”类不包含任何方法,就像我们所有的其他模型一样。它只包含描述爆炸的字段;它的位置,帧数,更新速度,当前帧,下一个状态。
class ExplosionModelList(object):
def __init__(self, game):
self.explosions = []
self.game = game
def add(self, explosion, nextState = None):
x, y, frames, speed = explosion
exp = ExplosionModel(x, y, frames, speed, nextState)
self.explosions.append(exp)
def cleanUp(self):
killList = []
for e in self.explosions:
if ( e.frame == e.maxFrames ):
killList.append(e)
nextState = None
for e in killList:
if (nextState == None and e.nextState != None):
nextState = e.nextState
self.explosions.remove(e)
if (nextState != None):
self.game.changeState(nextState)
cleanUp()方法需要一点解释。有了这个机制,我们可以在我们的爆炸中编码将游戏移动到另一个状态的能力。例如,当玩家死了,他们没有更多的生命,我们可以改变游戏的状态为“游戏结束”
class ExplosionView(object):
def __init__(self, explosions, explosionImg, width, height):
self.image = pygame.image.load(explosionImg)
self.image.set_colorkey((255, 0, 255))
self.explosions = explosions
self.width = width
self.height = height
def render(self, surface):
for e in self.explosions:
surface.blit(self.image, ( e.x, e.y, self.width, self.height ), (e.frame * self.width, 0, self.width, self.height) )
“爆炸视图”循环显示所有爆炸,并依次显示每个爆炸。
class ExplosionController(object):
def __init__(self, game):
self.list = ExplosionModelList(game)
def update(self, gameTime):
for e in self.list.explosions:
e.speed -= gameTime
if ( e.speed < 0 ):
e.speed += e.initialSpeed
e.frame += 1
self.list.cleanUp()
“爆炸控制器”是我们遇到的最简单的控制器。它有一个初始化方法和一个 update()方法,前者创建一个“ExplosionModelList”(一个组合的例子)。update()方法只需要增加帧计数。当计数达到最大帧数时,它会在“ExplosionModelList”类的 cleanUp()方法中自动移除。
碰撞控制器
“CollisionController”类不需要相应的模型或视图,因为它既不需要也不需要。它确实使用其他控制器和模型来确定是否发生了碰撞。如果有东西被击中,会发出适当的声音,并采取行动。
class CollisionController(object):
def __init__(self, game, swarm, player, explosionController, playState):
self.swarm = swarm
self.player = player
self.game = game
self.BulletController = player.bullets
self.EnemyBullets = swarm.bullets
self.expCtrl = explosionController
self.playGameState = playState
self.alienDeadSound = pygame.mixer.Sound('aliendie.wav')
self.playerDie = pygame.mixer.Sound('playerdie.wav')
“CollisionController”的构造函数接受游戏、群体控制器、玩家控制器、爆炸控制器实例和游戏状态。我们还加载了几个声音,当玩家撞到一个外星人时(“aliendie.wav”),或者如果一个外星人不幸撞到了玩家(“playerdie.wav”)。
def update(self, gameTime):
aliens = []
bullets = []
for b in self.BulletController.bullets:
if (bullets.count(b)>0):
continue
for inv in self.swarm.invaders:
if (inv.hit(b.x+3, b.y+3, 8, 12)):
aliens.append(inv)
bullets.append(b)
break
收集所有玩家的子弹和击中入侵者的外星人。
for b in bullets:
self.BulletController.removeBullet(b)
移除所有击中外星人的子弹
for inv in aliens:
self.swarm.invaders.remove(inv)
self.player.model.score += (10 * (inv.alientype + 1))
self.expCtrl.list.add((inv.x, inv.y, 6, 50))
self.alienDeadSound.play()
移除所有被玩家子弹击中的外星人。这部分还会递增玩家的分数,播放外星人死亡音。
playerHit = False
for b in self.EnemyBullets.bullets:
if ( self.player.hit (b.x+3, b.y+3, 8, 12 ) ):
self.player.model.lives -= 1
playerHit = True
break
现在我们检查敌人的子弹。如果他们中的任何一个击中了玩家,我们将“玩家击中”标志设置为“真”并“中断”for 循环。如果我们已经击中了玩家,就没有必要继续在子弹中搜索了。
if ( playerHit ):
self.EnemyBullets.clear()
self.player.bullets.clear()
if ( self.player.model.lives > 0 ):
self.player.pause(True)
getReadyState = InterstitialState( self.game, 'Get Ready!', 2000, self.playGameState )
self.expCtrl.list.add((self.player.model.x, self.player.model.y, 6, 50), getReadyState)
self.playerDie.play()
如果玩家被击中,我们清除游戏中的所有子弹。如果玩家还有生命,我们暂停玩家,将游戏状态改为“准备好”屏幕,并添加一个爆炸来显示玩家的坦克被摧毁。记住:我们可以改变爆炸后的状态(参见“爆炸控制器”类),这就是我们在这里设置的。
我们快完成了!还有两个文件。这些是主程序和主游戏状态。
主程序
主程序是一个名为“invaders.py”的文件。创建一个名为“invaders.py”的新文件,并输入以下代码。我们之前创建的“RaspberryPiGame”类需要一个初始状态。我们的主程序的功能是创建有限状态机(FSM)使用的状态,并设置初始状态。
import pygame, os, sys
from pygame.locals import *
# Our imports
from raspigame import *
from interstitial import *
from menu import MainMenuState
from invadersgame import PlayGameState
我们通常为 OS 和 PyGame 模块加上我们自己的本地模块导入。我们正在安装“raspigame.py”和“interstitial.py”中的所有内容,但只有“menu.py”中的 MainMenuState 和“invadersgame.py”中的 PlayGameState。
invadersGame = RaspberryPiGame("Invaders", 800, 600)
mainMenuState = MainMenuState( invadersGame )
gameOverState = InterstitialState( invadersGame, 'G A M E O V E R !', 5000, mainMenuState )
playGameState = PlayGameState( invadersGame, gameOverState )
getReadyState = InterstitialState( invadersGame, 'Get Ready!', 2000, playGameState )
mainMenuState.setPlayState( getReadyState )
创建游戏中使用的状态实例:主菜单、游戏结束、玩游戏和准备就绪状态。
invadersGame.run( mainMenuState )
将游戏的初始状态设置为主菜单。就这样——这是主程序。它的唯一目的是创建游戏状态之间的链接,并设置初始状态,这都是在六行代码中实现的。
保存此文件。剩下要做的最后一个类是主游戏状态。
主游戏状态
创建一个名为“invadersgame.py”的新文件。输入以下代码:
import pygame, os, sys
from pygame.locals import *
from raspigame import *
from swarm import *
from player import *
from collision import *
模块导入。
class PlayGameState(GameState):
def __init__(self, game, gameOverState):
super(PlayGameState, self).__init__(game)
self.controllers = None
self.renderers = None
self.player_controller = None
self.swarm_controller = None
self.swarmSpeed = 500
self.gameOverState = gameOverState
self.initialise()
我们的“PlayGameState”类派生自“GameState ”,因此构造函数必须调用基类“constructor”。控制器和“游戏结束”状态的字段被初始化。为了尽量减少这个方法,调用了 initialise()方法。
def onEnter(self, previousState):
self.player_controller.pause(False)
onEnter()方法是 GameState 类的一部分。我们唯一需要做的就是告诉玩家控制器它是未暂停的。
def initialise(self):
self.swarm_controller = SwarmController(800, 48, self.swarmSpeed)
swarm_renderer = InvaderView(self.swarm_controller, 'invaders.png')
self.player_controller = PlayerController(0, 540)
player_renderer = PlayerView(self.player_controller, 'ship.png')
lives_renderer = PlayerLivesView(self.player_controller, 'ship.png')
bullet_renderer = BulletView(self.player_controller.bullets, 'bullet.png')
alienbullet_renderer = BulletView(self.swarm_controller.bullets, 'alienbullet.png')
explosion_controller = ExplosionController(self.game)
collision_controller = CollisionController(self.game, self.swarm_controller, self.player_controller, explosion_controller, self)
explosion_view = ExplosionView(explosion_controller.list.explosions, 'explosion.png', 32, 32)
self.renderers = [ alienbullet_renderer, swarm_renderer, bullet_renderer, player_renderer, lives_renderer, explosion_view ]
self.controllers = [ self.swarm_controller, self.player_controller, collision_controller, explosion_controller ]
initialise()方法包含创建每个控制器和渲染器的实例的代码。这些然后被添加到“渲染器”和“控制器”字段。这些字段中的每一个都是一个列表,我们可以在 update()和 draw()方法中遍历它。
def update(self, gameTime):
for ctrl in self.controllers:
ctrl.update(gameTime)
遍历所有控制器,并在每个控制器上调用 update()方法。因为我们已经将控制器存储在一个列表中,所以更新每个控制器是一段相当简单的代码
if ( self.player_controller.model.lives == 0 ):
self.game.changeState( self.gameOverState )
如果玩家没有更多的生命了,我们将游戏状态改为“游戏结束”状态。按照现在的情况,后面是什么行并不重要,但是您可能希望在这里添加一个“return”来退出该方法。
if ( len(self.swarm_controller.invaders) == 0 ):
self.swarmSpeed -= 50
if ( self.swarmSpeed < 100 ):
self.swarmSpeed = 100
如果屏幕上没有外星人了,我们就开始新的一关。外星人的速度降低了;这意味着更新之间的增量时间减少了。这也意味着我们得到了更快的外星人。
self.swarm_controller.reset(48, self.swarmSpeed)
levelUpMessage = InterstitialState( invadersGame, 'Congratulations! Level Up!', 2000, self )
self.game.changeState ( levelUpMessage )
现在我们开始一个新的游戏(外星人群),我们告诉群控制器重置到新的速度,并改变游戏的状态以显示“升级”的消息。
def draw(self, surface):
for view in self.renderers:
view.render(surface)
最后一个方法在屏幕上绘制所有对象。
保存文件。我们完了!你现在有一个完整的“入侵者”游戏。
运行游戏
要运行游戏,请在“invaders.py”的代码编辑器中按 F5,或者在命令提示符下从“pygamebook/projects/invaders”文件夹中键入以下内容:
$ python ./invaders.py
使用箭头键选择菜单选项,按空格键选择,如图 20-5 所示。玩得开心,这是你应得的!
图 20-5。
入侵者主菜单
结论
我们已经使用 Raspberry Pi、Python 和 PyGame 构建了复杂的街机风格的游戏。使用 MVC(模型视图控制器)这样的模式,我们的程序类可以保持相对较短,并专注于做一件事,无论是渲染精灵、控制角色还是作为数据模型。
在本书的最后几章中,我们将结合我们在 Python 和 PyGame 中学到的知识,使用 Raspberry Pi 的通用输入/输出(GPIO)引脚来控制 led 并接收按钮输入,从而创建与我们环境的其他交互。
二十一、带有 GPIO 引脚的简单电子器件
到目前为止,我们已经看到 Raspberry Pi 作为输入设备与键盘和鼠标通信,作为输出设备与显示器通信。Raspberry Pi 可以与各种各样的外围设备(这是一个可以添加的花哨名称)以及发光二极管(led)或开关等电子组件进行通信。这是通过树莓派顶部的销钉连接设备来实现的。这些引脚被称为通用输入/输出引脚,简称 GPIOs。
如图 21-1 所示,树莓 Pi 型号 B+上有 40 个引脚。
图 21-1
Raspberry Pi 板上通用输入/输出(GPIO)引脚的位置
在连接和拆卸外围设备时,您应该始终小心,并且在这样做时应该关闭电源。保持电源接通并连接设备可能会损坏电脑。
将设备插入树莓派时要小心!在这样做之前,请务必关闭机器!
电压、电流和电阻
在我们将元件连接到 Raspberry Pi 之前,我们应该后退一步,讨论一些电子基础知识。当产生一条允许自由电子沿其移动的路径时,电子电路就形成了。这些电子的连续运动是一种电流,思河。导致这些电子流动的力被称为电压,是电路中两点之间势能的度量。
最后,还有阻力。不管你的电路设计得多好,你总会遇到一些阻力。这可能是由电线的缺陷、热损耗造成的能量损失等引起的。你甚至可以通过添加称为电阻器的装置来降低电流,从而增加不完美性。在图 21-2 中,我们看到一个包含一个发光二极管(一盏灯)、一个开关和一个电阻的电路。它们都与电池相连。电路是完整的,因为电池的两端——ve 和 ve 端——通过电阻、开关和 LED 连接。电路将完成,也就是说,当开关接通时,电子将自由地从电池的+ve 端流向–ve 端。这种流动称为常规电流。
图 21-2
当左边的开关被按下时,一个简单的电路将点亮灯
当按下开关时,该电路将点亮 LED。在本章的后半部分,我们将使用下列元件构建此电路。
你需要什么
对于本书中的其余项目,在开始使用 Raspberry Pi 创建电路之前,您需要一些东西。至少你需要
-
面包板
-
分线板
-
跳线
-
发光二极管
-
电阻器(330ω即可)
-
开关
树莓派可以提供 3.3V 电源以及接地。再次,小心连接和断开电线。如果您不注意连接的引脚,可能会损坏您的 Raspberry Pi。
试验板
试验板用于制作电子电路的原型。它允许轻松放置和移除 led、电线和开关等组件,而无需焊接或脱焊这些组件。如图 21-2 所示,试验板由塑料外壳保护的微小连接器覆盖。微小的连接器以非常特殊的方式排列。
在顶部和底部有两条线,标记为+(正)和-(负)。这些是电源线(3.3 伏)和地线(0 伏)。如果您将 Raspberry PI 的 3.3V 输出连接到+轨,该轨上的所有连接器接收 3.3V 电压。这使得将元件连接到+ve(正极)和–ve(负极)供电轨变得非常容易。
中间部分从中间分开,将每一行分成两半。每个行部分的列是相连的。同样,这使得将多个输出连接到一个引脚变得更加容易。每一行不与另一行相连。试验板上通常有一个编号系统,以便于创建电路。在下面的例子中,列标记为“a”到“j ”,行以 5: 1、5、10、15 等间隔编号。
分线板
分线板是一种简单的设备,可以轻松地将 Raspberry Pi 连接到原型制作试验板上。如图 21-3 所示,将器件放置在试验板上。每行的其余列允许跳线或电阻连接到该行的相关引脚。
图 21-3
用于制作电子电路原型的试验板
然后,可以使用随附的带状电缆连接 Raspberry Pi 和试验板。请阅读如何连接带状电缆的说明,因为它因供应商和型号而异。
大多数分线板如图 21-3 所示,但有些是 T 形的,以使连接更加容易。
请注意,分线板的侧面标有树莓 Pi 引脚。这使得在放置跳线时更容易识别管脚。Adafruit 为各种树莓 Pi 型号提供了一个名为“补鞋匠”的分线板。详见 www.adafruit.com/
。
跳线
跳线有各种长度和形状,如图 21-4 和 21-5 所示。有些是盒子里的预包装,有些是塑料袋里的随机分类。无论哪种方式,它们通常是一根坚固的电线,允许您连接发光二极管、开关等。,到树莓派别针。
图 21-5。
实芯焊丝的选择
图 21-4
试验板上的分线板(图片中间),跳线和电阻连接到引脚
发光二极管
发光二极管(led)允许电流沿一个方向流动(这是二极管部分),同时发光。它用于提供廉价的低成本照明,有多种包装。对于大多数电子设备,使用熟悉的彩色圆顶版本,如图 21-6 所示。
图 21-6。
一个 LED。注意,一条腿比另一条腿长。较长的引脚是阳极,总是连接到正电源轨。
你可能已经注意到一条腿比另一条腿长。这称为阳极,总是连接到+ve (3.3V)供电轨。较短的一脚称为阴极,连接到地或 0V 线。如果你把它接反了,灯就不亮了。你不会弄坏灯,它就是不亮。
当使用发光二极管时,你还需要在电路上连接一个电阻,因为它们会从树莓皮中吸取更多的能量,从而损坏电脑。
电阻
电阻器限制通过电路的电流量。电阻的测量单位称为欧姆(ω),电阻值越大,对电流的限制就越大。计算电路中电压、电流(以安培为单位)和电阻(欧姆)的公式为
V = IR
其中 V 是电压,I 是电流,R 是电阻。如果要计算在 3.3V 电压下流经 330ω电阻的电流,则为
V = IR
这意味着 I = V / R:
I = 3.3 / 330
I = 0.01 amps
如果我们将该电阻增加到 470ω,它会将电流从 0.01 安培降至 0.006 安培,几乎是初始值的一半,这意味着通过电阻另一端的电流会减少。
一条电阻如图 21-7 所示。电阻值已经写在纸条上,以便更容易识别其值:330ω。电阻器周围的带表示其值。有三到六个色带,但是我用了五个,如表 21-1 所示:
表 21-1
电阻器色带位置和含义
|颜色
|
第一乐队
|
第二波段
|
第三波段
|
乘数
|
容忍
|
| --- | --- | --- | --- | --- | --- |
| 黑色 | Zero | Zero | Zero | 1 小时 | |
| 棕色 | one | one | one | 20+ | ± 1% |
| 红色 | Two | Two | Two | 1 号机 | ± 2% |
| 橙色 | three | three | three | 10 号 | |
| 黄色 | four | four | four | 一百 | |
| 绿色 | five | five | five | 1MΩ | ± 0.5% |
| 蓝色 | six | six | six | 10MΩ | ± 0.25% |
| 紫色 | seven | seven | seven | 1 兆焦耳 | ± 0.1% |
| 灰色 | eight | eight | eight | 0.1Ω | ± 0.05% |
| 白色 | nine | nine | nine | 0.01Ω | |
| 黄金 | | | | | ± 5% |
| 白银 | | | | | ± 10% |
图 21-7
供应商提供的一条电阻器,其值标在纸带上
我拥有的电阻颜色如表 21-2 所示。
表 21-2
转换色带以确定电阻值
|颜色
|
价值
|
| --- | --- |
| 橙色 | three |
| 橙色 | three |
| 黑色 | Zero |
| 黑色 | 1 哦 |
| 黄金 | ± 5% |
这是 330×1ω或 330ω,容差为 5%。
在电路中放置 LED 时,使用电阻非常重要。LED 将试图吸收尽可能多的电流,而电阻是限制这种吸收的一个很好的方法。电阻没有方向性,不像 led,所以你可以把它们放在你喜欢的任何地方,为了清晰起见,我建议让它们都面向同一方向。
开关
图 21-8 显示了一个简单的按钮轻触开关。它包含成两对排列的四个引脚。每一对都与另一对断开。当开关被按下时,封装内的触点完成线对之间的连接。
图 21-8
一个简单的按钮轻触开关
建造一个电路
既然我们已经学习了电子电路的基础知识,让我们构建一个不需要编程的非常简单的电路。我们将根据图 21-1 重建电路。为此,我们需要
-
一块试验板
-
分线板
-
三根跳线
-
S7-1200 可编程控制器
-
轻触开关
-
发光二极管
将分线板连接到树莓派
点击 Raspberry Pi 菜单并选择“关闭…”关闭 Raspberry Pi,如图 21-9 所示。
图 21-9
Raspberry Pi 系统菜单上的关闭菜单项
等到机器完全关闭。不要切断电源!我们需要能量来建造电路。
将分线板的带状电缆连接到 Raspberry Pi 的 GPIO 引脚。注意不要用力,因为如果电缆没有正确固定,这样可能会弯曲针脚。
带状电缆上通常有一根白线。这表示第一个引脚,用于将 Raspberry Pi 与分线板对齐。如图 21-10 所示,确保白色电线最靠近电路板顶部,即远离 USB 和网络端口。
图 21-10
箭头指示电缆上白线的位置。请注意主板相对于 USB、网络和 HDMI 端口的方向
将分线板插入试验板上。分线板应跨在试验板的中间槽上,如图 21-4 所示。同样,注意不要弯曲任何引脚。
接下来,将带状电缆的另一端连接到分线板。在分线板的侧面有一个凹口,在带状连接器上有一个凸起部分,这就更容易做到了。它只能朝一个方向走,但可以肯定的是,电缆上的白线应该在电路板的顶部。
现在,您应该已经将覆盆子 Pi 连接到试验板了。即使树莓派是关闭的,电源仍然连接着它。我们可以用这个给我们的电路供电。
提供电源和接地
我们将添加的前两条跳线连接到 Raspberry Pi 的 3.3V 和接地引脚。这些位于分线板顶部附近,如图 21-11 所示。
图 21-11
3V3 引脚和接地引脚线
如图 21-11 所示,将电线的一端插入 3V3 (3.3 伏)引脚排。将另一端放在试验板的+ve 导轨上。这是我们的 3.3V 线。连接到那条线上的任何东西(在试验板上显示为红色)都将连接到 3.3V。
接下来拿另一根线,把它连接到标有 GND 或地的一排线上。这将是我们的地线。我们还没有地方放它,所以暂时让它漂浮着吧。
添加 LED
将 LED 放在主板上,阳极(较长的一条)放在一排,阴极(较短的一条)放在另一排。不要把针放在同一排,否则它不会工作!在图 21-12 中,我展示了如何放置 LED。只要你记得是哪个方向,哪个方向并不重要——箭头表示阳极。
图 21-12
LED 引脚放置在不同的行上。箭头指向阳极引脚。
为了将 LED 连接到 3.3V 线路,我们将使用 330ω电阻。
记住!务必使用带 LED 的电阻器!
如图 21-13 所示,将电阻的一端连接到与阳极相同的行,另一端连接到+ve 轨。
图 21-13
使用 330ω电阻将 LED 连接到 3.3V 线路
完成电路
我们现在将通过添加轻触开关和跳线来完成电路。如图 21-14 所示,在试验板上放置一个开关。
图 21-14
一个轻触开关跨在试验板中间的槽上
我喜欢把它放在板中间的槽上,但是你可以把它放在任何你想放的地方。请记住,开关有两对,每对的引脚相互连接。这意味着,如图 21-14 所示,我们需要将电线连接到顶部和底部引脚,以便当轻触开关被按下时,电路接通。如果我们连接两个顶部引脚或两个底部引脚,电路将完成。
将之前悬空的 0V(接地)线插入与开关顶部引脚同一行的连接器中。取另一根跨接导线,将其连接到与开关底部引脚相同的行,并连接到 LED 的阴极(短引脚)。
你现在应该有一个类似于图 21-15 所示的电路。
图 21-15。
完整的电路。分线板在图片顶部部分可见。
测试电路
完整电路如图 21-16 所示。您应该对照刚才制作的物理电路检查该电路中的连接。在按下轻触开关之前,确保所有连接都与图中的连接相匹配。
图 21-16。
显示 Raspberry Pi 和试验板之间连接的电路图。在按下轻触开关之前,使用此图确保您的电路构建正确。
如果一切正常,按下轻触开关,电路应该亮了!如果没有,检查你的线路。如果您断开了 Raspberry Pi 的电源——记住,我们需要这个电路的电源——您应该重新连接电源。这将打开你的树莓皮,但这很好。对于这个练习,我们只需要来自引脚的功率。
Pin 含义
每个引脚都有特定的用途,Raspberry Pi 允许您使用 SPI(串行外设接口)、I2C(内部集成电路)或 GPIO 引脚直接连接到外设。在本书的剩余部分,我们将专注于 GPIO 引脚本身。我们将在后面看到,我们必须告诉 Raspberry Pi 我们将如何使用 GPIO 引脚,也就是什么操作模式。图 21-17 显示了物理引脚的映射。
图 21-17
每个 GPIO、电压和接地引脚的位置
你会注意到的第一件事是编号的针没有按顺序排列。这些是 GPIO 引脚,可以通过 Python 直接访问。您可以连接开关、发光二极管等。,并让 Python 读取它们的值或向它们写入值以打开灯。
3v3 引脚输出 3.3 伏,5V 引脚输出 5 伏。建议您坚持使用 3.3V 线路,除非您尝试连接的外设需要 5v 电压。接地引脚是不可知的;不管你用的是 5 伏还是 3.3 伏,你的电路都使用相同的接地引脚。接地引脚在图 21-17 中标记为‘G’。
最后两个引脚是 GPIO 的引脚 0 和 1,不可访问。不应将设备连接到这两个引脚。在图中,那些针标有'-'。
让我们重建电路,用 Python 开灯。
gpiozero 库
为了与试验板上的电子元件“对话”,我们将使用 gpiozero 库。这是一组工具,可以轻松地与连接到 GPIO 引脚的组件通信并从中读取数据。
这本书将只涵盖这个库所涵盖的功能的很小一部分。如果您想连接运动传感器、温度计、电位计等。,那么我推荐在 https://gpiozero.readthedocs.io
通读文档。
我们将使用这个库创建的第一个程序是一个打开 LED 一秒钟然后关闭的程序。
该电路
图 21-18 所示的电路通过 GPIO 引脚 4 将 LED 的阳极(两个引脚中较长的一个)连接到 Raspberry Pi。电阻连接到 LED 的阴极(两个引脚中较短的一个),然后连接到 Raspberry Pi 的一个接地引脚。构建如图 21-18 所示的电路。
图 21-18
Python 控制的 led 电路
此时,LED 不应亮起。我们将用代码来实现。
Python 程序
打开空闲的 IDE,创建一个名为“ch21-1.py”的新文件,并将其放在“pygamebook”文件夹内名为“ch21”的新文件夹中。在该文件中,键入以下内容:
from gpiozero import LED
from time import sleep
导入 gpiozero 库来访问 LED 类。这个包装类使得打开和关闭 led 变得更加容易。第二个导入是 sleep()函数。Sleep 有一个参数,即计算机在执行下一条语句之前应该等待的秒数
led = LED(4)
创建一个连接到物理 LED 的 LED 对象,该物理 LED 连接到 GPIO 引脚 4。
led.on()
sleep(1)
打开 LED 并等待一秒钟。
led.off()
sleep(1)
关闭 LED 并等待一秒钟。
保存并运行程序。观察试验板上发生的情况 LED 亮起一秒钟,然后熄灭!
其他功能
on()和 off()函数非常适合于关闭和打开 LED,但是您也可以切换 LED 的状态,因此如果灯亮了,调用 toggle()将关闭它,反之亦然。最后,blink()会做到这一点,它会使 LED 闪烁。您可以指定灯打开的持续时间和灯关闭的持续时间。
获取按钮输入
除了将值输出到 GPIO 引脚,Raspberry Pi 还可以读取值。在本节中,我们将在电路中添加一个按钮,并控制 LED 的闪烁。修改后的电路如图 21-19 所示。
图 21-19
轻触开关控制的发光二极管电路
LED 仍然连接到 GPIO 引脚 4,就像在以前的电路中一样。该开关一端连接到 GPIO 17,另一端连接到 Raspberry Pi 上的接地引脚。Button 对象可用于使读取按钮的状态(按下、释放)变得容易。
在 Python 中读取按钮输入
在与上一个程序相同的文件夹中创建一个名为“ch21-2.py”的新文件。准确输入所写的代码:
from gpiozero import LED, Button
从 gpiozero 库中导入 LED 和 Button 类。
led = LED(4)
button = Button(17)
创建一个 LED 对象,其中物理 LED 连接到 GPIO 引脚 4。创建一个按钮对象,其中物理轻触开关连接到 GPIO 引脚 17。
ledOn = False
wasPressed = False
用于记录 LED 状态的标志–LED on。当程序启动时,它是关闭的,所以' ledOn '被设置为假。用于记住按钮最后状态的标志。这将防止按钮被持续按下和 LED 快速闪烁。
while True:
保持程序运行。要退出程序,请按 Ctrl+C。
if not button.is_pressed and wasPressed:
ledOn = not ledOn
如果按钮已被释放,即如果按钮已被按下且未被未被按下,则切换“ledOn”变量的状态。
if ledOn:
led.blink()
else:
led.off()
wasPressed = button.is_pressed
如果 LED 应该亮起,将其设置为闪烁。否则,应该将其关闭。“状态”变量被设置为按钮被按下的当前状态。
保存并运行程序。
短暂按下试验板上的轻触开关。LED 将开始闪烁。再次按下按钮将停止 LED 闪烁。按键盘上的 Ctrl+C 退出程序。您可以向电路板添加另一个按钮,并查询该按钮以确定程序是否应该停止。听起来像一个有趣的练习!你会怎么做?
结论
通用输入/输出(GPIO)引脚允许 Raspberry Pi 与 led、开关、运动传感器、温度计等电子组件进行对话。它具有额外的模式,允许您连接到支持 SPI 或 I2C 标准的外设。
使用 GPIO,我们可以通过闪烁 led 或从轻触开关获取输入来扩展我们的程序。在接下来的几章中,我们将看到如何使用 GPIO 进行游戏输入和输出。
二十二、游戏项目:记忆
游戏《记忆》是第一个 GPIO 项目。如图 22-1 中的成品板所示,该板设置有两排:一排有四个发光二极管,另一排有四个按钮。
图 22-1
两行试验板的建议布局:一行有四个 led,另一行有四个按钮
当序列在 led 上播放时,玩家通过按下 led 下方的行上的相应按钮来重复该序列。游戏开始时只有一个发光二极管,但随着游戏的进行,会增加到四个发光二极管。
布置试验板
我们将按照特定的顺序构建试验板:先是一排 led,然后是一排轻触开关。在每一行构建完成后,编写一个小脚本来测试组件并确保连接正确。
放置发光二极管
如图 22-1 所示排列发光二极管和轻触开关。发光二极管应对齐,使较长的一条腿(阳极)位于右侧。这没有技术上的原因,但它保持了设计的一致性,并确保电线将被放置在电路板上的正确孔中。图 22-2 显示了发光二极管应该如何连接。
图 22-2
从 GPIO 引脚到相应 led 的连接
每个阴极(较短的引脚)通过一个 330ω电阻连接到接地引脚 31。阳极(较长的引脚)连接到特定的 GPIO 引脚,如图所示。为了更容易地创建电路,我在试验板上的 31 号引脚(地)和–ve 轨之间放置了一个小跳线,如图 22-3 所示。将电阻连接到引脚 31 意味着将电阻连接到–ve 供电轨。
图 22-3
将接地引脚 31 连接到–ve 轨使连接电阻更容易
请注意,在较高的盒子中,试验板上只有三个裸露的孔与引脚 31(地)相连。通过使–ve 轨与引脚 31 连接,我们有效地连接了大约 50 个孔,具体取决于您的试验板的尺寸。
从图 22-3 中可以看出,较轻的电线连接到引脚 23、12、16 和 21。
测试电路
为了测试电路,我们将编写一个小的 Python 脚本来按顺序打开和关闭 led。在“pygamebook”的“projects”文件夹中创建一个名为“memory”的新文件夹在该文件夹中创建一个名为“ledtest.py”的新 Python 脚本。这是一个非常短的程序,轮流打开每个 LED 半秒钟,然后移动到下一个。
from gpiozero import LED
from time import sleep
导入 gpiozero 库来访问 LED 类。睡眠功能的导入时间。
leds = [ LED(23), LED(12), LED(16), LED(21) ]
创建 LED 对象阵列。请注意,这些数字是按照指示灯从左到右的顺序排列的。
while True:
for led in leds:
led.on()
sleep(0.5)
led.off()
该循环将保持程序运行,循环通过所有的灯,一个接一个地打开然后关闭它们。保存并运行程序。要退出程序,请按 Ctrl+C。
如果有任何问题,请检查接线以及哪些针脚连接到 led。
放置轻触开关
按钮或轻触开关放置在板上,并连接到 GPIO 引脚和地,如图 22-4 所示的(简化)图所示。
图 22-4
轻触开关连接到 GPIO 引脚和地
连接到引脚 31(地)的电线可以通过将电线从开关放置到–ve 轨来实现,就像我们之前对 led 所做的那样。
测试按钮电路
对于这个测试,我们将编写一个脚本来在按下开关时打开相应的 LED。从逻辑上看,GPIO 4 上的按钮会打开连接 GPIO 23 的 LED,GPIO 17 上的按钮会打开连接 GPIO 12 的 LED,以此类推。
我们的程序将使用 LED 和按钮类元组。
在“memory”文件夹中创建一个名为“buttontest.py”的新脚本,并输入以下代码:
from gpiozero import LED, Button
from time import sleep
该计划的导入。gpiozero 用于 LED 和按钮类,time 用于睡眠功能。
pair1 = (LED(23), Button(4))
pair2 = (LED(12), Button(17))
pair3 = (LED(16), Button(22))
pair4 = (LED(21), Button(6))
配对将每个 LED 与相应的按钮相匹配。元组的第零个元素是 LED,元组的第一个元素是按钮。记住:我们可以使用整数索引值来访问元组部分。
pairs = [ pair1, pair2, pair3, pair4 ]
为了使我们的程序简短,我们将使用一个配对列表并遍历它们。
while True:
for pair in pairs:
if pair[1].is_pressed:
pair[0].on()
else:
pair[0].off()
当我们测试按钮时,循环保持程序运行。列表中的每一对都是循环的。测试按钮的“is_pressed”属性,如果按钮被按下,相应的 LED 就会亮起。否则,LED 将关闭。
保存程序并运行它。依次按住每个开关。相应的 LED 应该点亮。如果没有,请检查您的接线并重试。
电路的完整接线如图 22-5 所示。
图 22-5
显示所有连接的游戏完成图
现在我们已经建立并测试了电路,我们可以制作游戏了。
记忆游戏
这个程序的基本算法是这样的:
-
随机选择一个、两个、三个、四个指示灯
-
播放发光二极管序列
-
等待玩家将序列输入回来
-
显示祝贺/坏运气消息(在控制台中)
-
继续下一个序列
在“内存”中启动一个名为“buttonled.py”的新 Python 脚本文件。这将包含我们项目的两个助手类。第一个辅助类是一个 LED/Button 聚合类,第二个是这个类的实例集合,它将处理随机序列的选择。
ButtonLED 和 ButtonLEDCollection 类
输入以下代码:
from gpiozero import LED, Button
import random
ButtonLED 和 ButtonLEDCollection 类的导入。gpiozero 是为 LED 和 Button 类导入的,random 是因为我们需要随机打乱 LED 列表,以使游戏每次都不同。
class ButtonLED(object):
def __init__(self, ledPin, buttonPin):
self.led = LED(ledPin)
self.button = Button(buttonPin)
构造函数接受两个参数:连接到 LED 的 GPIO 管脚号和连接到轻触开关的 GPIO 管脚号。
def on(self):
self.led.on()
打开 LED。
def off(self):
self.led.off()
关闭 LED。
def wait(self, timeout):
self.button.wait_for_press(timeout)
return self.button.is_pressed
wait()方法将等待,停止程序执行,直到按钮被按下。如果在“超时”值(秒)后没有按下按钮,程序将恢复。按钮的当前按下状态被返回给调用者。程序将使用该方法来确定玩家是否在主程序中以正确的顺序单击了按钮。
class ButtonLEDCollection(object):
def __init__(self):
led1 = ButtonLED(23, 4)
led2 = ButtonLED(12, 17)
led3 = ButtonLED(16, 22)
led4 = ButtonLED(21, 6)
self.items = [ led1, led2, led3, led4 ]
该构造函数创建按钮式对象,并将它们添加到名为“items”的内部列表中。
def pick(self, count):
leds = self.items
random.shuffle(leds)
picked = []
for n in range(0, count):
picked.append(leds[n])
return picked
pick()方法打乱 led 并选择第一个“计数”项目。假设初始序列指向 GPIO 引脚 6、7、8 和 9。洗牌后可能是 7,6,9,8。选择前三个将返回 7、6 和 9,表示第二、第一和第四个 led。这种方法是为记忆游戏创建随机发光二极管序列的核心。
def waitForClick(self):
isPressed = False
while not isPressed:
for led in self.items:
isPressed = isPressed or led.button.is_pressed
这将等待玩家按下任何轻触开关。这是一个阻塞呼叫,程序将无法继续,直到按下按钮。
if __name__=='__main__':
from time import sleep
collection = ButtonLEDCollection()
leds = collection.pick(4)
for led in leds:
led.on()
sleep(1)
led.off()
为了测试这些类并确保一切正常,创建了一个小的测试存根。它创建 ButtonLEDCollection 类的一个实例,并挑选四个 led 逐个打开和关闭。保存并运行脚本。如果你没有看到四个发光二极管以随机的顺序闪烁,你应该检查程序和接线,以确保你有正确的接线和编码。在进入主程序之前,请执行此操作。
主程序
主程序是一个名为“memorygame.py”的新文件。创建这个新文件,并输入以下代码:
#!/usr/bin/python3
import sys
from time import sleep
from buttonled import ButtonLEDCollection
游戏的导入包括在前面部分创建的“buttonled.py”文件。只有两个导入的类,我们可以使用*,但是我选择在这个实例中显式命名 ButtonLEDCollection,因为它是唯一需要的类。
collection = ButtonLEDCollection()
创建 ButtonLEDCollection 类的实例。
print ("Welcome to the Game of Memory!")
print ("A sequence of LEDs will flash, ")
print ("you will be asked to repeat the")
print ("pattern. Press any button to start")
向玩家显示欢迎消息。尽管大多数操作都发生在试验板上,但控制台上的一些信息还是很有帮助的。
collection.waitForClick()
等待玩家按下任何轻触开关。
for n in range(1, 5):
请记住,虽然范围值是从 1 到 n,但实际上值是从 1 到 n-1,这意味着它将在数字 1-4 之间循环。
leds = collection.pick(n)
print ("Remember this sequence")
for led in leds:
led.on()
sleep(1)
led.off()
随机选择一系列发光二极管。闪现序列,并告诉玩家他们需要记住序列。
print("Your turn!")
for led in leds:
if led.wait(1):
led.on()
sleep(0.5)
led.off()
else:
print ("Missed! Game Over!")
sys.exit()
现在轮到玩家了。led 对象被再次循环——记住,那些是 LED/按钮集合对象——并且按钮被测试。如果在 1 秒的给定时间内按下,则选择下一个 led 对象。否则,游戏结束。
print ("Congratulations!")
如果玩家正确记住了所有四个序列,则显示一条祝贺消息。保存文件。
通过键入以下命令运行该程序:
$ python3 memorygame.py
或者,更改脚本的执行模式并自行运行:
$ chmod +x memorygame.py
$ ./memorygame.py
游戏将开始,您将看到一个、两个、三个、最后四个随机的发光二极管。祝你好运!
完整列表 buttonled.py
buttonled.py 的完整列表有助于调试您可能遇到的任何问题:
from gpiozero import LED, Button
import random
class ButtonLED(object):
def __init__(self, ledPin, buttonPin):
self.led = LED(ledPin)
self.button = Button(buttonPin)
def on(self):
self.led.on()
def off(self):
self.led.off()
def wait(self, timeout):
self.button.wait_for_press(timeout)
return self.button.is_pressed
class ButtonLEDCollection(object):
def __init__(self):
led1 = ButtonLED(23, 4)
led2 = ButtonLED(12, 17)
led3 = ButtonLED(16, 22)
led4 = ButtonLED(21, 6)
self.items = [ led1, led2, led3, led4 ]
def pick(self, count):
leds = self.items
random.shuffle(leds)
picked = []
for n in range(0, count):
picked.append(leds[n])
return picked
def waitForClick(self):
isPressed = False
while not isPressed:
for led in self.items:
isPressed = isPressed or led.button.is_pressed
if __name__=='__main__':
from time import sleep
collection = ButtonLEDCollection()
leds = collection.pick(4)
for led in leds:
led.on()
sleep(1)
led.off()
完整列表 memorygame.py
memorygame.py 的完整列表有助于调试您可能遇到的任何问题:
#!/usr/bin/python3
import sys
from time import sleep
from buttonled import ButtonLED, ButtonLEDCollection
collection = ButtonLEDCollection()
print ("Welcome to the Game of Memory!")
print ("A sequence of LEDs will flash, ")
print ("you will be asked to repeat the")
print ("pattern. Press any button to start")
collection.waitForClick()
for n in range(1, 5):
leds = collection.pick(n)
print ("Remember this sequence")
for led in leds:
led.on()
sleep(1)
led.off()
print("Your turn!")
for led in leds:
if led.wait(1):
led.on()
sleep(0.5)
led.off()
else:
print ("Missed! Game Over!")
sys.exit()
print ("Congratulations!")
结论
这是一个有趣的小游戏,让你习惯用 Raspberry Pi 和 Python 编写硬件游戏。gpiozero 库使得访问 GPIO 引脚变得非常容易。
我总是建议在着手编写实际游戏之前,为你的电路创建测试程序,以证明它们能够工作。事实上,写测试真的很重要,你写的测试越多,你就能更好地证明你的程序将完成它所设定的目标。
为了增强记忆游戏,你可以用一个专用的开始按钮来开始游戏,而不是四个播放按钮。此外,你可以让玩家选择他们的技能水平。传递给 wait()方法的超时值可以更改;轻松 1.5 秒,正常 1 秒,困难 0.5 秒。
二十三、游戏项目:问答
这本书的最后一个项目是一个双人沙发问答游戏。玩家会遇到一系列选择题,他们必须选择正确的答案。游戏混合使用 PyGame 和电子设备;问题显示在监视器上,所有输入来自两对三个轻触开关。部分游戏画面如图 23-1 所示。
图 23-1
问答游戏的屏幕:飞溅,准备好,问题,和分数屏幕
在“pygamebook”“projects”文件夹中创建一个名为“quiz”的新文件夹这是我们将为这个项目编写的所有脚本的位置。
电子产品
对于这个游戏的电路,你需要以下:
-
一块试验板
-
六个轻触开关
-
不同长度的电线
图 23-2 显示了该项目的电路图。它由两对三个轻触开关组成。每个轻触开关通过 Raspberry Pi 上的引脚 31 接地。玩家 1 的按钮连接到 GPIO 引脚 4、17 和 22,玩家 2 的按钮连接到 GPIO 引脚 5、6 和 13。
图 23-2
显示两组轻触开关的问答游戏电路图
一旦电路在试验板上建立并连接到 Raspberry Pi,我们将使用一个简短的程序来测试按钮。为此,我们的程序将使用 PyGame 点亮屏幕显示。
测试按钮
测试程序如图 23-3 所示,显示两组三个圆。当轻触开关被按下时,圆圈“亮起来”,也就是说,红点以更亮的颜色出现。
图 23-3
测试程序运行显示三个轻触开关已被按下
在“测验”文件夹中创建一个名为“buttontest.py”的新脚本,并输入以下内容:
#!/usr/bin/python3
import pygame, os, sys
from pygame.locals import *
from gpiozero import Button
PyGame 和 gpiozero 库的标准导入。
def drawButtonState(surface, button, pos):
color = 32
if button.is_pressed:
color = 192
pygame.draw.circle(surface, (color, 0, 0), pos, 35)
绘制按钮的状态。如果按下按钮,会显示一个明亮的圆圈。
def drawPlayerState(surface, buttons, startx):
x = startx
for b in buttons:
drawButtonState(surface, b, (x, 240))
x = x + 80
return x
循环浏览给定的按钮,并检测每个按钮是否被按下。调用 drawButtonState。
pygame.init()
fpsClock = pygame.time.Clock()
surface = pygame.display.set_mode((640, 480))
初始化 PyGame 并创建一个屏幕和时钟。
player1 = [ Button(4), Button(17), Button(22) ]
player2 = [ Button(5), Button(6), Button(13) ]
创建两个按钮列表。每个按钮都连接到指定 GPIO 引脚上的轻触开关。
background = (0, 0, 0) # Black
while True:
surface.fill(background)
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
x = 80
x = drawPlayerState(surface, player1, x)
x = x + 80
drawPlayerState(surface, player2, x)
pygame.display.update()
fpsClock.tick(30)
保存并运行程序。按住每个轻触开关。当按下开关时,屏幕上的彩色圆圈应该“亮起”。如果不是这样,请检查电路并再次尝试该程序。
如果电路工作正常,我们可以进入项目的视觉部分。这将需要我们创建一个状态机。
有限状态机
游戏中共有五种状态,它们的转换如图 23-4 所示。这五种状态是
图 23-4
问答游戏的有限状态机(FSM)
-
闪屏–显示欢迎信息
-
准备好–显示“准备好”信息
-
选择问题–从列表中选择新问题
-
显示问题–显示问题、三个选项和倒计时
-
显示分数–显示双方玩家的当前分数
-
游戏结束–这与“显示分数”状态相同,但包含一个显示谁赢得了整个游戏的指示器
因为“显示分数”和“游戏结束”非常相似,我们只需要为这两种状态创建一个类,以及“准备好”和“闪屏”状态。表 23-1 显示了状态之间的移动规则,也称为状态转换。
表 23-1。
游戏状态、转换规则和职业
|状态
|
次状态
|
班级
|
转换条件
|
| --- | --- | --- | --- |
| 启动画面 | 准备好 | 标题文本屏幕 | 其中一个玩家按下了触动开关 |
| 准备好 | 选择问题 | 标题文本屏幕 | 在一定的持续时间后自动移动到下一个状态 |
| 选择问题 | 显示问题或游戏结束 | 选择一个问题 | 自动移动到下一个状态。如果没有更多的问题,下一个状态是“游戏结束” |
| 显示问题 | 显示分数 | 显示问题 | 在时间限制(倒计时)达到零时,或者双方玩家都选择了答案后,自动进入下一个状态 |
| 显示分数 | 选择问题 | ShowScore | 在一定的持续时间后自动移动到下一个状态 |
| 游戏结束 | 无-游戏结束 | ShowScore | 没有条件。游戏结束 |
制作游戏
除了前面提到的状态之外,还有一些额外的类需要构建。这些是
-
问题反序列化
-
基本状态类
-
游戏赛跑者
-
UI 助手类
我们将依次了解每一项。
这些问题
测验的问题来自于 Pub 测验问题 HQ ( https://pubquizquestionshq.com/
),这是一个免费开放的问题资源。这些问题被格式化在一个网页上,所以我花了一些时间将它们组织成一个 JSON 文件。生成的数据文件应作为“questions.json”保存到“测验”文件夹中:
{
"questions":
[
{
"question": "New York City Hall is in which Borough?",
"answer": "Manhattan",
"answers": [
"Queens",
"Brooklyn"
]
},
{
"question": "Which was the first baseball team in Texas to make it to the World Series?",
"answer": "Houston Astros",
"answers": [
"Houston Oilers",
"Texas Rangers"
]
},
{
"question": "Dwight D. Eisenhower was President from 1953 to 1961, but who was his Vice President?",
"answer": "Richard Nixon",
"answers": [
"John Kennedy",
"Lyndon Johnson"
]
},
{
"question": "Which was the most successful NFL team of the decade beginning in Jan 2000 with 4 Super Bowl wins?",
"answer": "New England Patriots",
"answers": [
"Buffalo Bills",
"San Diego Chargers"
]
},
{
"question": "Why was there no World Series played in 1994?",
"answer": "Player's strike",
"answers": [
"No one bought tickets",
"Ban on baseballs"
]
},
{
"question": "Lansing is the state capital of which northern state in America?",
"answer": "Michigan",
"answers": [
"Ilinois",
"Wisconsin"
]
},
{
"question": "As of 2013 the most widely circulated newspaper in the USA was The Wall Street Journal. Which company owns it?",
"answer": "News Corporation
"answers": [
"Chicago Tribune",
"Conde Nast"
]
},
{
"question": "Out of which city were Aerosmith formed?",
"answer": "Boston",
"answers": [
"New York",
"Los Angeles"
]
},
{
"question": "Which future president gained national fame through his role in the War of 1812, most famously where he won a decisive victory at the Battle of New Orleans?",
"answer": "Andrew Jackson",
"answers": [
"George Washington",
"Abraham Lincoln"
]
},
{
"question": "Born in Massachusetts, which painter's most famous work is 'Arrangement in Grey and Black No.1'?",
"answer": "James Abbott McNeill Whistler",
"answers": [
"Andy Warhol",
"Phillipe Stark"
]
}
]
}
JSON 文件被格式化为一个带有名为“questions”的列表属性的对象。数组中的每个对象都具有以下属性:
-
问题–问题的文本
-
答案–问题的正确答案
-
答案–不正确答案的列表
我选择使用一个函数来创建一系列问题。该函数从“questions.json”文件中加载问题,并填充“Question”对象列表。
创建一个名为“questions.py”的新文件,并输入以下内容:
#!/usr/bin/python3
import json
import random
JSON 序列化/反序列化的导入。随机导入将用于随机化问题和答案的顺序
class Question(object):
def __init__(self, jsonQuestion):
self.question = jsonQuestion['question']
self.answers = jsonQuestion['answers']
self.answer = jsonQuestion['answer']
self.answers.append(jsonQuestion['answer'])
random.shuffle(self.answers)
index = 0
for a in self.answers:
if a == jsonQuestion['answer']:
self.answerIndex = index
index = index + 1
“问题”类用于存储问题文本、正确答案和其他建议。正确答案的索引也被存储。这将使确定玩家是否选择了正确答案变得更容易一些;第一个按钮映射到第一个选择,依此类推。为了让游戏更有趣,每次玩这个游戏时,答案都会用“random.shuffle()”方法进行洗牌。这个简便的方法打乱了列表的元素。我们将在下面的“loadQuestions()”函数中看到它的使用。
def loadQuestions(filename):
f = open(filename)
questionFile = json.load(f)
f.close()
将问题文件的全部内容载入内存。
questions = []
for q in questionFile['questions']:
questions.append(Question(q))
对于文件中的每个问题,创建一个“问题”类的新实例,并将其附加到“问题”列表中。
random.shuffle(questions)
return questions
一旦所有的问题都被添加到列表中,再次使用“random.shuffle()”方法对问题进行重新排序,这样就不会有两个游戏是相同的。
if __name__ == '__main__':
questions = loadQuestions("questions.json")
for q in questions:
print(q.question)
print("Answer index %d" % q.answerIndex)
for a in q.answers:
if a == q.answer:
print("\t* %s" % a)
else:
print("\t%s" % a)
为了测试代码是否运行,我在文件的底部添加了一个测试存根。它加载到“questions.json”文件中,并显示问题和答案。正确答案标有星号(*)。
保存文件并运行它。在运行它之前,您必须添加执行位:
$ chmod +x questions.py
$ ./questions.py
您应该会看到屏幕上显示的问题列表。如果没有,请检查代码。
UI 助手类
UI 助手类包含在一个文件中。这些类别是
-
文本-基本文本组件
-
问题–显示问题和答案
-
倒计时–显示一个从 30 秒倒计时到 0 的进度条
创建一个名为“ui.py”的新文件,并输入以下文本:
import pygame
from pygame.locals import *
导入 PyGame 模块。
class Text(object):
def __init__(self, size, colour):
self.size = size
self.colour = colour
self.font = pygame.font.Font(None, size)
def draw(self, surface, msg, pos, centred = False):
x, y = pos
tempSurface = self.font.render(msg, True, self.colour)
if centred:
x = x - tempSurface.get_width() / 2
y = y + tempSurface.get_height() / 4
pos = (x, y)
surface.blit(tempSurface, pos)
Text 类是现有 PyGame 字体类的包装。它使文本在屏幕上的定位更容易,并提供了一种方便的方式来绘制以特定点为中心的文本。
class QuestionText(object):
def __init__(self):
self.questionText = Text(32, (255, 255, 0))
self.answerText = Text(32, (255, 255, 255))
self.disabledText = Text(32, (56, 56, 56))
QuestionText 类的构造函数。这将创建三个单独的文本实例:一个用于问题文本,一个用于答案文本,一个用于禁用状态。当这一轮结束时,正确答案会突出显示。禁用文本用于得出两个不正确的答案。
def draw(self, surface, question, answer, answers, showAnswer = False):
y = 64
maxWidth = 60
lineHeight = 32
if len(question) > maxWidth:
question.split(" ")
temp = ""
for word in question:
temp = temp + word
if len(temp) > maxWidth:
pos = (400, y)
self.questionText.draw(surface, temp, pos, True)
temp = ""
y = y + lineHeight
self.questionText.draw(surface, temp, (400, y), True)
else:
self.questionText.draw(surface, question, (400, y), True)
如果问题文本长于屏幕宽度,它将被拆分成单独的单词。每个单词都被添加到列表中,直到达到最大宽度。然后,该文本被绘制到屏幕上。然后处理剩余的文本,直到显示出整个问题。如果问题文本小于屏幕宽度,则正常显示。
y = y + lineHeight * 2
label = "A"
for a in answers:
font = self.answerText
if showAnswer and a != answer:
font = self.disabledText
font.draw(surface, "%s. %s" % (label, a), (100, y), False)
labelChar = ord(label)
labelChar = labelChar + 1
label = chr(labelChar)
y = y + 40
每个答案前面都会显示 A、B 或 C。为了达到这种“效果”,我们必须首先将当前标签转换成一个数字——这就是“ord()”函数的作用。它查找 ASCII(美国信息交换标准代码)表,并根据字符返回一个数字。第一次运行循环时,label = 'A '和 so ord()将返回 65,因为' A '位于 ASCII 表的第 65 位。该值递增到下一个字符,因此 65 将变成 66,并使用“chr()”函数将其转换为一个字符。ASCII 码中的 66 是 b。
class Countdown(object)
:
def __init__(self, seconds, pos, width, height, innerColour, borderColour, text):
self.maxSeconds = seconds
self.seconds = seconds
self.pos = pos
self.width = width
self.height = height
self.finished = False
self.text = text
self.innerColour = innerColour
self.borderColour = borderColour
self.fullRect = Rect(pos, (width, height))
self.rect = Rect(pos, (width, height))
这是一个相当长的构造函数!这些参数将被用来绘制一个进度条形状的倒计时器,它在屏幕上停留的时间越长越短。
def draw(self, surface):
pygame.draw.rect(surface, self.innerColour, self.rect)
pygame.draw.rect(surface, self.borderColour, self.fullRect, 2)
要绘制进度条,我们将使用 PyGame 提供的' draw.rect()'方法。它可以用两种方法之一绘制:填充或带边框。进度条的“内部”将被绘制成一个填充的矩形,进度条的“外部”将被绘制成一个边框。
倒计时的当前大小从“self.rect”中提取,完整的矩形“self.fullRect”被绘制在顶部,如图 23-5 所示。
图 23-5
问答游戏的进度条
x, y = self.pos
x = x + self.width / 2
pos = (x, y)
self.text.draw(surface, "%02d" % self.seconds, pos, True)
剩余的秒数绘制在进度条的顶部。
def reset(self):
self.finished = False
self.seconds = self.maxSeconds
每次显示问题时重置倒计时。
def update(self, deltaTime):
if self.seconds == 0:
return
self.seconds = self.seconds - deltaTime
if self.seconds < 0:
self.seconds = 0
self.finished = True
progressWidth = self.width * (self.seconds / self.maxSeconds)
self.rect = Rect(self.pos, (progressWidth, self.height))
通过减少“self.seconds”中的当前秒数来更新倒计时。如果秒数达到 0,那么我们不更新。如果定时器到达零,则‘self . finished’被设置为真。最后,为“draw()”方法计算并存储进度条内部的当前宽度。
保存文件。
游戏运行程序和基本状态类
游戏运行者是一个非常基本的框架类,它允许游戏在不同的状态之间转换。要创建编程接口,需要创建一个基本状态类。这也将用作所有其他状态类的基础。
“NullState”类将为游戏的 FSM 中的其他状态提供基础。“游戏玩家”类将
-
初始化 PyGame
-
更新当前状态
-
绘制当前状态
游戏更新方法也将在各种状态之间转换。稍后我们将为程序编写一个主入口点,它将创建一个“GameRunner”类的实例。
创建一个名为“gamerunner.py”的新文件,并输入以下内容:
import pygame
from pygame.locals import *
PyGame 的进口。
class NullState(object):
def update(self, deltaTime):
return None
def draw(self, surface):
pass
def onEnter(self):
pass
def onExit(self):
pass
' NullState '类是游戏中其他状态的基础。它包含四种方法,用于
-
更新
-
画
-
通知状态正在进入
-
通知该州它正在被转移出去
class GameRunner(object):
def __init__(self, dimensions, title, backColour, initialState):
self.state = initialState
self.clock = pygame.time.Clock()
self.backColour = backColour
self.surface = pygame.display.set_mode(dimensions)
pygame.display.set_caption(title)
初始化 PyGame 并创建一个时钟。这将创建显示并设置窗口的标题。
def update(self):
deltaTime = self.clock.tick(30) / 1000.0
if self.state != None:
self.state = self.state.update(deltaTime)
return self.state
计算从上次运行该方法到现在的时间,并存储在“deltaTime”中时间是以毫秒为单位的,所以为了把它变成秒,我们要除以 1000。调用当前状态的“update()”方法。状态的“update()”方法返回要转换到的下一个状态。当前状态被返回给调用者。调用者将是我们后面要写的主程序。
def draw(self):
self.surface.fill(self.backColour)
if self.state != None:
self.state.draw(self.surface)
pygame.display.update()
这将清除主表面,并获取当前状态以在顶部绘制自己,然后更新显示。
保存文件。
玩家输入
如果没有玩家的参与,我们将会制作电影!对于这个游戏,玩家的输入是使用“PlayerController”类捕获的。这个类也包含玩家当前的分数。创建一个名为“playercontroller.py”的新文件,并输入以下文本:
from gpiozero import Button
gpiozero 库的导入。
class PlayerController(object):
def __init__(self, pins):
self.buttons = []
self.score = 0
for pin in pins:
self.buttons.append(Button(pin))
“PlayerController”类的构造函数。请注意,它从传递给它的“pin”列表中创建了一个按钮列表。
def anyButton(self):
for button in self.buttons:
if button.is_pressed:
return True
return False
方法来确定是否按下了任何按钮。
def playerChoice(self):
index = 0
for button in self.buttons:
if button.is_pressed:
return index
index = index + 1
return -1
方法来确定玩家的答案选择。如果玩家没有选择,这个方法返回–1,或者玩家按下的按钮的“self.buttons”列表中的索引。
州级
将为游戏中的状态创建以下类:
-
选择一个问题
-
标题文本屏幕
-
显示问题
-
ShowScore
每个游戏状态都是重入。这意味着在程序执行期间,状态可以运行任意次。当通过调用“onEnter()”方法进入每个状态时,以及当通过调用“onExit()”方法不再是当前状态时,每个状态都会被告知。
当您创建自己的状态时,应该在“onEnter()”方法中执行状态的设置代码,并且应该在“onExit()”方法中执行拆卸(清理)操作。
分离阶级和国家
有限状态机(FSM)的状态是一个类的实例。没有必要创建多个执行相同或相似操作的类,因为它们代表不同的状态。在这个游戏中有两种使用相同职业的方法:
-
header text Screen–由“准备就绪”和“闪屏”状态使用
-
Show Score–由“显示分数”和“游戏结束”状态使用
当我们创建主文件时,将再次讨论这个主题。
维护游戏状态
游戏的当前状态分为两部分:当前正在执行的动作和动作正在处理的数据。数据存储在当前问题和每个玩家的控制器中。我们已经为玩家设置了单独的职业(“玩家控制器”),但是我们需要为当前的问题设置一个职业。创建名为“currentquestion.py”的新文件。这个文件中有一个当前显示问题的类定义。该信息将由“选择问题”状态改变,并由“显示问题”状态显示。
应该注意的是,正如我们将在后面看到的,其他状态不需要知道当前问题,因此没有给出该数据。
在“currentquestion.py”中输入以下代码:
class CurrentQuestion(object):
def __init__(self):
self.question = ""
self.answer = ""
self.answerIndex = -1
self.answers = []
仅此而已;只是当前问题的信息。保存文件。
选择问题类
“选择问题”状态选择创建一个名为“选择问题. py”的新文件。这个类将用于从问题列表中选择当前的问题。
from gamerunner import NullState
“ChooseQuestion”类扩展了“NullState ”,因此我们必须将“NullState”导入到该文件中。
class ChooseQuestion(NullState):
def __init__(self, nextState, gameOverState, currentQuestion, questions):
self.questions = questions
self.nextState = nextState
self.gameOverState = gameOverState
self.current = -1
self.currentQuestion = currentQuestion
构造函数接受四个参数。第一个是默认的游戏状态,如果有另一个问题要转换到这个状态。正如我们从表 23-1 中看到的,这通常是“展示问题”状态。但是,如果达到“游戏结束”条件,游戏将转换到“游戏结束”状态。
“当前问题”是在维护游戏状态中谈到的游戏状态的实例。最后一个参数是从包含问题的 JSON 文件中加载的“问题”实例列表。
def update(self, deltaTime):
self.current = self.current + 1
if self.current == len(self.questions):
self.currentQuestion.question = "
self.currentQuestion.answer = "
self.currentQuestion.answerIndex = -1
self.currentQuestion.answers = []
return self.gameOverState
else:
question = self.questions[self.current]
self.currentQuestion.question = question.question
self.currentQuestion.answer = question.answer
self.currentQuestion.answers = question.answers
self.currentQuestion.answerIndex = question.answerIndex
return self.nextState
索引“self.current”递增。如果该值等于“self.questions”的长度,则游戏结束。否则,设置当前问题的数据并返回“nextState”。
“ChooseQuestion”类没有“draw()”方法,因此我们不需要在这里为它添加重写方法;“NullState”已经提供了一个基本的“draw()”方法。保存文件。
HeaderTextScreen 类
“闪屏”和“准备就绪”状态都使用 HeaderTextScreen 向玩家显示信息文本。在闪屏的情况下,游戏的名称与“按任意按钮”一起显示以继续。使用“准备好”,显示文本“准备好”。这两种状态的区别在于闪屏需要玩家输入,而“准备好”实例在设定的持续时间后会自动转换到下一个状态。
创建一个名为“headertextscreen.py”的新文件,并输入以下文本:
from ui import *
from playercontroller import *
from gamerunner import NullState
必需的进口。
class HeaderTextScreen(NullState):
def __init__(self, nextState, player1, player2, waitTime = 0):
self.nextState = nextState
self.player1 = player1
self.player2 = player2
self.big = Text(128, (255, 192, 0))
self.small = Text(36, (255, 255, 255))
self.waitTime = waitTime
self.currentTime = 0
self.header = ""
self.subHeader = ""
构造函数接受四个参数:下一个状态、玩家控制器和等待时间。如果等待时间为零,则假设需要一些玩家交互,也就是说,其中一个玩家必须按下按钮才能移动到下一个状态。
def setHeader(self, header):
self.header = header
设置标题文本。
def setSub(self, subHeader):
self.subHeader = subHeader
设置副标题文本。
def setNextState(self, nextState):
self.nextState = nextState
设置下一个状态。
def update(self, deltaTime):
if self.waitTime > 0:
self.currentTime = self.currentTime + deltaTime
if self.currentTime >= self.waitTime:
return self.nextState
elif self.player1.anyButton() or self.player2.anyButton():
return self.nextState
return self
这将执行状态转换。如果“self.waitTime”大于零,则它是自动倒计时版本,否则它是用户控制的状态版本。
def draw(self, surface):
self.big.draw(surface, self.header, (400, 200), True)
self.small.draw(surface, self.subHeader, (400, 300), True)
保存文件。
ShowQuestion 类
“显示问题”状态显示当前问题、答案和倒计时。当倒计时到达 0(从 30 秒开始)或两个玩家都做出选择时,状态转换到下一个状态。国家利用“游戏者控制器”;每个玩家和“当前问题”实例各一个。
创建名为“showquestion.py”的新文件,并输入以下文本:
from gamerunner import NullState
from ui import Text, QuestionText, Countdown
正在为“NullState”类导入“gamerunner”文件。该类使用“ui”中的“Text”、“Countdown”和“QuestionText”类
class ShowQuestion(NullState):
def __init__(self, nextState, currentQuestion, player1, player2):
self.nextState = nextState
self.player1 = player1
self.player2 = player2
self.player1Choice = -1
self.player2Choice = -1
self.currentQuestion = currentQuestion
self.showAnswer = False
self.endCount = 3
self.questionText = QuestionText()
text = Text(32, (255, 255, 255))
self.countdown = Countdown(30, (80, 560), 640, 32, (128, 0, 0), (255, 0, 0), text)
ShowQuestion 的构造函数有四个参数:要转换到的下一个状态、当前的问题实例和两个从它们那里获取输入的播放器控制器。
def calcScore(self):
if self.player1Choice == self.currentQuestion.answerIndex:
self.player1.score = self.player1.score + 1
if self.player2Choice == self.currentQuestion.answerIndex:
self.player2.score = self.player2.score + 1
计算玩家分数的辅助函数。
def update(self, deltaTime):
if self.player1Choice == -1:
p1 = self.player1.playerChoice()
if p1 >= 0:
self.player1Choice = p1
if self.player2Choice == -1:
p2 = self.player2.playerChoice()
if p2 >= 0:
self.player2Choice = p2
if self.player1Choice >= 0 and self.player2Choice >= 0:
self.showAnswer = True
if not self.showAnswer:
self.countdown.update(deltaTime)
if self.countdown.finished:
self.showAnswer = True
else:
self.endCount = self.endCount - deltaTime
if self.endCount <= 0:
self.calcScore()
return self.nextState
return self
如果“self.showAnswer”为 False,则 update 方法会启动倒计时计时器。当倒数计时器到达零或者两个玩家都做出选择时,“self.showAnswer”被设置为真。一旦玩家选择了答案,他们就不能更改。
def draw(self, surface):
self.questionText.draw(surface, self.currentQuestion.question, self.currentQuestion.answer, self.currentQuestion.answers, self.showAnswer)
if not self.showAnswer:
self.countdown.draw(surface)
绘制问题和答案,将“self.showAnswer”字段值传递给 questionText 的“Draw()”方法以突出显示正确的答案。如果倒计时激活,显示出来。
def onExit(self):
self.endCount = 3
self.showAnswer = False
self.countdown.reset()
退出时清除当前状态。
def onEnter(self):
self.player1Choice = -1
self.player2Choice = -1
在进入状态时设置玩家数据。
保存文件。
ShowScore 类
“显示分数”和“游戏结束”状态都属于这个类。在每个问题之间,会显示玩家的分数。显示“游戏结束”屏幕时,会显示得分和“赢家”或“平局”。“赢家”标签显示在赢得游戏的玩家下方。
对于这个文件,我创建了一个简单的测试存根来验证屏幕上文本的位置。
创建一个名为“showscore.py”的新文件,并输入以下文本:
#!/usr/bin/python3
import pygame
from pygame.locals import *
from gamerunner import NullState
from ui import Text
“ShowScore”类所需的导入。
class ShowScore(NullState):
def __init__(self, nextState, player1, player2, showWinner = False):
self.nextState = nextState
self.player1 = player1
self.player2 = player2
self.counter = 3
self.showWinner = showWinner
self.scoreText = Text(300, (255, 255, 0))
self.playerText = Text(128, (255, 255, 255))
“ShowScore”构造函数有四个参数。第一个是要转换到的下一个状态,接下来是第一个和第二个玩家的控制器。这些是“PlayerController”类的“score”字段所必需的。最后,“showWinner”参数用于显示“赢家”或“平局”,这取决于当所有问题都被问完时游戏的结束状态。
def update(self, deltaTime):
self.counter = self.counter - deltaTime
if self.counter <= 0:
return self.nextState
return self
分数屏幕仅在特定时间内显示。一旦该时间到期,状态转换到下一个。
def draw(self, surface):
self.playerText.draw(surface, "Player 1", (200, 85), True)
self.playerText.draw(surface, "Player 2", (600, 85), True)
self.scoreText.draw(surface, str(self.player1.score), (200, 150), True)
self.scoreText.draw(surface, str(self.player2.score), (600, 150), True)
if self.showWinner:
winner = "WINNER!"
pos = 200
if self.player1.score == self.player2.score:
winner = "TIE!"
pos = 400
elif self.player2.score > self.player1.score:
pos = 600
self.playerText.draw(surface, winner, (pos, 400), True)
画屏幕。
def onEnter(self):
self.counter = 3
进入该状态时,将当前计数器设置为 3 秒。
if __name__ == '__main__':
import sys
class P(object):
def __init__(self, s):
self.score = s
pygame.init()
fpsClock = pygame.time.Clock()
surface = pygame.display.set_mode((800, 600))
score = ShowScore(None, P(55), P(10), True)
background = (0, 0, 0) # Black
while True:
surface.fill(background)
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
deltaTime = fpsClock.tick(30) / 1000.0
score.draw(surface)
pygame.display.update()
测试存根。这将显示“游戏结束”状态。保存并运行文件以查看。如果您想看到“显示分数”屏幕,请更改以下行:
score = ShowScore(None, P(55), P(10), True)
到
score = ShowScore(None, P(55), P(10))
主文件
主文件实际上只有几行代码,其中大部分是设置有限状态机。创建一个名为“quiz.py”的新文件,并输入以下文本:
#!/usr/bin/python3
import pygame
from gamerunner import GameRunner
from questions import *
from headertextscreen import HeaderTextScreen
from choosequestion import ChooseQuestion
from playercontroller import PlayerController
from showquestion import ShowQuestion
from showscore import ShowScore
from currentquestion import CurrentQuestion
程序的所有导入。
pygame.init()
player1 = PlayerController([4, 17, 22])
player2 = PlayerController([5, 6, 13])
currentQuestion = CurrentQuestion()
初始化 PyGame 并设置存储在“PlayerController”实例和“CurrentQuestion”实例中的游戏状态数据。
questions = loadQuestions("questions.json")
从 JSON 文件中加载问题。
showQuestion = ShowQuestion(None, currentQuestion, player1, player2)
gameOver = ShowScore(None, player1, player2, True)
chooseQuestion = ChooseQuestion(showQuestion, gameOver, currentQuestion, questions)
showScore = ShowScore(chooseQuestion, player1, player2)
showQuestion.nextState = showScore
“ShowQuestion”、“ShowScore”和“ChooseQuestion”类用于构建游戏中使用的一些状态。由于状态的创建,无法为“ShowQuestion”设置初始状态,而是手动设置了“showQuestion”实例的“nextState”,并且没有将任何状态传递给“ShowQuestion”的构造函数
interstitial = HeaderTextScreen(chooseQuestion, player1, player2, 3)
interstitial.setHeader("Get Ready!")
interstitial.setSub("")
splashScreen = HeaderTextScreen(interstitial, player1, player2)
splashScreen.setHeader("QUIZ!")
splashScreen.setSub("Press any button to start")
“准备好!”的间隙(游戏间隙)屏幕还有闪屏。注意,我们没有为闪屏和“准备好!”创建单独的类,它只使用了两个独立的“HeaderTextScreen”实例
当我们从一种状态转换到另一种状态时,我们从一个类的一个实例转换到另一个。所以没有必要为每个状态编写完全独立的类。
game = GameRunner((800, 600), "Quiz", (0, 0, 0), splashScreen)
game runner 的实例被设置为 800×600 大小的窗口,背景为黑色(0,0,0),初始状态为闪屏实例“splash screen”
lastState = None
while game.state != None:
nextState = game.update()
if nextState != lastState:
if game.state != None:
game.state.onExit()
if nextState != None:
nextState.onEnter()
lastState = nextState
game.draw()
pygame.quit()
主程序循环包括调用游戏的“update()”和“draw()”方法。可以认为这个循环应该放在“GameRunner”的“run()”方法中,我的意思是它在名字中。我将把它作为读者的一个练习;在运行循环的“GameRunner”上创建一个名为“run()”的方法。
保存文件。
玩游戏
玩这个游戏你需要一个对手;这毕竟是一个基于沙发的问答游戏。请坐在沙发上,运行“quiz.py”文件。您需要为文件设置执行位:
$ chmod +x quiz.py
然后运行它:
$ ./quiz.py
一旦游戏开始,你们中的一个人按下试验板上的按钮开始测验。试着回答每个出现的问题。如果你在 30 秒内没有回答,你将失去这一分。获胜者是比赛结束时得分最多的人。祝你好运!
结论
这是一个有趣的游戏,展示了如何构建基于 PyGame 的游戏,与电子组件进行交互。您可以重写早期项目(如 Brick、Snake 和 Invaders)的输入例程,使用轻触开关代替计算机按键进行输入。
二十四、总结
到目前为止,您应该对 Python 语言以及 PyGame 库有了很好的理解。随着游戏包括在本文中,你应该有一个很好的理解如何创建一个视频游戏。的确,有了一个好主意,你应该有足够的知识来自己制作一个游戏!在本书中,我们介绍了播放器输入、显示图形、播放声音、在屏幕上移动字符,以及以读写 GPIO 引脚的形式输入和输出的替代形式。
除了游戏,我们还看了面向对象编程和一些相关的设计模式,如有限状态机(FSM)和模型视图控制器(MVC)。这些将有助于你构建自己的游戏,如果你想更进一步,可能会在游戏行业发展。
希望到了这个阶段,你应该对 Python 语言本身和 PyGame 有了很好的理解。由于本文中包含了三个游戏(砖块、蛇和入侵者),你已经理解了制作一个视频游戏需要什么。有了这里的一切,你应该有足够的时间来创造你自己的。只需要一个好主意!
现在去哪里?既然你有编程缺陷(原谅双关语),天空是极限。也许你想学 C++和做一些 3D 游戏。
如果你想了解更多关于 Python 的内容,你应该前往 https://docs.python.org/
。PyGame 在 www.pygame.org/wiki/index
也有完整的文档。
即使你没有决定把编程作为一份全职工作,把制作游戏作为一种爱好仍然是一件非常有趣的事情。
可以考虑参加类似 Ludum Dare ( https://ldjam.com
)或者其他在 https://itch.io/jams
上市的游戏 jam。在短时间内,通常是在一个周末,开发一个游戏是非常有趣的。你甚至可以带朋友来帮你。谁知道呢,你甚至可能会创造出下一个“核王座”、“超级肉仔”或“星谷”
我希望你喜欢这本书,无论你选择做什么,我都希望你能从中得到乐趣。
编码快乐!