Pygame-游戏开发入门指南-一-

93 阅读1小时+

Pygame 游戏开发入门指南(一)

原文:Beginning Python games development with PyGame

协议:CC BY-NC-SA 4.0

零、简介

游戏开发是一个编程领域,它同时结合了许多主题:数学、物理、图形、用户界面、人工智能等等。由于这个原因,游戏开发既有挑战性又有回报。在大多数语言中,游戏开发并不推荐给任何编程初学者。然而,使用 Python 这样的友好语言和 Pygame 这样的模块进行游戏开发,可以让任何人轻松进入游戏开发领域。即使你是 Python 编程的新手,或者是经验丰富的开发人员,你也会发现游戏开发是进一步学习编程的好方法。通常情况下,编程不是一种非常视觉化的体验,但游戏改变了这一点。您可以看到您的编程逻辑在以一种通常不可能的方式工作。

这本书是给谁的

这本书是写给任何想创造一个电脑游戏,或者想学习游戏开发背后的技术的人的。尽管 Python 是本书的首选工具,但其中涉及的许多技术同样适用于其他语言。

这本书的结构

使用 Pygame 开始 Python 游戏开发分为 12 章,每一章都建立在前一章的基础上——有几个明显的例外。我已经把它组织好了,这样你可以很快得到结果,并在屏幕上看到一些东西,如果你像我一样不耐烦,你可能会喜欢。几乎所有的列表都是独立运行的小项目,它们都是独立的,并且很有娱乐性。因为实验是最好的学习方法,所以鼓励您使用示例代码并修改它以产生不同的效果。您也可以在自己的项目中使用任何代码——得到我的许可!前两章以相当对话的方式介绍了 Python 语言。如果你用 Python 提示阅读它们,你会发现你可以很快学会这门语言。这两章并不构成一个完整的语言教程,但是足够让你理解书中的 Python 代码并编写自己的代码。偶尔,在本书的其余部分会引入新的语法和语言特性,但是我会在第一次使用它们的地方解释它们。如果你精通 Python,可以直接跳到第三章。

第三章是你对 Pygame 的第一次介绍,涵盖了它的历史和功能。它还解释了设置图形显示和处理事件的基础知识,这些技能对于任何游戏都是必不可少的。您将会非常熟悉本章介绍的代码,因为在本书其余部分的所有示例代码中都会用到它。

第四章直接进入创建视觉效果和用 Pygame 在屏幕上绘图的各种方法。

第五章探讨了游戏程序员用来移动这些图像的技术。你会发现关于基于时间的运动的讨论特别有价值,因为它对于游戏中的任何动画都是必不可少的。

第六章告诉你所有你需要知道的,让你的游戏与几乎任何游戏设备连接。本章中的示例代码将让你用键盘、鼠标和操纵杆移动一个角色。

第七章有点不寻常,因为它比其他章节更独立,不太依赖前面的章节。它涵盖了人工智能的主题,并包括一个完整的工作模拟一个蚂蚁的巢,但我在这一章解释的技术可以用来添加看似智能的角色到任何游戏。

第八章和 9 温和地介绍了在 Pygame 中使用三维图形,这是一个重要的话题,因为现在大多数游戏都有 3D 元素——即使它们不是完整的 3D 游戏。我用直观的术语解释数学,使它更容易掌握,你应该会发现它并不像第一次出现时那样令人生畏。

第十章从 3D 图形中稍作休息,讨论如何使用 Pygame 添加音效和音乐,甚至包括一个完全工作的点唱机应用。

最后两章建立在第八章和第九章的基础上,以提高你的 3D 图形知识,并解释如何利用你的显卡上的专用游戏硬件。到《??》第十一章结束时,你将拥有足够的知识来渲染和操作屏幕上的三维物体。第十二章探讨了几种你可以用来创造更令人印象深刻的 3D 视觉效果和产生特殊效果的技术。

除了 12 章之外,还有 2 个附录:附录 A 是整本书使用的游戏对象库的参考,附录 B 解释了如何打包你的游戏并发送给其他人。

先决条件

要运行本书中的代码,您至少需要 Python 的 3.4 版本和 Pygame 的 1.7.1 版本,您可以分别从www.python.orgwww.pygame.org下载。如果你想运行 3D 样本代码,你还需要 PyOpenGL,你可以从pyopengl.sourceforge.net下载。所有这些都是免费软件,这本书包含了如何安装和开始使用它们的说明。如果你发现你有困难,我们将包括安装这些软件包。

下载代码

读者可以在本书的主页www.apress.com/9781484209714上找到本书的源代码。只需向下滚动大约一半,然后单击源代码/下载选项卡。你也可以提交勘误表,从出版社找到相关的书目。

联系作者

我很乐意回答任何关于这本书的内容和源代码的问题。请随时通过http://pythonprogramming.netHSKinsley@gmail.com联系我。

一、Python 简介

我们将用来创建游戏的编程语言是 Python,它之所以得名是因为该语言的原作者是英国电视连续剧巨蟒剧团的粉丝。虽然我们将使用 Python 来创建游戏,但这种语言是一种通用编程语言,用于数据分析、机器人、创建网站等等。Google、NASA 和 Instagram 等公司和机构都非常依赖 Python。

有很多可供选择的语言可以用来创建游戏,但我选择了 Python,因为它倾向于处理细节,让您(程序员)专注于解决问题。对我们来说,解决问题意味着在屏幕上显示游戏角色,让他们看起来很棒,并让他们与虚拟环境互动。

本章是对 Python 的友好介绍;它将帮助您快速掌握这门语言,以便您能够阅读示例代码并开始编写自己的代码。如果你熟悉 Python,那么可以跳过前两章。如果您对 Python 完全陌生,或者您想参加进修课程,请继续阅读。

要开始使用 Python,你首先需要为你的电脑安装一个 Python 解释器。有适用于 PC、Linux 和 Mac 的版本。我们将在本书中使用 Python 3.4 版本。要获得 Python,请前往http://python.org/downloads

Image 到本书出版时,可能会有更高版本的 Python 问世。Python 2 和 Python 3 之间的差异相当显著,尽管未来的更新预计会很小。请随意下载 Python 的最新版本。

您对 Python 的第一印象

运行 Python 代码的通常方式是将其保存到一个文件中,然后运行它。我们很快就会这样做,但现在我们将在交互模式中使用 Python,这让我们一次输入一行代码,并立即收到反馈。你会发现这是 Python 的优势之一。它是学习语言的一个很好的辅助工具,并且经常被有经验的程序员用于数据分析等主题,因为您可以很容易地改变一行并看到即时输出。

一旦您在系统上安装了 Python,您就可以像运行任何其他程序一样运行它。如果你有 Windows,只需双击图标或在开始菜单中选择它。对于其他有命令行的系统,只需键入"python"就可以在交互模式下启动 Python。如果你同时安装了 Python 2 和 Python 3,你可能需要输入python3,而不仅仅是"python"

当您第一次运行 Python 解释器时,您会看到如下内容:

Python 3.4.2 (v3.4.2:ab2c023a9432, Oct  6 2014, 22:16:31)
 [MSC v.1600 64 bit (AMD64)] on win32
Type "copyright", "credits" or "license()" for more information.
>>>

根据您运行的 Python 版本和平台(Windows、Mac、Linux 等),文本可能会有所不同。)你正在运行它。重要的部分是三个 v 形符号(>>>),这是 Python 的提示——是你输入一些代码的邀请,然后 Python 试图运行这些代码。

计算机语言教程的一个长期传统是,你写的第一个程序显示文本“Hello,World!”在银幕上——我又有什么资格打破传统呢!所以深呼吸,在提示后输入print("Hello, World!")。Python 窗口现在会在提示行中显示以下内容:

>>> print("Hello, World!")

如果您按下 Enter 键,Python 将运行您刚才输入的代码行,如果一切顺利,您将在屏幕上看到以下内容:

>>> print("Hello, World!")
Hello, World!
>>> _

Python 已经执行了您的代码行,显示了结果,并给出了一个新的提示让您输入更多代码。那么我们的代码到底是如何工作的呢?单词print是一个函数,它告诉 Python 将随后的内容打印到屏幕上。在print函数之后是一个字符串,它只是字母和/或数字的集合。Python 将引号(")之间的任何内容视为字符串。尝试在引号之间输入您自己的文本,您应该会发现 Python 会像以前一样将它打印到屏幕上。

数字

我们稍后将回到字符串,但是现在让我们从 Python 可以处理的最简单的信息开始:数字。Python 非常擅长处理数字,你几乎可以像使用计算器一样使用它。要查看它的运行情况,请在 Python 中键入以下内容(您不需要键入提示,因为 Python 会为您显示出来):

>>> 2+2

猜猜 Python 会对这一行做什么,然后按回车键。如果你猜对了 4,那就吃块饼干吧——这正是它的作用。Python 对求值 2+2,这在 Python 术语中称为表达式,并显示结果。你也可以用做减法,*做乘法,/做除法。这些符号被称为运算符。你可能会用得最多的是+*/。以下是一些例子:

>>> 105
5
>>> 2*4
8
>>> 6/2+1
4
>>> 2+7
5

在现实世界中,只有一种数字,但是计算机——以及随之而来的 Python——有几种表示数字的方式。两种最常用的数字类型是整数 ?? 和浮点数 ??。整数是没有小数点的整数,而浮点有小数点,可以存储小数值。通常你应该使用哪一个是显而易见的——例如,如果你的游戏有生命的概念,你会使用一个整数来存储它们,因为你不太可能有半个生命或 3.673 个生命。浮点值更常用于需要精度的真实世界的值,例如,在一个赛车游戏中,您的汽车可能有每小时 92.4302 英里的速度,您可以将它存储在一个浮点中。

到目前为止,您输入的数字都是整数。要告诉 Python 一个数字是浮点数,只需包含一个小数点。比如 5 和 10 都是整数,但是 5。和 10.0 都是浮点数。

>>> 3/2
1.5
>>> 3.0/2.
1.5
>>> 3./2.
1.5

除了基本的数学之外,你还可以用数字做很多其他的事情。括号是用来保证某个东西先计算;这里有一个例子:

>>> 3/2+1
2.5
>>> 3/(2+1)
1.0

第一行先计算 3 除以 2,然后加 1,结果是 2.5。第二行首先计算 2 加 1,因此结果为 3 除以 3,即 1。

您可以使用的另一个运算符是运算符,它将值提升到幂。例如,2 的 3 次方等于 222。幂运算符是**,作用于整数和浮点数。这里有两个幂操作符的例子:

>>> 2**3
8
>>> 3.0**4
81.0

Python 3 以一种非常可预期的方式处理计算和数字信息,不像它的前身或许多其他语言。许多语言的范围从-2147 百万到 2147 百万,给你一个超过 40 亿的范围。然而,Python 3 并不是这样有界的。

让我们通过计算 2 的 100 次方来创建一个大数,也就是 2×2×2×2……×2 重复 100 次。

>>> 2**100
1267650600228229401496703205376

这是一个很大的数字!如果你感到勇敢,试着计算2**1000甚至2**10000,然后看着你的屏幕被巨大的数字填满。

下一节之前再给大家介绍一个运营商吧。模数 ( %)运算符计算除法的余数。例如,15 的模数 6 是 3,因为 6 除以 15 的两倍,还剩下 3。让我们请 Python 来帮我们做这件事:

>>> 15%6
3

有了这几个运算符,你现在可以计算任何可以计算的东西,无论是两盘河豚的 15%小费,还是一个兽人用+1 斧头击中盔甲造成的伤害。

我不太了解兽人,但让我们计算一下两碟河豚(生河豚,一种日本美食,我希望有一天能尝试一下)的小费。河豚很贵,任何东西都要 200 美元,因为如果不是由受过专门训练的厨师烹制的,吃了它会致命!假设我们在东京找到一家餐馆,供应一盘诱人的河豚,价格为 100 美元。我们可以用 Python 来为我们计算小费:

>>> (100*2)*15/100
30.0

这相当于两个 100 美元盘子价格的 15%——30 美元的小费。对这家餐厅来说足够好了,但是数字会根据我们购买河豚的地点和服务质量而变化。我们可以通过使用变量来使其更加清晰和灵活。变量是一个值的标签,当你创建变量时,你可以用它来代替数字本身。在我们的小费计算中,我们有三个变量:河豚的价格、盘子的数量和小费的百分比。要创建变量,请键入其名称,后跟等号(=),然后键入您要赋予它的值:

>>> price = 100
>>> plates = 2
>>> tip = 15

等号(=)被称为赋值运算符。

Image 注意 Python 变量是区分大小写的,这意味着如果变量名大写不同,Python 会将其视为完全唯一的——这意味着ApplesAPPLESApPlEs被视为三个不同的变量。

我们现在可以用这三个变量代替数字。让我们再计算一下我们的小费:

>>> (price*plates)*(tip/100)
30.0

这也计算了相同的值,但是现在更清楚了一点,因为我们一眼就能看出这些数字代表什么。它也更加灵活,因为我们可以改变变量,重新计算。假设第二天早上我们吃河豚,但在一家便宜的餐馆(75 美元一盘),那里的服务不太好,只值 5%的小费:

>>> price = 75
>>> tip = 5
>>> (price*plates)*(tip/100.)
7.5

这是 7.50 美元的小费,因为服务员很慢才送来清酒,我讨厌为了我的清酒而等待。

用线串

Python 可以存储的另一条信息是字符串。字符串是字符的集合(一个字符是字母、数字、符号等。)并且可以用来存储几乎任何种类的信息。一个字符串可以包含一个图像、一个声音文件,甚至一个视频,但是字符串最常见的用途是存储文本。要在 Python 中输入字符串,请用单引号(')或双引号(")将其括起来。这里有两根弦;两者包含完全相同的信息:

"Hello"
'Hello'

那么,为什么创建字符串的方式不止一种呢?好问题;假设我们想存储这个句子:

我对巫师说了“巫术”。

如果我们将整个句子放在一个带双引号的字符串中,Python 无法知道您想要在单词 wizard 之后结束字符串,并将假定字符串在 said 之后的空格处结束。让我们试一试,看看会发生什么:

>>> print("I said "hocus pocus" to the wizard.")
Traceback ( File "<interactive input>", line 1
     print("I said "hocus pocus" to the wizard.")
                          ^
SyntaxError: invalid syntax

Python 已经抛出了一个异常。本书后面会有更多关于异常的内容,但是现在如果你看到这样的异常,Python 会告诉你你输入的代码有问题。我们可以通过使用替代引号符号来解决在字符串中包含引号的问题。让我们来试试同一个句子,但这次用单引号(')括起来:

>>> print('I said "hocus pocus" to the wizard.')
I said "hocus pocus" to the wizard.

Python 对此相当满意,这次没有抛出异常。这可能是解决报价问题的最简单的方法,但是还有其他选择。如果您在引号前键入反斜杠字符(),它会告诉 Python 您不想在此结束字符串-您只想在字符串中包含引号符号。在 Python 中,反斜杠字符被称为“转义字符”。下面是一个例子:

>>> print("I said \"hocus pocus\" to the wizard.")
I said "hocus pocus" to the wizard.

这样解决问题的方式不同,但结果是一样的。尽管信息太多可能会增加您的负担,但是还有一种定义字符串的方法:如果您以三重单引号(''')或三重双引号(""")开始一个字符串,Python 知道直到它到达另一组相同类型的三重引号时才结束该字符串。这很有用,因为文本很少在行中包含三个引号。这是我们的向导字符串,使用了三重引号:

>>> print("""I said "hocus pocus" to the wizard.""")
I said "hocus pocus" to the wizard.

串联字符串

现在你有几种创建字符串的方法,但是你能用它们做什么呢?就像数字一样,字符串也有可以用来创建新字符串的运算符。如果将两个字符串相加,将得到一个新字符串,其中包含第一个字符串,第二个字符串附加在末尾。您可以使用+操作符添加字符串,就像处理数字一样;我们来试试:

>>> "I love "+"Python!"
'I love Python!'

Python 将两个字符串加在一起并显示了结果。像这样把字符串加在一起叫做字符串串联 。您可以连接任意两个字符串,但不能将一个字符串与一个数字连接起来。让我们试试看会发生什么:

>>> "high "+5
Traceback (most recent call last):
  File "<interactive input>", line 1, in ?
TypeError: cannot concatenate 'str' and 'int' objects

在这里,我们试图通过将数字 5 加到一个字符串上来生成字符串'high 5'。这对 Python 来说没有意义,它通过抛出另一个异常让你知道。如果你想把一个数字加到一个字符串上,你必须先把这个数字转换成一个字符串。你可以通过从数字中构造一个新的字符串来轻松地从数字中创建字符串。下面是如何创建我们的high 5字符串。

>>> "high "+str(5)
'high 5'

这是因为str(5)从数字 5 构造了一个字符串,Python 很乐意将这个字符串与另一个字符串连接起来。

也可以对字符串使用乘法(*)运算符,但只能将字符串乘以整数。猜猜下面一行 Python 代码会做什么:

>>> 'eek! '*10

你可以看到 Python 可以相当直观;如果你把一个字符串乘以 10,它会重复 10 次。字符串并不支持所有的数学运算符,比如/,因为它们会做什么并不直观。"apples"–"oranges"可能意味着什么?

解析字符串

因为字符串可以被看作是字符的集合,所以能够引用它的一部分而不是整体通常是有用的。 Python 用索引操作符来做这件事,它由方括号[]组成,包含字符的偏移量。第一个角色是[0],第二个是[1],第三个是[2],以此类推。从 0 开始而不是从 1 开始可能看起来有点奇怪,但这是计算机语言中的一个传统,当你编写更多的 Python 代码时,你会发现它实际上简化了事情。让我们看看字符串索引的实际应用。首先,我们将创建一个包含字符串的变量,就像处理数字一样:

>>> my_string = 'fugu-sashi'
>>> print(my_string)
'fugu-sashi'

通常你会给字符串取一个更好的名字,但是对于这个小例子,我们就称它为my_string(在mystring之间的下划线字符用来代替空格,因为 Python 不允许变量名中有空格)。我们可以用索引运算符从字符串中挑选出单个字母:

>>> my_string[0]
'f'
>>> my_string[3]
'u'

my_string[0]给你一个字符串,第一个字符在fugu-sashi里,是f。第二行给出了第四个字符*,因为第一个字符的偏移量是 0 而不是 1。尽量不要把偏移量看作字符本身的数字,而是看作字符之间的空格(见图 1-1);这将使索引更加直观。*

9781484209714_Fig01-01.jpg

图 1-1 。字符串索引

假设我们想找到一个字符串中的最后一个字符。从图 1-1 可以看到,最后一个字符是偏移 9 的“I”,但是如果我们事先不知道字符串呢?我们可以从一个文件中提取字符串,或者玩家可以在一个高分表中键入它。为了找到最后的偏移量,我们首先需要找到字符串的长度,这可以用len 函数来完成。把函数想象成存储的 Python 代码;您向该函数传递一些信息,它使用这些信息来执行一个操作,然后返回,可能带有新的信息。这正是len所做的;我们给它一个字符串,它返回这个字符串的长度。让我们试试my_string上的len功能:

>>> len(my_string)
10

my_string中有 10 个字符,但是我们不能用 10 作为偏移量,因为它正好在字符串的末尾。为了得到结束字符,我们需要 10 之前的偏移量,简单来说就是 9,所以我们减去 1。下面是如何使用len来查找字符串中的最后一个字符:

>>> my_string[len(my_string)-1]
'i'

很简单,我希望你会同意!但是 Python 可以通过使用负索引 让我们变得更容易。如果你用负数做索引,Python 会把它当作从字符串末尾的偏移量,所以[-1]是最后一个字符,[-2]是倒数第二个字符,依此类推(见图 1-2 )。

9781484209714_Fig01-02.jpg

图 1-2 。负索引

我们现在可以用更少的代码找到最后一个字符:

>>> my_string[-1]
'i'

分割字符串

除了提取字符串中的单个字符,你还可以通过分割字符串来挑选出一组字符。切片的工作方式很像索引,但是您使用了两个由冒号(:)分隔的偏移量。第一个偏移量是 Python 应该开始切片的地方;第二个偏移量是应该停止切片的地方。同样,将偏移量视为字符之间的空格,而不是字符本身。

>>> my_string[2:4]
'gu'
>>> my_string[5:10]
'sashi'

第一行告诉 Python 在偏移量 2 和 4 之间切片。从图中可以看出,这些偏移量之间有两个字符:gu。Python 将它们作为单个字符串返回,'gu'。第二行在偏移量 5 和 10 之间分割字符串,并返回字符串'sashi'。如果省略第一个偏移量,Python 会使用字符串的开头;如果省略第二个,它使用字符串的结尾。

>>> my_string[:4]
'fugu'
>>> my_string[5:]
'sashi'

切片可以再取一个值,用作步骤的值。如果步长值为 1 或者您没有提供它,Python 将简单地返回前两个偏移之间的切片。如果您使用步长值 2 进行切片,那么将返回包含原始字符串中每隔一个字符的字符串。步长 3 将返回每第三个字符,依此类推。以下是这种切片的一些示例:

>>> my_string[::2]
'fg-ah'
>>> my_string[1::3]
'u-s'

第一行从字符串的开头到结尾分段(因为省略了前两个偏移量),但是因为步长值是 2,所以它每隔一个字符进行一次分段。第二行从偏移量 1(在u处)开始,切片到末尾,每三个字符取一个。切片中的步长值也可以是负值,这有一个有趣的效果。当 Python 看到一个负步长时,它会颠倒切片的顺序,从第二个偏移到第一个偏移。您可以使用此功能轻松反转字符串:

>>> my_string[::-1]
'ihsas-uguf'
>>> my_string[::-2]
'issuu'

第一行只是返回一个字符顺序相反的字符串。因为步长值是负的,所以它从字符串的结尾到开头。

字符串方法

除了这些操作符,字符串还有许多方法,它们是包含在 Python 对象中的的函数,并对它们执行一些操作。Python 字符串包含许多有用的方法来帮助您处理字符串。下面是其中的一些,适用于我们的赋格弦乐:

>>> my_string.upper()
'FUGU-SASHI'
>>> my_string.capitalize()
'Fugu-sashi'
>>> my_string.title()
'Fugu-Sashi'

这里我们对一个字符串应用不同的方法。每一个都返回一个以某种方式修改过的新字符串。我们可以看到,upper返回一个所有字母都转换为大写的字符串,capitalize返回一个第一个字符转换为大写的新字符串,title返回一个每个单词的第一个字符转换为大写的新字符串。这些方法不需要任何其他信息,但是括号仍然是告诉 Python 调用函数所必需的。

Image 注意 Python 字符串是不可变的,也就是说一个字符串一旦创建就不能修改,但是可以从中创建新的字符串。实际上你很少会注意到这一点,因为创建新的字符串是如此容易(这就是我们一直在做的事情)!

列表和元组

像大多数语言一样,Python 有存储对象组的方法,这很幸运,因为只有一个外星人、一颗子弹或一种武器的游戏会很无聊!存储其他对象的 Python 对象被称为集合,最简单和最常用的集合之一是 list。让我们首先创建一个空列表:

>>> my_list=[]

方括号创建了一个空列表,然后将其分配给变量my_list。要向列表中添加内容,可以使用append方法,该方法将任何 Python 对象添加到列表的末尾。让我们假设我们的列表将包含我们一周的购物,并添加几个项目:

>>> my_list.append('chopsticks')
>>> my_list.append('soy sauce')

在这里,我们向my_list添加了两个字符串,但是我们可以很容易地添加 Python 的任何其他对象,包括其他列表。如果您现在在 Python 提示符下键入my_list,它将为您显示它的内容:

>>> my_list
['chopsticks', 'soy sauce']

在这里,我们可以看到这两个字符串现在存储在列表中。我们不能只靠筷子和酱油生活,所以让我们在购物清单上增加几样东西:

>>> my_list.append('wasabi')
>>> my_list.append('fugu')
>>> my_list.append('sake')
>>> my_list.append('apple pie')
>>> my_list
['chopsticks', 'soy sauce', 'wasabi', 'fugu', 'sake', 'apple pie']

修改列表项

Python 列表是可变的,这意味着你可以在它们被创建后改变它们。因此,除了使用 index 操作符检索列表内容之外,您还可以通过向它分配一个新项来更改任何索引处的项。假设我们特别想得到酱油;我们可以通过赋值操作符(= ): 为第二项赋值来改变它

>>> my_list[1]='dark soy sauce'
>>> my_list
['chopsticks', 'dark soy sauce', 'wasabi', 'fugu', 'sake', 'apple pie']

移除列表项目

除了更改列表中的项目,您还可以从列表中删除项目。假设我们想删除apple pie,因为它似乎不适合我们购物清单的其余部分。我们可以使用del操作符来完成这项工作,它将从我们的列表中删除任何一项——在本例中,它是最后一项,因此我们将使用负索引:

>>> del my_list[-1]
>>> my_list
['chopsticks', 'dark soy sauce', 'wasabi', 'fugu', 'sake']

列表支持许多操作符,它们的工作方式与字符串相似。让我们看一下切片和索引,您应该会觉得非常熟悉:

>>> my_list[2]
'wasabi'
>>> my_list[-1]
'sake'

第一行返回偏移量为 2 的字符串,这是我们购物列表中的第三个位置。就像字符串一样,列表中的第一项总是 0。第二行使用负索引,就像 strings [-1]返回最后一项。

切片列表的工作方式类似于切片字符串,除了它们返回一个新的列表而不是一个字符串。让我们把购物清单分成两部分:

>>> my_list[:2]
['chopsticks', 'dark soy sauce']
>>> my_list[2:]
['wasabi', 'fugu', 'sake']
>>>

在第一个片段中,我们要求 Python 给出从列表开始到偏移量 2 的所有项目;在第二个片段中,我们请求了从偏移量 2 到列表末尾的所有内容。列表偏移量的工作方式就像字符串偏移量一样,所以尽量把它们看作列表中对象之间的空格,而不是对象本身。因此,偏移量 0 在第一项之前,偏移量 1 在第一项之后第二项之前。

您也可以使用+操作符添加列表。当您一起添加列表时,它会创建一个包含两个列表中的项目的列表。让我们创建一个新列表,并将其添加到我们的购物清单中:

>>> my_list2 = ['ramen', 'shiitake mushrooms']
>>> my_list += my_list2
>>> my_list
['chopsticks', 'dark soy sauce', 'wasabi', 'fugu', 'sake', 'ramen', 
`'shiitake mushrooms']`

`第一行创建了一个名为 my_list2 的新字符串列表。我们创建的第二个列表与第一个略有不同;我们没有创建一个空白列表并一次添加一个条目,而是创建了一个已经有两个条目的列表。第二行使用+=操作符,这是一种很有用的简写:my_list+=my_list2 与 my_list=my_list+my_list2 相同,其效果是将两个列表相加,并将结果存储回 my_list 中。

列出方法

除了这些操作符,列表还支持许多方法。让我们使用sort方法将我们的购物清单按字母顺序排序:

>>> my_list.sort()
>>> my_list
['chopsticks', 'dark soy sauce', 'fugu', 'ramen', 'sake', 
`'shiitake mushrooms', 'wasabi']`

`方法对列表的内容进行排序。顺序取决于列表的内容,但对于字符串列表,排序是按字母顺序进行的。

你会注意到 Python 在调用sort后没有打印任何东西;这是因为sort不返回一个排序列表,而只是对它被调用的列表进行排序。第二行是要求 Python 显示我们的列表内容所必需的。

假设我们要去购物,我们想从清单上拿走一件商品,然后去超市找。我们可以用pop方法做到这一点,该方法从列表中删除一个条目并返回它:

>>> my_list.pop(0)
'chopsticks'

我们已经要求my_list在偏移量 0 处“弹出”项目,这是chopsticks。如果我们现在显示购物清单的内容,我们应该看到第一项确实已经被删除:

>>> my_list
['fugu', 'ramen', 'sake', 'shiitake mushrooms', 'soy sauce', 'wasabi']

列表方法比我们在这里介绍的要多;详见表 1-1 。

表 1-1 。Python 列表方法

|

方法方法

|

描述

| | --- | --- | | append | 将项目追加到列表中 | | count | 统计项目在列表中出现的次数 | | extend | 添加另一个集合中的项目 | | index | 查找字符串的偏移量 | | insert | 将项目插入列表 | | pop | 从列表中移除一个偏移量处的项并返回它 | | remove | 从列表中移除特定项目 | | reverse | 反转列表 | | sort | 对列表排序 |

元组

我们将在本节介绍的另一个集合是元组。元组类似于列表,除了它们是不可变的;也就是说,就像字符串一样,一旦创建,内容就不能更改。当元组包含的信息以某种方式联系在一起时,通常优先于列表使用元组,例如,元组可以表示电话号码和区号,因为这两部分都需要拨号。它们的创建方式与列表类似,但是使用圆括号(),而不是方括号。让我们创建一个存储我们最喜欢的寿司外卖店的电话号码的元组:

>>> my_tuple=('555', 'EATFUGU')
>>> my_tuple
('555', 'EATFUGU')

这里我们创建了一个包含两个字符串的元组,其中包含我们的河豚外卖的区号和号码。为了证明元组是不可变的,让我们尝试向它追加一个项:

>>> my_tuple.append('ramen')
Traceback (most recent call last):
  File "<interactive input>", line 1, in ?
AttributeError: 'tuple' object has no attribute 'append'

Python 抛出了一个AttributeError异常,让你知道元组不支持append。如果您尝试做任何修改元组的事情,您将得到类似的结果。然而,元组支持所有的索引和切片操作符,因为这些操作符不修改元组。

>>> my_tuple[0]
'555'
>>> my_tuple[1]
'EATFUGU'

打开包装

因为元组通常用于传递组值,Python 提供了一种简单的提取方法,称为解包。让我们把元组分解成两个变量:一个表示区号,一个表示号码。

>>> my_tuple=('555', 'EATFUGU')
>>> area_code, number = my_tuple
>>> area_code
'555'
>>> number
'EATFUGU'

在这里,您可以看到在一行代码中,Python 将元组的两个部分解包为两个独立的值。解包实际上适用于列表和其他 Python 对象,但是您最常将它用于元组。

提取元组中的值的另一种方法是将其转换为列表。您可以通过用元组作为参数构造一个列表来实现这一点——例如,list(my_tuple)返回等价列表,即['555', 'EATFUGU']。您也可以反过来,通过调用列表上的tuple来创建一个元组——例如,tuple(['555', 'EATFUGU'])返回我们的原始元组。

在接下来的章节中,你将了解到使用元组而不是列表的最佳位置;现在使用经验法则,如果您从不需要修改内容,您应该使用元组。

Image 注意创建一个包含一个或零个条目的元组与列表略有不同。这是因为 Python 也使用括号来定义类似数学表达式中的优先级。要定义只有一个项目的元组,请在项目后添加逗号;要定义空元组,只需在括号中包含逗号本身。比如,('ramen',)是一个只有一项的元组,(,)是一个空元组。

字典

我们要看的最后一个集合类型是字典。我们之前看到的集合都是序列集合,因为值从开始到结束都是一个序列,你可以通过它们在列表中的位置来访问它们。字典是映射集合,因为它们将一条信息映射到另一条信息。我们可以使用字典通过将食品名称映射到其价格来存储购物清单的价格。假设河豚 100 美元,拉面 5 美元;我们可以创建一个保存这些信息的字典,如下所示:

>>> my_dictionary={'ramen': 5.0, 'fugu': 100.0}

花括号创建了一个字典。在大括号中,我们有一个字符串'ramen',后跟一个冒号,然后是数字5.0(以美元表示的价格)。这告诉 Python 字符串映射到数字;换句话说,如果我们知道食物的名称,我们就可以查找价格。字典中的多个条目用逗号分隔;在这个例子中,我们有第二个条目,它将'fugu'映射到值100.0

为了检索该信息,我们再次使用方括号([])操作符,传入我们想要搜索的(在本例中,键不是fugu就是ramen)。字典返回与关键字相关联的——商品的价格。让我们来看看我们的两把钥匙:

>>> my_dictionary['fugu']
100.0
>>> my_dictionary['ramen']
5.0

您也可以通过向字典分配新值来向字典添加新项目:

>>> my_dictionary['chopsticks']=7.50
>>> my_dictionary['sake']=19.95
>>> my_dictionary
{'sake': 19.0, 'ramen': 5.0, 'chopsticks': 7.5, 'fugu': 100.0}

这里我们给词典增加了两个新词。您可能已经注意到,当 Python 为我们显示列表时,条目的顺序与我们最初创建它时的顺序不同。这是因为字典对字典中的键没有任何顺序概念,你看到的显示内容没有特定的顺序。重要的是 Python 记得什么键映射到什么值——这一点它做得很好!

一个循环是一种不止一次运行一段代码的方式。循环是编程语言的基础,你会发现你在游戏中写的几乎每一行代码都在某种循环中。像许多其他编程语言一样,Python 有两种类型的循环来处理所有的循环需求:while 循环的和 for 循环的*。*

While 循环

当你只在一个条件为真时重复一段代码时,使用一个while循环。让我们使用一个简单的 while 循环来显示从 1 到 5 的数字。我们将从在解释器中输入以下几行开始:

>>> count=1
>>> while count<=5:
...

当您在第二行之后按 Enter 键时,您会注意到现在看到的不是通常的 Python 提示符,而是三个句点(...)。这是因为行尾的冒号表示后面还有更多代码。在 while 循环的情况下,它是我们想要重复的代码。

所有语言都需要某种方式来标记代码块的开始和结束。有的用花括号({ })之类的符号,有的用doend之类的词。Python 做的事情略有不同,使用缩进来定义代码块。要告诉 Python 一行代码是块的一部分,而不是代码的其余部分,请在该行之前插入一个制表符(通过按 tab 键):

...     print(count)
...     count+=1

Image 注意在一些系统中,你可能会发现一个制表符会自动插入到程序块的第一行。如果一个块中有大量代码,这可能会很方便。删除制表符,并按 Enter 键正常结束块。

在最后一行之后按两次 Enter 空行告诉解释器您已经完成了代码块的输入。while 循环现在运行并显示数字 1 到 5。那么这是如何工作的呢?嗯,while语句后面是一个条件 ( count<=5,可以读作“是count 小于等于5?”Python 第一次遇到 while 循环,count是 1,满足我们小于等于 5 的条件——所以 Python 运行代码块。代码块中的两行先打印出count的值,然后加 1。第二次,count是 2,也满足条件,我们再次循环。最终count变成 6,肯定是不是小于等于 5,这次 Python 跳过了代码块。

小于或等于(<=)只是一个比较运算符。其他可以使用的请参见表 1-2 。

表 1-2 。比较运算符

|

操作员

|

描述

| | --- | --- | | < | 不到 | | <= | 小于或等于 | | > | 大于 | | >= | 大于或等于 | | == | 等于 | | != | 不等于 |

Image 小心小心你的循环!如果你使用一个永远为真的条件,比如2>1,Python 将永远循环下去。如果您最终陷入这种困境,请按 Ctrl+C 来停止 Python。每个程序员都至少陷入过一次无限循环!

对于循环

虽然循环有它们的用途,知道如何使用它们也很重要,但是 for 循环通常是更好的选择。一个 for 循环遍历一个 iterable Python 对象,给你一个新值,直到没有剩余的条目。你以前遇到过 iterables:列表,元组,字典,甚至字符串都是 iterable 对象。让我们将 while 循环示例重写为 for 循环:

>>> for count in range(0,6):
...     print(count)

这里我们迭代了range函数的结果,它创建了一个从第一个参数到第二个参数的值列表,但不包括第二个参数。

如您所见,对range的调用创建了一个包含数字 0 到 5 的列表,这正是我们想要在循环中显示的内容。当 Python 第一次遍历 for 循环时,它从列表中挑选第一个值,并将其赋给变量count;然后它运行循环中的代码,简单地将count的当前值打印到屏幕上。当循环到达列表末尾时,它在五次循环后结束。

如果您想从除 0 以外的任何数字开始,您将需要这两个参数,但请尝试:

>>> for count in range(6):
print(count)

Image 注意你应该看到你得到了和以前一样的输出。

实践中的 Python

在我们进入下一章之前,让我们把我们所学的应用到实际中去。心算从来都不是我的强项,所以我想写一小段 Python 代码来遍历我们的购物清单并找到总价。首先,我们将创建一个包含本周杂货的列表和一个将每件商品的名称与其价格对应起来的字典:

>>> shopping=['fugu', 'ramen', 'sake', 'shiitake mushrooms', 'soy sauce', 'wasabi']
>>> prices={'fugu':100.0, 'ramen':5.0, 'sake':45.0, 'shiitake mushrooms':3.5, 'soy sauce':7.50, 'wasabi':10.0}
>>> total=0.00

好极了。我们现在有两个 Python 集合来存储关于我们的杂货的所有信息,还有一个变量来存储总数。我们现在需要做的是循环遍历shopping,在prices中查找每个价格,并将其添加到total:

>>> for item in shopping:
...   total+= prices[item]
>>> total
171.0

这就够了!变量total现在保存了我们购物清单中每一项的总和,我们可以看到总数是非常合理的 171 美元。别担心,接下来几章中的示例代码会比购物清单有趣得多!

摘要

在第一章中,我们已经探索了一些基本的 Python 结构,其中大部分在编写新代码时会经常用到。当涉及到编写游戏和其他 Python 程序时,你可以把你到目前为止学到的东西看作是最基本的工具。数据(数字和字符串)和集合(元组、列表和字典)尤其重要,因为可以在其中存储游戏的每个方面。

在接下来的章节中,你将学习如何将你所学的知识整合起来,以创建更复杂的程序。您将发现如何使用逻辑、创建函数以及利用面向对象编程的力量。``

二、探索 Python

在前一章中,我们一次输入一行 Python 代码,但现在我们将把交互式解释器放到一边,开始创建 Python 文件。在这一章中,我们将介绍更多 Python 代码的构建模块,并向你展示如何使用类来帮助创建游戏。我们还将解释如何使用所有 Python 安装所附带的代码库。

创建脚本

包含 Python 代码的文件称为脚本。创建脚本只需要一个简单的文本编辑器,但最好使用编辑器,通常称为“IDE”。IDLE(Python 的标准发行版附带的)是我们在这里将使用的,尽管有许多替代方法。

要运行一个脚本,你需要在编辑器中编写代码,保存它,然后你通常只需双击它,或者如果你更喜欢命令行,键入python后跟一个空格和你的脚本的名称。大多数 Python 编辑器都有一个快捷键来运行您正在编辑的脚本。在空闲状态下运行脚本的快捷方式是 F5 键。要打开 IDLE,如果桌面上有快捷方式,请单击它,或者搜索 IDLE。打开它,你看到的是交互式编辑器。转到文件新建,这是一个空文档。你在这里写的是你的*剧本*。完成后,您需要保存它,您可以运行它。

`Image 注意大多数 Python 编辑器会自动为你缩进。通常默认缩进是四个空格。

运行后(F5 或 Run image Run 模块),应该会看到另一个窗口弹出,这是控制台输出,参见图 2-1 。

9781484209714_Fig02-01.jpg

图 2-1 。空闲的 Python 代码和输出控制台窗口

使用逻辑

不仅仅对于热血的瓦肯人来说,逻辑是任何软件的重要组成部分——包括游戏。任何游戏都需要根据给定或计算的信息做出决策。如果一束激光击中了玩家的悬停坦克,游戏必须决定是否已经造成足够的破坏来摧毁它——如果已经造成,则显示一个爆炸动画。这只是一个例子,为了让我们相信它不仅仅是一台愚蠢的机器,电脑游戏必须做出一系列决定。所以请戴上你的斯波克耳朵,我们将涵盖逻辑。

理解布尔值

计算机使用的逻辑是布尔逻辑,这样称呼是因为它是由乔治·布尔在 19 世纪发明的——在 PlayStation 上市前几年。

你已经看到了在上一章中作为 while 循环的一部分使用的逻辑;count<=5是一个逻辑表达式,像所有逻辑表达式一样,它要么产生True要么产生False。这些真值,正如它们被称为的那样,在做决定时被使用。在 while 循环的情况下,True的值告诉 Python 继续循环一次,但是False的值导致 Python 跳过代码块。(见清单 2-1 中逻辑表达式的几个例子。)布尔逻辑需要记住的重要一点是,没有中间值:你不可能有 25%的正确和 75%的错误——它总是非此即彼。

清单 2-1 。简单的逻辑

score = 100
health = 60
damage = 50
fugu = "tasty"
print(score != 100)
print(health - damage > 0)
print(fugu == "tasty")

运行这个简单的脚本会产生以下输出:

False

True

True

Image TrueFalse也有对应的数字:1 代表True,0 代表False。这是计算机科学的核心,它完全归结为 0 和 1,对应于电信号的存在(1 或True)或不存在(0 或False)。

布尔值可以像任何其他 Python 类型一样处理,因此可以有一个引用布尔值的变量。例如,如果我们要添加行is_fugu_tasty = (fugu == "tasty"),那么is_fugu_tasty将引用值True

如果语句

使用if语句时,逻辑表达式发挥了自己的作用。只有当条件为真时,才使用if语句来运行代码。如果条件为假,那么 Python 会跳过代码块的末尾。下面是一个简单的if语句的例子:

if fugu == "tasty":
   print("Eat the fugu!")

该条件使用比较运算符(==)将变量与字符串进行比较。假设我们使用清单 2-1 中的值fugu,这个比较将产生True,这给 Python 运行缩进代码块开了绿灯。

逻辑积算符

通常情况下,您需要检查几个条件。假设我们想吃河豚,如果它好吃的话低于 100 美元一盘。我们可以用and操作符:将这两个条件结合起来

price = 50.0
if fugu == ("tasty" and price < 100.0):
    print("Eat the fugu!")

这里我们只吃河豚,如果fugu被设置为"tasty" 并且price的值小于100.0

Image 一个常见的逻辑错误是使用 if 和 and 语句,比如:if 河豚和酱油== "tasty:"认为这是在问河豚和酱油是否都好吃。相反,这只是要求河豚是真实的,酱油是“美味的”。正确的方法应该是:如果河豚==“好吃”,酱油==“好吃”。

表 2-1 列出了and运算符如何组合值,但我希望它是不言自明的。你可以在现实生活中找到这个操作者;例如,我的汽车只有在电池没有耗尽我还有汽油的情况下才能启动。

表 2-1 。And 运算符的真值表

|

逻辑

|

结果

| | --- | --- | | False and False | False | | True and False | False | | False and True | False | | True and True | True |

Or 运算符

为了补充and操作符,我们有了or操作符,如果第一个或第二个值是True,就会产生True。假设我们想吃河豚,如果它好吃或者它很便宜(毕竟,谁能拒绝便宜的河豚呢?):

if fugu == "tasty" or price < 20.0:
    print("Eat the fugu!")

就像and运算符一样,or运算符在现实生活中有很多应用。如果我没油了,我的车就会停下来或者电池没电了。or真值表见表 2-2 。

表 2-2 。or 运算符的真值表

|

逻辑

|

结果

| | --- | --- | | False or False | False | | True or False | True | | False or True | True | | True or True | True |

“非”算符

我们要看的最后一个逻辑运算符是not运算符,它交换一个布尔值的状态,因此True变成了False,而False变成了True(参见表 2-3 )。你可以用这个来逆转任何情况:

if not (fugu == "tasty" and price < 100.0):
    print("Don't eat the fugu!")

这里我们颠倒了fugu == "tasty" and price < 100.0条件,这样 Python 只有在条件为而不是真(即假)时才运行代码块。

表 2-3 。Not 运算符的真值表

|

逻辑

|

结果

| | --- | --- | | not True | False | | not False | True |

else 语句

你可能已经注意到,前面的片段与我们第一个河豚逻辑语句相反。我们有一个在条件为真时发生的动作,另一个在相同的条件不为真时运行。这是一种常见的情况,Python 有一种方法来附加一个替代动作到一个if语句上。else语句跟在if语句之后,引入了一个新的代码块,该代码块只有在条件为假时才会运行。让我们看一个如何使用else的例子:

if fugu == "tasty":
    print("Eat the fugu!")
else:
    print("Don't eat the fugu!")

Python 运行这段代码时,如果条件为真,就会运行第一条print语句;else它会运行第二个条件。

elif 语句

通常另一个if语句会跟在一个else语句之后。Python 将一个else后跟一个if组合成一条语句:elif语句。假设我们想根据价格将河豚分为三类。为了便于讨论,我们将 20-100 美元归类为价格合理的河豚,高于这个范围的归类为昂贵的河豚,低于这个范围的归类为便宜的河豚。Python 可以使用elif : 来为我们做这件事

if price < 20:
    print("Cheap fugu!")
elif price < 100:
    print("Reasonably priced fugu.")
else:
    print("Expensive fugu!")

这里我们有三个代码块,但是只有一个运行。如果price小于 20,那么第一块运行;如果price小于 100,并且第一个块为假,那么第二个块将运行;并且price的任何其他值将导致第三个块运行。在if之后,你可以有任意多的elif语句,但是如果你有一个else语句,它必须在最后。

了解功能

函数是一段存储的 Python 代码,您可以向它传递信息,也可以从它那里获取信息。Python 提供了大量有用的函数(参见表 2-4 中的一些例子),但是你也可以创建自己的函数。

表 2-4 。一些内置的 Python 函数

|

功能

|

描述

|

例子

| | --- | --- | --- | | abs | 求一个数的绝对值 | abs(-3) | | help | 显示任何 Python 对象的使用信息 | help([]) | | len | 返回字符串或集合的长度 | len("hello") | | max | 返回最大值 | max(3, 5) | | min | 返回最小值 | min(3, 4) | | round | 将浮点数舍入到给定的精度 | round(10.2756, 2) |

*如需更全面的 Python 内置函数列表,请参阅 Python 文档,或访问 http://doc.python.org

定义函数

要在 Python 中定义一个函数,您可以使用def语句,后跟您想要赋予函数的名称。您可以使用任何您想要的名称,但是给它一个描述它实际做什么的名称是一个好主意!函数名通常是小写的,并且可以使用下划线来分隔单词。清单 2-2 是一个简单的 Python 函数,用于计算河豚的小费。

清单 2-2 。计算河豚的小费

def fugu_tip(price, num_plates, tip):
    total = price * num_plates
    tip = total * (tip / 100)
    return tip

print(fugu_tip(100.0, 2, 15.0))
print(fugu_tip(50.0, 1, 5.0))

该脚本产生以下输出:

30.0
2.5

当 Python 第一次遇到一个def语句时,它知道期待一个函数定义,由函数名和括号中的一系列参数组成。正如forwhileif语句一样,冒号用于引入代码块(称为函数体)。在前面提到的语句中,代码块不会立即运行——它只是被存储起来,直到需要的时候。调用该函数会导致 Python 跳转到函数体的开头,并将调用中给出的信息赋给每个参数。所以在清单 2-2 中,对fugu_tip的第一次调用是在price设置为 100、num_plates设置为 2、tip设置为 15 的情况下运行的。

fugu_tip中唯一没有遇到过的是return语句,它告诉 Python 从函数跳回,可能带有一些新信息。在fugu_tip的例子中,我们返回提示的值,但是函数可以返回任何 Python 对象。

Image 注意函数中不需要return语句。如果没有return语句,函数将在到达代码块末尾时返回值None——这是一个特殊的 Python 值,表示“这里什么都没有”函数可以用来计算或返回数据,或者它们可以简单地运行一段代码,比如发送一封电子邮件。

你可能已经注意到在fugu_tip里面创建了两个变量;这些被称为局部变量,因为它们只存在于函数内部。当函数返回时,totaltip将不再存在于 Python 的内存中——尽管函数外部可能有同名的变量。

默认值

参数可以有一个默认的值,如果您没有在函数调用中提供一个值,就使用这个值。如果没有默认值,Python 会在您忘记参数时抛出异常。让我们给fugu_tip赋予默认值。我给小费很大方,所以我们将默认的tip设为 15(代表餐费的百分比),由于我不喜欢一个人吃饭,我们将默认的num_plates设为 2。

要在 Python 中设置默认值,请在参数名后面附加一个=符号,后面跟一个您想要给它的值。参见清单 2-3 中的以获得带有这些默认值的修改后的fugu_tip函数。fugu_tip现在可以只用一个值来调用;如果忽略其他两个值,它们会自动填充。函数定义中可以有任意多的默认值,但是带默认值的参数必须出现在参数列表的末尾。

清单 2-3 。计算河豚的小费

def fugu_tip(price, num_plates=2, tip=15.):
    total = price * num_plates
    tip = total * (tip / 100.)
    return tip

print(fugu_tip(100.0))
print(fugu_tip(50.0, 1, 5.0))
print(fugu_tip(50.0, tip=10.0))

运行此代码将为我们提供以下提示值:

30.0
2.5
10.0

你可能已经注意到清单 2-3 中的有些不寻常。对fugu_tip的第三次调用省略了num_plates的值,并通过名称设置了tip的值。当您像这样显式地设置参数时,它们被称为关键字参数。如果您的函数有许多参数,但您只需要设置其中的几个参数,那么它们就很有用。如果没有默认值,参数必须以与参数列表相同的顺序给出。

介绍面向对象编程

你可能听说过面向对象编程(OOP)这个术语。但是如果你不熟悉它也不用担心,因为这个概念非常简单。

那么,用面向对象的术语来说,对象到底是什么?嗯,字面上可以是任何东西。在一个游戏中,我们可能有一个粒子的物体——比方说,爆炸产生的燃烧灰烬,或者引起爆炸的悬浮坦克。事实上,整个游戏世界都可能是一个物体。对象的目的是包含信息,并给予程序员用这些信息做事情的能力。

当构造一个对象时,通常最好从计算出它包含什么信息,或属性开始。让我们想想在一个被设计成代表未来悬浮坦克的物体中会发现什么。它应至少包含以下属性:

  • 坦克在哪里?
  • 它面向哪个方向?
  • 它跑得有多快?
  • 它有多少护甲?
  • 它有几个壳?

现在我们有了描述坦克和它在做什么的信息,我们需要给它执行坦克在游戏中需要做的所有动作的能力。在 OOP 中,这些动作被称为方法。我能想到下面这些方法是一辆坦克绝对需要的,但可能会更多:

  • Move:向前移动坦克。
  • Turn:向左/向右旋转水箱。
  • Fire:发射炮弹。
  • Hit:这是敌方炮弹击中坦克时的动作。
  • 用爆炸动画替换坦克。

您可以看到,这些方法通常会更改对象的属性。当使用Move方法时,它将更新坦克的Position。同样,当使用Fire方法时,会更新Ammo的值(当然除非没有剩下Ammo;然后Fire什么都不会做!).

使用类

一个是 Python 定义对象的方式。你以前实际上使用过类;列表,字典,甚至字符串都是类,但是你也可以自己创建。把一个类想象成一个对象的一种模板,因为你定义了这个类一次,然后用它来创建你需要的任意多的对象。让我们写一个简单的Tank类(见清单 2-4);我们稍后将使用它来创建一个简单的游戏。

清单 2-4 。储罐等级定义示例

class Tank(object):
    def __init__(self, name):
        self.name  = name
        self.alive = True
        self.ammo  = 5
        self.armor = 60

当 Python 遇到class Tank(object):时,它会创建一个名为Tank的类,这个类是从名为object基类派生而来的*。从一个类派生意味着建立在它所做的基础上。我们可以先创建一个叫Vehicle的职业,它可以处理移动和转弯,然后从它派生并增加发射武器的能力来创建一辆坦克。这种方法的优点是Vehicle可以被重用来给其他游戏实体旋转和移动的能力。对于这个例子,我们没有另一个类来构建,所以我们的基类将是object,这是 Python 本身内置的一个简单的类。*

Image 注意我可能给你的印象是object做的不多,但实际上它在与类一起工作时在幕后做了很多有用的事情——你只是不直接使用它。

class语句后缩进代码块中的所有内容都是类定义。在这里,我们设置用于描述对象的属性,并提供它将需要的所有方法。在 Python 中,属性只是存储在对象中的变量,而方法是处理对象的函数。在我们的Tank类中,有一个奇怪命名的方法叫做__init__,它对 Python 有特殊的意义。当你创建一个对象时,Python 会自动调用这个方法。Python 程序员通常使用它来为对象分配属性,但是在首次创建对象时,您可以做任何其他可能需要的事情。

这个__init__方法有两个参数:selfname。因为方法可能用于许多对象,所以我们需要某种方法来知道我们正在使用哪个对象。这就是self的用武之地——它是对 Python 自动提供给所有方法调用的当前对象的引用。第二个参数(name)是一个字符串,我们将使用它来区分坦克,因为坦克不止一个。

__init__中的代码首先将name参数复制到一个属性中,以便我们稍后可以检索它;然后它会分配一些我们需要的其他属性。在我计划的游戏中,我们不需要坦克的大量信息;我们只需要知道坦克是否还活着(self.alive),它还有多少弹药(self.ammo,以及它还有多少装甲(self.armor)。

Image 注意你不用调用第一个参数self。您可以将其命名为任何您想要的名称,但是坚持使用self是一个好主意,这样当您阅读您的代码时,您将确切地知道它的用途。Python 程序员倾向于坚持这种约定,所以在交换代码时不会产生混淆。

创建对象

现在我们有了一个坦克定义,我们可以通过调用Tank来创建一个新的坦克,我们提供了一个字符串。我们来看一个例子:

my_tank = Tank("Bob")

这创建了一个名为 Bob 的新 tank,并调用__init__来初始化它。然后坦克鲍勃被分配给变量my_tank,这个变量被称为Tank类的实例。我们现在可以将my_tank视为一个单独的对象——将它传递给函数,存储在列表中,等等,或者我们可以单独访问属性。例如,print my_tank.name会显示Bob

加入我们的行列

只有一个方法,Tank类做不出任何有趣的事情。让我们用清单 2-5 中的一些方法来充实它。

清单 2-5 。扩展坦克等级

def __str__(self):
    if self.alive:
        return "%s (%i armor, %i shells)" % (self.name, self.armor, self.ammo)
    else:
        return "%s (DEAD)" % self.name

def fire_at(self, enemy):
    if self.ammo >= 1:
        self.ammo -= 1
        print(self.name, "fires on", enemy.name)
        enemy.hit()
    else:
        print(self.name, "has no shells!")

def hit(self):
    self.armor -= 20
    print(self.name, "is hit!")
    if self.armor <= 0:
        self.explode()

def explode(self):
    self.alive = False
    print(self.name, "explodes!")

清单 2-5 中的第一个方法是另一个特殊的方法。任何名字的开头和结尾都有两个下划线,这对 Python 来说有着特殊的意义。__str __ 的作用是返回一个描述对象的字符串;当您试图用str将对象转换成字符串时,它会被调用,这将在您打印它时发生。因此,如果我们要做print my_tank,它应该显示一个字符串,其中包含一些关于坦克鲍勃的有用信息。清单 2-5 中的__str__根据坦克是活的还是死的返回不同的字符串。如果坦克是活的,那么这一行将运行:

return "%s (%i armor, %i shells)" % (self.name, self.armor, self.ammo)

这可能是你以前没见过的。使用%操作符将字符串"%s (%i armor, %i shells)"与元组(self.name, self.armor, self.ammo)组合在一起。这被称为字符串格式化,这是创建复杂字符串的一个很好的方式,没有太多的麻烦。字符串中的前两个字符是%s,这告诉 Python 用元组中的第一项替换它们,元组是包含坦克名称的字符串。在字符串的后面,Python 到达了%i,它被元组中的第二个项目(一个整数)替换,以此类推,直到元组中不再有项目。字符串插值通常比将许多小字符串相加更容易使用。这一行做同样的事情,但是使用简单的字符串连接:

return self.name+" ("+str(self.armor)+" armor, "+str(self.ammo)+" shells)"

这有点复杂,因为我相信你会同意!字符串格式化可以用多种方式格式化整数、浮点数和字符串。更多信息参见 Python 文档(https://docs.python.org/3.4/library/string.html)。

Tank类中的第二个方法fire_at是,事情变得有趣起来。它带有参数enemy,这是我们想要射击的坦克目标。首先,它检查还有多少ammo剩余。如果至少有一发炮弹,则减少self.ammo1(因为我们刚刚发射了一发炮弹),调用敌方坦克的hit方法。在敌人坦克的hit方法中减少self.armor20。如果没有装甲剩余,那么敌人已经死了,所以我们调用它的explode方法来标记坦克已经死了。

如果这是一个我们正在开发的图形游戏,这些方法会创造一些视觉效果。fire_at将创建一个外壳图像或 3D 模型,并设置其轨迹,explode可能会显示某种令人印象深刻的爆炸动画。但是对于这个小测试游戏,我们将只使用几个print语句来描述当前正在发生的事情。

清单 2-6 完整地展示了Tank类;另存为tank.py。如果你运行这个脚本,它什么也不会做,因为它只是定义了Tank类。我们将使用游戏代码的其余部分创建另一个 Python 脚本。

清单 2-6 。tank.py

class Tank(object):
    def __init__(self, name):
        self.name  = name
        self.alive = True
        self.ammo  = 5
        self.armor = 60

    def __str__(self):
        if self.alive:
            return "%s (%i armor, %i shells)" % (self.name, self.armor, self.ammo)
        else:
            return "%s (DEAD)" % self.name

    def fire_at(self, enemy):
        if self.ammo >= 1:
            self.ammo -= 1
            print(self.name, "fires on", enemy.name)
            enemy.hit()
        else:
            print(self.name, "has no shells!")

    def hit(self):
        self.armor -= 20
        print(self.name, "is hit!")
        if self.armor <= 0:
            self.explode()

    def explode(self):
        self.alive = False
        print(self.name, "explodes!")

实践中的 Python

我们将要创建的游戏与其说是一个游戏,不如说是一个模拟游戏,但它应该足以介绍几个重要的游戏概念。我们将创造一些坦克,让他们互相射击。胜利者只是游戏中剩下的最后一辆坦克。清单 2-7 显示了完成坦克游戏的代码。

清单 2-7 。tankgame.py

from tank import Tank

tanks = {"a":Tank("Alice"), "b":Tank("Bob"), "c":Tank("Carol") }
alive_tanks = len(tanks)

while alive_tanks > 1:

      for tank_name in sorted( tanks.keys() ):
            print(tank_name, tanks[tank_name])

      first = input("Who fires? ").lower()
      second = input("Who at? " ).lower()

      try:
            first_tank = tanks[first]
            second_tank = tanks[second]
      except KeyError as name:
            print("No such tank!", name)
            continue

      if not first_tank.alive or not second_tank.alive:
            print("One of those tanks is dead!")
            continue

      print("*"30)

      first_tank.fire_at(second_tank)
      if not second_tank.alive:
          alive_tanks -= 1

      print("*"30)

for tank in tanks.values():
      if tank.alive:
         print(tank.name, "is the winner!")
         break

当你第一次(以任何语言)看到任何一段代码时,都会有点害怕。但是一旦你把它分解,你会发现它是由熟悉的东西组成的。所以让我们像一个训练有素的厨师准备河豚一样剖析清单 2-7 !

tankgame.py需要做的第一件事就是导入我们的坦克模块(tank.py),里面包含了Tank类。当一个新的脚本运行时,它只能访问内置的类,比如字符串和列表。如果您想使用另一个没有直接定义的类,您首先必须从另一个 Python 文件中导入它。行from tank import Tank告诉 Python 寻找名为tank(假设为.py)的模块,并在Tank类中读取。另一种方法是做一个简单的import tank,让我们访问tank.py中的所有内容。

Image 注意当你执行from tank import Tank时,它将Tank类(大写 T)导入到当前的名称空间——这意味着你现在可以使用Tank,就像你刚刚将它剪切并粘贴到你的脚本中一样。然而,如果你只是做了import tank,你已经导入了 tank 名称空间,这意味着你必须将Tank类称为tank.Tank,就像在my_tank = tank.Tank("Bob")中一样。有关import语句的详细信息,请参阅本章后面的“导入简介”一节。

接下来我们创建一个名为tanks的字典,它将用于存储我们所有的坦克对象。我们将使用三个,但是如果你喜欢的话,可以随意添加更多的坦克。

tanks = { "a":Tank("Alice"), "b":Tank("Bob"), "c":Tank("Carol") }
alive_tanks = len(tanks)

三个坦克都有字符串"a""b""c"作为关键字,所以我们可以很容易地查找它们。一旦我们创建了自己的坦克,我们就将坦克的数量存储在alive_tanks中,这样我们就可以继续计算游戏中的坦克数量:

while alive_tanks > 1:

这是一个 while 循环的开始,当有一个以上的坦克幸存时,这个循环会继续下去。游戏的核心总是有一个大循环。对于一个视觉游戏,主循环每帧运行一次,以更新和显示视觉效果,但这里的循环代表模拟中的一个回合。

在 while 循环中,我们首先打印一个空行,使每一轮的文本更容易分开。然后,我们有另一个循环,显示每个储罐的一些信息:

print
for tank_name in sorted( tanks.keys() ):
    print(tank_name, tanks[tank_name])

字典的keys方法返回它所包含的键的列表,但是由于字典的性质,这些键不一定按照它们被添加的顺序。因此,当我们得到tanks的键列表时,我们立即将它传递给sorted,这是一个内置函数,返回列表的排序副本。

for 循环中的print语句在tanks字典中查找键,并打印它找到的 tank 对象。记住,打印一个对象调用它的__str__函数来获得一些有用的信息。

接下来我们要求用户提供两个坦克:开火的坦克(first)和被击中的坦克(second):

first  = input("Who fires? ").lower()
second = input("Who at? " ).lower()

内置函数raw_input显示一个提示,并等待用户输入一些文本,然后以字符串的形式返回。在前面的代码中,我们调用返回字符串的lower方法将其转换为小写,因为我们需要一个小写字符串来查找适当的坦克,但是我们不介意用户使用大写字母输入名称。

有了这两个坦克钥匙,我们可以用它们来查找实际的坦克物体。这很简单:我们只需做tanks[first]来取回坦克:

try:
    first_tank  = tanks[first]
    second_tank = tanks[second]
except KeyError as name:
    print("No such tank!", name)
    continue

但是因为用户可以在提示符下输入任何东西,所以我们需要某种方法来处理用户出错或故意破坏游戏的情况!

每当 Python 无法完成要求它做的事情时,它就会抛出一个异常。如果您不做任何事情来处理这些异常,Python 脚本将会退出——这在真实的游戏中将是灾难性的。幸运的是,预测潜在的异常并在发生时处理它们是可能的。如果firstsecondtanks字典中是而不是键,那么当我们试图查找它们时,Python 将抛出一个KeyError异常。这不会使脚本退出,因为我们在一个try:块中查找键,这告诉 Python 代码块可能会抛出异常。如果发生了KeyError,Python 将跳转到except KeyError:下的代码(如果没有异常发生,将被忽略)。

在我们的KeyError异常处理程序中,我们首先显示一条简短的消息,通知用户他们做错了什么,然后继续执行一条continue语句,告诉 Python 忽略这个循环中的其余代码,并跳回到最内层循环的顶部。

if not first_tank.alive or not second_tank.alive:
   print("One of those tanks is dead!")
   continue

这段代码处理一个或两个坦克都死了的情况——因为对一个死了的坦克开火是没有意义的,而且死了的坦克无论如何也不能开火!它只是显示一条消息并执行另一个continue

如果我们已经设法在代码中做到这一点,我们有两个有效的坦克对象:first_tanksecond_tank:

first_tank.fire_at(second_tank)
if not second_tank.alive:
     alive_tanks -= 1

第一辆坦克负责开火,所以我们调用它的fire_at方法,并将第二辆坦克作为敌人传入。如果第二辆坦克被第一辆杀死(armor达到 0),其alive属性将被设置为False。当这种情况发生时,alive_tanks计数减少 1。

最终,在几轮游戏之后,alive_tanks的值将达到 1。当这种情况发生时,主游戏循环将结束,因为它只在alive_tanks大于 1 时循环。

最后一段代码的目的是显示哪辆坦克赢得了比赛:

for tank in tanks.values():

    if tank.alive:
        print(tank.name, "is the winner!")
        break

这是另一个遍历tanks.values()中每个值的循环,是对keys()的补充——它给出了我们所有坦克对象的列表。我们知道只有一个坦克的alive设置为True,所以我们用一个简单的if语句来测试它。一旦我们找到最后剩下的坦克,我们打印一个小消息,然后执行break语句。break语句是continue的伙伴,但是它不是跳到循环的开头,而是跳到结尾并停止循环。

这就是我们的小游戏。现在我第一个承认这不是最令人兴奋的游戏。它不是雷神之锤,但即使是雷神之锤也会做类似的事情。所有的 3D 射手必须记录生命值/护甲和弹药,以及谁还活着。不过,到本书结束时,我们的游戏对象将会以令人惊叹的 3D 方式呈现,而不是一行文本。以下是tankgame.py的输出:

a Alice (60 armor, 5 shells)
b Bob (60 armor, 5 shells)
c Carol (60 armor, 5 shells)
Who fires? a
Who at? b

******************************
Alice fires on Bob
Bob is hit!
******************************

a Alice (60 armor, 4 shells)
b Bob (40 armor, 5 shells)
c Carol (60 armor, 5 shells)
Who fires?

使用标准库

Python 打包了大量的类和函数,被称为标准库。这就是为什么 Python 经常被描述为包含电池,因为你可以利用 Python 专家编写的代码做任何事情,从三角学到下载网页和发送电子邮件。库被组织成模块或包,每个都有特定的用途。你可以通过导入这些模块来使用它们,就像坦克游戏(见清单 2-7 )导入Tank类一样。

当您在 Python 中导入内容时,它会首先在当前目录中查找相应的 Python 文件。如果没有找到,Python 会在标准库中寻找一个模块。

让我们看一下标准库中的几个模块。我们不可能涵盖所有的内容——这本身就需要一本书——但是如果您需要任何模块的更多信息,可以看看 Python 发行版附带的文档,或者在https://docs.python.org/3.4/library/上在线浏览。

引进进口

有几种方法可以从你自己的代码或者标准库中导入东西。使用哪种方法取决于您希望如何访问模块中包含的类和函数。模块可以用关键字import导入,后跟一个模块名。例如,下面的代码行将导入一个名为mymodule的模块:

import mymodule

以这种方式导入模块会创建一个新的名称空间,这意味着您需要在您使用的任何类或函数之前键入模块的名称和一个点。例如,如果mymodule中有一个名为myfunction的函数,您可以这样调用它:

mymodule.myfunction()

这是从标准库中导入模块的通常方式,因为它将每个模块中的东西分开;如果在不同的模块中有另一个名为myfunction的函数,就不会混淆哪个函数被调用。

也可以使用from语句从模块中导入特定的类或函数。下面一行将myclassmymodule导入到当前名称空间:

from mymodule import myclass

如果你只想从模块中得到一些东西,并且你知道它们的名字不会和你的脚本中的任何东西冲突,那么就使用这个方法。您可以通过在每个类、函数之间添加逗号来导入多个类、函数等。所以from mymodule import myclassmyfunction将从mymodule类中导入两个东西。

您可以使用一个*符号来表示您想要将模块中的所有内容导入到当前的名称空间中。例如,下面一行将把所有内容从mymodule导入到当前名称空间:

from mymodule import *

这种导入方法节省了输入时间,因为您不需要模块的名称来引用它——但是只有当模块包含少量内容并且您知道名称不会与脚本中的其他类或函数冲突时,才使用这种方法。对于这种导入来说,math模块是一个很好的选择。

最后,您还可以使用单词作为来导入模块或模块中的函数,以更改相关的名称。例如:

import mymodule as mm

现在,不需要键入 mymodule 来引用该模块,只需键入 mm 即可。您也可以使用从模块导入的函数来实现这一点。

游戏的有用模块

标准库包含了大量的模块,但是你在游戏中只会用到其中的一小部分。让我们来看看一些更常用的模块。

数学模块

当我告诉人们我不太擅长数学时,他们经常感到惊讶。“可你是电脑程序员啊!”他们惊呼。“没错,”我告诉他们。"我让计算机替我做算术。"Python 内置了基础数学;您可以在不导入特殊模块的情况下进行加法、减法和乘法运算。但是你确实需要math模块来实现更高级的功能——那种你可以在科学计算器上找到的东西。其中几个见表 2-5 。

表 2-5 。数学模块中的一些函数

|

功能

|

描述

|

例子

| | --- | --- | --- | | sin | 返回一个数的正弦值,以弧度为单位 | sin(angle) | | cos | 返回一个数字的余弦值,以弧度为单位 | cos(angle) | | tan | 返回一个数字的正切值,以弧度为单位 | tan(angle) | | ceil | 返回大于或等于一个数字的最大整数 | ceil(3.4323) | | fabs | 返回数字的绝对值(不带符号) | fabs(–2.65) | | floor | 返回小于或等于一个数字的最大整数 | floor(7.234) | | pi | 圆周率的值 | pi*radius**2 |

给定一个圆的半径,让我们使用math模块来计算它的面积。如果你还记得学校的话,这个公式是圆周率乘以半径的平方,其中圆周率是一个幻数,等于 3.14 左右。幸运的是 Python 对数字的记忆比我好,你可以相信它对圆周率有更精确的表示。这个函数非常简单,我们将使用交互式解释器:

>>> from math import pi
>>> def area_of_circle(radius):
...     return pi*radius**2
...
>>> area_of_circle(5)
78.53981633974483

首先,我们从 math 模块导入 pi。然后,我们定义一个非常简单的函数,它获取半径并返回圆的面积。为了测试它,我们计算一个半径为 5 个单位的圆的面积,结果是 78.5 个单位多一点。

日期时间模块

模块有许多处理日期和时间的函数和类。您可以用它来查询电脑内部时钟的时间,并计算日期之间的时差。这听起来可能是一个简单的任务,因为我们经常对日期进行心算,但当您考虑闰年和时区时,这可能会变得有点复杂!幸运的是,我们可以依靠一些聪明的程序员的工作,让 Python 毫不费力地做到这一点。在datetime模块中有一个同名的类。让我们用它来查找当前时间:

>>> from datetime import datetime
>>> the_time = datetime.now()
>>> the_time.ctime()
'Thu Feb 19 13:04:14 2015'

datetime模块导入datetime类后,我们调用函数now返回一个带有当前时间的datetime对象。函数now就是所谓的静态方法,因为你在一个类上使用它,而不是用那个类创建的一个对象。一旦我们将当前日期和时间存储在the_time中,我们就调用ctime方法,该方法以字符串的形式返回时间的友好表示。显然,当您运行它时,它将返回不同的结果。

那么在游戏中找时间有什么用呢?嗯,你可能想存储一个保存游戏和高分的时间戳。你也可以把游戏和一天中的当前时间联系起来,这样中午的时候会很明亮和阳光灿烂,但是如果你在晚上玩的话,会很黑暗和阴沉。请看一下表 2-6 ,你可以在datetime模块中找到一些东西。

表 2-6 。日期时间模块中的一些类

|

班级

|

描述

| | --- | --- | | timedelta | 存储两个时间之间的差异 | | date | 存储日期值 | | datetime | 存储日期和时间值 | | time | 存储时间值 |

随机模块

当您得知random模块用于生成随机数时,您不会感到惊讶,尽管您可能会惊讶地发现它生成的数字并不是真正随机的。这是因为计算机实际上不能随机选择某些东西;在相同的条件下,他们会再次做同样的事情。random生成的数字是伪随机,这意味着它们是从一个很长的数字序列中抽取的,这些数字看起来是随机的,但如果你生成足够多的数字,它们最终会重复出现。幸运的是,你可以在游戏中使用它们,因为没有人会注意到它们重复了几十亿次!

随机(或伪随机数)在游戏中非常有用,可以防止它们变得可预测。如果一个游戏没有随机元素,玩家最终会记住所有的动作序列,使它变得不那么有趣(对大多数人来说)。

让我们写一个简短的脚本来模拟一个标准六面骰子的十次投掷(清单 2-8 )。

清单 2-8 。骰子模拟器

import random
for roll in range(10):
    print(random.randint(1,6))

哇,就三行。所有这些只是导入random模块,然后调用random.randint十次并打印结果。函数randint带两个参数ab,返回一个在ab范围内的伪随机数(可能包括结束值)。所以randint(1, 6)像骰子一样返回 1、2、3、4、5 或 6。

Image 注意你可能已经注意到在清单 2-8 中roll的值实际上从来没有在循环中使用过。对xrange(10)的调用生成了从 0 到 9 的数字,但是我们忽略了它们,因为我们感兴趣的只是重复循环十次。与其为一个从未使用过的值想一个名字,不如用一个下划线代替它。所以清单 2-8 中的循环可能会被重写为for _ in range(10):

虽然清单 2-8 产生的数字看起来是随机的,但它们实际上是伪随机的,这意味着它们是从一个大的数学生成的序列中选择的。有时候,您可能需要重复一系列伪随机数——例如,在播放演示时。您可以通过调用random.seed函数告诉random模块从序列中的特定点开始生成数字。如果你用相同的值调用它两次,它将导致random模块重现相同的数字序列。清单 2-9 展示了如何使用seed函数来创建可预测的序列。

清单 2-9 。一个更好的骰子模拟器

import random
random.seed(100)

for roll in range(10):
    print(random.randint(1, 6))

print("Re-seeded")
random.seed(100)

for roll in range(10):
    print(random.randint(1, 6))

如果您运行这个小脚本,您将看到相同的数字序列,重复两次。查看表 2-7 ,了解random模块的一些功能。

表 2-7 。随机模块中的一些函数

|

功能

|

描述

| | --- | --- | | seed | 播种随机数生成器 | | randint | 返回两个值之间的随机整数 | | choice | 从集合中选择一个随机元素 | | random | 返回介于 0 和 1 之间的浮点数 |

摘要

我们已经看到,您可以在 Python 代码中使用布尔逻辑来做出决策。if语句接受一个布尔表达式,比如a > 3,并且仅当该条件导致True时才运行一个代码块。您可以在一个if语句后添加一个或多个else语句,这些语句仅在条件为False时运行它们的代码块。使用andor运算符可以合并逻辑,使用not运算符可以反转逻辑。

函数存储在 Python 代码中,用def语句创建。当您定义函数时,您指定一个参数列表,该列表是函数运行和可选返回值所需的信息列表。许多内置函数可供您使用。

面向对象编程是一个简单概念的花哨术语。它仅仅意味着存储描述某个事物所需的信息以及处理这些信息的大量操作。在游戏中,几乎所有的东西都会被定义为一个对象。Python 类是用class语句定义的,您可以将它视为创建新对象的模板。在一个class语句中创建的函数被称为方法,除了第一个参数是方法应用的对象之外,它与其他函数相似。__init__函数是一个特殊的方法,当一个对象第一次被创建时被调用;您使用它来初始化包含在对象中的信息,或属性

Python 有一个很大的标准库,可以做各种有用的事情。该库被组织成多个模块,其中可以包含类、函数或其他 Python 对象。

在下一章,我们将介绍如何使用 Pygame 模块打开一个窗口并显示图形。`

三、Pygame 简介

你曾经打开过你的电脑,看看里面的情况吗?现在不需要这样做,但是您会发现它是由许多必要的部件构建而成,可以提供您的计算体验。视频卡生成图像并向您的显示器发送信号。声卡将声音混合在一起,并将音频发送到您的扬声器。然后是输入设备,如键盘、鼠标和操纵杆,以及各种其他电子小发明——所有这些在制作游戏中都是必不可少的。

在家用电脑的早期,发型难看、戴着厚框眼镜的程序员不得不掌握电脑的每一个部件。游戏程序员必须阅读每台设备的技术手册,以便编写计算机代码与之通信——所有这些都是在开发实际游戏之前进行的。当制造商推出具有新功能的不同设备和现有设备版本时,情况只会变得更糟。程序员希望支持尽可能多的设备,以便他们的游戏有更大的市场,但他们发现自己陷入了使用这些新显卡和声卡的细节中。对于购买游戏的公众来说,这也是一个痛苦,他们必须仔细检查盒子,看看他们是否有正确的设备组合来使游戏运行。

随着微软视窗等图形操作系统的引入,事情变得简单了一些。他们给了游戏程序员一种与设备通信的方式。这意味着程序员可以扔掉技术手册,因为制造商提供了*驱动程序、*处理操作系统和硬件之间通信的小程序。

快进到更近的时代,程序员仍然留着糟糕的发型,但戴着更薄的眼镜。游戏程序员的生活仍然不容易。尽管有一种通用的图形、音频和输入交流方式,但由于市场上硬件的多样性,编写游戏仍然很棘手。妈妈在当地超市购买的廉价家用电脑与公司高管购买的顶级电脑大相径庭。正是这种多样性使得初始化硬件并准备好在游戏中使用变得如此困难。幸运的是,现在 Pygame 在这里,我们有了一种创建游戏的方法,而不必担心这些细节(游戏程序员也有时间出去做体面的发型)。

在这一章中,我们将向你介绍 Pygame,并解释如何使用它来创建一个图形显示和读取输入设备的状态。

Pygame 的历史

Pygame 建立在另一个名为简单直接媒体层(SDL) 的游戏创作库之上。《SDL》是 Sam Lantinga 在洛基软件公司(一家现已倒闭的游戏公司)工作时写的,旨在简化游戏从一个平台移植到另一个平台的任务。它提供了一种在多个平台上创建显示以及使用图形和输入设备的通用方法。因为它操作起来非常简单,所以在 1998 年发布时,它非常受游戏开发人员的欢迎,并且已经被用于许多业余爱好和商业游戏。

SDL 是用 C 语言写的,这是一种常用于游戏的语言,因为它的速度快,并且能够在底层与硬件一起工作。但是用 C 或它的后继者 C++ 开发可能会很慢并且容易出错。因此,程序员开发了与他们喜欢的语言的绑定,现在几乎可以在任何语言中使用 SDL。Pygame 就是这样一个绑定,它允许 Python 程序员使用强大的 SDL 库。

Pygame 和 SDL 已经积极开发了许多年,因为它们都是开源的,所以大量的程序员致力于改进和增强这个用于创建游戏的高超工具。

安装 Pygame

安装模块可能是学习 Python 的最大挑战之一。随着时间的推移,Python 的开发者试图让这个过程变得简单一些。安装模块会因你的操作系统而异,但我会尽我所能来说明主要模块的安装方法:Windows,Mac,Linux。

您首先要做的是解决 Python 安装的位版本问题。默认是 32 位,但您可能已经获得了 64 位安装。要找出你有什么,打开空闲,并阅读最上面的文字。下面是一个在 Windows 操作系统上安装 32 位 Python 的示例:

Python 3.4.3 (v3.4.3:9b73f1c3e601, Feb 24 2015, 22:43:06) [MSC v.1600 32 bit (Intel)] on win32
Type "copyright", "credits" or "license()" for more information.

注意哪里写着 32 位(Intel);这意味着我使用的是 32 位版本的 Python。这意味着我所有的模块必须是 32 位模块。如果您使用的是 64 位版本的 Python,那么您的所有模块都必须是 64 位的(尽管您可能有所期待)。然而,在 64 位版本的 Windows 上运行 32 位版本的 Python 是可能的。

因此,假设您使用 Python 的默认安装,即 32 位,那么您现在正在寻找 Pygame 的 32 位版本。安装 Pygame 最简单的方法是打开 bash、您的终端或命令行,键入:

pip install pygame

就这么简单!你在这里可能不成功。您可能会遇到如下错误:

'pip' is not recognized as an internal or external command, operable program or batch file

如果是这样,那么尝试直接引用 pip 脚本。pip 脚本实际上是包含在 Python 安装中的 pip.py 文件。找到您的 Python 安装目录,在其中查找“脚本”目录,然后您应该会看到其中的 pip.py 文件。记下这个完整路径,并使用它。例如,如果您在 Windows 上,您应该键入:

C:/Python34/Scripts/pip install pygame

在 Windows 或 Linux 上,您应该可以成功完成前面的尝试。

对于 Mac 用户来说,事情可能没有这么简单。历史上,Mac OS 在使用各种形式的 Python GUI 相关模块时遇到了很多麻烦。这是一个正在进行的活动,所以你最好的选择是查看来自http://pygame.org/wiki/macintosh的最新安装说明。

自从我上次在那里看了以后,说明已经改变了,所以它们可能会随着时间的推移而改变。

如果你使用的是 Windows 系统,你可能会发现一切都还存在问题。一种相对较新的安装模块的方法是使用扩展名为. whl 的 Wheel 文件,这种方法可能会更加流行。

要安装这些,您只需下载。whl 文件,然后使用 pip 通过引用。whl 文件作为您想要安装的文件。例如,您可以下载一个名为 py game-1 . 9 . 2 A0-cp34-none-win32 . whl 的. whl 文件。要安装该文件,假设您已将该文件下载到下载目录中,您可以键入如下内容:

pip install C:/Users/H/Downloads/pygame-1.9.2a0-cp34-none-win32.whl

同样,您可能需要给出 pip 脚本的完整路径。

使用 Pygame

Pygame 包包含许多可以独立使用的模块。你可能在游戏中使用的每一个设备都有一个模块,还有许多其他模块可以让游戏创作变得轻而易举。所有 Pygame 模块见表 3-1 。您通过pygame名称空间访问这些模块;例如,pygame.display指的是display模块。

表 3-1 。Pygame 包中的模块

|

模块名

|

目的

| | --- | --- | | pygame.cdrom | 访问和控制 CD 驱动器 | | pygame.cursors | 加载光标图像 | | pygame.display | 访问显示屏 | | pygame.draw | 绘制形状、线条和点 | | pygame.event | 经理外部事件 | | pygame.font | 使用系统字体 | | pygame.image | 加载并保存图像 | | pygame.joystick | 使用操纵杆和类似设备 | | pygame.key | 从键盘上读取按键 | | pygame.mixer | 加载并播放声音 | | pygame.mouse | 管理鼠标 | | pygame.movie | 播放电影文件 | | pygame.music | 处理音乐和流式音频 | | pygame.overlay | 访问高级视频叠加 pygame | |   | 包含高级 Pygame 函数 | | pygame.rect | 管理矩形区域 | | pygame.sndarray | 处理声音数据 | | pygame.sprite | 管理运动图像 | | pygame.surface | 管理图像和屏幕 | | pygame.surfarray | 处理图像像素数据 | | pygame.time | 管理时序和帧速率 | | pygame.transform | 调整大小和移动图像 |

*有关 Pygame 模块的完整文档,请参见 http://pygame.org/docs/

一些你会在每个游戏中用到的模块。你总是会有某种显示器,所以display模块是必不可少的,你肯定会需要某种输入,无论是键盘、操纵杆还是鼠标。其他模块不太常用,但结合起来,它们给你一个最强大的游戏创作工具。

并不是表 3-1 中的所有模块都能保证出现在每个平台上。运行游戏的硬件可能不具备某些功能,或者没有安装所需的驱动程序。如果是这种情况,Pygame 会将模块设置为None,这样便于测试。下面的代码片段检测pygame.font模块是否可用,如果不可用就退出:

if pygame.font is None:
   print("The font module is not available!")
   exit()

你好世界重访

正如我在第一章中提到的,学习新语言时有一个传统,你写的第一个代码会显示“你好,世界!”在屏幕上。从技术上来说,我们已经用一个print 'Hello, World!'语句做到了这一点——但这有点令人失望,因为作为游戏程序员,我们感兴趣的是创造吸引人的视觉效果,而一行文本根本做不到!我们将使用 Pygame 创建一个 Hello World 脚本,它会在您的桌面上打开一个图形窗口,并在标准鼠标光标下绘制一个图像。运行时,你会看到一个类似于图 3-1 中所示的窗口。

9781484209714_Fig03-01.jpg

图 3-1 。Pygame 中的 Hello World

代码见清单 3-1 。如果你愿意,现在就运行它;我们将在本章中一步一步地介绍它。

清单 3-1 。Hello World Redux (helloworld.py)

#!/usr/bin/env python

background_image_filename = 'sushiplate.jpg'
mouse_image_filename = 'fugu.png'

import pygame
from pygame.locals import *
from sys import exit

pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)
pygame.display.set_caption("Hello, World!")

background = pygame.image.load(background_image_filename).convert()
mouse_cursor = pygame.image.load(mouse_image_filename).convert_alpha()

while True:

    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            exit()

        screen.blit(background, (0,0))

        x, y = pygame.mouse.get_pos()
        x-= mouse_cursor.get_width() / 2
        y-= mouse_cursor.get_height() / 2
        screen.blit(mouse_cursor, (x, y))

        pygame.display.update()

我们需要两张清单 3-1 的图片:一张作为背景,另一张作为鼠标光标。您可以从 Apress 网站的源代码/下载部分下载这个示例和其他示例的文件。如果你现在不能上网,你可以使用你硬盘上的图像文件,或者用任何图形或照片编辑软件制作。任何图像都可以作为背景,只要它的尺寸至少是 640×480(再大一些,超出的部分就会被剪掉)。对于鼠标光标,你将需要一个更小的图像,以适应舒适的背景;合适的尺寸是 80 乘 80。继续第一章的河豚主题,官方背景将是一幅碗筷的图片,和一幅非常原始的河豚图片供鼠标光标使用。前两行设置图像的文件名;如果您使用不同的图像,您应该用图像的位置替换文件名。让我们把这个脚本分成小块。在脚本的顶部,我们导入了运行示例时需要的外部模块、类、函数等:

import pygame
from pygame.locals import *
from sys import exit

第一行导入了pygame包,让我们可以访问它的所有子模块,比如pygame.imagepygame.sound。第二行将一些函数和常量(不变的值)导入顶级名称空间。为了使用 Pygame,这样做并不是必须的,但是这样做很方便,因为我们不必在常用值前面加上pygame名称空间。最后一个import语句从sys(标准库中的一个模块)导入一个函数。你可能已经猜到了,exit的目的是立即完成脚本。调用它会导致 Pygame 窗口消失,Python 关闭。当用户点击关闭按钮时,脚本调用exit;否则,用户将无法关闭窗口!

Image 提示如果你遇到无法关闭 Pygame 窗口的情况,你也许可以通过按 Ctrl+C 来停止 Python 的运行

这一行相当简单的 Python 代码实际上做了很多工作:

pygame.init()

它会初始化pygame包中的每个子模块,这可能会加载驱动程序并查询硬件,以便 Pygame 准备好使用您计算机上的所有设备。您可以通过单独调用每个子模块中的init函数来仅初始化您想要使用的模块;例如,pygame.sound.init()会初始化声音模块。这可以使脚本启动得更快一点,因为只有您实际使用的模块才会被初始化。对于游戏来说,你将需要大部分的模块,如果不是全部的话——所以我们将坚持使用这个包罗万象的初始化函数。在我们调用它之后,我们就拥有了 Pygame 的全部力量!

在初始化 Pygame 之后,我们需要创建一个显示表面:

screen = pygame.display.set_mode((640, 480), 0, 32)
pygame.display.set_caption("Hello, World!")

显示可以是你桌面上的一个窗口,也可以是整个屏幕,但是你总是通过 Pygame Surface 对象来访问它。我们脚本中对 pygame.display.set_mode 的调用返回表示桌面上窗口的表面对象。它需要三个参数:只有第一个是必需的,它应该是一个包含我们想要创建的显示的宽度和高度的元组。我们的窗口将是 640 x 480 像素,这足够大,所以我们可以看到正在发生的事情,但也不会大到遮住太多的桌面。我们给 set_mode 的下一个参数是一个包含显示创建中使用的标志的值。标志是可以打开或关闭的特征;您可以用按位 OR 运算符(|)组合几个标志。例如,要创建双缓冲硬件表面,请将 flags 参数设置为 DOUBLEBUF|HWSURFACE。您可以使用的旗帜见表 3-2 。我将在本章后面的“打开显示器”部分更详细地介绍它们。对于第一个 Pygame 脚本,我们不会启用任何标志,所以我们给标志的值只是 0,这也是默认值。

表 3-2 。pygame.display.set_mode 的标志

|

|

目的

| | --- | --- | | FULLSCREEN | 创建一个充满整个屏幕的显示。 | | DOUBLEBUF | 创建“双缓冲”显示。推荐给HWSURFACEOPENGL。 | | HWSURFACE | 创建硬件加速显示(必须与FULLSCREEN标志结合使用)。 | | OPENGL | 创建 OpenGL 可渲染显示。 | | RESIZABLE | 创建可调整大小的显示。 | | NOFRAME | 从显示中删除边框和标题栏。 |

下一个参数指定显示表面的深度,,这是用于在显示器中存储颜色的的数量。一位,或二进制数字是计算机中最基本的存储单位。位正好有两个潜在值,1 或 0,在内存中以 8 位为一组排列。一组 8 位称为一个字节。如果这对你来说听起来像是技术术语,不要担心;Python 倾向于对程序员隐藏这种事情。我们将使用值 32 作为我们的位深度,因为它给了我们最多的颜色;其他潜在的位深度值见表 3-3 。如果你不提供深度值或者设置为 0,Pygame 将使用你桌面的深度。

表 3-3 。位深度值

|

比特深度

|

颜色数量

| | --- | --- | | 8 位 | 256 种颜色,从更大的调色板中选择 | | 15 位 | 32,768 种颜色,带备用位 | | 16 位 | 65,536 种颜色 | | 24 位 | 1670 万色 | | 32 位 | 1670 万色,带备用 8 位 |

*也可能有其他位深度,但这些是最常见的。

Image 有时 Pygame 无法给我们所要求的精确显示。可能是显卡不支持我们要求的功能。幸运的是,Pygame 将选择与硬件兼容的显示器,并模拟我们实际要求的显示器。谢谢你,Pygame!

如果一切顺利,对set_mode的调用将在你的桌面上显示一个 Pygame 窗口,并返回一个Surface对象,该对象随后被存储在变量screen中。我们对新创建的 surface 做的第一件事是调用display模块中的set_caption来设置 Pygame 窗口的标题栏。我们将标题设置为“你好,世界!”—只是为了使它成为一个有效的 Hello World 脚本!

接下来,我们使用pygame.image中的load函数来加载背景和鼠标光标的两幅图像。我们传入脚本开始时存储的图像的文件名:

background   = pygame.image.load(background_image_filename).convert()
mouse_cursor = pygame.image.load(mouse_image_filename).convert_alpha()

load函数从硬盘读取一个文件,并返回一个包含图像数据的表面。这些是与我们的显示器相同类型的对象,但是它们代表存储在内存中的图像,并且在我们将它们绘制到主显示器之前是不可见的。对pygame.image.load的第一次调用读入背景图像,然后立即调用convert,它是Surface对象的成员函数。该函数将图像转换为与我们的显示器相同的格式,因为如果显示器具有相同的深度,绘制图像会更快。鼠标光标以类似的方式加载,但是我们调用convert_alpha而不是convert。这是因为我们的鼠标光标图像包含了 alpha 信息,这意味着图像的一部分可以是半透明的或者完全不可见的。如果鼠标图像中没有 alpha 信息,我们只能使用难看的正方形或长方形作为鼠标光标!下一章将更详细地介绍 alpha 和图像格式。

脚本中的下一行直接跳到主游戏循环:

while True:

这个 while 循环将True作为条件,这意味着它将继续循环,直到我们break退出它,或者以其他方式强制它退出。所有游戏都会有一个类似的循环,通常每次屏幕刷新都会重复一次。

在主游戏循环中,我们有另一个循环——事件循环,大多数游戏也会有这样或那样的形式:

for event in pygame.event.get():
    if event.type == QUIT:
        pygame.quit()
        exit()

事件是 Pygame 通知你在你的代码之外发生了什么的方式。事件是为许多事情而创建的,从按键到从互联网接收信息,并且在您处理它们之前一直在为您排队。pygame.event 模块中的函数 get 返回等待我们的任何事件,然后我们在 for 循环中遍历这些事件。对于这个脚本,我们只对退出事件感兴趣,当用户单击 Pygame 窗口中的关闭按钮时,由 Pygame 生成该事件。因此,如果事件类型是 QUIT,我们调用 exit 来关闭,所有其他事件都被忽略。当然,在一个游戏中,我们必须处理更多的事件。为了完全关闭 pygame,调用 pygame.quit()和 exit()非常重要。Pygame.quit()取消 Pygame 的初始化。

下一行将背景图像拼接到屏幕上(拼接表示从一个图像复制到另一个图像):

screen.blit(background, (0,0))

这一行使用 screen Surface对象的blit成员函数,它接受一个源图像——在本例中是我们的 640 x 480 背景——和一个包含目标位置的元组。背景从不移动;我们只是希望它覆盖整个 Pygame 窗口,所以我们 blit 到坐标(0,0),这是屏幕的左上角。

Image 提示重要的是,你可以轻松地浏览屏幕的每个部分。如果你不这样做,当你制作动画时可能会出现奇怪的视觉效果,并且你的游戏在每台运行它的计算机上可能看起来不一样。尝试注释掉对screen.blit的调用,看看会发生什么。

在我们绘制背景之后,我们想要在通常的鼠标指针下面绘制mouse_cursor:

x, y = pygame.mouse.get_pos()
x -= mouse_cursor.get_width()/2
y -= mouse_cursor.get_height()/2
screen.blit(mouse_cursor, (x, y))

获得鼠标的位置很好也很简单;pygame.mouse模块包含了我们使用鼠标所需的一切,包括get_pos,它返回一个包含鼠标坐标的元组。为了方便起见,第一行将这个元组解包为两个值:xy。当我们 blit 鼠标光标时,我们可以使用这两个值作为坐标,但是这样会把图像的左上角放在鼠标下面,我们希望图像的中心在鼠标下面。所以我们做了一点计算(不要害怕!)调整xy,使鼠标图像向上移动一半高度,向左移动一半宽度。使用这些坐标将图像的中心放在鼠标指针的正下方,这样看起来更好。至少对于鱼的图像是这样的——如果您想使用更典型的指针图像,请调整坐标,使指针位于实际鼠标坐标的下方。

对鼠标图像进行位图化和对背景进行位图化的方法是一样的,但是我们使用的是我们计算的坐标而不是(0,0)。这足以产生我们想要的效果,但是在我们看到任何东西之前,我们还必须做一件事:

pygame.display.update()

当你通过 blits 构建一个图像到屏幕表面时,你不会马上看到它们。这是因为 Pygame 首先将图像构建到一个后台缓冲区,这是一个在显示之前在内存中不可见的显示。如果我们没有这一步,用户将会看到单独的 blits,这将是最不愉快的闪烁。对于游戏程序员来说,闪烁是敌人!我们希望看到如丝般光滑,令人信服的动画。幸运的是,我们只需要调用pygame.display.update()就可以确保我们在内存中创建的图像没有闪烁地显示给用户。

当您运行这个脚本时,您应该会看到类似于图 3-1 的内容。如果你使用的是“官方”图片,那么一条长相奇怪的鱼会忠实地跟随鼠标光标。

了解事件

在 Hello World 中,我们只处理了QUIT事件,这是必不可少的,除非你想拥有不朽的 Pygame 窗口!Pygame 创建其他事件来通知你诸如鼠标移动和按键之类的事情。

事件可以在任何时候生成,不管你的程序正在做什么。例如,当用户按下游戏手柄上的 fire 按钮时,您的代码可能会在屏幕上绘制一辆坦克。因为您不能在事件发生时立即做出反应,Pygame 将它们存储在一个队列中,直到您准备好处理它们(通常在主游戏循环的开始)。您可以将事件队列想象成一排等待进入大楼的人,每个人都携带关于某个事件的特定信息。当玩家按下 fire 按钮时,joystick事件到达,携带关于哪个键被按下的信息。类似地,当玩家释放 fire 按钮时,同一个joystick事件的克隆会出现,并带有关于被释放按钮的信息。接下来可能会发生mouse事件和key事件。

检索事件

在前面的例子中,我们调用了pygame.event.get() 来检索所有的事件并将它们从队列中删除,这就像打开门让所有人进来一样。这可能是处理事件的最佳方式,因为它确保我们在继续向屏幕绘制内容之前已经处理了所有的事情——但是还有其他方式来处理事件队列。如果调用pygame.event.wait(),Pygame 会等待一个事件发生后再返回,这就像在门口等着,直到有人来。这个函数不常用于游戏,因为它会暂停脚本,直到有事情发生,但是对于与系统上的其他程序(比如媒体播放器)协作更多的 Pygame 应用来说,这个函数会很有用。另一种方法是pygame.event.poll(),如果有一个等待,它将返回一个事件,如果队列中没有事件,它将返回一个类型为NOEVENT的虚拟事件。无论使用哪种方法,重要的是不要让它们堆积起来,因为事件队列的大小是有限的,如果队列溢出,事件将会丢失。

有必要定期调用至少一个事件处理函数,以便 Pygame 可以在内部处理事件。如果不使用任何事件处理函数,可以调用pygame.event.pump()来代替事件循环。

事件对象包含一些描述发生的事件的成员变量。它们包含的信息因事件而异。所有事件对象的唯一共同点是type,它是一个指示事件类型的值。您首先查询的就是这个值,这样您就可以决定如何处理它。表 3-4 列出了您可能收到的标准事件;我们将在本章中讨论其中的一些。

表 3-4 。标准事件

|

事件

|

目的

|

因素

| | --- | --- | --- | | QUIT | 用户已单击关闭按钮。 | none | | ACTIVEEVENT | Pygame 已被激活或隐藏。 | gain, state | | KEYDOWN | 键已被按下。 | unicode, key, mod | | KEYUP | 钥匙已被释放。 | key, mod | | MOUSEMOTION | 鼠标已被移动。 | pos, rel, buttons | | MOUSEBUTTONDOWN | 鼠标按钮被按下。 | pos, button | | MOUSEBUTTONUP | 释放了鼠标按钮。 | pos, button | | JOYAXISMOTION | 操纵杆或手柄被移动。 | joy, axis, value | | JOYBALLMOTION | 欢乐球被感动了。 | joy, ball, rel | | JOYHATMOTION | 操纵杆帽被移动了。 | joy, hat, value | | JOYBUTTONDOWN | 操纵杆或键盘按钮被按下。 | joy, button | | JOYBUTTONUP | 操纵杆或键盘按钮被释放。 | joy, button | | VIDEORESIZE | Pygame 窗口已调整大小。 | size, w, h | | VIDEOEXPOSE | 部分或全部 Pygame 窗口暴露。 | none | | USEREVENT | 发生了用户事件。 | code |

让我们编写一个简单的 Pygame 脚本来显示所有生成的事件。清单 3-2 使用pygame.event.wait()来等待单个事件。一旦得到一个,它就用str把它转换成一个字符串,并把它添加到一个列表中。代码的其余部分显示新事件以及尽可能多的先前事件,以适合屏幕。它使用font模块来显示文本(我们将在后面讨论)。

Image 提示如果将清单 3-2 中的填充颜色改为(0,0,0),字体颜色改为(0,255,0),看起来会有点像矩阵风格的代码。你可能需要发挥一下你的想象力!

清单 3-2 。显示消息队列导入 pygame

from pygame.locals import *
from sys import exit

pygame.init()
SCREEN_SIZE = (800, 600)
screen = pygame.display.set_mode(SCREEN_SIZE, 0, 32)

while True:

    for event in pygame.event.get():

        if event.type == QUIT:
            pygame.quit()
            exit()

        print(event)

    pygame.display.update()

如果你运行清单 3-2 中的,你会看到一个简单的黑色窗口。如果你在窗口上移动鼠标,你将开始看到控制台的打印输出(参见图 3-2 )。这些事件指定鼠标的当前位置、自上次运动事件以来鼠标移动了多远,以及当前按下了哪些按钮。你可以用 pygame.mouse 模块获得鼠标的当前位置,就像我们在 Hello World 的例子中所做的那样,但是你冒着丢失玩家一直在做的事情的信息的风险。这对于在后台做大量工作的台式电脑来说是一个特别的问题,可能偶尔会让你的游戏暂停一小段时间。对于一个鼠标光标,你只需要知道鼠标在每一帧开始的位置,那么使用 pygame.mouse.get_pos()是合理的。如果你使用鼠标移动来驱动坦克和按钮来开火,那么最好使用事件,这样游戏就可以更密切地监控玩家在做什么。

9781484209714_Fig03-02.jpg

图 3-2 。事件脚本的输出

处理鼠标运动事件

正如您所看到的,每当您将鼠标移动到 Pygame 窗口上时,MOUSEMOTION事件就会发出。它们包含这三个值:

  • **buttons—**A tuple of three numbers that correspond to the buttons on the mouse.

    所以buttons[0]是鼠标左键,buttons[1]是中键(像鼠标滚轮一样),而buttons[2]是右键。如果按钮被按下,那么它的值被设置为 1;如果未按下,该值将为 0。可以同时按下多个按钮。

  • pos— 包含事件生成时鼠标位置的元组。

  • rel— 一个元组,包含自上次鼠标运动事件以来鼠标移动的距离(有时称为鼠标 mickies )。

处理鼠标按钮事件

除了运动事件,鼠标还产生MOUSEBUTTONDOWNMOUSEBUTTONUP事件。如果您在消息队列脚本上单击鼠标,您将首先看到向下事件,然后当您将手指从按钮上移开时,会看到向上事件。那么为什么会有这两个事件呢?如果您使用鼠标按钮作为发射火箭的触发器,您将只需要其中一个事件,但您可能有不同类型的武器,例如在按住按钮时连续发射的连锁枪。在这种情况下,您将在下降事件时启动链枪加速,并使其开火,直到您获得相应的上升事件。两种类型的鼠标按钮事件都包含以下两个值:

  • 按钮— 被按下的按钮的号码。值 1 表示按下了鼠标左键,2 表示按下了中键,3 表示按下了右键。
  • pos— 包含事件生成时鼠标位置的元组。

处理键盘事件

键盘和操纵杆有相似的向上和向下事件;按下按键时发出KEYDOWN,松开按键时发出KEYUP。 清单 3-3 演示了如何响应KEYUPKEYDOWN事件,用光标键移动屏幕上的东西。如果运行这个清单,您将看到一个包含简单背景图像的窗口。按向上、向下、向左或向右,背景将向那个方向滑动。将手指从光标键上拿开,背景将停止移动。

清单 3-3 。使用键盘事件移动背景

import pygame
from pygame.locals import *
from sys import exit

background_image_filename = 'sushiplate.jpg'
pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)
background = pygame.image.load(background_image_filename).convert()

x, y = 0, 0
move_x, move_y = 0, 0

while True:

    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            exit()

        if event.type == KEYDOWN:
            if event.key == K_LEFT:
                move_x = -1
            elif event.key == K_RIGHT:
                move_x = +1
            elif event.key == K_UP:
                move_y = -1
            elif event.key == K_DOWN:
                move_y = +1

        elif event.type == KEYUP:
            if event.key == K_LEFT:
                move_x = 0
            elif event.key == K_RIGHT:
                move_x = 0
            elif event.key == K_UP:
                move_y = 0
            elif event.key == K_DOWN:
                move_y = 0

        x+= move_x
        y+= move_y

        screen.fill((0, 0, 0))
        screen.blit(background, (x, y))

        pygame.display.update()

清单 3-3 开头就像 Hello World 它导入并初始化 Pygame,然后加载一个背景图片。这个脚本中的事件循环是不同的,因为它处理KEYDOWNKEYUP。这些关键事件都包含相同的三个值:

  • 键— 这是一个代表被按下或释放的键的数字。键盘上的每个物理键都有一个以K_开头的常数。字母键是从K_aK_z,但是其他键也有常量,比如K_SPACEK_RETURN。有关您可以使用的关键常量的完整列表,请参见www.pygame.org/docs/ref/key.html
  • mod— 该值表示与其他键组合使用的键,如 Shift、Alt 和 Ctrl。这些修饰键中的每一个都由一个以KMOD_开头的常数表示,比如KMOD_SHIFTKMOD_ALTKMOD_CTRL。使用按位 AND 运算符检查这些值。例如,如果按下 Ctrl 键,mod & KMOD_CTRL将计算为Truewww.pygame.org/docs/ref/key.html提供了修饰键的完整列表。
  • unicode— 这是被按下的键的 Unicode 值。它是通过将被按下的键与被按下的任何修饰键组合而产生的。英语字母表和其他语言中的每个符号都有一个 Unicode 值。你不会经常在游戏中使用这个值,因为按键更像是开关而不是输入文本。一个例外是输入一个高分表,您希望玩家能够输入非英文字母以及混合大写和小写字母。

KEYDOWN的处理程序中,我们检查对应于光标键的四个键常量。如果按下K_LEFT,则move_x的值被设置为–1;如果按下K_RIGHT,则设置为+1。这个值随后被添加到背景的 x 坐标上,以便将其向左或向右移动。还有一个move_y值,如果按下K_UPK_DOWN则设置该值,这将垂直移动背景。

我们还处理了KEYUP事件,因为我们希望当用户释放光标键时背景停止移动。KEYUP事件处理程序中的代码类似于 down 事件,但是它将move_xmove_y设置回零以阻止背景移动。

事件循环之后,我们要做的就是将值move_xmove_y加到xy上,然后在(x,y)处绘制背景。你之前唯一没见过的是screen.fill((0, 0, 0)),它用来把显示屏清成黑色(颜色在第四章的中解释)。这条线是必要的,因为如果我们移动背景图像,它就不再覆盖整个显示——我猜想从技术上讲,这意味着它不再是背景!

过滤事件

并不是所有的事件都需要在每个游戏中处理,而且通常有其他方法可以获得事件可能给你的信息。例如,如果您正在使用pygame.mouse.get_pos(),那么您不需要响应MOUSEMOTION事件。

有时,您还需要暂停某些事件的处理。如果你要在关卡之间播放过场动画,你可能会忽略输入事件,直到它结束。Pygame 事件模块有许多函数可以帮助你做到这一点。

您可以使用set_block功能阻止事件队列中的事件。例如,下面一行将禁止鼠标移动:

pygame.event.set_blocked(MOUSEMOTION)

如果您传入一个事件类型列表,所有这些事件都将被阻止。例如,下面的行将通过阻止KEYDOWNKEYUP事件来禁用所有键盘输入:

pygame.event.set_blocked([KEYDOWN, KEYUP])

如果你想解锁所有事件,将None的值传递给set_blocked。该行将允许事件队列中的所有事件发生:

pygame.event.set_blocked(None)

set_blocked相对的是set_allowed,它选择应该被允许(解除阻止)的事件。它还接受单个事件类型或事件类型列表。但是如果你传入None的值,它会有效地阻止所有的事件。你可以用pygame.event.get_block询问 Pygame 某个事件当前是否被阻止,它采用单一事件类型。

发布事件

通常是 Pygame 为你创建所有的事件,但是你也可以创建你自己的事件。您可以使用这种能力来回放演示(通过复制玩家的输入),或者模拟猫走过键盘的效果(我喜欢让我的游戏防猫)。

要发送一个事件,首先用pygame.event.Event构造一个事件对象,然后用pygame.event.post发布它。事件将被放在队列的末尾,准备在事件循环中检索。下面是如何模拟玩家按空格键:

my_event = pygame.event.Event(KEYDOWN, key=K_SPACE, mod=0, unicode=u' ')
pgame.event.post(my_event)

Event构造函数接受事件的类型,比如表 3-4 中的一个事件,后面是事件应该包含的值。因为我们正在模拟KEYDOWN事件,所以我们需要提供事件处理程序期望出现的所有值。如果您愿意,可以将这些值作为字典提供。这一行创建相同的事件对象:

my_event = pygame.event.Event(KEYDOWN, {"key":K_SPACE, "mod":0, "unicode":u' '})

除了模拟 Pygame 生成的事件,您还可以创建全新的事件。您所要做的就是为事件使用一个大于USEREVENT的值,这是 Pygame 将为自己的事件 id 使用的最大值。如果您想在继续绘制到屏幕之前在事件循环中做一些事情,这有时会很有用。下面是一个用户事件响应猫走过键盘的例子:

CATONKEYBOARD = USEREVENT+1
my_event = pygame.event.Event(CATONKEYBOARD, message="Bad cat!")
pgame.event.post(my_event)

处理用户事件的方式与 Pygame 生成的普通事件相同——只需检查事件类型,看它是否与您的自定义事件匹配。以下是您处理CATONKEYBOARD事件的方式:

for event in pygame.event.get():
    if event.type == CATONKEYBOARD:
        print(event.message)

打开显示器

在 Hello World 示例中,我故意忽略了打开一个显示器,因为我们只需要一个简单的显示器,但是 Pygame 有多种显示器选项。您创建的显示类型取决于游戏。使用固定分辨率(显示尺寸)通常更容易,因为它可以简化您的代码。你的决定还取决于你在游戏中有多少动作——你在屏幕上一次移动的东西越多,游戏运行得越慢。您可能需要选择较低的分辨率来进行补偿(这将再次加快速度)。

最好的解决方案通常是让玩家决定他们想要运行的分辨率,以便他们可以调整显示,直到他们在视觉质量和游戏运行的流畅程度之间取得良好的妥协。如果你走这条路,你必须确保你的游戏在所有可能的分辨率下看起来都没问题!

在编写游戏之前,不要担心这个问题。在您尝试 Pygame 脚本时,只需选择一个适合您的分辨率,但也可以随意尝试一下。

全屏显示

在 Hello World 中,我们使用以下代码行创建了一个 Pygame 窗口:

screen = pygame.display.set_mode((640, 480), 0, 32)

第一个参数是我们想要创建的窗口的大小。大小为(640,480)会创建一个适合大多数桌面的小窗口,但是如果您愿意,您可以选择不同的大小。在窗口中运行对于调试来说是非常好的,但是大多数游戏用动作填满了整个屏幕,没有通常的边框和标题栏。全屏模式通常更快,因为你的 Pygame 脚本不必与你桌面上的其他窗口配合。要设置全屏模式,使用set_mode的第二个参数的FULLSCREEN标志:

screen = pygame.display.set_mode((640, 480), FULLSCREEN, 32)

Image 注意如果你的脚本在全屏模式下出了问题,有时很难回到你的桌面。所以最好先在窗口模式下测试。您还应该提供退出脚本的替代方法,因为关闭按钮在全屏模式下是不可见的。

当您进入全屏模式时,您的视频卡可能会切换到不同的视频模式,这将改变显示器的宽度和高度,并可能改变它一次可以显示多少种颜色。显卡只支持几种大小和颜色数量的组合,但是如果你尝试选择一种显卡不直接支持的视频模式,Pygame 会帮你。如果不支持您要求的显示器尺寸,Pygame 将选择下一个尺寸并将您的显示器复制到它的中心,这可能会导致显示器的顶部和底部出现黑色边框。要避免这些边框,请选择几乎所有显卡都支持的标准分辨率之一:(640、480)、(800、600)或(1024、768)。要查看您的显示器支持哪些分辨率,您可以使用pygame.display.list_modes(),它会返回包含支持的分辨率的元组列表。让我们从交互式解释器中尝试一下:

>>> import pygame
>>> pygame.init()
(6, 0)
>>> pygame.display.list_modes()
[(1920, 1080), (1680, 1050), (1600, 1024), (1600, 900), (1366, 768), (1360, 768), (1280, 1024), (1280, 960), (1280, 800), (1280, 768), (1280, 720), (1152, 864), (1024, 768), (800, 600), (720, 576), (720, 480), (640, 480)]

Image 注意注意pygame.init()是如何返回一个元组的(6,0). pygame.init()返回一个元组,该元组包含成功初始化的次数,后跟失败初始化的次数。

如果显卡无法提供您要求的颜色数量,Pygame 将自动转换显示面中的颜色以适应(这可能会导致图像质量略有下降)。

清单 3-4 是一个演示从窗口模式到全屏模式的简短脚本。如果您按下 F 键,显示屏将会填满整个屏幕(发生这种情况时,可能会有几秒钟的延迟)。第二次按 F,显示屏将返回到一个窗口。

清单 3-4 。全屏示例

import pygame
from pygame.locals import *
from sys import exit

background_image_filename = 'sushiplate.jpg'

pygame.init()
screen = pygame.display.set_mode((640, 480), 0, 32)
background = pygame.image.load(background_image_filename).convert()

Fullscreen = False

while True:

     for event in pygame.event.get():
         if event.type == QUIT:
             pygame.quit()
             exit()
     if event.type == KEYDOWN:
        if event.key == K_f:
            Fullscreen = not Fullscreen
            if Fullscreen:
                 screen = pygame.display.set_mode((640, 480), FULLSCREEN, 32)
            else:
                 screen = pygame.display.set_mode((640, 480), 0, 32)

     screen.blit(background, (0,0))
     pygame.display.update()

Resizable Pygame Windows

有时你可能希望用户能够调整 Pygame 窗口的大小,这通常是通过点击窗口的一角并用鼠标拖动来实现的。当您调用set_mode时,通过使用RESIZABLE标志很容易做到这一点。Pygame 通过发送一个包含新的窗口宽度和高度的VIDEORESIZE事件来通知你的代码用户是否改变了窗口大小。当您得到这些事件之一时,您应该再次调用pygame.display.set_mode来将显示设置为新的尺寸。清单 3-5 展示了如何响应VIDEORESIZE事件。

清单 3-5 。使用可调整大小的窗口

import pygame
from pygame.locals import *
from sys import exit

background_image_filename = 'sushiplate.jpg'

SCREEN_SIZE = (640, 480)

pygame.init()
screen = pygame.display.set_mode(SCREEN_SIZE, RESIZABLE, 32)

background = pygame.image.load(background_image_filename).convert()

while True:

     event = pygame.event.wait()
     if event.type == QUIT:
          pygame.quit()
          exit()
     if event.type == VIDEORESIZE:
          SCREEN_SIZE = event.size
          screen = pygame.display.set_mode(SCREEN_SIZE, RESIZABLE, 32)
          pygame.display.set_caption("Window resized to "+str(event.size))

     screen_width, screen_height = SCREEN_SIZE
     forin range(0, screen_height, background.get_height()):
        forin range(0, screen_width, background.get_width()):
            screen.blit(background, (x, y))

     pygame.display.update()

当您运行这个脚本时,它将显示一个简单的 Pygame 窗口和一个背景图片。如果你点击窗口的角或者边,用鼠标拖动,脚本会得到一个VIDEORESIZE事件。在该消息的处理程序中是对set_mode的另一个调用,它创建了一个与新尺寸匹配的新屏幕表面。调整大小消息包含以下值:

  • size— 这是一个包含窗口新维度的元组;size[0]是宽度,size[1]是高度。
  • w— 这个值包含窗口的新宽度。它与size[0]的值相同,但可能更方便。
  • h— 这个值包含窗口的新高度。它与size[1]的值相同,但可能更方便。

因为这个脚本的显示大小会有所不同,所以我们绘制背景的方式会稍有不同,方法是根据需要多次将背景图像块化以覆盖显示。对range的两次调用产生了放置这些背景图像所需的坐标。

大多数游戏都是全屏运行的,所以可调整大小的显示屏可能不是你经常使用的功能。但是如果你需要的话,它就在你的工具箱里!

没有边框的窗口

通常当你创建一个 Pygame 窗口时,你会想要一个带有标题栏和边框的标准窗口。但是,也可以创建一个没有这些功能的窗口,这样用户就不能移动或调整窗口大小,或者通过关闭按钮关闭窗口。这种用法的一个例子是用于闪屏的窗口。有些游戏可能需要一段时间才能加载,因为它们包含许多图像和声音文件。如果发生这种情况时,屏幕上什么也看不见,玩家可能会觉得游戏没有运行,并试图再次启动它。要设置无边框显示,调用set_mode时使用NOFRAME标志。例如,下面的行将创建一个“裸”窗口:

screen = pygame.display.set_mode(SCREEN_SIZE, RESIZABLE, 32)

附加显示标志

在对set_mode的调用中还可以使用一些标志。我认为它们是高级的,因为如果使用不当,它们会影响性能,或者在某些平台上导致兼容性问题。通常最好使用值0显示窗口,使用值FULLSCREEN显示全屏,以确保你的游戏能在所有平台上正常运行。也就是说,如果你知道你在做什么,你可以为额外的性能设置一些高级标志。做实验也没有坏处(不会伤害你的电脑)。

如果你设置了HWSURFACE标志,它将创建一个所谓的硬件表面 。这是一种特殊的显示表面,存储在图形卡的内存中。它只能与FULLSCREEN标志结合使用,比如:

screen = pygame.display.set_mode(SCREEN_SIZE, HWSURFACE | FULLSCREEN, 32)

硬件表面可以比在系统(常规)内存中创建的表面更快,因为它们可以利用图形卡的更多功能来加速位块传输。硬件表面的缺点是它们在所有平台上都没有得到很好的支持。它们倾向于在 Windows 平台上工作,但在其他平台上就不那么好了。硬件表面也受益于DOUBLEBUF标志。这有效地创建了两个硬件表面,但是在任一时刻只有一个是可见的。以下代码行创建一个双缓冲硬件表面:

screen = pygame.display.set_mode(SCREEN_SIZE, DOUBLEBUF | HWSURFACE | FULLSCREEN, 32)

通常当你调用pygame.display.update()`时,整个屏幕会从内存复制到显示器上——这需要一点时间。双缓冲表面允许你立即切换到新的屏幕,从而使你的游戏运行得更快。

您可以使用的最后一个显示标志是OPENGL。OpenGL ( www.opengl.org/)是一个图形库,它使用几乎每个显卡上都有的 3D 图形加速器。使用这个标志的缺点是你将不能再使用 Pygame 的 2D 图形功能。我们将在第九章的中介绍使用 OpenGL 创建 3D。

Image 注意如果使用双缓冲显示器,应该调用pygame.display.flip()而不是pygame.display.update()。这是即时显示切换,而不是复制屏幕数据。

使用字体模块

我承诺将介绍我们在事件队列脚本中使用的字体模块。在屏幕上绘制文本的能力确实有助于测试脚本;您可能还需要它来显示游戏说明、菜单选项等等。字体模块使用 TrueType 字体(TTF),这种字体在大多数系统上用于呈现高质量的平滑文本。您的计算机上会安装许多这样的字体,可供字体模块使用。

要使用字体,必须先创建一个Font对象。最简单的方法是使用pygame.font.SysFont,它使用你已经安装在电脑上的一种字体。下面一行为 Arial 字体(一种易于阅读的通用字体)创建了一个Font对象:

my_font = pygame.font.SysFont("arial", 16)

第一个参数是要创建的字体名称,下一个参数以像素为单位指定字体大小。Pygame 将在您安装的字体中查找名称为“arial”的字体;如果没有找到,将返回默认字体。您可以通过调用pygame.font.get_fonts()获得系统上安装的字体列表。也可以通过调用pygame.font.Font直接从.ttf文件中创建字体,它需要一个文件名。下面一行加载文件my_font.ttf并返回一个Font对象:

my_font = pygame.font.Font("my_font.ttf", 16)

一旦你创建了一个Font对象,你可以用它来渲染文本到一个新的表面。要渲染文本,使用Font对象的render成员函数。它会创建一个包含文本的新表面,然后您可以将它 blit 到显示器上。以下代码行呈现一段文本并返回一个新表面:

text_surface = my_font.render("Pygame is cool!", True, (0,0,0), (255, 255, 255))

render的第一个参数是你要渲染的文本。它必须是一条线;如果您想要多行,您将不得不打破字符串,并使用多个渲染调用。第二个参数是一个布尔值(TrueFalse),用于启用抗锯齿文本。如果设置为True,文本将具有现代、平滑的外观;否则,它会显得更加像素化。render的下两个参数是文本颜色,后面是背景颜色。背景是可选的,如果你省略它(或者设置为None,背景将是透明的。

为了完成对字体模块的介绍,让我们编写一个小脚本来将我的名字渲染到一个表面上,并保存为一个图像。请随意更改您自己的名字。如果你修改清单 3-6 中的第一行,它就会这样做。

清单 3-6 。将您的名字写入图像文件

import pygame
my_name = "Harrison Kinsley"
pygame.init()
my_font = pygame.font.SysFont("arial", 64)
name_surface = my_font.render(my_name, True, (0, 0, 0), (255, 255, 255))
pygame.image.save(name_surface, "name.png")

这个脚本非常简单,我们甚至不需要创建一个显示!当您运行清单 3-6 中的时,您不会在屏幕上看到太多变化,但是代码会在与脚本相同的位置创建一个名为 name.png 的图像文件。您可以用任何图像浏览器软件打开该文件。将表面保存到一个文件是通过 pygame.image 模块完成的,我们将在下一章中介绍。

字体模块提供了其他功能以及Font对象,您可能偶尔需要使用。它们主要是信息性的,旨在检索关于字体的各种信息。有一些函数可以模拟粗体和斜体文本,但是最好使用专用的粗体或斜体字体。有关字体模块的完整信息,请参见www.pygame.org/docs/ref/font.html中的文档。

Image 注意安装的字体因电脑而异,你不能总是依赖于现有的特定字体。如果 Pygame 没有找到你要求的字体,它将使用一个默认的字体,看起来可能不一样。解决方案是分发。ttf 文件,但是要确保你得到了字体作者的许可!对于免费分发的字体,你可以使用来自比特流 Vera 家族的东西(http://en.wikipedia.org/wiki/Bitstream_Vera)。

Pygame 在行动

当我还是个孩子的时候,“scrolly 消息”在业余图形程序员中非常流行。scrolly 消息,或者现在所知的 marquee ,就是从右向左滑过屏幕的文本。清单 3-7 是 scrolly 消息的 Pygame 实现。它也不是没有缺点,其中最主要的是它的移动速度不一致,在不同的计算机上可能更快或更慢。这是一个你将在下一章学习如何解决的问题。

这个脚本的大部分现在应该都很熟悉了,所以我就不分解了。尝试调整代码以产生不同的结果。您可能还想插入自己选择的文本,这可以通过修改脚本开头的message字符串来实现。

清单 3-7 。Scrolly 消息脚本

import pygame
from pygame.locals import *
from sys import exit

background_image_filename = 'sushiplate.jpg'
SCREEN_SIZE = (640, 480)
message="    This is a demonstration of the scrolly message script. "

pygame.init()
screen = pygame.display.set_mode(SCREEN_SIZE)

font = pygame.font.SysFont("arial", 80);
text_surface = font.render(message, True, (0, 0, 255))

x = 0
y = ( SCREEN_SIZE[1] - text_surface.get_height() ) / 2

background = pygame.image.load(background_image_filename).convert()

while True:

    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            exit()

    screen.blit(background, (0,0))

    x-= 2
    if x < -text_surface.get_width():
        x = 0

    screen.blit(text_surface, (x, y))
    screen.blit(text_surface, (x+text_surface.get_width(), y))
    pygame.display.update()

摘要

Pygame 是一个构建游戏的强大平台。它由许多子模块组成,用于各种游戏相关的任务。Pygame 在大量平台上同样运行良好。所有主要的桌面系统甚至一些控制台都有端口,因此您可以在自己喜欢的平台上开发游戏,并在另一个平台上玩。

我们制作了一个 Hello World 脚本,演示了启动 Pygame、创建显示、接收事件,然后绘制到屏幕上的基本操作,这些步骤将在您创建更复杂的游戏和演示时使用。如果你曾经用 C 或 C++ 做过游戏编程,你会体会到代码的简单性,尤其是创建显示的一行程序。

我们探讨了在创建显示时可以使用的标志,这些标志可以提高性能或增加功能。最好是禁用这些标志,至少在你对 Pygame 和计算机图形更加熟悉之前。您应该会发现默认设置仍然可以提供出色的性能。

您还学习了如何管理事件队列来处理 Pygame 发送给您的各种事件,甚至学习了如何创建自定义事件。清单 2-3 让您确切地看到生成了什么事件以及它们包含的信息。当您尝试自己的脚本时,您会发现这个清单是一个方便的工具。

本章涵盖了使用 Pygame 需要的所有样板代码。下一章研究图形、运动和动画。`