第一章:安装 Python 和 Pygame
开始之前您应该知道的事情
在阅读本书之前,如果您了解一些 Python 编程知识(或者知道如何使用 Python 之外的其他语言进行编程),可能会有所帮助;但是即使您没有,您仍然可以阅读本书。编程并不像人们想象的那么难。如果您遇到问题,可以在线阅读免费书籍“使用 Python 发明自己的电脑游戏”http://inventwithpython.com,或者在 Invent with Python 维基 http://inventwithpython.com/wiki 上查找您觉得困惑的主题。
在阅读本书之前,您不需要知道如何使用 Pygame 库。下一章是关于 Pygame 的所有主要特性和功能的简要教程。
如果您还没有阅读第一本书并在计算机上安装了 Python 和 Pygame,安装说明在本章中。如果您已经安装了这两者,那么您可以跳过本章。
下载和安装 Python
在我们开始编程之前,您需要在计算机上安装名为 Python 解释器的软件。(在这里,您可能需要向成年人寻求帮助。)解释器是一个程序,它能理解您将用 Python 语言编写(或者说,打出来)的指令。没有解释器,您的计算机将无法运行 Python 程序。我们从现在开始将“Python 解释器”简称为“Python”。
Python 解释器软件可以从 Python 编程语言的官方网站 http://www.python.org 下载。您可能需要他人的帮助来下载和安装 Python 软件。安装方式略有不同,取决于您的计算机操作系统是 Windows、Mac OS X 还是 Ubuntu 等 Linux 操作系统。您还可以在网上找到人们在计算机上安装 Python 软件的视频,网址是 http://invpy.com/installing。
Windows 说明
当您访问 http://python.org 时,您应该会看到左侧的链接列表(如“关于”,“新闻”,“文档”,“下载”等)。单击下载链接转到下载页面,然后查找名为“Python 3.2 Windows Installer(Windows 二进制文件-不包括源代码)”的文件,并单击其链接以下载 Windows 的 Python。
双击刚刚下载的python-3.2.msi文件以启动 Python 安装程序。(如果没有启动,请尝试右键单击文件并选择安装。)一旦安装程序启动,只需不断单击下一步按钮,并在安装程序中接受选择(无需进行任何更改)。安装完成后,单击完成。
Mac OS X 说明
Mac OS X 10.5 预装了由苹果提供的 Python 2.5.1。目前,Pygame 仅支持 Python 2 而不支持 Python 3。但是,本书中的程序适用于 Python 2 和 3。
Python 网站还提供了有关在 Mac 上使用 Python 的一些额外信息,网址是 http://docs.python.org/dev/using/mac.html。
Ubuntu 和 Linux 说明
Linux 的 Pygame 也仅支持 Python 2,不支持 Python 3。如果您的操作系统是 Ubuntu,您可以通过打开终端窗口(从桌面单击应用程序>附件>终端)并输入sudo apt-get install python2.7
然后按 Enter 来安装 Python。您需要输入根密码来安装 Python,因此如果您不知道密码,请向计算机所有者询问他们输入密码。
您还需要安装 IDLE 软件。从终端中输入“sudo apt-get install idle
”。还需要根密码来安装 IDLE(如果您不知道密码,请让计算机的所有者为您输入密码)。
开始 Python
我们将使用 IDLE 软件来输入和运行程序。IDLE 代表Interactive DeveLopment Environment。开发环境是一种使编写 Python 程序变得容易的软件,就像文字处理软件使编写书籍变得容易一样。
如果您的操作系统是 Windows XP,您应该可以通过单击“开始”按钮,然后选择 Programs,Python 3.1,IDLE(Python GUI)来运行 Python。对于 Windows Vista 或 Windows 7,只需单击左下角的 Windows 按钮,输入IDLE
,然后选择“IDLE(Python GUI)”。
如果您的操作系统是 Max OS X,请通过打开 Finder 窗口并单击 Applications,然后单击 Python 3.2,然后单击 IDLE 图标来启动 IDLE。
如果您的操作系统是 Ubuntu 或 Linux,请通过打开终端窗口,然后输入“idle3
”并按 Enter 来启动 IDLE。您还可以单击屏幕顶部的 Applications,然后选择 Programming,然后选择 IDLE 3。
当您首次运行 IDLE 时出现的窗口称为交互式 shell。Shell 是一个允许您向计算机输入指令的程序。Python shell 允许您输入 Python 指令,并且 shell 会将这些指令发送给 Python 解释器执行。
安装 Pygame
Pygame 不随 Python 一起提供。与 Python 一样,Pygame 也是免费提供的。您需要下载并安装 Pygame,就像下载和安装 Python 解释器一样简单。在 Web 浏览器中,转到网址 http://pygame.org,然后单击网站左侧的“Downloads”链接。本书假设您使用的是 Windows 操作系统,但 Pygame 对每个操作系统都是相同的。您需要为您的操作系统和已安装的 Python 版本下载 Pygame 安装程序。
您不想下载 Pygame 的“源代码”,而是下载适用于您操作系统的 Pygame“二进制文件”。对于 Windows,请下载 pygame-1.9.1.win32-py3.2.msi 文件。(这是适用于 Windows 上 Python 3.2 的 Pygame。如果您安装了不同版本的 Python(如 2.7 或 2.6),请下载适用于您 Python 版本的.msi 文件。)本书编写时的 Pygame 当前版本为 1.9.1。如果您在网站上看到更新版本,请下载并安装更新的 Pygame。
对于 Mac OS X,请下载您所拥有的 Python 版本的.zip 或.dmg 文件并运行它。
对于 Linux,打开终端并运行sudo apt-get install python-pygame
。
在 Windows 上,双击下载的文件以安装 Pygame。要检查 Pygame 是否正确安装,请在交互式 shell 中输入以下内容:
>>> import pygame
如果按 Enter 键后没有任何显示,那么您就知道 Pygame 已成功安装。如果出现错误 ImportError: No module named pygame,则尝试重新安装 Pygame(并确保您正确输入了import pygame
)。
本章包含五个小程序,演示了如何使用 Pygame 提供的不同功能。在最后一章中,您将使用这些功能来编写使用 Pygame 编写的 Python 完整游戏。
有关如何安装 Pygame 的视频教程可在本书网站 http://invpy.com/videos 上找到。
如何使用本书
“使用 Python 和 Pygame 制作游戏”与其他编程书籍不同,因为它专注于几个游戏程序的完整源代码。本书不是教授编程概念,然后让您自己想出如何使用这些概念制作程序,而是向您展示一些程序,然后解释它们是如何组合在一起的。
一般来说,你应该按顺序阅读这些章节。在这些游戏中有许多概念会反复使用,它们只在它们首次出现的第一个游戏中详细解释。但如果有一个你觉得有趣的游戏,可以直接跳到那一章。如果你超前了,可以随时回头阅读之前的章节。
特色程序
每章都专注于一个游戏程序,并解释代码的不同部分是如何工作的。通过逐行从本书中输入代码来复制这些程序非常有帮助。
然而,你也可以从本书的网站下载源代码文件。在网页浏览器中,访问网址 http://invpy.com/source,并按照说明下载源代码文件。但是自己输入代码确实有助于更好地学习代码。
下载图形和声音文件
虽然你可以直接输入本书中的代码,但你需要从 http://invpy.com/downloads 下载本书中游戏使用的图形和声音文件。确保这些图像和声音文件位于与.py Python 文件相同的文件夹中,否则你的 Python 程序将无法找到这些文件。
行号和空格
在输入源代码时,不要输入每行开头出现的行号。例如,如果你在书中看到这样:
number = random.randint(1, 20) spam = 42 print('Hello world!')
你不需要在左侧输入1.
,或者紧随其后的空格。只需像这样输入:
number = random.randint(1, 20) spam = 42 print('Hello world!')
这些数字仅用于本书引用代码中特定行。它们不是实际程序的一部分。
除了行号之外,确保按照代码的原样输入。注意到一些行不是从页面的最左边开始的,而是缩进了四个或八个或更多的空格。确保在每行开头输入正确数量的空格。(由于 IDLE 中的每个字符都是相同宽度,你可以通过计算你正在查看的行上方或下方的字符数量来计算空格的数量。)
例如,在下面的代码中,你可以看到第二行缩进了四个空格,因为上一行的四个字符(whil
)在缩进的空格上方。第三行再次缩进了四个空格(上一行的四个字符if n
在第三行的缩进空格上方):
while spam < 10: if number == 42: print('Hello')
本书中的文本换行
本书中的一些代码行太长,无法在一页上完全显示,代码的文本会换行到下一行。当你将这些行输入到文件编辑器中时,输入所有代码在一行上而不要按回车键。
你可以通过查看代码左侧的行号来判断新的一行开始了。例如,下面的代码只有两行,即使第一行换行了:
print('This is the first line! xxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxx’) print('This is the second line, not the third line.')
在线检查你的代码
本书中的一些程序有点长。虽然通过逐行输入这些程序的源代码来学习 Python 非常有帮助,但你可能会因为打错字而导致程序崩溃。可能不明显打错字的位置在哪里。
你可以将源代码的文本复制粘贴到本书网站上的在线 diff 工具中。diff 工具将显示书中源代码与你输入的源代码之间的任何差异。这是一种查找程序中任何打错字的简单方法。
复制和粘贴文本是一项非常有用的计算机技能,特别是对于计算机编程。本书的网站上有关复制和粘贴的视频教程,请访问 http://invpy.com/copypaste。
在线 diff 工具位于此网页:http://invpy.com/diff/pygame。本书的网站上也有关于如何使用这个工具的视频教程。
更多信息链接请访问 http://invpy.com
关于编程,有很多东西可以学习。但你现在不需要学习所有这些。在这本书中有几个地方,你可能想学习这些额外的细节和解释,但如果我把它们包括在这本书中,那么会增加很多页。如果这本更大更重的书意外地掉在你身上,这些额外的页的重量会压垮你,导致死亡。相反,我在这本书中包含了“更多信息”链接,你可以在这本书的网站上查看。你不必阅读这些额外的信息来理解这本书中的任何内容,但如果你好奇的话,它们就在那里。这些(以及其他)链接已经被缩短,并以 http://invpy.com 开头。
所有这些“更多信息”链接中的信息也可以从 http://invpy.com/pygamemoreinfo 下载。
即使这本书并不危险地沉重,请不要让它无论如何掉在你身上。
第二章:Pygame 基础
原文:
inventwithpython.com/pygame/chapter2.html
译者:飞龙
就像 Python 自带了几个模块,比如random
、math
或time
,为你的程序提供了额外的功能一样,Pygame 框架包括了几个模块,提供了绘制图形、播放声音、处理鼠标输入等功能。
本章将介绍 Pygame 提供的基本模块和函数,并假设你已经了解基本的 Python 编程。如果你对一些编程概念有困难,你可以在invpy.com/book
上在线阅读“用 Python 发明你自己的电脑游戏”一书。这本书是面向完全初学者的编程。
“Invent with Python”这本书还有几章涵盖了 Pygame。你可以在invpy.com/chap17
上在线阅读它们。
一旦你了解了更多关于 Pygame 的知识,你可以从pygame.org/docs
的在线文档中查看 Pygame 提供的其他模块。
GUI vs. CLI
你可以使用 Python 的内置函数编写的 Python 程序只通过print()
和input()
函数处理文本。你的程序可以在屏幕上显示文本,并让用户从键盘输入文本。这种类型的程序具有命令行界面,或 CLI(发音类似于“climb”的第一个部分,押韵“sky”)。这些程序有一定的局限性,因为它们不能显示图形,有颜色,或使用鼠标。这些 CLI 程序只能通过input()
函数从键盘获取输入,即使用户必须在程序能够响应输入之前按下 Enter。这意味着实时(即,继续运行代码而不等待用户)动作游戏是不可能制作的。
Pygame 提供了创建图形用户界面(GUI)程序的函数。与基于文本的 CLI 不同,具有基于图形的 GUI 的程序可以显示带有图像和颜色的窗口。
使用 Pygame 制作 Hello World 的源代码
我们用 Pygame 制作的第一个程序是一个小程序,它在屏幕上显示“Hello World!”的窗口。点击 IDLE 的文件菜单,然后新建窗口,打开一个新的文件编辑器窗口。在 IDLE 的文件编辑器中输入以下代码,并将其保存为blankpygame.py。然后通过按下F5或从文件编辑器顶部的菜单中选择运行 > 运行模块来运行程序。
记住,不要在每行开头输入数字或句号(这只是本书的参考)。
import pygame, sys from pygame.locals import * pygame.init() DISPLAYSURF = pygame.display.set_mode((400, 300)) pygame.display.set_caption('Hello World!') while True: # main game loop for event in pygame.event.get(): if event.type == QUIT: pygame.quit() sys.exit() pygame.display.update()
当你运行这个程序时,会出现一个黑色的窗口,就像这样:
耶!你刚刚制作了世界上最无聊的视频游戏!它只是一个空白窗口,窗口顶部显示着“Hello World!”(在称为窗口标题栏的地方,它包含标题文本)。但创建窗口是制作图形游戏的第一步。当你点击窗口角落的 X 按钮时,程序将结束,窗口将消失。
调用print()
函数在窗口中显示文本是行不通的,因为print()
是用于 CLI 程序的函数。input()
也是一样,用于从用户那里获取键盘输入。Pygame 使用其他函数进行输入和输出,这些将在本章后面进行解释。现在,让我们更详细地看一下“Hello World”程序中的每一行。
设置 Pygame 程序
“Hello World”程序的前几行代码是几乎每个使用 Pygame 的程序的开头。
import pygame, sys
第 1 行是一个简单的import
语句,导入了pygame
和sys
模块,以便我们的程序可以使用它们中的函数。所有与图形、声音和其他 Pygame 功能相关的函数都在pygame
模块中。
请注意,当导入pygame
模块时,还会自动导入pygame
模块中的所有模块,如pygame.images
和pygame.mixer.music
。不需要使用额外的import
语句导入这些模块内的模块。
from pygame.locals import *
第 2 行也是一个import
语句。但是,它使用的是from modulename import *
格式,而不是import modulename
格式。通常,如果要调用模块中的函数,必须在导入模块后使用modulename.functionname()
格式。但是,使用from modulename import *
,可以省略modulename.
部分,直接使用functionname()
(就像 Python 的内置函数一样)。
我们之所以使用这种形式的import
语句导入pygame.locals
,是因为pygame.locals
包含了几个常量变量,很容易识别为pygame.locals
模块中的变量,而不需要在它们前面加上pygame.locals.
。对于所有其他模块,通常要使用常规的import modulename
格式。(有关为什么要这样做的更多信息,请参阅invpy.com/namespaces
。)
pygame.init()
第 4 行是pygame.init()
函数调用,它总是需要在导入pygame
模块后调用,并在调用任何其他 Pygame 函数之前调用。你不需要知道这个函数做了什么,你只需要知道它需要首先被调用,以便许多 Pygame 函数能够工作。如果你看到类似 pygame.error: font not initialized 的错误消息,请检查是否忘记在程序开头调用pygame.init()
。
DISPLAYSURF = pygame.display.set_mode((400, 300))
第 5 行是对pygame.display.set_mode()
函数的调用,该函数返回窗口的pygame.Surface
对象。(Surface 对象将在本章后面进行描述。)请注意,我们向函数传递了一个包含两个整数的元组值:(400, 300)
。这个元组告诉set_mode()
函数窗口的宽度和高度分别是多少像素。(400, 300)
将创建一个宽度为 400 像素,高度为 300 像素的窗口。
记住要向set_mode()
传递一个包含两个整数的元组,而不仅仅是两个整数本身。调用函数的正确方式是这样的:pygame.display.set_mode((400, 300))。像 pygame.display.set_mode(400, 300)这样的函数调用将导致以下错误:TypeError: argument 1 must be 2-item sequence, not int。
返回的pygame.Surface
对象(我们简称为 Surface 对象)存储在名为DISPLAYSURF
的变量中。
pygame.display.set_caption('Hello World!')
第 6 行通过调用pygame.display.set_caption()
函数设置窗口顶部将显示的标题文本。在这个函数调用中传递了字符串值'Hello World!'
,以使该文本显示为标题。
游戏循环和游戏状态
while True: # main game loop for event in pygame.event.get():
第 7 行是一个while
循环,其条件只是简单的值True
。这意味着它永远不会因为条件评估为False
而退出。程序执行将退出循环的唯一方法是执行break
语句(将执行移动到循环后的第一行)或sys.exit()
(终止程序)。如果这样的循环在函数内部,return
语句也会将执行移出循环(以及函数本身)。
本书中的游戏都包含这样的while True
循环,以及一个称为“主游戏循环”的注释。游戏循环(也称为主循环)是一个循环,其中代码执行三件事:
- 处理事件。
- 更新游戏状态。
- 将游戏状态绘制到屏幕上。
游戏状态简单地指的是游戏程序中所有变量的一组值。在许多游戏中,游戏状态包括跟踪玩家健康和位置的变量的值,任何敌人的健康和位置,标记在棋盘上的标记,得分,或者轮到谁了。每当发生像玩家受到伤害(降低他们的健康值)或敌人移动到某个地方,或者游戏世界发生了什么事情,我们就说游戏状态已经改变了。
如果你玩过一个可以保存的游戏,那么“保存状态”就是你保存时的游戏状态。在大多数游戏中,暂停游戏会阻止游戏状态的改变。
由于游戏状态通常是响应事件(如鼠标点击或键盘按键)或时间流逝而更新的,游戏循环会不断地每秒检查和重新检查是否有新事件发生。主循环中有代码来查看已经创建了哪些事件(使用 Pygame,这是通过调用pygame.event.get()
函数来完成的)。主循环还有根据已创建的事件更新游戏状态的代码。这通常被称为事件处理。
pygame.event.Event
对象
每当用户执行一些动作(它们在本章后面列出)比如按键盘键或在程序窗口上移动鼠标时,Pygame 库会创建一个pygame.event.Event
对象来记录这个“事件”。(这是一种称为Event
的对象,存在于event
模块中,而event
模块本身位于pygame
模块中。)我们可以通过调用pygame.event.get()
函数来找出发生了哪些事件,它会返回一个pygame.event.Event
对象的列表(我们简称为 Event 对象)。
Event 对象的列表将是自上次调用pygame.event.get()
函数以来发生的每个事件。(或者,如果从未调用过pygame.event.get()
,则是自程序启动以来发生的事件。)
while True: # main game loop for event in pygame.event.get():
第 8 行是一个for
循环,它将遍历由pygame.event.get()
返回的 Event 对象列表。在每次循环中,一个名为event
的变量将被赋予该列表中下一个事件对象的值。从pygame.event.get()
返回的 Event 对象列表将按事件发生的顺序排列。如果用户先点击鼠标,然后按键盘键,那么鼠标点击的 Event 对象将是列表中的第一项,键盘按键的 Event 对象将是第二项。如果没有发生事件,那么pygame.event.get()
将返回一个空列表。
QUIT
事件和pygame.quit()
函数
if event.type == QUIT: pygame.quit() sys.exit()
Event
对象有一个成员变量(也称为属性)名为type
,它告诉我们对象代表什么类型的事件。Pygame 在pygame.locals
模块中为每种可能的类型都有一个常量变量。第 9 行检查 Event 对象的type
是否等于常量QUIT
。请记住,由于我们使用了from pygame.locals import *
形式的import
语句,我们只需要输入QUIT
而不是pygame.locals.QUIT
。
如果 Event 对象是一个退出事件,那么会调用pygame.quit()
和sys.exit()
函数。pygame.quit()
函数有点像pygame.init()
函数的相反:它运行的代码会停用 Pygame 库。在终止程序之前,你的程序应该始终调用pygame.quit()
而不是sys.exit()
。通常情况下并不重要,因为 Python 在程序退出时会自动关闭它。但是在 IDLE 中有一个错误,如果在调用pygame.quit()
之前终止 Pygame 程序,IDLE 会挂起。
由于我们没有运行代码来处理其他类型的事件对象的if
语句,因此当用户点击鼠标,按键盘键,或者导致任何其他类型的事件对象被创建时,没有事件处理代码。用户可以做一些事情来创建这些事件对象,但这并不会改变程序中的任何内容,因为程序没有这些类型事件对象的事件处理代码。在第 8 行的for
循环处理了pygame.event.get()
返回的所有事件对象后,程序执行将继续到第 12 行。
pygame.display.update()
第 12 行调用pygame.display.update()
函数,该函数将pygame.display.set_mode()
返回的 Surface 对象绘制到屏幕上(记住我们将这个对象存储在DISPLAYSURF
变量中)。由于 Surface 对象没有改变(例如,通过本章后面解释的一些绘图函数),每次调用pygame.display.update()
时,相同的黑色图像都会重新绘制到屏幕上。
这就是整个程序。在第 12 行之后,无限的while
循环再次从头开始。这个程序除了在屏幕上显示一个黑色窗口,不断检查QUIT
事件,然后一遍又一遍地重新绘制未改变的黑色窗口之外,什么也不做。让我们学习如何在这个窗口上显示有趣的东西,而不仅仅是黑色,学习关于像素、Surface 对象、颜色对象、Rect 对象和 Pygame 绘图函数。
像素坐标
“Hello World”程序创建的窗口只是由屏幕上的小方点像素组成。每个像素最初是黑色的,但可以设置为不同的颜色。想象一下,如果我们有一个 8x8 像素的 Surface 对象,而不是一个宽 400 像素,高 300 像素的 Surface 对象。如果将这个微小的 8x8 Surface 放大,使每个像素看起来像网格中的一个正方形,并为 X 和 Y 轴添加数字,那么它的一个良好的表示可能看起来像这样:
我们可以使用笛卡尔坐标系来引用特定的像素。X 轴的每一列和 Y 轴的每一行都将有一个从 0 到 7 的整数“地址”,以便我们可以通过指定 X 和 Y 轴整数来定位任何像素。
例如,在上面的 8x8 图像中,我们可以看到 XY 坐标(4, 0)、(2, 2)、(0, 5)和(5, 6)的像素被涂成黑色,坐标(2, 4)的像素被涂成灰色,而所有其他像素都被涂成白色。XY 坐标也称为点。如果您上过数学课并学过笛卡尔坐标,您可能会注意到 Y 轴从顶部开始为 0,然后向下增加,而不是向上增加。这就是 Pygame 中笛卡尔坐标的工作方式(几乎所有编程语言都是如此)。
Pygame 框架通常将笛卡尔坐标表示为两个整数的元组,例如(4, 0)或(2, 2)。第一个整数是 X 坐标,第二个是 Y 坐标。(笛卡尔坐标在“用 Python 编写自己的计算机游戏”第 12 章中有更详细的介绍,网址为invpy.com/chap12
)
关于函数、方法、构造函数和模块中的函数的提醒(以及它们之间的区别)
函数和方法几乎是一样的。它们都可以被调用来执行其中的代码。函数和方法之间的区别在于方法总是附加到一个对象上。通常方法会改变关于那个特定对象的某些东西(您可以将附加的对象看作是传递给方法的一种永久参数)。
这是一个名为foo()
的函数调用:
foo()
这是一个方法调用,也是名为foo()
的方法调用,它附加到一个存储在名为duckie
的变量中的对象上:
duckie.foo()
在模块内部调用函数可能看起来像是方法调用。要区分它们,你需要查看第一个名称,看它是一个模块的名称还是一个包含对象的变量的名称。你可以通过sys.exit()
是一个模块内部函数调用来判断,因为程序顶部会有一个像import sys
这样的导入语句。
构造函数与普通函数调用相同,只是它的返回值是一个新对象。仅仅通过查看源代码,函数和构造函数看起来是一样的。构造函数(有时简称为“构造函数”或者“ctor”(“see-tor”))只是给返回新对象的函数起的一个名字。但通常构造函数以大写字母开头。这就是为什么当你编写自己的程序时,函数名应该只以小写字母开头。
例如,pygame.Rect()
和pygame.Surface()
都是pygame
模块内的构造函数,返回新的 Rect 和 Surface 对象。(这些对象将在后面描述。)
以下是一个函数调用、方法调用和在模块内部调用函数的示例:
import whammy fizzy() egg = Wombat() egg.bluhbluh() whammy.spam()
尽管这些名称都是虚构的,但你可以分辨出哪个是函数调用、方法调用和在方法内部调用函数。名称whammy
指的是一个模块,因为你可以在第一行看到它被导入。fizzy
名称前面没有任何东西,后面跟着括号,所以你知道它是一个函数调用。
Wombat()
也是一个函数调用,这种情况下它是一个返回对象的构造函数。(它以大写字母开头并不保证它是一个构造函数而不是普通函数,但这是一个安全的打赌。)对象存储在一个名为egg
的变量中。egg.bluhbluh()
调用是一个方法调用,你可以通过bluhbluh
附加到一个包含对象的变量上来判断。
同时,whammy.spam()
是一个函数调用,而不是方法调用。你可以通过它不是一个方法来判断,因为whammy
的名称在之前被导入为一个模块。
Surface 对象和窗口
Surface 对象是代表矩形 2D 图像的对象。Surface 对象的像素可以通过调用 Pygame 绘图函数(本章后面描述)来改变,然后显示在屏幕上。窗口边框、标题栏和按钮不是显示 Surface 对象的一部分。
特别是,pygame.display.set_mode()
返回的 Surface 对象被称为显示 Surface。在显示 Surface 对象上绘制的任何东西都将在调用pygame.display.update()
函数时显示在窗口上。在 Surface 对象上绘制(它只存在于计算机内存中)比将 Surface 对象绘制到计算机屏幕上要快得多。计算机内存比显示器上的像素更快地改变。
通常你的程序会向 Surface 对象绘制几种不同的东西。一旦你在显示 Surface 对象上绘制完这次游戏循环的所有东西(称为一帧,就像暂停的 DVD 上的静止图像一样),它就可以被绘制到屏幕上。计算机可以非常快地绘制帧,我们的程序通常会以每秒 30 帧(即 30 FPS)的速度运行。这被称为“帧率”,本章后面会解释。
在本章后面将介绍在 Surface 对象上绘制的“基本绘图函数”和“绘制图像”部分。
颜色
光有三种原色:红色、绿色和蓝色。(红色、蓝色和黄色是颜料和颜料的原色,但计算机显示器使用的是光,而不是颜料。)通过组合这三种颜色的不同量,你可以形成任何其他颜色。在 Pygame 中,我们用三个整数的元组表示颜色。元组中的第一个值是颜色中的红色量。整数值为0
表示这种颜色中没有红色,值为255
表示这种颜色中有最大量的红色。第二个值是绿色,第三个值是蓝色。用于表示颜色的这三个整数的元组通常被称为 RGB 值。
因为你可以使用每个三原色的0
到255
的任意组合,这意味着 Pygame 可以绘制 16,777,216 种不同的颜色(即 256 x 256 x 256 种颜色)。但是,如果尝试使用大于255
的数字或负数,你将会得到一个看起来像“ValueError: invalid color argument
”的错误。
例如,我们将创建元组(0, 0, 0)并将其存储在名为BLACK
的变量中。没有红色、绿色或蓝色,得到的颜色是完全黑色。黑色是任何颜色的缺失。元组(255, 255, 255)代表最大量的红色、绿色和蓝色,得到白色。白色是红色、绿色和蓝色的完全组合。元组(255, 0, 0)
代表最大量的红色但没有绿色和蓝色,所以得到的颜色是红色。同样,(0, 255, 0)
是绿色,(0, 0, 255)
是蓝色。
你可以混合红色、绿色和蓝色的量来形成其他颜色。以下是一些常见颜色的 RGB 值:
颜色 | RGB 值 |
水绿色 | (0, 255, 255) |
黑色 | (0, 0, 0) |
蓝色 | (0, 0, 255) |
紫红色 | (255, 0, 255) |
灰色 | (128, 128, 128) |
绿色 | (0, 128, 0) |
酸橙色 | (0, 255, 0) |
栗色 | (128, 0, 0) |
海军蓝 | (0, 0, 128) |
橄榄色 | (128, 128, 0) |
紫色 | (128, 0, 128) |
红色 | (255, 0, 0) |
银色 | (192, 192, 192) |
青色 | (0, 128, 128) |
白色 | (255, 255, 255) |
黄色 | (255, 255, 0) |
透明颜色
当你透过一个有深红色色调的玻璃窗户看时,所有背后的颜色都会被添加上红色色调。你可以通过在颜色值中添加第四个0
到255
的整数值来模仿这种效果。
这个值被称为 alpha 值。它是颜色不透明程度的度量。通常当你在表面对象上绘制一个像素时,新颜色会完全替换已经存在的颜色。但是对于具有 alpha 值的颜色,你可以只是给已经存在的颜色添加一个有色的色调。
例如,这个由三个整数组成的元组是绿色的:(0, 255, 0)
。但是如果我们添加一个第四个整数作为 alpha 值,我们可以得到一个半透明的绿色:(0, 255, 0, 128)
。alpha 值为255
表示完全不透明(即完全不透明)。颜色(0, 255, 0)
和(0, 255, 0, 255)
看起来完全一样。alpha 值为0
表示颜色是完全透明的。如果你在表面对象上绘制任何具有 alpha 值为0
的颜色,它将没有任何效果,因为这种颜色是完全透明和不可见的。
为了使用透明颜色进行绘制,你必须使用convert_alpha()
方法创建一个 Surface 对象。例如,以下代码创建了一个可以在其上绘制透明颜色的 Surface 对象:
anotherSurface = DISPLAYSURF.convert_alpha()
一旦在存储在anotherSurface
中的 Surface 对象上绘制了东西,那么anotherSurface
就可以“blitted”(即复制)到DISPLAYSURF
上,这样它就会出现在屏幕上。(见本章后面的“使用pygame.image.load()
和blit()
绘制图像”部分。)
需要注意的是,不能在未从convert_alpha()
调用返回的 Surface 对象上使用透明颜色,包括从pygame.display.set_mode()
返回的显示 Surface。
如果我们要创建一个颜色元组来绘制传说中的隐形粉红独角兽,我们将使用(255, 192, 192, 0)
,这样看起来完全看不见,就像任何其他 alpha 值为0
的颜色一样。毕竟,它是隐形的。
(上面是隐形粉红独角兽的绘图截图。)
pygame.Color
对象
您需要知道如何表示颜色,因为 Pygame 的绘图函数需要知道您想要用什么颜色绘制。三个或四个整数的元组是一种方法。另一种方法是作为pygame.Color
对象。您可以通过调用pygame.Color()
构造函数并传递三个或四个整数来创建 Color 对象。您可以像存储元组一样将此 Color 对象存储在变量中。尝试在交互式 shell 中输入以下内容:
>>> import pygame >>> pygame.Color(255, 0, 0) (255, 0, 0, 255) >>> myColor = pygame.Color(255, 0, 0, 128) >>> myColor == (255, 0, 0, 128) True >>>
Pygame 中的任何绘图函数(我们稍后将学习)都可以接受颜色的元组形式或 Color 对象形式作为参数。即使它们是不同的数据类型,如果它们都表示相同的颜色,Color 对象等于四个整数的元组(就像42 == 42.0
将评估为True
一样)。
现在您知道如何表示颜色(作为pygame.Color
对象或三个或四个整数的元组,用于红色,绿色,蓝色,可选的 alpha)和坐标(作为两个整数的元组,用于 X 和 Y),让我们了解一下pygame.Rect
对象,这样我们就可以开始使用 Pygame 的绘图函数。
矩形对象
Pygame 有两种表示矩形区域的方法(就像有两种表示颜色的方法一样)。第一种是四个整数的元组:
- 左上角的 X 坐标。
- 左上角的 Y 坐标。
- 矩形的宽度(以像素为单位)。
- 矩形的高度(以像素为单位)。
第二种方法是作为pygame.Rect
对象,我们将简称为 Rect 对象。例如,下面的代码创建了一个顶点在(10, 20)处,宽度为 200 像素,高度为 300 像素的 Rect 对象:
>>> import pygame >>> spamRect = pygame.Rect(10, 20, 200, 300) >>> spamRect == (10, 20, 200, 300) True
这个方便的地方是 Rect 对象会自动计算矩形的其他特征的坐标。例如,如果您需要知道存储在spamRect
变量中的pygame.Rect
对象的右边缘的 X 坐标,您只需访问 Rect 对象的right
属性:
>>> spamRect.right 210
Rect 对象的 Pygame 代码自动计算了,如果左边缘位于 X 坐标 10,矩形宽度为 200 像素,那么右边缘必须位于 X 坐标 210。如果重新分配right
属性,则所有其他属性都会自动重新计算:
>>> spamRect.right = 350 >>> spamRect.left 150
以下是pygame.Rect
对象提供的所有属性列表(在我们的示例中,Rect 对象存储在名为spamRect
的变量中):
属性名称 | 描述 |
myRect.left |
矩形左侧的 X 坐标的整数值。 |
myRect.right |
矩形右侧的 X 坐标的整数值。 |
myRect.top |
矩形顶部的 Y 坐标的整数值。 |
myRect.bottom |
底部 Y 坐标的整数值。 |
myRect.centerx |
矩形中心的 X 坐标的整数值。 |
myRect.centery |
矩形中心的 Y 坐标的整数值。 |
myRect.width |
矩形的宽度的整数值。 |
myRect.height |
矩形的高度的整数值。 |
myRect.size |
两个整数的元组:(宽度,高度) |
myRect.topleft |
两个整数的元组:(左,顶部) |
myRect.topright |
两个整数的元组:(右,顶部) |
myRect.bottomleft |
两个整数的元组:(左,底部) |
myRect.bottomright |
两个整数的元组:(右,底部) |
myRect.midleft |
两个整数的元组:(左,中央 Y) |
myRect.midright |
两个整数的元组:(左,中央 Y) |
myRect.midtop |
两个整数的元组:(中央 X,顶部) |
myRect.midbottom |
两个整数的元组:(中央 X,底部) |
基本绘图函数
Pygame 提供了几个不同的函数来在表面对象上绘制不同的形状。这些形状,如矩形、圆、椭圆、线条或单个像素通常被称为绘图原语。打开 IDLE 的文件编辑器,输入以下程序,并将其保存为drawing.py。
import pygame, sys from pygame.locals import * pygame.init() # set up the window DISPLAYSURF = pygame.display.set_mode((500, 400), 0, 32) pygame.display.set_caption('Drawing') # set up the colors BLACK = ( 0, 0, 0) WHITE = (255, 255, 255) RED = (255, 0, 0) GREEN = ( 0, 255, 0) BLUE = ( 0, 0, 255) # draw on the surface object DISPLAYSURF.fill(WHITE) pygame.draw.polygon(DISPLAYSURF, GREEN, ((146, 0), (291, 106), (236, 277), (56, 277), (0, 106))) pygame.draw.line(DISPLAYSURF, BLUE, (60, 60), (120, 60), 4) pygame.draw.line(DISPLAYSURF, BLUE, (120, 60), (60, 120)) pygame.draw.line(DISPLAYSURF, BLUE, (60, 120), (120, 120), 4) pygame.draw.circle(DISPLAYSURF, BLUE, (300, 50), 20, 0) pygame.draw.ellipse(DISPLAYSURF, RED, (300, 250, 40, 80), 1) pygame.draw.rect(DISPLAYSURF, RED, (200, 150, 100, 50)) pixObj = pygame.PixelArray(DISPLAYSURF) pixObj[480][380] = BLACK pixObj[482][382] = BLACK pixObj[484][384] = BLACK pixObj[486][386] = BLACK pixObj[488][388] = BLACK del pixObj # run the game loop while True: for event in pygame.event.get(): if event.type == QUIT: pygame.quit() sys.exit() pygame.display.update()
- 当运行这个程序时,直到用户关闭窗口,将显示以下窗口:
注意我们为每种颜色创建了常量变量。这样做使我们的代码更易读,因为在源代码中看到GREEN
比看到(0, 255, 0)
更容易理解为代表绿色。
这些绘图函数的命名是根据它们绘制的形状命名的。你传递给这些函数的参数告诉它们在哪个表面对象上绘制,要在哪里绘制形状(以及大小),用什么颜色绘制,以及线条要多宽。你可以在drawing.py程序中看到这些函数是如何调用的,但这里是对每个函数的简短描述:
- fill(color) -
fill()
方法不是一个函数,而是pygame.Surface
对象的一个方法。它将使用你传递给color
参数的颜色值完全填充整个 Surface 对象。 - pygame.draw.polygon(surface, color, pointlist, width) - 多边形是由平面边组成的形状。
surface
和color
参数告诉函数在哪个表面上绘制多边形,以及用什么颜色绘制它。
pointlist
参数是一个点的元组或列表(即,XY 坐标的两个整数元组或列表)。多边形是通过在每个点和元组中的下一个点之间画线来绘制的。然后从最后一个点画一条线到第一个点。你也可以传递一个点的列表而不是一个点的元组。
width
参数是可选的。如果你不传递它,那么绘制的多边形将被填充,就像我们屏幕上的绿色多边形被填充一样。如果你为width
参数传递一个整数值,那么只会绘制多边形的轮廓。这个整数表示多边形轮廓的宽度。传递1
给width
参数会得到一个细的多边形,而传递4
、10
或20
会得到更厚的多边形。如果你为width
参数传递整数0
,多边形将被填充(就像如果你完全不传递width
参数一样)。
所有的pygame.draw
绘图函数都有可选的width
参数,它们的工作方式与pygame.draw.polygon()
的width
参数相同。也许width
参数的更好名称应该是thickness
,因为该参数控制你绘制的线条有多厚。
- pygame.draw.line(surface, color, start_point, end_point, width) - 这个函数在
start_point
和end_point
参数之间画一条线。 - pygame.draw.lines(surface, color, closed, pointlist, width) - 这个函数从一个点到下一个点画一系列的线,就像
pygame.draw.polygon()
一样。唯一的区别是,如果你在closed
参数中传递False
,那么pointlist
参数中的最后一个点到第一个点之间将不会有一条线。如果你传递True
,那么它将会从最后一个点画一条线到第一个点。 - pygame.draw.circle(surface, color, center_point, radius, width) - 这个函数画一个圆。圆的中心在
center_point
参数处。传递给radius
参数的整数设置了圆的大小。
圆的半径是从中心到边缘的距离。(圆的半径始终是直径的一半。)将20
作为radius
参数传递将绘制一个半径为 20 像素的圆。
- pygame.draw.ellipse(surface, color, bounding_rectangle, width) - 此函数绘制椭圆(类似于被挤压或拉伸的圆)。此函数具有所有常规参数,但为了告诉函数如何绘制椭圆的大小和位置,必须指定椭圆的边界矩形。边界矩形是可以绘制在形状周围的最小矩形。以下是椭圆及其边界矩形的示例:
bounding_rectangle
参数可以是pygame.Rect
对象或四个整数的元组。请注意,您不像对pygame.draw.circle()
函数那样为椭圆指定中心点。
- pygame.draw.rect(surface, color, rectangle_tuple, width) - 此函数绘制矩形。
rectangle_tuple
可以是四个整数的元组(用于左上角的 XY 坐标,以及宽度和高度),也可以传递pygame.Rect
对象。如果rectangle_tuple
的宽度和高度相同,则将绘制一个正方形。
pygame.PixelArray
对象
不幸的是,您无法调用单个函数将单个像素设置为颜色(除非使用相同的起点和终点调用pygame.draw.line()
)。Pygame 框架需要在绘制 Surface 对象之前和之后运行一些代码。如果它必须为您想要设置的每个单个像素执行此操作,您的程序将运行得更慢。(根据我的快速测试,以这种方式绘制像素要慢两到三倍。)
相反,您应该创建 Surface 对象的pygame.PixelArray
对象(我们简称为 PixelArray 对象),然后设置单个像素。创建 Surface 对象的 PixelArray 对象将“锁定”Surface 对象。在 Surface 对象被锁定时,仍然可以调用绘图函数,但不能使用blit()
方法在其上绘制 PNG 或 JPG 图像。(blit()
方法将在本章后面解释。)
如果要查看 Surface 对象是否被锁定,get_locked()
Surface 方法将返回True
(如果被锁定)和False
(如果未被锁定)。
从pygame.PixelArray()
返回的 PixelArray 对象可以通过两个索引访问并设置单个像素。例如,第 28 行的pixObj[480][380] = BLACK
将把 X 坐标为 480,Y 坐标为 380 的像素设置为黑色(请记住,BLACK
变量存储颜色元组(0, 0, 0)
)。
要告诉 Pygame 您已经完成了绘制单个像素,可以使用del
语句删除 PixelArray 对象。这就是第 33 行的作用。删除 PixelArray 对象将“解锁”Surface 对象,以便您可以再次在其上绘制图像。如果忘记删除 PixelArray 对象,下次尝试使用blit()
方法将图像绘制到 Surface 时,程序将引发错误,显示pygame.error: Surfaces must not be locked during blit
。
pygame.display.update()
函数
在调用绘图函数完成使显示 Surface 对象看起来符合您的要求后,必须调用pygame.display.update()
使显示 Surface 实际出现在用户的监视器上。
您必须记住的一件事是,pygame.display.update()
只会使显示表面(即从调用pygame.display.set_mode()
返回的 Surface 对象)出现在屏幕上。如果您希望其他 Surface 对象上的图像出现在屏幕上,您必须使用blit()
方法(下面在“绘制图像”部分中解释)将它们“blit”(即复制)到显示 Surface 对象上。
动画
现在我们知道如何让 Pygame 框架绘制到屏幕上,让我们学习如何制作动画图片。一个只有静止不动的图像的游戏会相当乏味。(我的游戏“看这块石头”的销售情况令人失望。)动画图像是在屏幕上绘制图像,然后在短短的一瞬间后在屏幕上绘制一个略微不同的图像的结果。想象一下,程序的窗口宽 6 像素,高 1 像素,所有像素都是白色,除了 4,0 处有一个黑色像素。它会看起来像这样:
如果您更改窗口,使 3,0 为黑色,4,0 为白色,它会看起来像这样:
对于用户来说,黑色像素看起来已经“移动”到了左边。如果你重新绘制窗口,使黑色像素位于 2,0,它会继续看起来像黑色像素向左移动:
黑色像素看起来可能在移动,但这只是一种幻觉。对于计算机来说,它只是显示了三个不同的图像,每个图像恰好有一个黑色像素。考虑一下,如果以下三个图像在屏幕上快速显示:
对于用户来说,看起来猫正在向松鼠移动。但对于计算机来说,它们只是一堆像素。制作看起来可信的动画的诀窍是让您的程序向窗口绘制一幅图片,等待一小会儿,然后再绘制一幅稍微不同的图片。
这是一个演示简单动画的示例程序。将此代码输入到 IDLE 的文件编辑器中,并将其保存为catanimation.py。它还需要图像文件 cat.png 与catanimation.py文件在同一个文件夹中。您可以从invpy.com/cat.png
下载这个图像。此代码可在invpy.com/catanimation.py
上找到。
import pygame, sys from pygame.locals import * pygame.init() FPS = 30 # frames per second setting fpsClock = pygame.time.Clock() # set up the window DISPLAYSURF = pygame.display.set_mode((400, 300), 0, 32) pygame.display.set_caption('Animation') WHITE = (255, 255, 255) catImg = pygame.image.load('cat.png') catx = 10 caty = 10 direction = 'right' while True: # the main game loop DISPLAYSURF.fill(WHITE) if direction == 'right': catx += 5 if catx == 280: direction = 'down' elif direction == 'down': caty += 5 if caty == 220: direction = 'left' elif direction == 'left': catx -= 5 if catx == 10: direction = 'up' elif direction == 'up': caty -= 5 if caty == 10: direction = 'right' DISPLAYSURF.blit(catImg, (catx, caty)) for event in pygame.event.get(): if event.type == QUIT: pygame.quit() sys.exit() pygame.display.update() fpsClock.tick(FPS)
看那只动画猫!这个程序将比我的游戏“看这块石头 2:另一块石头”更成功。
每秒帧数和pygame.time.Clock
对象
帧速率或刷新率是程序每秒绘制的图片数量,以 FPS 或每秒帧数来衡量。(在计算机显示器上,FPS 的常用名称是赫兹。许多显示器的帧速率为 60 赫兹,或每秒 60 帧。)视频游戏中的低帧速率会使游戏看起来断断续续或跳跃。如果程序有太多代码需要运行以频繁地绘制到屏幕上,那么 FPS 就会下降。但是,本书中的游戏足够简单,即使在旧计算机上也不会出现这个问题。
pygame.time.Clock
对象可以帮助我们确保我们的程序以某个最大 FPS 运行。这个Clock
对象将确保我们的游戏程序不会运行得太快,而是在游戏循环的每次迭代中放入小的暂停。如果没有这些暂停,我们的游戏程序将以计算机能够运行的速度运行。这对于玩家来说通常太快了,随着计算机的速度变快,游戏也会运行得更快。在游戏循环中调用Clock
对象的tick()
方法可以确保游戏以相同的速度运行,无论它在多快的计算机上运行。Clock
对象在catanimation.py程序的第 7 行创建。
fpsClock = pygame.time.Clock()
Clock
对象的tick()
方法应该在游戏循环的最后调用,即在调用pygame.display.update()
之后。暂停的长度是根据自上次调用tick()
以来的时间计算的,这将发生在上一次游戏循环迭代的末尾。(第一次调用tick()
方法时,根本不会暂停。)在动画程序中,它在第 47 行作为游戏循环中的最后一条指令运行。
你只需要知道应该在循环的每次迭代结束时调用tick()
方法。通常是在调用pygame.display.update()
之后。
fpsClock.tick(FPS)
尝试修改FPS
常量变量以以不同的帧率运行相同的程序。将其设置为较低的值会使程序运行更慢。将其设置为较高的值会使程序运行更快。
使用pygame.image.load()
和blit()
绘制图像
如果要在屏幕上绘制简单的形状,那么绘图函数是很好用的,但是许多游戏都有图像(也称为精灵)。Pygame 能够从 PNG、JPG、GIF 和 BMP 图像文件加载图像到 Surface 对象中。这些图像文件格式之间的区别在invpy.com/formats
中有描述。
猫的图像存储在一个名为cat.png的文件中。要加载此文件的图像,将字符串’cat.png’传递给pygame.image.load()
函数。pygame.image.load()
函数调用将返回一个 Surface 对象,该对象上绘制了图像。这个 Surface 对象将是一个独立的 Surface 对象,不同于显示 Surface 对象,因此我们必须将图像的 Surface 对象 blit(即复制)到显示 Surface 对象。Blitting 是将一个 Surface 的内容绘制到另一个 Surface 上。它是用blit()
Surface 对象方法完成的。
如果在调用pygame.image.load()
时出现类似“pygame.error:无法打开 cat.png”的错误消息,则在运行程序之前确保cat.png文件与catanimation.py文件在同一个文件夹中。
DISPLAYSURF.blit(catImg, (catx, caty))
动画程序的第 39 行使用blit()
方法将catImg
复制到DISPLAYSURF
。blit()
有两个参数。第一个是源 Surface 对象,它将被复制到DISPLAYSURF
Surface 对象上。第二个参数是一个包含 X 和 Y 值的二元组,表示图像应该被 blit 到的左上角的位置。
如果catx
和caty
设置为100
和200
,catImg
的宽度为125
,高度为79
,则此blit()
调用将复制此图像到DISPLAYSURF
,使catImg
的左上角位于 XY 坐标(100, 200),右下角的 XY 坐标位于(225, 279)。
请注意,您不能将内容 blit 到当前“锁定”的 Surface 上(例如当从中创建了 PixelArray 对象但尚未删除时)。
游戏循环的其余部分只是改变catx
、caty
和direction
变量,使猫在窗口周围移动。还有一个调用pygame.event.get()
来处理QUIT
事件。
字体
如果要在屏幕上绘制文本,您可以写几个pygame.draw.line()
调用来绘制每个字母的线条。这将是一个头疼的工作,要输入所有这些pygame.draw.line()
调用并找出所有 XY 坐标,而且可能看起来不太好。
上面的消息需要调用pygame.draw.line()
函数四十一次才能完成。相反,Pygame 提供了一些更简单的函数来处理字体和创建文本。下面是一个使用 Pygame 的字体函数的小型 Hello World 程序。将其输入到 IDLE 的文件编辑器中,并保存为fonttext.py:
import pygame, sys from pygame.locals import * pygame.init() DISPLAYSURF = pygame.display.set_mode((400, 300)) pygame.display.set_caption('Hello World!') WHITE = (255, 255, 255) GREEN = (0, 255, 0) BLUE = (0, 0, 128) fontObj = pygame.font.Font('freesansbold.ttf', 32) textSurfaceObj = fontObj.render('Hello world!', True, GREEN, BLUE) textRectObj = textSurfaceObj.get_rect() textRectObj.center = (200, 150) while True: # main game loop DISPLAYSURF.fill(WHITE) DISPLAYSURF.blit(textSurfaceObj, textRectObj) for event in pygame.event.get(): if event.type == QUIT: pygame.quit() sys.exit() pygame.display.update()
使文本显示在屏幕上有六个步骤:
- 创建一个
pygame.font.Font 对象
。(就像第 12 行一样) - 使用 Font 对象的
render()
方法在其上绘制文本创建一个 Surface 对象。(第 13 行) - 通过调用 Surface 对象的
get_rect()
方法从 Surface 对象创建一个 Rect 对象。(第 14 行) 这个 Rect 对象将正确设置为呈现的文本的宽度和高度,但 top 和 left 属性将为0
。 - 通过改变 Rect 对象的属性来设置 Rect 对象的位置。在第 15 行,我们将 Rect 对象的中心设置为 200, 150。
- 使用
pygame.display.set_mode()
返回的 Surface 对象将带有文本的 Surface 对象贴到上面。 (第 19 行) - 调用
pygame.display.update()
使显示 Surface 出现在屏幕上。(第 24 行)
pygame.font.Font()
构造函数的参数是要使用的字体文件的字符串和字体大小的整数(以点为单位,就像文字处理器测量字体大小一样)。在第 12 行,我们传递了'freesansbold.ttf'
(这是 Pygame 附带的字体)和整数32
(32 点大小的字体)。
有关使用其他字体的更多信息,请参见invpy.com/usingotherfonts
。
render()
方法调用的参数是要渲染的文本字符串,一个布尔值,用于指定是否要使用抗锯齿(本章后面将解释),文本的颜色和背景的颜色。如果要透明背景,则在方法调用中省略背景颜色参数。
抗锯齿
抗锯齿是一种图形技术,通过在边缘添加一点模糊来使文本和形状看起来不那么方块。使用抗锯齿绘制需要更多的计算时间,因此尽管图形可能看起来更好,但程序可能运行得更慢(但只是一点点)。
如果您放大一个有锯齿的线和一个抗锯齿的线,它们看起来像这样:
要使 Pygame 的文本使用抗锯齿,只需将render()
方法的第二个参数传递为True
。pygame.draw.aaline()
和pygame.draw.aalines()
函数具有与pygame.draw.line()
和pygame.draw.lines()
相同的参数,只是它们会绘制抗锯齿(平滑)线,而不是锯齿(方块)线。
播放声音
播放存储在声音文件中的声音比从图像文件中显示图像更简单。首先,您必须通过调用pygame.mixer.Sound()
构造函数创建一个pygame.mixer.Sound
对象(我们简称为 Sound 对象)。它需要一个字符串参数,即声音文件的文件名。Pygame 可以加载 WAV、MP3 或 OGG 文件。这些音频文件格式的区别在invpy.com/formats
中有解释。
要播放此声音,请调用 Sound 对象的play()
方法。如果要立即停止 Sound 对象的播放,请调用stop()
方法。stop()
方法没有参数。以下是一些示例代码:
soundObj = pygame.mixer.Sound('beeps.wav') soundObj.play() import time time.sleep(1) # wait and let the sound play for 1 second soundObj.stop()
您可以从invpy.com/beeps.wav
下载beeps.wav文件。
在调用play()
后,程序立即继续执行;它不会等待声音播放完毕再继续下一行代码。
Sound 对象适用于在玩家受伤、挥舞剑或收集硬币时播放声音效果。但是,如果游戏中发生什么情况,无论如何都播放背景音乐,您的游戏可能会更好。Pygame 一次只能加载一个背景音乐文件进行播放。要加载背景音乐文件,请调用pygame.mixer.music.load()
函数并传递一个字符串参数,表示要加载的声音文件。该文件可以是 WAV、MP3 或 MIDI 格式。
要开始播放加载的声音文件作为背景音乐,请调用pygame.mixer.music.play(-1, 0.0)
函数。-1
参数使背景音乐在达到声音文件结尾时永久循环。如果将其设置为整数0
或更大,则音乐将只循环该次数,而不是永久循环。0.0
表示从头开始播放声音文件。如果传递一个较大的整数或浮点数,音乐将从声音文件的那么多秒开始播放。例如,如果将第二个参数传递为13.5
,则声音文件将从开头的第 13.5 秒开始播放。
要立即停止播放背景音乐,请调用pygame.mixer.music.stop()
函数。此函数没有参数。
以下是一些声音方法和函数的示例代码:
# Loading and playing a sound effect: soundObj = pygame.mixer.Sound('beepingsound.wav') soundObj.play() # Loading and playing background music: pygame.mixer.music.load('backgroundmusic.mp3') pygame.mixer.music.play(-1, 0.0) # ...some more of your code goes here... pygame.mixer.music.stop()
总结
这本书涵盖了使用 Pygame 框架制作图形游戏的基础知识。当然,仅仅阅读这些函数的介绍可能不足以帮助你学会如何使用这些函数来制作游戏。本书的其余章节都专注于一些小型完整游戏的源代码。这将让你了解完整游戏程序的“样子”,这样你就可以得到一些关于如何编写自己的游戏程序的想法。
与“用 Python 发明自己的电脑游戏”一书不同,这本书假设你已经了解 Python 编程的基础知识。如果你对变量、函数、循环、if
-else
语句和条件的工作原理有困难,你可能可以通过查看代码和程序的行为来弄清楚。但如果你仍然卡住了,你可以免费在线阅读“用 Python 发明”一书(这是为完全新手编程的人准备的)inventwithpython.com
。
第三章:记忆迷题
原文:
inventwithpython.com/pygame/chapter3.html
译者:飞龙
如何玩记忆迷题
在记忆迷题游戏中,几个图标被白色方框覆盖。每种图标有两个。玩家可以点击两个方框,看看它们后面是什么图标。如果图标匹配,那么这些方框将保持打开状态。当棋盘上的所有方框都被打开时,玩家获胜。为了给玩家一个提示,方框在游戏开始时会迅速打开一次。
嵌套的for
循环
你将在记忆迷题(以及本书中大多数游戏)中看到的一个概念是在一个for
循环内部使用另一个for
循环。这些被称为嵌套的for
循环。嵌套的for
循环对于遍历两个列表的每种可能的组合非常方便。在交互式 shell 中输入以下内容:
>>> for x in [0, 1, 2, 3, 4]: ... for y in ['a', 'b', 'c']: ... print(x, y) ... 0 a 0 b 0 c 1 a 1 b 1 c 2 a 2 b 2 c 3 a 3 b 3 c 4 a 4 b 4 c >>>
在记忆迷题代码中有几次我们需要遍历棋盘上的每个可能的 X 和 Y 坐标。我们将使用嵌套的for
循环来确保我们得到每一种组合。请注意,内部的for
循环(在另一个for
循环内部的for
循环)将在进行下一个外部for
循环的迭代之前完成其所有迭代。如果我们颠倒for
循环的顺序,将打印相同的值,但它们将以不同的顺序打印。将以下代码输入交互式 shell,并将其打印值的顺序与先前嵌套的for
循环示例中的顺序进行比较:
>>> for y in ['a', 'b', 'c']: ... for x in [0, 1, 2, 3, 4]: ... print(x, y) ... 0 a 1 a 2 a 3 a 4 a 0 b 1 b 2 b 3 b 4 b 0 c 1 c 2 c 3 c 4 c >>>
##记忆迷题的源代码
这个源代码可以从invpy.com/memorypuzzle.py
下载。
请先在 IDLE 的文件编辑器中输入整个程序,将其保存为memorypuzzle.py,然后运行它。如果出现任何错误消息,请查看错误消息中提到的行号,并检查你的代码是否有任何拼写错误。你也可以将你的代码复制粘贴到invpy.com/diff/memorypuzzle
的网页表单中,以查看你的代码与书中代码之间的差异。
通过输入一次代码,你可能会对程序的工作方式有一些想法。当你输入完毕后,你可以自己玩这个游戏。
# Memory Puzzle # By Al Sweigart [[email protected]](/cdn-cgi/l/email-protection) # http://inventwithpython.com/pygame # Released under a "Simplified BSD" license import random, pygame, sys from pygame.locals import * FPS = 30 # frames per second, the general speed of the program WINDOWWIDTH = 640 # size of window's width in pixels WINDOWHEIGHT = 480 # size of windows' height in pixels REVEALSPEED = 8 # speed boxes' sliding reveals and covers BOXSIZE = 40 # size of box height & width in pixels GAPSIZE = 10 # size of gap between boxes in pixels BOARDWIDTH = 10 # number of columns of icons BOARDHEIGHT = 7 # number of rows of icons assert (BOARDWIDTH * BOARDHEIGHT) % 2 == 0, 'Board needs to have an even number of boxes for pairs of matches.' XMARGIN = int((WINDOWWIDTH - (BOARDWIDTH * (BOXSIZE + GAPSIZE))) / 2) YMARGIN = int((WINDOWHEIGHT - (BOARDHEIGHT * (BOXSIZE + GAPSIZE))) / 2) # R G B GRAY = (100, 100, 100) NAVYBLUE = ( 60, 60, 100) WHITE = (255, 255, 255) RED = (255, 0, 0) GREEN = ( 0, 255, 0) BLUE = ( 0, 0, 255) YELLOW = (255, 255, 0) ORANGE = (255, 128, 0) PURPLE = (255, 0, 255) CYAN = ( 0, 255, 255) BGCOLOR = NAVYBLUE LIGHTBGCOLOR = GRAY BOXCOLOR = WHITE HIGHLIGHTCOLOR = BLUE DONUT = 'donut' SQUARE = 'square' DIAMOND = 'diamond' LINES = 'lines' OVAL = 'oval' ALLCOLORS = (RED, GREEN, BLUE, YELLOW, ORANGE, PURPLE, CYAN) ALLSHAPES = (DONUT, SQUARE, DIAMOND, LINES, OVAL) assert len(ALLCOLORS) * len(ALLSHAPES) * 2 >= BOARDWIDTH * BOARDHEIGHT, "Board is too big for the number of shapes/colors defined." def main(): global FPSCLOCK, DISPLAYSURF pygame.init() FPSCLOCK = pygame.time.Clock() DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT)) mousex = 0 # used to store x coordinate of mouse event mousey = 0 # used to store y coordinate of mouse event pygame.display.set_caption('Memory Game') mainBoard = getRandomizedBoard() revealedBoxes = generateRevealedBoxesData(False) firstSelection = None # stores the (x, y) of the first box clicked. DISPLAYSURF.fill(BGCOLOR) startGameAnimation(mainBoard) while True: # main game loop mouseClicked = False DISPLAYSURF.fill(BGCOLOR) # drawing the window drawBoard(mainBoard, revealedBoxes) for event in pygame.event.get(): # event handling loop if event.type == QUIT or (event.type == KEYUP and event.key == K_ESCAPE): pygame.quit() sys.exit() elif event.type == MOUSEMOTION: mousex, mousey = event.pos elif event.type == MOUSEBUTTONUP: mousex, mousey = event.pos mouseClicked = True boxx, boxy = getBoxAtPixel(mousex, mousey) if boxx != None and boxy != None: # The mouse is currently over a box. if not revealedBoxes[boxx][boxy]: drawHighlightBox(boxx, boxy) if not revealedBoxes[boxx][boxy] and mouseClicked: revealBoxesAnimation(mainBoard, [(boxx, boxy)]) revealedBoxes[boxx][boxy] = True # set the box as "revealed" if firstSelection == None: # the current box was the first box clicked firstSelection = (boxx, boxy) else: # the current box was the second box clicked # Check if there is a match between the two icons. icon1shape, icon1color = getShapeAndColor(mainBoard, firstSelection[0], firstSelection[1]) icon2shape, icon2color = getShapeAndColor(mainBoard, boxx, boxy) if icon1shape != icon2shape or icon1color != icon2color: # Icons don't match. Re-cover up both selections. pygame.time.wait(1000) # 1000 milliseconds = 1 sec coverBoxesAnimation(mainBoard, [(firstSelection[0], firstSelection[1]), (boxx, boxy)]) revealedBoxes[firstSelection[0]][firstSelection [1]] = False revealedBoxes[boxx][boxy] = False elif hasWon(revealedBoxes): # check if all pairs found gameWonAnimation(mainBoard) pygame.time.wait(2000) # Reset the board mainBoard = getRandomizedBoard() revealedBoxes = generateRevealedBoxesData(False) # Show the fully unrevealed board for a second. drawBoard(mainBoard, revealedBoxes) pygame.display.update() pygame.time.wait(1000) # Replay the start game animation. startGameAnimation(mainBoard) firstSelection = None # reset firstSelection variable # Redraw the screen and wait a clock tick. pygame.display.update() FPSCLOCK.tick(FPS) def generateRevealedBoxesData(val): revealedBoxes = [] for i in range(BOARDWIDTH): revealedBoxes.append([val] * BOARDHEIGHT) return revealedBoxes def getRandomizedBoard(): # Get a list of every possible shape in every possible color. icons = [] for color in ALLCOLORS: for shape in ALLSHAPES: icons.append( (shape, color) ) random.shuffle(icons) # randomize the order of the icons list numIconsUsed = int(BOARDWIDTH * BOARDHEIGHT / 2) # calculate how many icons are needed icons = icons[:numIconsUsed] * 2 # make two of each random.shuffle(icons) # Create the board data structure, with randomly placed icons. board = [] for x in range(BOARDWIDTH): column = [] for y in range(BOARDHEIGHT): column.append(icons[0]) del icons[0] # remove the icons as we assign them board.append(column) return board def splitIntoGroupsOf(groupSize, theList): # splits a list into a list of lists, where the inner lists have at # most groupSize number of items. result = [] for i in range(0, len(theList), groupSize): result.append(theList[i:i + groupSize]) return result def leftTopCoordsOfBox(boxx, boxy): # Convert board coordinates to pixel coordinates left = boxx * (BOXSIZE + GAPSIZE) + XMARGIN top = boxy * (BOXSIZE + GAPSIZE) + YMARGIN return (left, top) def getBoxAtPixel(x, y): for boxx in range(BOARDWIDTH): for boxy in range(BOARDHEIGHT): left, top = leftTopCoordsOfBox(boxx, boxy) boxRect = pygame.Rect(left, top, BOXSIZE, BOXSIZE) if boxRect.collidepoint(x, y): return (boxx, boxy) return (None, None) def drawIcon(shape, color, boxx, boxy): quarter = int(BOXSIZE * 0.25) # syntactic sugar half = int(BOXSIZE * 0.5) # syntactic sugar left, top = leftTopCoordsOfBox(boxx, boxy) # get pixel coords from board coords # Draw the shapes if shape == DONUT: pygame.draw.circle(DISPLAYSURF, color, (left + half, top + half), half - 5) pygame.draw.circle(DISPLAYSURF, BGCOLOR, (left + half, top + half), quarter - 5) elif shape == SQUARE: pygame.draw.rect(DISPLAYSURF, color, (left + quarter, top + quarter, BOXSIZE - half, BOXSIZE - half)) elif shape == DIAMOND: pygame.draw.polygon(DISPLAYSURF, color, ((left + half, top), (left + BOXSIZE - 1, top + half), (left + half, top + BOXSIZE - 1), (left, top + half))) elif shape == LINES: for i in range(0, BOXSIZE, 4): pygame.draw.line(DISPLAYSURF, color, (left, top + i), (left + i, top)) pygame.draw.line(DISPLAYSURF, color, (left + i, top + BOXSIZE - 1), (left + BOXSIZE - 1, top + i)) elif shape == OVAL: pygame.draw.ellipse(DISPLAYSURF, color, (left, top + quarter, BOXSIZE, half)) def getShapeAndColor(board, boxx, boxy): # shape value for x, y spot is stored in board[x][y][0] # color value for x, y spot is stored in board[x][y][1] return board[boxx][boxy][0], board[boxx][boxy][1] def drawBoxCovers(board, boxes, coverage): # Draws boxes being covered/revealed. "boxes" is a list # of two-item lists, which have the x & y spot of the box. for box in boxes: left, top = leftTopCoordsOfBox(box[0], box[1]) pygame.draw.rect(DISPLAYSURF, BGCOLOR, (left, top, BOXSIZE, BOXSIZE)) shape, color = getShapeAndColor(board, box[0], box[1]) drawIcon(shape, color, box[0], box[1]) if coverage > 0: # only draw the cover if there is an coverage pygame.draw.rect(DISPLAYSURF, BOXCOLOR, (left, top, coverage, BOXSIZE)) pygame.display.update() FPSCLOCK.tick(FPS) def revealBoxesAnimation(board, boxesToReveal): # Do the "box reveal" animation. for coverage in range(BOXSIZE, (-REVEALSPEED) - 1, - REVEALSPEED): drawBoxCovers(board, boxesToReveal, coverage) def coverBoxesAnimation(board, boxesToCover): # Do the "box cover" animation. for coverage in range(0, BOXSIZE + REVEALSPEED, REVEALSPEED): drawBoxCovers(board, boxesToCover, coverage) def drawBoard(board, revealed): # Draws all of the boxes in their covered or revealed state. for boxx in range(BOARDWIDTH): for boxy in range(BOARDHEIGHT): left, top = leftTopCoordsOfBox(boxx, boxy) if not revealed[boxx][boxy]: # Draw a covered box. pygame.draw.rect(DISPLAYSURF, BOXCOLOR, (left, top, BOXSIZE, BOXSIZE)) else: # Draw the (revealed) icon. shape, color = getShapeAndColor(board, boxx, boxy) drawIcon(shape, color, boxx, boxy) def drawHighlightBox(boxx, boxy): left, top = leftTopCoordsOfBox(boxx, boxy) pygame.draw.rect(DISPLAYSURF, HIGHLIGHTCOLOR, (left - 5, top - 5, BOXSIZE + 10, BOXSIZE + 10), 4) def startGameAnimation(board): # Randomly reveal the boxes 8 at a time. coveredBoxes = generateRevealedBoxesData(False) boxes = [] for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): boxes.append( (x, y) ) random.shuffle(boxes) boxGroups = splitIntoGroupsOf(8, boxes) drawBoard(board, coveredBoxes) for boxGroup in boxGroups: revealBoxesAnimation(board, boxGroup) coverBoxesAnimation(board, boxGroup) def gameWonAnimation(board): # flash the background color when the player has won coveredBoxes = generateRevealedBoxesData(True) color1 = LIGHTBGCOLOR color2 = BGCOLOR for i in range(13): color1, color2 = color2, color1 # swap colors DISPLAYSURF.fill(color1) drawBoard(board, coveredBoxes) pygame.display.update() pygame.time.wait(300) def hasWon(revealedBoxes): # Returns True if all the boxes have been revealed, otherwise False for i in revealedBoxes: if False in i: return False # return False if any boxes are covered. return True if __name__ == '__main__': main()
制作和导入
# Memory Puzzle # By Al Sweigart [[email protected]](/cdn-cgi/l/email-protection) # http://inventwithpython.com/pygame # Released under a "Simplified BSD" license import random, pygame, sys from pygame.locals import *
程序顶部有关于游戏内容、制作者以及用户可以找到更多信息的注释。还有一条注释,指出源代码在“简化 BSD”许可下可以自由复制。简化 BSD 许可更适合软件,而不是创作共同许可(本书发布的许可),但它们基本上意思相同:人们可以自由复制和分享这个游戏。有关许可的更多信息可以在invpy.com/licenses
找到。
这个程序利用了其他模块中的许多函数,所以在第 6 行导入了这些模块。第 7 行也是一个import
语句,格式为from (module name) import *
,这意味着你不必在前面输入模块名。pygame.locals
模块中没有函数,但其中有几个常量变量,我们想要使用,比如MOUSEMOTION
、KEYUP
或QUIT
。使用这种import
语句的风格,我们只需要输入MOUSEMOTION
,而不是pygame.locals.MOUSEMOTION
。
魔法数字是不好的
FPS = 30 # frames per second, the general speed of the program WINDOWWIDTH = 640 # size of window's width in pixels WINDOWHEIGHT = 480 # size of windows' height in pixels REVEALSPEED = 8 # speed boxes' sliding reveals and covers BOXSIZE = 40 # size of box height & width in pixels GAPSIZE = 10 # size of gap between boxes in pixels
本书中的游戏程序使用了许多常量变量。你可能没有意识到它们为什么如此方便。例如,我们的代码中使用BOXSIZE
变量,我们可以直接在代码中输入整数40
。但是使用常量变量有两个原因。
首先,如果我们以后想要更改每个框的大小,我们将不得不浏览整个程序,并找到并替换每次输入40
的地方。只需使用BOXSIZE
常量,我们只需要更改第 13 行,程序的其余部分已经是最新的。这样做要好得多,特别是因为我们可能会将整数值40
用于除了白色框的大小之外的其他用途,而意外更改40
会导致程序中的错误。
其次,它使代码更易读。转到下一节,看看第 18 行。这设置了XMARGIN
常量的计算,它是整个板的侧面有多少像素。这是一个看起来复杂的表达式,但你可以仔细地理清它的含义。第 18 行看起来像这样:
XMARGIN = int((WINDOWWIDTH - (BOARDWIDTH * (BOXSIZE + GAPSIZE))) / 2)
但是,如果第 18 行没有使用常量变量,它将如下所示:
XMARGIN = int((640 – (10 * (40 + 10))) / 2)
现在变得不可能记住程序员的确切意图是什么。源代码中的这些未解释的数字通常被称为魔术数字。每当你发现自己输入魔术数字时,你应该考虑用常量变量替换它们。对于 Python 解释器来说,前两行是完全相同的。但是对于阅读源代码并试图理解其工作原理的人类程序员来说,第 18 行的第二个版本根本没有多大意义!常量确实有助于提高源代码的可读性。
当然,你也可以用常量变量替换数字。看看下面的代码:
ZERO = 0 ONE = 1 TWO = 99999999 TWOANDTHREEQUARTERS = 2.75
不要写那样的代码。那太傻了。
使用assert
语句进行理智检查
BOARDWIDTH = 10 # number of columns of icons BOARDHEIGHT = 7 # number of rows of icons assert (BOARDWIDTH * BOARDHEIGHT) % 2 == 0, 'Board needs to have an even number of boxes for pairs of matches.' XMARGIN = int((WINDOWWIDTH - (BOARDWIDTH * (BOXSIZE + GAPSIZE))) / 2) YMARGIN = int((WINDOWHEIGHT - (BOARDHEIGHT * (BOXSIZE + GAPSIZE))) / 2)
第 17 行的assert
语句确保我们选择的板宽度和高度将导致偶数个框(因为在这个游戏中我们将有一对图标)。assert
语句有三个部分:assert
关键字,一个表达式,如果为False
,则导致程序崩溃。表达式后的第三部分(在逗号后的字符串)是程序因断言而崩溃时出现的字符串。
带有表达式的assert
语句基本上表示:“程序员断言这个表达式必须为True
,否则崩溃程序。”这是向程序添加一个理智检查的好方法,以确保如果执行通过了断言,我们至少知道该代码正在按预期工作。
判断一个数字是偶数还是奇数
如果板宽度和高度的乘积除以 2 有余数为 0(%
模运算符评估余数是多少),那么这个数字是偶数。偶数除以 2 将始终有零的余数。奇数除以 2 将始终有一个余数。如果你的代码需要判断一个数字是偶数还是奇数,这是一个很好的技巧:
>>> isEven = someNumber % 2 == 0 >>> isOdd = someNumber % 2 != 0
在上面的情况下,如果someNumber
中的整数是偶数,那么isEven
将是True
。如果是奇数,那么isOdd
将是True
。
尽早崩溃,经常崩溃!
程序崩溃是一件坏事。当你的程序在代码中出现错误并且无法继续时,就会发生这种情况。但也有一些情况下,尽早崩溃程序可以避免以后更糟糕的错误。
如果我们在第 15 和 16 行选择的BOARDWIDTH
和BOARDHEIGHT
的值导致了一个有奇数个框的板(比如如果宽度为 3,高度为 5),那么总会有一个多余的图标没有配对。这将在程序的后面导致错误,并且可能需要大量的调试工作才能找出错误的真正来源是在程序的一开始。事实上,只是为了好玩,试着注释掉断言,这样它就不会运行,然后将BOARDWIDTH
和BOARDHEIGHT
常量都设置为奇数。当你运行程序时,它会立即显示在memorypuzzle.py的第 149 行发生错误,这是在getRandomizedBoard()
函数中!
Traceback (most recent call last): File "C:\book2svn\src\memorypuzzle.py", line 292, in <module> main() File "C:\book2svn\src\memorypuzzle.py", line 58, in main mainBoard = getRandomizedBoard() File "C:\book2svn\src\memorypuzzle.py", line 149, in getRandomizedBoard columns.append(icons[0]) IndexError: list index out of range
我们可能会花费很多时间查看getRandomizedBoard()
,试图弄清楚其中的问题,然后才意识到getRandomizedBoard()
是完全正常的:错误的真正来源是在第 15 行和第 16 行,我们设置了BOARDWIDTH
和BOARDHEIGHT
常量。
断言确保这种情况永远不会发生。如果我们的代码将崩溃,我们希望它在检测到某些严重错误时尽快崩溃,否则该错误可能直到程序的后期才会显现。尽早崩溃!
每当程序中有一些条件必须始终始终始终为True
时,您都要添加assert
语句。经常崩溃!您不必过度使用assert
语句,但是经常使用assert
语句可以在检测错误的真正来源时大有裨益。尽早崩溃,经常崩溃!
(在您的代码中。比如说,不是骑马时。)
使源代码看起来漂亮
# R G B GRAY = (100, 100, 100) NAVYBLUE = ( 60, 60, 100) WHITE = (255, 255, 255) RED = (255, 0, 0) GREEN = ( 0, 255, 0) BLUE = ( 0, 0, 255) YELLOW = (255, 255, 0) ORANGE = (255, 128, 0) PURPLE = (255, 0, 255) CYAN = ( 0, 255, 255) BGCOLOR = NAVYBLUE LIGHTBGCOLOR = GRAY BOXCOLOR = WHITE HIGHLIGHTCOLOR = BLUE
请记住,Pygame 中的颜色由一个包含从0
到255
的三个整数的元组表示。这三个整数代表颜色中红色、绿色和蓝色的数量,这就是为什么这些元组被称为 RGB 值。请注意,22 到 31 行中元组的间距是这样的,即 R、G 和 B 整数是对齐的。在 Python 中,缩进(即行首的空格)需要精确,但是行的其余部分的间距并不那么严格。通过将元组中的整数间隔开,我们可以清楚地看到 RGB 值之间的比较。(有关间距和缩进的更多信息,请参见invpy.com/whitespace
。)
这样做可以使您的代码更易读,但不要花太多时间。代码不一定非得漂亮才能工作。在某个时候,您只会花更多的时间打空格,而不是通过可读的元组值节省的时间。
使用常量变量而不是字符串
DONUT = 'donut' SQUARE = 'square' DIAMOND = 'diamond' LINES = 'lines' OVAL = 'oval'
程序还为一些字符串设置了常量变量。这些常量将用于棋盘的数据结构,跟踪棋盘上哪些空格有哪些图标。使用常量变量而不是字符串值是一个好主意。看下面的代码,来自第 187 行:
if shape == DONUT:
形状变量将设置为字符串'donut'
、'square'
、'diamond'
、'lines'
或'oval'
中的一个,然后与DONUT
常量进行比较。例如,如果我们在编写第 187 行时犯了拼写错误,就像这样:
if shape == DUNOT:
然后 Python 会崩溃,并显示错误消息,说没有名为DUNOT
的变量。这很好。由于程序在第 187 行崩溃,当我们检查该行时,很容易看出错误是由拼写错误引起的。但是,如果我们使用字符串而不是常量变量,并且犯了同样的拼写错误,第 187 行将如下所示:
if shape == 'dunot':
这是完全可以接受的 Python 代码,因此当您运行它时,一开始不会崩溃。但是,这将导致我们程序后来出现奇怪的错误。因为代码不会立即在问题发生的地方崩溃,所以要找到它可能会更加困难。
确保我们有足够的图标
ALLCOLORS = (RED, GREEN, BLUE, YELLOW, ORANGE, PURPLE, CYAN) ALLSHAPES = (DONUT, SQUARE, DIAMOND, LINES, OVAL) assert len(ALLCOLORS) * len(ALLSHAPES) * 2 >= BOARDWIDTH * BOARDHEIGHT, "Board is too big for the number of shapes/colors defined."
为了使我们的游戏程序能够创建每种可能的颜色和形状组合的图标,我们需要创建一个包含所有这些值的元组。还有另一个断言在第 46 行,以确保我们有足够的颜色/形状组合适应我们的棋盘大小。如果没有足够的话,程序将在第 46 行崩溃,我们将知道我们要么添加更多的颜色和形状,要么使棋盘的宽度和高度更小。有 7 种颜色和 5 种形状,我们可以制作 35(即 7 x 5)种不同的图标。因为每种图标都有一对,这意味着我们的棋盘最多可以有 70 个空格(即 35 x 2,或 7 x 5 x 2)。
元组与列表,不可变与可变
你可能已经注意到ALLCOLORS
和ALLSHAPES
变量是元组而不是列表。我们何时使用元组,何时使用列表?它们之间的区别是什么?
元组和列表在所有方面都是相同的,除了两点:元组使用括号而不是方括号,并且元组中的项不能被修改(但列表中的项可以被修改)。我们通常称列表为可变的(可以被更改),元组为不可变的(不能被更改)。
要尝试更改列表和元组中的值,请查看以下代码示例:
>>> listVal = [1, 1, 2, 3, 5, 8] >>> tupleVal = (1, 1, 2, 3, 5, 8) >>> listVal[4] = 'hello!' >>> listVal [1, 1, 2, 3, 'hello!', 8] >>> tupleVal[4] = 'hello!' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'tuple' object does not support item assignment >>> tupleVal (1, 1, 2, 3, 5, 8) >>> tupleVal[4] 5
注意,当我们尝试更改元组中索引为2
的项时,Python 会给出一个错误消息,表示元组对象不支持“项赋值”。
元组的不可变性有一个愚蠢的好处和一个重要的好处。愚蠢的好处是使用元组的代码比使用列表的代码稍微快一些。(Python 能够进行一些优化,因为知道元组中的值永远不会改变。)但是让你的代码运行快几纳秒并不重要。
使用元组的重要好处类似于使用常量变量的好处:这表明元组中的值永远不会改变,因此以后阅读代码的人可以说,“我可以期望这个元组永远是一样的。否则程序员会使用列表。”这也让未来阅读你代码的程序员说,“如果我看到一个列表值,我知道它可能在程序的某个时刻被修改。否则,编写这段代码的程序员会使用元组。”
你仍然可以将新的元组值分配给一个变量:
>>> tupleVal = (1, 2, 3) >>> tupleVal = (1, 2, 3, 4)
这段代码之所以有效是因为代码没有更改第二行的(1, 2, 3)
元组。它是将全新的元组(1, 2, 3, 4)
分配给tupleVal
,并覆盖旧的元组值。但是,你不能使用方括号修改元组中的项。
字符串也是一种不可变的数据类型。你可以使用方括号来读取字符串中的单个字符,但你不能改变字符串中的单个字符。
>>> strVal = 'Hello' >>> strVal[1] 'e' >>> strVal[1] = 'X' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'str' object does not support item assignment
一个项元组需要一个尾随逗号
关于元组的一个小细节:如果你需要编写关于一个只有一个值的元组的代码,那么它需要在最后加上一个逗号,就像这样:
oneValueTuple = (42, )
如果你忘记了这个逗号(这很容易忘记),那么 Python 将无法区分这个和只是改变操作顺序的一对括号。例如,看下面的两行代码:
variableA = (5 * 6) variableB = (5 * 6, )
存储在variableA
中的值只是整数30
。然而,variableB
赋值语句的表达式是单项元组值(30, )
。空元组值不需要逗号,它们可以只是一对括号:()
在列表和元组之间转换
你可以像转换字符串和整数值一样转换列表和元组值。只需将一个元组值传递给list()
函数,它将返回该元组值的列表形式。或者,将一个列表值传递给tuple()
函数,它将返回该列表值的元组形式。尝试在交互式 shell 中输入以下内容:
>>> spam = (1, 2, 3, 4) >>> spam = list(spam) >>> spam [1, 2, 3, 4] >>> spam = tuple(spam) >>> spam (1, 2, 3, 4) >>>
global
语句,以及全局变量为何不好
def main(): global FPSCLOCK, DISPLAYSURF pygame.init() FPSCLOCK = pygame.time.Clock() DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT)) mousex = 0 # used to store x coordinate of mouse event mousey = 0 # used to store y coordinate of mouse event pygame.display.set_caption('Memory Game')
这是main()
函数的开始,这是游戏代码的主要部分。main()
函数中调用的函数将在本章后面解释。
第 49 行是一个global
语句。global
语句是global
关键字后跟逗号分隔的变量名列表。然后这些变量名被标记为全局变量。在main()
函数内,这些名称不是指可能恰好与全局变量同名的局部变量。它们是全局变量。在main()
函数中分配给它们的任何值将在main()
函数外持续存在。我们将FPSCLOCK
和DISPLAYSURF
变量标记为全局,因为它们在程序中的其他几个函数中使用。(更多信息请参阅invpy.com/scope
。)
有四条简单的规则来确定变量是局部的还是全局的:
- 如果函数开头有一个变量的全局语句,那么该变量是全局的。
- 如果函数中的变量名与全局变量同名,并且函数从未为变量分配值,那么该变量就是全局变量。
- 如果函数中的变量名与全局变量同名,并且函数确实为变量分配了一个值,那么该变量是局部变量。
- 如果在函数中没有与函数中的变量同名的全局变量,那么该变量显然是局部变量。
通常希望避免在函数内部使用全局变量。函数应该像程序中的迷你程序,具有特定的输入(参数)和输出(返回值)。但是读取和写入全局变量的函数具有额外的输入和输出。由于在调用函数之前全局变量可能已在许多地方被修改,因此跟踪涉及全局变量中设置错误值的错误可能会很棘手。
将函数作为一个单独的迷你程序,不使用全局变量,可以更容易地找到代码中的错误,因为函数的参数是明确已知的。它还使得更改函数中的代码更容易,因为如果新函数与相同的参数一起工作并给出相同的返回值,它将自动与程序的其余部分一样工作,就像旧函数一样。
基本上,使用全局变量可能会使编写程序变得更容易,但它们通常会使调试变得更加困难。
在本书中的游戏中,全局变量主要用于永远不会改变的全局常量,但需要先调用pygame.init()
函数。由于这发生在main()
函数中,它们在main()
函数中设置并且必须是全局的,以便其他函数可以看到它们。但是全局变量被用作常量,不会改变,因此不太可能引起混乱的错误。
如果您不理解这一点,不要担心。只需编写代码,使您将值传递给函数,而不是让函数读取全局变量作为一般规则。
数据结构和二维列表
mainBoard = getRandomizedBoard() revealedBoxes = generateRevealedBoxesData(False)
getRandomizedBoard()
函数返回表示棋盘状态的数据结构。generateRevealedBoxesData()
函数返回表示哪些方框被覆盖的数据结构。这些函数的返回值是二维(2D)列表,或者说是列表的列表。列表的列表的值将是一个 3D 列表。另一个表示二维或多维列表的词是多维列表。
如果我们在名为spam
的变量中存储了一个列表值,我们可以使用方括号访问该列表中的值,比如spam[2]
来检索列表中的第三个值。如果spam[2]
处的值本身是一个列表,那么我们可以使用另一组方括号来检索该列表中的值。例如,spam[2][4]
将检索spam
中第三个值中的第五个值。使用这种列表的列表的表示法使得将 2D 板映射到 2D 列表值变得容易。由于mainBoard
变量将在其中存储图标,如果我们想要获取板上位置(4, 5)处的图标,我们可以使用表达式mainBoard[4][5]
。由于图标本身存储为形状和颜色的两个元组,完整的数据结构是一个两个元组的列表的列表。呼!
这里有一个小例子。假设板看起来是这样的:
相应的数据结构将是:
mainBoard = [[(DONUT, BLUE), (LINES, BLUE), (SQUARE, ORANGE)], [(SQUARE, GREEN), (DONUT, BLUE), (DIAMOND, YELLOW)], [(SQUARE, GREEN), (OVAL, YELLOW), (SQUARE, ORANGE)], [(DIAMOND, YELLOW), (LINES, BLUE), (OVAL, YELLOW)]]
(如果你的书是黑白的,你可以在invpy.com/memoryboard
看到上面图片的彩色版本。)你会注意到mainBoard[x][y]
将对应于板上坐标(x, y)处的图标。
同时,“revealed boxes”数据结构也是一个二维列表,不同于棋盘数据结构的是,它包含布尔值:如果该 x,y 坐标处的方块已经被揭示,则为True
,如果被覆盖,则为False
。将False
传递给generateRevealedBoxesData()
函数会将所有布尔值设置为False
。(此函数将在后面详细解释。)
这两个数据结构用于跟踪游戏板的状态。
“开始游戏”动画
firstSelection = None # stores the (x, y) of the first box clicked. DISPLAYSURF.fill(BGCOLOR) startGameAnimation(mainBoard)
第 61 行设置了一个名为firstSelection
的变量,其值为None
。(None
是表示缺少值的值。它是数据类型NoneType
的唯一值。更多信息请参阅invpy.com/None
) 当玩家点击棋盘上的图标时,程序需要跟踪这是否是点击的一对图标中的第一个图标还是第二个图标。如果firstSelection
是None
,则点击是在第一个图标上,我们将 XY 坐标存储在firstSelection
变量中,作为两个整数的元组(一个用于 X 值,另一个用于 Y 值)。在第二次点击时,值将是这个元组而不是None
,这是程序跟踪到第二个图标点击的方式。第 63 行用背景颜色填充整个表面。这也会覆盖表面上原来的任何东西,这给了我们一个干净的板子来开始绘制图形。
如果你玩过记忆迷题游戏,你会注意到在游戏开始时,所有的方块都会被快速地随机覆盖和揭示,以便玩家偷看哪些图标在哪些方块下。这一切都发生在startGameAnimation()
函数中,这将在本章后面详细解释。
给玩家这个偷看的机会很重要(但不要让玩家轻松记住图标的位置),否则他们将不知道任何图标在哪里。盲目点击图标不如有一点提示来得有趣。
游戏循环
while True: # main game loop mouseClicked = False DISPLAYSURF.fill(BGCOLOR) # drawing the window drawBoard(mainBoard, revealedBoxes)
游戏循环是一个无限循环,从第 66 行开始,只要游戏正在进行中就会不断迭代。请记住,游戏循环处理事件,更新游戏状态,并将游戏状态绘制到屏幕上。
记忆迷题程序的游戏状态存储在以下变量中:
mainBoard
revealedBoxes
firstSelection
mouseClicked
mousex
mousey
在记忆迷题程序的游戏循环的每次迭代中,mouseClicked
变量存储一个布尔值,如果玩家在游戏循环中的这次迭代中点击了鼠标,则为True
。(这是跟踪游戏状态的一部分。)
在第 69 行,表面被涂上背景颜色,擦除之前绘制的任何东西。然后程序调用drawBoard()
根据我们传递给它的棋盘和“揭示方块”数据结构来绘制棋盘的当前状态。(这些代码行是绘制和更新屏幕的一部分。)
请记住,我们的绘图函数只在内存中的显示 Surface 对象上绘制。这个 Surface 对象直到我们调用pygame.display.update()
才会真正出现在屏幕上,这是在第 121 行的游戏循环的末尾完成的。
事件处理循环
for event in pygame.event.get(): # event handling loop if event.type == QUIT or (event.type == KEYUP and event.key == K_ESCAPE): pygame.quit() sys.exit() elif event.type == MOUSEMOTION: mousex, mousey = event.pos elif event.type == MOUSEBUTTONUP: mousex, mousey = event.pos mouseClicked = True
第 72 行的for
循环执行自上次游戏循环迭代以来发生的每个事件的代码。这个循环被称为事件处理循环(与游戏循环不同,尽管事件处理循环在游戏循环内部),并迭代由pygame.event.get()
调用返回的pygame.Event
对象列表。
如果事件对象是QUIT
事件或KEYUP
事件(即 Esc 键),则程序应该终止。否则,在MOUSEMOTION
事件(即鼠标光标移动)或MOUSEBUTTONUP
事件(即先前按下鼠标按钮,现在释放按钮)的情况下,鼠标光标的位置应该存储在mousex
和mousey
变量中。如果这是一个MOUSEBUTTONUP
事件,mouseClicked
也应该设置为True
。
一旦我们处理了所有事件,存储在mousex
,mousey
和mouseClicked
中的值将告诉我们玩家给了我们任何输入。现在我们应该更新游戏状态并将结果绘制到屏幕上。
检查鼠标光标在哪个方块上
boxx, boxy = getBoxAtPixel(mousex, mousey) if boxx != None and boxy != None: # The mouse is currently over a box. if not revealedBoxes[boxx][boxy]: drawHighlightBox(boxx, boxy)
getBoxAtPixel()
函数将返回两个整数的元组。这些整数表示鼠标坐标所在方块的 XY 棋盘坐标。getBoxAtPixel()
是如何做到的将在后面解释。现在我们只需要知道,如果mousex
和mousey
坐标在一个方块上,函数将返回 XY 棋盘坐标的元组,并存储在boxx
和boxy
中。如果鼠标光标不在任何方块上(例如,如果它在棋盘的一侧或在方块之间的间隙中),那么函数将返回元组(None, None)
,并且boxx
和boxy
都将存储None
。
我们只关心boxx
和boxy
中没有None
的情况,所以下面几行代码是在第 83 行的if
语句后面的块中。如果执行进入了这个块,我们知道用户将鼠标光标放在了一个方块上(也许还点击了鼠标,这取决于mouseClicked
中存储的值)。
第 85 行的if
语句检查方块是否被盖住,通过读取revealedBoxes[boxx][boxy]
中存储的值。如果是False
,那么我们知道方块被盖住了。每当鼠标光标在被盖住的方块上时,我们希望在方块周围绘制蓝色的高亮,以通知玩家他们可以点击它。这种高亮是不会为已经被揭示的方块绘制的。高亮绘制由我们的drawHighlightBox()
函数处理,这将在后面解释。
if not revealedBoxes[boxx][boxy] and mouseClicked: revealBoxesAnimation(mainBoard, [(boxx, boxy)]) revealedBoxes[boxx][boxy] = True # set the box as "revealed"
在第 87 行,我们检查鼠标光标不仅覆盖了一个被盖住的方块,而且鼠标也被点击了。在这种情况下,我们希望通过调用我们的revealBoxesAnimation()
函数来播放该方块的“揭示”动画(就像本章后面解释的所有其他函数一样)。您应该注意,调用此函数只会绘制方块被揭示的动画。直到第 89 行我们设置revealedBoxes[boxx][boxy] = True
,游戏状态的数据结构才会更新。
如果注释掉第 89 行然后运行程序,您会注意到点击方块后,揭示动画会播放,但然后方块立即再次被覆盖。这是因为revealedBoxes[boxx][boxy]
仍然设置为False
,因此在游戏循环的下一次迭代中,该方块将被覆盖。在我们的程序中,没有第 89 行会导致一个相当奇怪的错误。
处理第一个点击的方块
if firstSelection == None: # the current box was the first box clicked firstSelection = (boxx, boxy) else: # the current box was the second box clicked # Check if there is a match between the two icons. icon1shape, icon1color = getShapeAndColor(mainBoard, firstSelection[0], firstSelection[1]) icon2shape, icon2color = getShapeAndColor(mainBoard, boxx, boxy)
在执行进入游戏循环之前,firstSelection
变量被设置为None
。我们的程序将解释为没有点击任何方块,因此如果第 90 行的条件为True
,这意味着这是可能匹配的两个方块中的第一个被点击的。我们希望播放方块的揭示动画,然后保持该方块未覆盖。我们还将firstSelection
变量设置为被点击方块的坐标元组。
如果这是玩家点击的第二个方块,我们希望播放该方块的揭示动画,然后检查方块下的两个图标是否匹配。getShapeAndColor()
函数(稍后解释)将检索图标的形状和颜色值。(这些值将是ALLCOLORS
和ALLSHAPES
元组中的一个值。)
处理不匹配的图标对
if icon1shape != icon2shape or icon1color != icon2color: # Icons don't match. Re-cover up both selections. pygame.time.wait(1000) # 1000 milliseconds = 1 sec coverBoxesAnimation(mainBoard, [(firstSelection[0], firstSelection[1]), (boxx, boxy)]) revealedBoxes[firstSelection[0]][firstSelection [1]] = False revealedBoxes[boxx][boxy] = False
第 97 行的if
语句检查两个图标的形状或颜色是否不匹配。如果是这种情况,我们希望通过调用pygame.time.wait(1000)
暂停游戏 1000 毫秒(即 1 秒),以便玩家有机会看到两个图标不匹配。然后播放两个方块的“覆盖”动画。我们还希望更新游戏状态,将这些方块标记为未揭示(即覆盖)。
处理玩家获胜
elif hasWon(revealedBoxes): # check if all pairs found gameWonAnimation(mainBoard) pygame.time.wait(2000) # Reset the board mainBoard = getRandomizedBoard() revealedBoxes = generateRevealedBoxesData(False) # Show the fully unrevealed board for a second. drawBoard(mainBoard, revealedBoxes) pygame.display.update() pygame.time.wait(1000) # Replay the start game animation. startGameAnimation(mainBoard) firstSelection = None # reset firstSelection variable
否则,如果第 97 行的条件为False
,那么两个图标必须匹配。此时程序实际上不必对方块做任何其他操作:它可以将两个方块保持在揭示状态。但是,程序应该检查这是否是板上最后一对要匹配的图标。这是在我们的hasWon()
函数内完成的,如果板处于获胜状态(即所有方块都被揭示),则返回True
。
如果是这种情况,我们希望通过调用gameWonAnimation()
来播放“游戏获胜”动画,然后稍微暂停一下,让玩家沉浸在胜利中,然后重置mainBoard
和revealedBoxes
中的数据结构,以开始新游戏。
117 行再次播放“开始游戏”动画。之后,程序执行将像往常一样循环执行游戏循环,玩家可以继续玩游戏,直到退出程序。
无论两个方块是否匹配,第 118 行在第二个方块被点击后将firstSelection
变量设置回None
,以便玩家下一个点击的方块被解释为可能匹配的图标对中的第一个点击的方块。
将游戏状态绘制到屏幕上。
# Redraw the screen and wait a clock tick. pygame.display.update() FPSCLOCK.tick(FPS)
此时,游戏状态已根据玩家的输入进行了更新,并且最新的游戏状态已绘制到DISPLAYSURF
显示表面对象上。我们已经到达游戏循环的末尾,因此我们调用pygame.display.update()
将DISPLAYSURF
表面对象绘制到计算机屏幕上。
第 9 行将 FPS 常量设置为整数值30
,这意味着我们希望游戏以(最多)30 帧每秒的速度运行。如果我们希望程序运行得更快,可以增加这个数字。如果我们希望程序运行得更慢,可以减少这个数字。甚至可以将其设置为像0.5
这样的浮点值,这将以每秒半帧的速度运行程序,即每两秒一帧。
为了以每秒 30 帧的速度运行,每帧必须在 1/30 秒内绘制。这意味着 pygame.display.update()
和游戏循环中的所有代码必须在 33.3 毫秒内执行。任何现代计算机都可以轻松做到这一点,并且还有充足的时间。为了防止程序运行得太快,我们调用 FPSCLOCK
对象的 tick()
方法,让它暂停程序剩下的 33.3 毫秒。
由于这是在游戏循环的最后完成的,它确保每次游戏循环迭代花费(至少)33.3 毫秒。如果由于某种原因 pygame.display.update()
调用和游戏循环中的代码花费的时间超过 33.3 毫秒,那么 tick()
方法将不会等待,立即返回。
我一直在说其他函数将在本章后面解释。现在我们已经讨论了 main()
函数,你对整个程序的工作原理有了一个概念,让我们详细了解从 main()
调用的所有其他函数。
创建“已揭示的盒子”数据结构
def generateRevealedBoxesData(val): revealedBoxes = [] for i in range(BOARDWIDTH): revealedBoxes.append([val] * BOARDHEIGHT) return revealedBoxes
generateRevealedBoxesData()
函数需要创建一个布尔值的列表列表。布尔值将作为 val
参数传递给函数。我们将数据结构作为空列表存储在 revealedBoxes
变量中。
为了使数据结构具有 revealedBoxes[x][y]
结构,我们需要确保内部列表表示棋盘的垂直列而不是水平行。否则,数据结构将具有 revealedBoxes[y][x]
结构。
for
循环将创建列,然后将它们附加到 revealedBoxes
。使用列表复制创建列列表,使得列列表具有 BOARDHEIGHT
规定的数量的 val
值。
创建棋盘数据结构:步骤 1 - 获取所有可能的图标
def getRandomizedBoard(): # Get a list of every possible shape in every possible color. icons = [] for color in ALLCOLORS: for shape in ALLSHAPES: icons.append( (shape, color) )
棋盘数据结构只是一个元组的列表列表,每个元组有两个值:一个是图标的形状,一个是图标的颜色。但是创建这个数据结构有点复杂。我们需要确保棋盘上的盒子数量与图标数量完全一样,并且确保每种类型只有两个图标。
首先要做的是创建一个包含每种形状和颜色的可能组合的列表。回想一下,我们在 ALLCOLORS
和 ALLSHAPES
中有每种颜色和形状的列表,所以在第 135 和 136 行的嵌套 for
循环将遍历每种可能的颜色和形状组合。这些都将添加到第 137 行的 icons
变量中的列表中。
步骤 2 - 洗牌和截断所有图标的列表
random.shuffle(icons) # randomize the order of the icons list numIconsUsed = int(BOARDWIDTH * BOARDHEIGHT / 2) # calculate how many icons are needed icons = icons[:numIconsUsed] * 2 # make two of each random.shuffle(icons)
但请记住,可能的组合可能比棋盘上的空格多。我们需要通过将 BOARDWIDTH
乘以 BOARDHEIGHT
来计算棋盘上的空格数。然后我们将这个数字除以 2,因为我们将有图标的成对出现。在一个有 70 个空格的棋盘上,我们只需要 35 种不同的图标,因为每种图标将有两个。这个数字将存储在 numIconsUsed
中。
第 141 行使用列表切片来获取列表中的前 numIconsUsed
个图标。(如果你忘记了列表切片的工作原理,请查看 invpy.com/slicing
。)这个列表在第 139 行被打乱,所以每局游戏它都不会是相同的图标。然后使用 *
运算符复制这个列表,使得每种图标都有两个。这个新的重复列表将覆盖 icons
变量中的旧列表。由于这个新列表的前半部分与后半部分相同,我们再次调用 shuffle()
方法来随机混合图标的顺序。
步骤 3 - 将图标放置在棋盘上
# Create the board data structure, with randomly placed icons. board = [] for x in range(BOARDWIDTH): column = [] for y in range(BOARDHEIGHT): column.append(icons[0]) del icons[0] # remove the icons as we assign them board.append(column) return board
现在我们需要为棋盘创建一个列表的列表数据结构。我们可以使用嵌套的for
循环来做到这一点,就像generateRevealedBoxesData()
函数一样。对于棋盘上的每一列,我们将创建一个随机选择图标的列表。当我们向列添加图标时,在第 149 行,我们将从第 150 行的icons
列表中删除它们。这样,随着icons
列表变得越来越短,icons[0]
将有一个不同的图标添加到列中。
为了更好地理解这一点,请将以下代码输入交互式 shell 中。注意del
语句如何改变myList
列表。
>>> myList = ['cat', 'dog', 'mouse', 'lizard'] >>> del myList[0] >>> myList ['dog', 'mouse', 'lizard'] >>> del myList[0] >>> myList ['mouse', 'lizard'] >>> del myList[0] >>> myList ['lizard'] >>> del myList[0] >>> myList [] >>>
因为我们删除了列表前面的项目,其他项目向前移动,以便列表中的下一个项目成为新的“第一个”项目。这与第 150 行的工作方式相同。
将列表拆分为列表的列表
def splitIntoGroupsOf(groupSize, theList): # splits a list into a list of lists, where the inner lists have at # most groupSize number of items. result = [] for i in range(0, len(theList), groupSize): result.append(theList[i:i + groupSize]) return result
splitIntoGroupsOf()
函数(将被startGameAnimation()
函数调用)将一个列表拆分为一个列表的列表,其中内部列表中有groupSize
个项目。(如果剩下的项目少于groupSize
,最后一个列表可能会少一些。)
第 159 行对range()
的调用使用了range()
的三参数形式。(如果您对这种形式不熟悉,请看一下invpy.com/range
。)让我们举个例子。如果列表的长度是20
,groupSize
参数是8
,那么range(0, len(theList), groupSize)
将评估为range(0, 20, 8)
。这将使i
变量在for
循环的三次迭代中分别取值0
、8
和16
。
在第 160 行的列表切片 theList[i:i + groupSize]创建了添加到result
列表中的列表。在每次i
为0
、8
和16
(groupSize
为8
)的迭代中,这个列表切片表达式将是theList[0:8]
,然后在第二次迭代时是theList[8:16]
,然后在第三次迭代时是theList[16:24]
。
请注意,即使在我们的示例中theList
的最大索引是19
,theList[16:24]
也不会引发IndexError
错误,即使24
大于19
。它只会创建一个包含列表中剩余项目的列表切片。列表切片不会破坏或更改存储在theList
中的原始列表。它只是复制其中的一部分以评估为新的列表值。这个新的列表值是追加到第 160 行result
变量中的列表。因此,当我们在这个函数的末尾返回result
时,我们返回的是一个列表的列表。
不同的坐标系
def leftTopCoordsOfBox(boxx, boxy): # Convert board coordinates to pixel coordinates left = boxx * (BOXSIZE + GAPSIZE) + XMARGIN top = boxy * (BOXSIZE + GAPSIZE) + YMARGIN return (left, top)
您应该熟悉笛卡尔坐标系。(如果您想对这个主题进行复习,请阅读invpy.com/coordinates
。)在我们的大多数游戏中,我们将使用多个笛卡尔坐标系。在记忆拼图游戏中使用的坐标系之一是像素或屏幕坐标。但我们还将为盒子使用另一个坐标系。这是因为使用(3,2)来指代从左边数第 4 个方块和从上方数第 3 个方块(请记住,数字从 0 开始,而不是 1)会比使用方块左上角的像素坐标(220,165)更容易。但是,我们需要一种方法来在这两个坐标系之间进行转换。
这是游戏的图片和两种不同的坐标系统。请记住,窗口宽度为 640 像素,高度为 480 像素,因此(639,479)是右下角(因为左上角的像素是(0,0),而不是(1,1))。
leftTopCoordsOfBox()
函数将接受盒子坐标并返回像素坐标。因为一个盒子在屏幕上占据多个像素,我们将始终返回盒子左上角的单个像素。这个值将作为一个由两个整数组成的元组返回。当我们需要绘制这些盒子时,leftTopCoordsOfBox()
函数经常会被使用来获取像素坐标。
从像素坐标转换为盒子坐标
def getBoxAtPixel(x, y): for boxx in range(BOARDWIDTH): for boxy in range(BOARDHEIGHT): left, top = leftTopCoordsOfBox(boxx, boxy) boxRect = pygame.Rect(left, top, BOXSIZE, BOXSIZE) if boxRect.collidepoint(x, y): return (boxx, boxy) return (None, None)
我们还需要一个函数来将像素坐标(鼠标点击和鼠标移动事件使用的)转换为框坐标(这样我们就可以找出鼠标事件发生在哪个框上)。Rect 对象有一个collidepoint()
方法,您可以传递 X 和 Y 坐标,如果坐标在 Rect 对象的区域内(即与之相撞),它将返回True
。
为了找出鼠标坐标所在的框,我们将遍历每个框的坐标,并在具有这些坐标的 Rect 对象上调用collidepoint()
方法。当collidepoint()
返回True
时,我们知道找到了被点击或移动的框,并将返回框坐标。如果它们都没有返回True
,那么getBoxAtPixel()
函数将返回值(None, None)
。返回这个元组而不是简单地返回None
,是因为getBoxAtPixel()
的调用者期望返回两个值的元组。
绘制图标和语法糖
def drawIcon(shape, color, boxx, boxy): quarter = int(BOXSIZE * 0.25) # syntactic sugar half = int(BOXSIZE * 0.5) # syntactic sugar left, top = leftTopCoordsOfBox(boxx, boxy) # get pixel coords from board coords
drawIcon()
函数将在给定boxx
和boxy
参数中的空间上绘制一个图标(具有指定的shape
和color
)。每种可能的形状都有不同的 Pygame 绘图函数调用集,因此我们必须有一大堆if
和elif
语句来区分它们。(这些语句在第 187 到 198 行。)
可以通过调用leftTopCoordsOfBox()
函数获得框的左边和顶部边缘的 X 和 Y 坐标。框的宽度和高度都在BOXSIZE
常量中设置。然而,许多形状绘制函数调用也使用框的中点和四分点。我们可以计算这个并将其存储在变量quarter
和half
中。我们可以很容易地使用代码int(BOXSIZE * 0.25)
代替变量quarter
,但这样代码变得更易读,因为更容易理解quarter
的含义,而不是int(BOXSIZE * 0.25)
。
这些变量是语法糖的一个例子。语法糖是指我们添加的代码,本来可以用另一种方式编写(可能使用更少的实际代码和变量),但确实使源代码更易于阅读。常量变量是语法糖的一种形式。预先计算一个值并将其存储在变量中是另一种类型的语法糖。(例如,在getRandomizedBoard()
函数中,我们可以很容易地将第 140 行和第 141 行的代码合并为一行代码。但将其分开阅读更容易。)我们不需要额外的quarter
和half
变量,但是拥有它们使得代码更易于阅读。易于阅读的代码易于调试和将来升级。
# Draw the shapes if shape == DONUT: pygame.draw.circle(DISPLAYSURF, color, (left + half, top + half), half - 5) pygame.draw.circle(DISPLAYSURF, BGCOLOR, (left + half, top + half), quarter - 5) elif shape == SQUARE: pygame.draw.rect(DISPLAYSURF, color, (left + quarter, top + quarter, BOXSIZE - half, BOXSIZE - half)) elif shape == DIAMOND: pygame.draw.polygon(DISPLAYSURF, color, ((left + half, top), (left + BOXSIZE - 1, top + half), (left + half, top + BOXSIZE - 1), (left, top + half))) elif shape == LINES: for i in range(0, BOXSIZE, 4): pygame.draw.line(DISPLAYSURF, color, (left, top + i), (left + i, top)) pygame.draw.line(DISPLAYSURF, color, (left + i, top + BOXSIZE - 1), (left + BOXSIZE - 1, top + i)) elif shape == OVAL: pygame.draw.ellipse(DISPLAYSURF, color, (left, top + quarter, BOXSIZE, half))
每个甜甜圈、正方形、菱形、线条和椭圆函数都需要不同的绘图原语函数调用。
通过获取板块空间的图标形状和颜色来实现语法糖
def getShapeAndColor(board, boxx, boxy): # shape value for x, y spot is stored in board[x][y][0] # color value for x, y spot is stored in board[x][y][1] return board[boxx][boxy][0], board[boxx][boxy][1]
getShapeAndColor()
函数只有一行。您可能会想知道为什么我们要使用函数而不是在需要时键入那一行代码。这是因为它提高了代码的可读性,就像我们使用常量变量的原因一样。
像shape, color = getShapeAndColor()
这样的代码很容易理解。但是如果你看到像shape, color = board[boxx][boxy][0], board[boxx][boxy][1]
这样的代码,可能会更难理解。
绘制框盖
def drawBoxCovers(board, boxes, coverage): # Draws boxes being covered/revealed. "boxes" is a list # of two-item lists, which have the x & y spot of the box. for box in boxes: left, top = leftTopCoordsOfBox(box[0], box[1]) pygame.draw.rect(DISPLAYSURF, BGCOLOR, (left, top, BOXSIZE, BOXSIZE)) shape, color = getShapeAndColor(board, box[0], box[1]) drawIcon(shape, color, box[0], box[1]) if coverage > 0: # only draw the cover if there is an coverage pygame.draw.rect(DISPLAYSURF, BOXCOLOR, (left, top, coverage, BOXSIZE)) pygame.display.update() FPSCLOCK.tick(FPS)
drawBoxCovers()
函数有三个参数:板块数据结构,每个应该绘制盖子的框的(X,Y)元组列表,以及要为框绘制的覆盖量。
由于我们想要对boxes
参数中的每个方块使用相同的绘制代码,所以我们将在第 211 行使用for
循环,以便在boxes
列表中的每个方块上执行相同的代码。在这个for
循环内部,代码应该做三件事:绘制背景颜色(覆盖之前的任何东西),绘制图标,然后绘制所需的白色方块覆盖在图标上。leftTopCoordsOfBox()
函数将返回方块左上角的像素坐标。第 216 行的if
语句确保如果coverage
中的数字恰好小于0
,我们不会调用pygame.draw.rect()
函数。
当coverage
参数为0
时,完全没有覆盖。当coverage
设置为20
时,有一个 20 像素宽的白色方块覆盖图标。我们希望coverage
设置的最大尺寸是BOXSIZE
中的数字,这样整个图标就完全被覆盖了。
drawBoxCovers()
将在一个单独的循环中被调用,而不是游戏循环。因此,它需要有自己的pygame.display.update()
和FPSCLOCK.tick(FPS)
的调用来显示动画。(这意味着在这个循环内部,没有任何代码来处理生成的任何事件。这没关系,因为覆盖和揭示动画只需要一秒左右的时间来播放。)
处理揭示和覆盖动画
def revealBoxesAnimation(board, boxesToReveal): # Do the "box reveal" animation. for coverage in range(BOXSIZE, (-REVEALSPEED) - 1, - REVEALSPEED): drawBoxCovers(board, boxesToReveal, coverage) def coverBoxesAnimation(board, boxesToCover): # Do the "box cover" animation. for coverage in range(0, BOXSIZE + REVEALSPEED, REVEALSPEED): drawBoxCovers(board, boxesToCover, coverage)
记住,动画只是在短暂的时间内显示不同的图像,它们一起让人觉得屏幕上的东西在移动。revealBoxesAnimation()
和coverBoxesAnimation()
只需要绘制一个带有不同覆盖量的白色方块的图标。我们可以编写一个名为drawBoxCovers()
的单个函数来做到这一点,然后让我们的动画函数为每一帧动画调用drawBoxCovers()
。正如我们在上一节中看到的,drawBoxCovers()
本身调用pygame.display.update()
和FPSCLOCK.tick(FPS)
。
为了做到这一点,我们将设置一个for
循环来使converage
参数递减(在revealBoxesAnimation()
的情况下)或递增(在coverBoxesAnimation()
的情况下)。coverage
变量将递减/递增的数量是REVEALSPEED
常量中的数字。在第 12 行,我们将这个常量设置为8
,这意味着在每次调用drawBoxCovers()
时,白色方块将在每次迭代中减少/增加 8 像素。如果我们增加这个数字,那么每次调用时将绘制更多的像素,这意味着白色方块的大小将更快地减少/增加。如果我们将其设置为1
,那么白色方块将只在每次迭代中减少或增加 1 像素,使整个揭示或覆盖动画需要更长的时间。
想象一下像爬楼梯一样。如果在每一步上,你爬了一级楼梯,那么爬完整个楼梯就需要正常的时间。但是如果你每一步上爬两级楼梯(并且每一步的时间和以前一样长),你就可以以两倍的速度爬完整个楼梯。如果你一次爬 8 级楼梯,那么你就可以以 8 倍的速度爬完整个楼梯。
绘制整个棋盘
def drawBoard(board, revealed): # Draws all of the boxes in their covered or revealed state. for boxx in range(BOARDWIDTH): for boxy in range(BOARDHEIGHT): left, top = leftTopCoordsOfBox(boxx, boxy) if not revealed[boxx][boxy]: # Draw a covered box. pygame.draw.rect(DISPLAYSURF, BOXCOLOR, (left, top, BOXSIZE, BOXSIZE)) else: # Draw the (revealed) icon. shape, color = getShapeAndColor(board, boxx, boxy) drawIcon(shape, color, boxx, boxy)
drawBoard()
函数为棋盘上的每个方块调用drawIcon()
。第 236 和 237 行的嵌套for
循环将循环遍历每个可能的 X 和 Y 坐标的方块,并在该位置绘制图标或绘制一个白色方块(代表被覆盖的方块)。
绘制高亮
def drawHighlightBox(boxx, boxy): left, top = leftTopCoordsOfBox(boxx, boxy) pygame.draw.rect(DISPLAYSURF, HIGHLIGHTCOLOR, (left - 5, top - 5, BOXSIZE + 10, BOXSIZE + 10), 4)
为了帮助玩家认识到他们可以点击覆盖的方块来揭示它,我们将在方块周围绘制一个蓝色的轮廓来突出显示它。这个轮廓是通过调用pygame.draw.rect()
来绘制一个宽度为 4 像素的矩形。
“开始游戏”动画
def startGameAnimation(board): # Randomly reveal the boxes 8 at a time. coveredBoxes = generateRevealedBoxesData(False) boxes = [] for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): boxes.append( (x, y) ) random.shuffle(boxes) boxGroups = splitIntoGroupsOf(8, boxes)
游戏开始时播放的动画为玩家提供了所有图标位置的快速提示。为了制作这个动画,我们必须逐个显示和覆盖一组又一组的框。为此,首先我们将创建一个包含棋盘上每个可能空间的列表。在第 257 和 258 行的嵌套for
循环将(X, Y)元组添加到boxes
变量中的列表中。
我们将逐个显示和覆盖此列表中的前 8 个框,然后是接下来的 8 个,然后是接下来的 8 个,依此类推。然而,由于框中(X, Y)元组的顺序每次都是相同的,因此将显示相同顺序的框。(尝试注释掉第 260 行,然后运行程序几次以查看此效果。)
为了在每次游戏开始时改变框的位置,我们将调用random.shuffle()
函数来随机打乱框列表中元组的顺序。然后当我们显示和覆盖此列表中的前 8 个框(以及之后的每组 8 个框)时,将是随机的 8 个框组。
为了获得 8 个框的列表,我们调用我们的splitIntoGroupsOf()
函数,传递8
和boxes
中的列表。函数返回的列表的列表将存储在名为boxGroups
的变量中。
显示和覆盖框的组
drawBoard(board, coveredBoxes) for boxGroup in boxGroups: revealBoxesAnimation(board, boxGroup) coverBoxesAnimation(board, boxGroup)
首先,我们绘制棋盘。由于coveredBoxes
中的每个值都设置为False
,因此这次调用drawBoard()
最终将只绘制覆盖的白色框。revealBoxesAnimation()
和coverBoxesAnimation()
函数将在这些白色框的空间上绘制。
for
循环将遍历boxGroups
列表中的每个内部列表。我们将这些传递给revealBoxesAnimation()
,它将执行动画,将白色框拉开以显示下面的图标。然后调用coverBoxesAnimation()
将动画化的白色框扩展以覆盖图标。然后for
循环进入下一个迭代,以动画化下一组 8 个框。
“游戏赢了”动画
def gameWonAnimation(board): # flash the background color when the player has won coveredBoxes = generateRevealedBoxesData(True) color1 = LIGHTBGCOLOR color2 = BGCOLOR for i in range(13): color1, color2 = color2, color1 # swap colors DISPLAYSURF.fill(color1) drawBoard(board, coveredBoxes) pygame.display.update() pygame.time.wait(300)
当玩家通过匹配棋盘上的每一对图标来揭开所有框时,我们希望通过闪烁背景颜色来祝贺他们。for
循环将在color1
变量中绘制背景颜色,然后在其上绘制棋盘。然而,在for
循环的每次迭代中,color1
和color2
的值将在第 276 行互换。这样程序将在两种不同的背景颜色之间交替绘制。
请记住,此函数需要调用pygame.display.update()
来实际使DISPLAYSURF
表面出现在屏幕上。
判断玩家是否赢了
def hasWon(revealedBoxes): # Returns True if all the boxes have been revealed, otherwise False for i in revealedBoxes: if False in i: return False # return False if any boxes are covered. return True
当所有图标对都匹配时,玩家赢得了比赛。由于“revealed”数据结构中的值设置为True
,因为图标已经匹配,我们可以简单地遍历revealedBoxes
中的每个空格,寻找False
值。如果revealedBoxes
中有一个False
值,那么我们知道棋盘上仍然有未匹配的图标。
请注意,由于revealedBoxes
是一个列表的列表,因此第 285 行的for
循环将内部列表设置为i
的值。但是我们可以使用in
运算符在整个内部列表中搜索False
值。这样我们就不需要编写额外的代码并像这样有两个嵌套的for
循环。
for x in revealedBoxes: for y in revealedBoxes[x]: if False == revealedBoxes[x][y]: return False
为什么要有一个main()
函数?
if __name__ == '__main__': main()
似乎将main()
函数放在那里是毫无意义的,因为你可以将该代码放在程序底部的全局范围内,代码将运行完全相同。然而,将它们放在main()
函数内部有两个很好的理由。
首先,这样可以拥有局部变量,否则main()
函数中的局部变量将不得不变成全局变量。限制全局变量的数量是保持代码简单且更易于调试的好方法。(请参阅本章中的“为什么全局变量是邪恶的”部分。)
其次,这还可以让您导入程序,以便您可以从交互式 shell 中调用和测试单个函数。如果memorypuzzle.py文件位于 C:\Python32 文件夹中,则可以从交互式 shell 中导入它。输入以下内容以测试splitIntoGroupsOf()
和getBoxAtPixel()
函数,以确保它们返回正确的返回值:
>>> import memorypuzzle >>> memorypuzzle.splitIntoGroupsOf(3, [0,1,2,3,4,5,6,7,8,9]) [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] >>> memorypuzzle.getBoxAtPixel(0, 0) (None, None) >>> memorypuzzle.getBoxAtPixel(150, 150) (1, 1)
当导入模块时,其中的所有代码都会运行。如果我们没有main()
函数,并且将其代码放在全局范围内,那么游戏将在我们导入它时自动启动,这实际上不会让我们在其中调用单个函数。
这就是为什么代码在一个我们称为main()
的单独函数中。然后我们检查内置的 Python 变量__name__
,看看我们是否应该调用main()
函数。如果程序本身正在运行,则 Python 解释器会自动将此变量设置为字符串'__main__'
,如果正在导入程序,则设置为'memorypuzzle'
。这就是为什么在交互式 shell 中执行import memorypuzzle
语句时,main()
函数不会运行。
这是一种方便的技术,可以从交互式 shell 中导入您正在工作的程序,并通过逐个调用测试单个函数是否返回正确的值。
为什么要关心可读性?
本章中的许多建议并不是关于如何编写计算机可以运行的程序,而是关于如何编写程序,以便程序员可以阅读。您可能不明白这为什么重要。毕竟,只要代码能运行,谁在乎它对人类程序员来说是难还是容易呢?
然而,关于软件的重要一点是,它很少被单独留下。当您创建自己的游戏时,您很少会“完成”程序。您总是会得到想要添加的新游戏功能的新想法,或者发现程序中的新错误。因此,重要的是您的程序是可读的,以便您可以查看代码并理解它。理解代码是更改代码以添加更多代码或修复错误的第一步。
例如,这是记忆迷题程序的一个混淆版本,完全无法阅读。如果您键入它(或从invpy.com/memorypuzzle_obfuscated.py
下载它)并运行它,您会发现它与本章开头的代码完全相同。但是,如果这段代码有错误,那么阅读代码并理解发生了什么,更不用说修复错误了。
计算机不介意代码是否难以阅读。对它来说都一样。
import random, pygame, sys from pygame.locals import * def hhh(): global a, b pygame.init() a = pygame.time.Clock() b = pygame.display.set_mode((640, 480)) j = 0 k = 0 pygame.display.set_caption('Memory Game') i = c() hh = d(False) h = None b.fill((60, 60, 100)) g(i) while True: e = False b.fill((60, 60, 100)) f(i, hh) for eee in pygame.event.get(): if eee.type == QUIT or (eee.type == KEYUP and eee.key == K_ESCAPE): pygame.quit() sys.exit() elif eee.type == MOUSEMOTION: j, k = eee.pos elif eee.type == MOUSEBUTTONUP: j, k = eee.pos e = True bb, ee = m(j, k) if bb != None and ee != None: if not hh[bb][ee]: n(bb, ee) if not hh[bb][ee] and e: o(i, [(bb, ee)]) hh[bb][ee] = True if h == None: h = (bb, ee) else: q, fff = s(i, h[0], h[1]) r, ggg = s(i, bb, ee) if q != r or fff != ggg: pygame.time.wait(1000) p(i, [(h[0], h[1]), (bb, ee)]) hh[h[0]][h[1]] = False hh[bb][ee] = False elif ii(hh): jj(i) pygame.time.wait(2000) i = c() hh = d(False) f(i, hh) pygame.display.update() pygame.time.wait(1000) g(i) h = None pygame.display.update() a.tick(30) def d(ccc): hh = [] for i in range(10): hh.append([ccc] * 7) return hh def c(): rr = [] for tt in ((255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0), (255, 128, 0), (255, 0, 255), (0, 255, 255)): for ss in ('a', 'b', 'c', 'd', 'e'): rr.append( (ss, tt) ) random.shuffle(rr) rr = rr[:35] * 2 random.shuffle(rr) bbb = [] for x in range(10): v = [] for y in range(7): v.append(rr[0]) del rr[0] bbb.append(v) return bbb def t(vv, uu): ww = [] for i in range(0, len(uu), vv): ww.append(uu[i:i + vv]) return ww def aa(bb, ee): return (bb * 50 + 70, ee * 50 + 65) def m(x, y): for bb in range(10): for ee in range(7): oo, ddd = aa(bb, ee) aaa = pygame.Rect(oo, ddd, 40, 40) if aaa.collidepoint(x, y): return (bb, ee) return (None, None) def w(ss, tt, bb, ee): oo, ddd = aa(bb, ee) if ss == 'a': pygame.draw.circle(b, tt, (oo + 20, ddd + 20), 15) pygame.draw.circle(b, (60, 60, 100), (oo + 20, ddd + 20), 5) elif ss == 'b': pygame.draw.rect(b, tt, (oo + 10, ddd + 10, 20, 20)) elif ss == 'c': pygame.draw.polygon(b, tt, ((oo + 20, ddd), (oo + 40 - 1, ddd + 20), (oo + 20, ddd + 40 - 1), (oo, ddd + 20))) elif ss == 'd': for i in range(0, 40, 4): pygame.draw.line(b, tt, (oo, ddd + i), (oo + i, ddd)) pygame.draw.line(b, tt, (oo + i, ddd + 39), (oo + 39, ddd + i)) elif ss == 'e': pygame.draw.ellipse(b, tt, (oo, ddd + 10, 40, 20)) def s(bbb, bb, ee): return bbb[bb][ee][0], bbb[bb][ee][1] def dd(bbb, boxes, gg): for box in boxes: oo, ddd = aa(box[0], box[1]) pygame.draw.rect(b, (60, 60, 100), (oo, ddd, 40, 40)) ss, tt = s(bbb, box[0], box[1]) w(ss, tt, box[0], box[1]) if gg > 0: pygame.draw.rect(b, (255, 255, 255), (oo, ddd, gg, 40)) pygame.display.update() a.tick(30) def o(bbb, cc): for gg in range(40, (-8) - 1, -8): dd(bbb, cc, gg) def p(bbb, ff): for gg in range(0, 48, 8): dd(bbb, ff, gg) def f(bbb, pp): for bb in range(10): for ee in range(7): oo, ddd = aa(bb, ee) if not pp[bb][ee]: pygame.draw.rect(b, (255, 255, 255), (oo, ddd, 40, 40)) else: ss, tt = s(bbb, bb, ee) w(ss, tt, bb, ee) def n(bb, ee): oo, ddd = aa(bb, ee) pygame.draw.rect(b, (0, 0, 255), (oo - 5, ddd - 5, 50, 50), 4) def g(bbb): mm = d(False) boxes = [] for x in range(10): for y in range(7): boxes.append( (x, y) ) random.shuffle(boxes) kk = t(8, boxes) f(bbb, mm) for nn in kk: o(bbb, nn) p(bbb, nn) def jj(bbb): mm = d(True) tt1 = (100, 100, 100) tt2 = (60, 60, 100) for i in range(13): tt1, tt2 = tt2, tt1 b.fill(tt1) f(bbb, mm) pygame.display.update() pygame.time.wait(300) def ii(hh): for i in hh: if False in i: return False return True if __name__ == '__main__': hhh()
永远不要编写这样的代码。如果您在浴室里面对着镜子编写代码,灯关了,阿达·洛夫莱斯的幽灵会从镜子里出来,把您扔进雅克卡德织布机的夹口。
总结和黑客建议
本章涵盖了记忆迷题程序的整个解释。再次阅读本章和源代码,以更好地理解它。本书中的许多其他游戏程序都使用相同的编程概念(如嵌套的for
循环、语法糖和同一程序中的不同坐标系统),因此不会再次解释,以保持本书简短。
尝试了解代码如何工作的一个想法是故意通过注释掉随机行来破坏它。这样做可能会导致语法错误,从而完全阻止脚本的运行。但注释掉其他行将导致奇怪的错误和其他酷炫效果。尝试这样做,然后弄清楚为什么程序有它的错误。
这也是能够向程序添加自己的秘密作弊或黑客的第一步。通过打破程序的正常运行,你可以学会如何改变它以产生一些有趣的效果(比如秘密给你提示如何解决谜题)。随意尝试实验。如果你想再次玩正常的游戏,你可以随时保存未更改的源代码副本到另一个文件中。
事实上,如果你想练习修复错误,这个游戏的源代码有几个版本存在小错误。你可以从invpy.com/buggy/memorypuzzle
下载这些有 bug 的版本。尝试运行程序,找出错误在哪里,以及程序为什么会那样运行。
第四章:滑动拼图
原文:
inventwithpython.com/pygame/chapter4.html
译者:飞龙
如何玩滑动拼图
棋盘是一个 4x4 的网格,有 15 个方块(从左到右编号为 1 到 15)和一个空白格。方块最初以随机位置开始,玩家必须将方块滑动到它们的原始顺序。
滑动拼图的源代码
此源代码可从invpy.com/slidepuzzle.py
下载。如果出现任何错误消息,请查看错误消息中提到的行号,并检查代码中是否有任何拼写错误。您还可以将代码复制粘贴到invpy.com/diff/slidepuzzle
的网络表单中,以查看您的代码与书中代码之间的差异。
# Slide Puzzle # By Al Sweigart [[email protected]](/cdn-cgi/l/email-protection) # http://inventwithpython.com/pygame # Creative Commons BY-NC-SA 3.0 US import pygame, sys, random from pygame.locals import * # Create the constants (go ahead and experiment with different values) BOARDWIDTH = 4 # number of columns in the board BOARDHEIGHT = 4 # number of rows in the board TILESIZE = 80 WINDOWWIDTH = 640 WINDOWHEIGHT = 480 FPS = 30 BLANK = None # R G B BLACK = ( 0, 0, 0) WHITE = (255, 255, 255) BRIGHTBLUE = ( 0, 50, 255) DARKTURQUOISE = ( 3, 54, 73) GREEN = ( 0, 204, 0) BGCOLOR = DARKTURQUOISE TILECOLOR = GREEN TEXTCOLOR = WHITE BORDERCOLOR = BRIGHTBLUE BASICFONTSIZE = 20 BUTTONCOLOR = WHITE BUTTONTEXTCOLOR = BLACK MESSAGECOLOR = WHITE XMARGIN = int((WINDOWWIDTH - (TILESIZE * BOARDWIDTH + (BOARDWIDTH - 1))) / 2) YMARGIN = int((WINDOWHEIGHT - (TILESIZE * BOARDHEIGHT + (BOARDHEIGHT - 1))) / 2) UP = 'up' DOWN = 'down' LEFT = 'left' RIGHT = 'right' def main(): global FPSCLOCK, DISPLAYSURF, BASICFONT, RESET_SURF, RESET_RECT, NEW_SURF, NEW_RECT, SOLVE_SURF, SOLVE_RECT pygame.init() FPSCLOCK = pygame.time.Clock() DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT)) pygame.display.set_caption('Slide Puzzle') BASICFONT = pygame.font.Font('freesansbold.ttf', BASICFONTSIZE) # Store the option buttons and their rectangles in OPTIONS. RESET_SURF, RESET_RECT = makeText('Reset', TEXTCOLOR, TILECOLOR, WINDOWWIDTH - 120, WINDOWHEIGHT - 90) NEW_SURF, NEW_RECT = makeText('New Game', TEXTCOLOR, TILECOLOR, WINDOWWIDTH - 120, WINDOWHEIGHT - 60) SOLVE_SURF, SOLVE_RECT = makeText('Solve', TEXTCOLOR, TILECOLOR, WINDOWWIDTH - 120, WINDOWHEIGHT - 30) mainBoard, solutionSeq = generateNewPuzzle(80) SOLVEDBOARD = getStartingBoard() # a solved board is the same as the board in a start state. allMoves = [] # list of moves made from the solved configuration while True: # main game loop slideTo = None # the direction, if any, a tile should slide msg = '' # contains the message to show in the upper left corner. if mainBoard == SOLVEDBOARD: msg = 'Solved!' drawBoard(mainBoard, msg) checkForQuit() for event in pygame.event.get(): # event handling loop if event.type == MOUSEBUTTONUP: spotx, spoty = getSpotClicked(mainBoard, event.pos[0], event.pos[1]) if (spotx, spoty) == (None, None): # check if the user clicked on an option button if RESET_RECT.collidepoint(event.pos): resetAnimation(mainBoard, allMoves) # clicked on Reset button allMoves = [] elif NEW_RECT.collidepoint(event.pos): mainBoard, solutionSeq = generateNewPuzzle(80) # clicked on New Game button allMoves = [] elif SOLVE_RECT.collidepoint(event.pos): resetAnimation(mainBoard, solutionSeq + allMoves) # clicked on Solve button allMoves = [] else: # check if the clicked tile was next to the blank spot blankx, blanky = getBlankPosition(mainBoard) if spotx == blankx + 1 and spoty == blanky: slideTo = LEFT elif spotx == blankx - 1 and spoty == blanky: slideTo = RIGHT elif spotx == blankx and spoty == blanky + 1: slideTo = UP elif spotx == blankx and spoty == blanky - 1: slideTo = DOWN elif event.type == KEYUP: # check if the user pressed a key to slide a tile if event.key in (K_LEFT, K_a) and isValidMove(mainBoard, LEFT): slideTo = LEFT elif event.key in (K_RIGHT, K_d) and isValidMove(mainBoard, RIGHT): slideTo = RIGHT elif event.key in (K_UP, K_w) and isValidMove(mainBoard, UP): slideTo = UP elif event.key in (K_DOWN, K_s) and isValidMove(mainBoard, DOWN): slideTo = DOWN if slideTo: slideAnimation(mainBoard, slideTo, 'Click tile or press arrow keys to slide.', 8) # show slide on screen makeMove(mainBoard, slideTo) allMoves.append(slideTo) # record the slide pygame.display.update() FPSCLOCK.tick(FPS) def terminate(): pygame.quit() sys.exit() def checkForQuit(): for event in pygame.event.get(QUIT): # get all the QUIT events terminate() # terminate if any QUIT events are present for event in pygame.event.get(KEYUP): # get all the KEYUP events if event.key == K_ESCAPE: terminate() # terminate if the KEYUP event was for the Esc key pygame.event.post(event) # put the other KEYUP event objects back def getStartingBoard(): # Return a board data structure with tiles in the solved state. # For example, if BOARDWIDTH and BOARDHEIGHT are both 3, this function # returns [[1, 4, 7], [2, 5, 8], [3, 6, None]] counter = 1 board = [] for x in range(BOARDWIDTH): column = [] for y in range(BOARDHEIGHT): column.append(counter) counter += BOARDWIDTH board.append(column) counter -= BOARDWIDTH * (BOARDHEIGHT - 1) + BOARDWIDTH - 1 board[BOARDWIDTH-1][BOARDHEIGHT-1] = None return board def getBlankPosition(board): # Return the x and y of board coordinates of the blank space. for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): if board[x][y] == None: return (x, y) def makeMove(board, move): # This function does not check if the move is valid. blankx, blanky = getBlankPosition(board) if move == UP: board[blankx][blanky], board[blankx][blanky + 1] = board[blankx][blanky + 1], board[blankx][blanky] elif move == DOWN: board[blankx][blanky], board[blankx][blanky - 1] = board[blankx][blanky - 1], board[blankx][blanky] elif move == LEFT: board[blankx][blanky], board[blankx + 1][blanky] = board[blankx + 1][blanky], board[blankx][blanky] elif move == RIGHT: board[blankx][blanky], board[blankx - 1][blanky] = board[blankx - 1][blanky], board[blankx][blanky] def isValidMove(board, move): blankx, blanky = getBlankPosition(board) return (move == UP and blanky != len(board[0]) - 1) or \ (move == DOWN and blanky != 0) or \ (move == LEFT and blankx != len(board) - 1) or \ (move == RIGHT and blankx != 0) def getRandomMove(board, lastMove=None): # start with a full list of all four moves validMoves = [UP, DOWN, LEFT, RIGHT] # remove moves from the list as they are disqualified if lastMove == UP or not isValidMove(board, DOWN): validMoves.remove(DOWN) if lastMove == DOWN or not isValidMove(board, UP): validMoves.remove(UP) if lastMove == LEFT or not isValidMove(board, RIGHT): validMoves.remove(RIGHT) if lastMove == RIGHT or not isValidMove(board, LEFT): validMoves.remove(LEFT) # return a random move from the list of remaining moves return random.choice(validMoves) def getLeftTopOfTile(tileX, tileY): left = XMARGIN + (tileX * TILESIZE) + (tileX - 1) top = YMARGIN + (tileY * TILESIZE) + (tileY - 1) return (left, top) def getSpotClicked(board, x, y): # from the x & y pixel coordinates, get the x & y board coordinates for tileX in range(len(board)): for tileY in range(len(board[0])): left, top = getLeftTopOfTile(tileX, tileY) tileRect = pygame.Rect(left, top, TILESIZE, TILESIZE) if tileRect.collidepoint(x, y): return (tileX, tileY) return (None, None) def drawTile(tilex, tiley, number, adjx=0, adjy=0): # draw a tile at board coordinates tilex and tiley, optionally a few # pixels over (determined by adjx and adjy) left, top = getLeftTopOfTile(tilex, tiley) pygame.draw.rect(DISPLAYSURF, TILECOLOR, (left + adjx, top + adjy, TILESIZE, TILESIZE)) textSurf = BASICFONT.render(str(number), True, TEXTCOLOR) textRect = textSurf.get_rect() textRect.center = left + int(TILESIZE / 2) + adjx, top + int(TILESIZE / 2) + adjy DISPLAYSURF.blit(textSurf, textRect) def makeText(text, color, bgcolor, top, left): # create the Surface and Rect objects for some text. textSurf = BASICFONT.render(text, True, color, bgcolor) textRect = textSurf.get_rect() textRect.topleft = (top, left) return (textSurf, textRect) def drawBoard(board, message): DISPLAYSURF.fill(BGCOLOR) if message: textSurf, textRect = makeText(message, MESSAGECOLOR, BGCOLOR, 5, 5) DISPLAYSURF.blit(textSurf, textRect) for tilex in range(len(board)): for tiley in range(len(board[0])): if board[tilex][tiley]: drawTile(tilex, tiley, board[tilex][tiley]) left, top = getLeftTopOfTile(0, 0) width = BOARDWIDTH * TILESIZE height = BOARDHEIGHT * TILESIZE pygame.draw.rect(DISPLAYSURF, BORDERCOLOR, (left - 5, top - 5, width + 11, height + 11), 4) DISPLAYSURF.blit(RESET_SURF, RESET_RECT) DISPLAYSURF.blit(NEW_SURF, NEW_RECT) DISPLAYSURF.blit(SOLVE_SURF, SOLVE_RECT) def slideAnimation(board, direction, message, animationSpeed): # Note: This function does not check if the move is valid. blankx, blanky = getBlankPosition(board) if direction == UP: movex = blankx movey = blanky + 1 elif direction == DOWN: movex = blankx movey = blanky - 1 elif direction == LEFT: movex = blankx + 1 movey = blanky elif direction == RIGHT: movex = blankx - 1 movey = blanky # prepare the base surface drawBoard(board, message) baseSurf = DISPLAYSURF.copy() # draw a blank space over the moving tile on the baseSurf Surface. moveLeft, moveTop = getLeftTopOfTile(movex, movey) pygame.draw.rect(baseSurf, BGCOLOR, (moveLeft, moveTop, TILESIZE, TILESIZE)) for i in range(0, TILESIZE, animationSpeed): # animate the tile sliding over checkForQuit() DISPLAYSURF.blit(baseSurf, (0, 0)) if direction == UP: drawTile(movex, movey, board[movex][movey], 0, -i) if direction == DOWN: drawTile(movex, movey, board[movex][movey], 0, i) if direction == LEFT: drawTile(movex, movey, board[movex][movey], -i, 0) if direction == RIGHT: drawTile(movex, movey, board[movex][movey], i, 0) pygame.display.update() FPSCLOCK.tick(FPS) def generateNewPuzzle(numSlides): # From a starting configuration, make numSlides number of moves (and # animate these moves). sequence = [] board = getStartingBoard() drawBoard(board, '') pygame.display.update() pygame.time.wait(500) # pause 500 milliseconds for effect lastMove = None for i in range(numSlides): move = getRandomMove(board, lastMove) slideAnimation(board, move, 'Generating new puzzle...', int(TILESIZE / 3)) makeMove(board, move) sequence.append(move) lastMove = move return (board, sequence) def resetAnimation(board, allMoves): # make all of the moves in allMoves in reverse. revAllMoves = allMoves[:] # gets a copy of the list revAllMoves.reverse() for move in revAllMoves: if move == UP: oppositeMove = DOWN elif move == DOWN: oppositeMove = UP elif move == RIGHT: oppositeMove = LEFT elif move == LEFT: oppositeMove = RIGHT slideAnimation(board, oppositeMove, '', int(TILESIZE / 2)) makeMove(board, oppositeMove) if __name__ == '__main__': main()
第二节,与第一节相同
贪吃虫中的大部分代码与我们之前看过的游戏非常相似,特别是在代码开头设置常量的部分。
# Slide Puzzle # By Al Sweigart [[email protected]](/cdn-cgi/l/email-protection) # http://inventwithpython.com/pygame # Creative Commons BY-NC-SA 3.0 US import pygame, sys, random from pygame.locals import * # Create the constants (go ahead and experiment with different values) BOARDWIDTH = 4 # number of columns in the board BOARDHEIGHT = 4 # number of rows in the board TILESIZE = 80 WINDOWWIDTH = 640 WINDOWHEIGHT = 480 FPS = 30 BLANK = None # R G B BLACK = ( 0, 0, 0) WHITE = (255, 255, 255) BRIGHTBLUE = ( 0, 50, 255) DARKTURQUOISE = ( 3, 54, 73) GREEN = ( 0, 204, 0) BGCOLOR = DARKTURQUOISE TILECOLOR = GREEN TEXTCOLOR = WHITE BORDERCOLOR = BRIGHTBLUE BASICFONTSIZE = 20 BUTTONCOLOR = WHITE BUTTONTEXTCOLOR = BLACK MESSAGECOLOR = WHITE XMARGIN = int((WINDOWWIDTH - (TILESIZE * BOARDWIDTH + (BOARDWIDTH - 1))) / 2) YMARGIN = int((WINDOWHEIGHT - (TILESIZE * BOARDHEIGHT + (BOARDHEIGHT - 1))) / 2) UP = 'up' DOWN = 'down' LEFT = 'left' RIGHT = 'right'
程序顶部的这段代码只是处理了所有基本模块的导入和创建常量。这就像上一章的记忆拼图游戏的开头一样。
设置按钮
def main(): global FPSCLOCK, DISPLAYSURF, BASICFONT, RESET_SURF, RESET_RECT, NEW_SURF, NEW_RECT, SOLVE_SURF, SOLVE_RECT pygame.init() FPSCLOCK = pygame.time.Clock() DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT)) pygame.display.set_caption('Slide Puzzle') BASICFONT = pygame.font.Font('freesansbold.ttf', BASICFONTSIZE) # Store the option buttons and their rectangles in OPTIONS. RESET_SURF, RESET_RECT = makeText('Reset', TEXTCOLOR, TILECOLOR, WINDOWWIDTH - 120, WINDOWHEIGHT - 90) NEW_SURF, NEW_RECT = makeText('New Game', TEXTCOLOR, TILECOLOR, WINDOWWIDTH - 120, WINDOWHEIGHT - 60) SOLVE_SURF, SOLVE_RECT = makeText('Solve', TEXTCOLOR, TILECOLOR, WINDOWWIDTH - 120, WINDOWHEIGHT - 30) mainBoard, solutionSeq = generateNewPuzzle(80) SOLVEDBOARD = getStartingBoard() # a solved board is the same as the board in a start state.
就像上一章一样,从 main()
函数调用的函数将在本章后面解释。现在,你只需要知道它们做什么以及它们返回什么值。你不需要知道它们是如何工作的。
main()
函数的第一部分将处理创建窗口、时钟对象和字体对象。makeText()
函数在程序的后面定义,但现在你只需要知道它返回一个 pygame.Surface
对象和一个 pygame.Rect
对象,可以用来制作可点击的按钮。滑动拼图游戏将有三个按钮:一个“重置”按钮,可以撤消玩家所做的任何移动,一个“新建”按钮,可以创建一个新的滑动拼图,以及一个“解决”按钮,可以为玩家解决拼图。
对于这个程序,我们将需要两个棋盘数据结构。一个棋盘将表示当前的游戏状态。另一个棋盘将使其方块处于“解决”状态,这意味着所有方块都按顺序排列。当当前游戏状态的棋盘与解决的棋盘完全相同时,我们就知道玩家赢了。(我们永远不会改变第二个棋盘。它只是用来比较当前游戏状态棋盘的。)
generateNewPuzzle()
将创建一个棋盘数据结构,它最初处于有序的解决状态,然后对其进行了 80 次随机滑动(因为我们向其传递了整数 80
)。如果我们希望棋盘更加混乱,那么我们可以向其传递一个更大的整数。这将使棋盘变成一个随机混乱的状态,玩家将不得不解决它(这将存储在一个名为 mainBoard
的变量中)。generateNewBoard()
还返回了在其上执行的所有随机移动的列表(这将存储在一个名为 solutionSeq
的变量中)。
通过使用愚蠢的代码变得聪明
allMoves = [] # list of moves made from the solved configuration
解决滑动拼图可能会非常棘手。我们可以让计算机来做,但这需要我们找出一个可以解决滑动拼图的算法。这将非常困难,并且需要大量的聪明和努力来将其放入这个程序中。
幸运的是,有一个更简单的方法。我们可以让计算机记住创建棋盘数据结构时所做的所有随机滑动,然后通过执行相反的滑动来解决棋盘。由于棋盘最初是处于解决状态的,撤消所有滑动将使其返回到解决状态。
例如,下面我们在页面左侧的棋盘上执行了一个“向右”滑动,这将使棋盘处于页面右侧的状态:
在正确的滑动之后,如果我们进行相反的滑动(向左滑动),那么板将恢复到原始状态。因此,要在进行几次滑动后恢复到原始状态,我们只需按相反的顺序进行相反的滑动。如果我们进行了右滑动,然后又进行了右滑动,然后进行了下滑动,我们将不得不进行上滑动、左滑动和左滑动来撤消这前三次滑动。这比编写一个可以简单地查看它们的当前状态来解决这些谜题的函数要容易得多。
主游戏循环
while True: # main game loop slideTo = None # the direction, if any, a tile should slide msg = '' # contains the message to show in the upper left corner. if mainBoard == SOLVEDBOARD: msg = 'Solved!' drawBoard(mainBoard, msg)
在主游戏循环中,slideTo
变量将跟踪玩家想要滑动瓷砖的方向(在游戏循环的开始时它开始为None
,稍后设置),msg
变量跟踪在窗口顶部显示的字符串。程序在第 64 行进行快速检查,看看板数据结构是否与存储在SOLVEDBOARD
中的解决板数据结构具有相同的值。如果是,则msg
变量更改为字符串'已解决!'
。这将不会出现在屏幕上,直到调用drawBoard()
将其绘制到DISPLAYSURF
Surface 对象(在第 67 行执行)并调用pygame.display.update()
将显示 Surface 对象绘制到实际计算机屏幕上(在游戏循环结束时的第 291 行执行)。
点击按钮
checkForQuit() for event in pygame.event.get(): # event handling loop if event.type == MOUSEBUTTONUP: spotx, spoty = getSpotClicked(mainBoard, event.pos[0], event.pos[1]) if (spotx, spoty) == (None, None): # check if the user clicked on an option button if RESET_RECT.collidepoint(event.pos): resetAnimation(mainBoard, allMoves) # clicked on Reset button allMoves = [] elif NEW_RECT.collidepoint(event.pos): mainBoard, solutionSeq = generateNewPuzzle(80) # clicked on New Game button allMoves = [] elif SOLVE_RECT.collidepoint(event.pos): resetAnimation(mainBoard, solutionSeq + allMoves) # clicked on Solve button allMoves = []
在进入事件循环之前,程序在第 69 行调用checkForQuit()
来查看是否已创建任何QUIT
事件(如果有,则终止程序)。为什么我们有一个单独的函数(checkForQuit()
函数)来处理QUIT
事件将在后面解释。第 70 行的for
循环执行自上次调用pygame.event.get()
以来创建的任何其他事件的事件处理代码(或者自程序启动以来,如果以前从未调用过pygame.event.get()
)。
如果事件类型是MOUSEBUTTONUP
事件(即玩家在窗口的某个地方释放了鼠标按钮),那么我们将鼠标坐标传递给我们的getSpotClicked()
函数,该函数将返回鼠标释放发生的板上位置的坐标。event.pos[0]
是 X 坐标,event.pos[1]
是 Y 坐标。
如果鼠标释放按钮没有发生在板上的空格之一上(但显然仍然发生在窗口的某个地方,因为创建了MOUSEBUTTONUP
事件),那么getSpotClicked()
将返回None
。如果是这种情况,我们希望进行额外的检查,看看玩家是否可能点击了重置、新建或解决按钮(这些按钮不位于板上)。
这些按钮在窗口上的坐标存储在RESET_RECT
、NEW_RECT
和SOLVE_RECT
变量中存储的pygame.Rect
对象中。我们可以将事件对象的鼠标坐标传递给collidepoint()
方法。如果鼠标坐标在 Rect 对象的区域内,则此方法将返回True
,否则返回False
。
用鼠标滑动瓷砖
else: # check if the clicked tile was next to the blank spot blankx, blanky = getBlankPosition(mainBoard) if spotx == blankx + 1 and spoty == blanky: slideTo = LEFT elif spotx == blankx - 1 and spoty == blanky: slideTo = RIGHT elif spotx == blankx and spoty == blanky + 1: slideTo = UP elif spotx == blankx and spoty == blanky - 1: slideTo = DOWN
如果getSpotClicked()
没有返回(None, None)
,那么它将返回一个包含两个整数值的元组,表示点击的板上的位置的 X 和 Y 坐标。然后,第 89 到 96 行的if
和elif
语句检查被点击的位置是否是靠近空白位置的瓷砖(否则瓷砖将没有地方滑动)。
我们的getBlankPosition()
函数将采取板数据结构并返回空白位置的 X 和 Y 板坐标,我们将其存储在变量blankx
和blanky
中。如果用户点击的位置在空白处旁边,我们将使用应该滑动的值设置slideTo
变量。
用键盘滑动瓷砖
elif event.type == KEYUP: # check if the user pressed a key to slide a tile if event.key in (K_LEFT, K_a) and isValidMove(mainBoard, LEFT): slideTo = LEFT elif event.key in (K_RIGHT, K_d) and isValidMove(mainBoard, RIGHT): slideTo = RIGHT elif event.key in (K_UP, K_w) and isValidMove(mainBoard, UP): slideTo = UP elif event.key in (K_DOWN, K_s) and isValidMove(mainBoard, DOWN): slideTo = DOWN
我们还可以让用户通过按键盘键来滑动瓷砖。第 100 至 107 行的if
和elif
语句允许用户通过按箭头键或 WASD 键(稍后解释)来设置slideTo
变量。每个if
和elif
语句还都调用了isValidMove()
来确保瓷砖可以朝那个方向滑动。(我们在鼠标点击时不必进行这个调用,因为对于相邻的空白空间的检查也会做同样的事情。)
“等于多个值中的一个”技巧与in
运算符
表达式event.key in (K_LEFT, K_a)
只是 Python 中的一个技巧,使代码更简单。这是一种说“如果event.key
等于K_LEFT
或K_a
中的一个,则评估为True
”。以下两个表达式将以相同的方式进行评估:
event.key in (K_LEFT, K_a) event.key == K_LEFT or event.key == K_a
当你需要检查一个值是否等于多个值中的一个时,使用这个技巧可以节省一些空间。以下两个表达式将以相同的方式进行评估:
spam == 'dog' or spam == 'cat' or spam == 'mouse' or spam == 'horse' or spam == 42 or spam == 'dingo' spam in ('dog', 'cat', 'mouse', 'horse', 42, 'dingo')
WASD 和箭头键
W、A、S 和 D 键(合称 WASD 键,发音为“waz-dee”)在电脑游戏中通常用来做与箭头键相同的事情,只不过玩家可以使用左手(因为 WASD 键在键盘的左侧)。W 代表上,A 代表左,S 代表下,D 代表右。你可以很容易地记住这一点,因为 WASD 键的布局与箭头键相同:
实际执行瓷砖滑动
if slideTo: slideAnimation(mainBoard, slideTo, 'Click tile or press arrow keys to slide.', 8) # show slide on screen makeMove(mainBoard, slideTo) allMoves.append(slideTo) # record the slide pygame.display.update() FPSCLOCK.tick(FPS)
现在所有事件都已处理完毕,我们应该更新游戏状态的变量,并在屏幕上显示新状态。如果slideTo
已经设置(无论是由鼠标事件还是键盘事件处理代码),我们都可以调用slideAnimation()
来执行滑动动画。参数是棋盘数据结构、滑动的方向、在滑动瓷砖时显示的消息以及滑动的速度。
在它返回后,我们需要更新实际的棋盘数据结构(由makeMove()
函数完成),然后将滑动添加到迄今为止所有滑动的allMoves
列表中。这样,如果玩家点击“重置”按钮,我们就知道如何撤消玩家的所有滑动。
IDLE 和终止 Pygame 程序
def terminate(): pygame.quit() sys.exit()
这是一个我们可以调用的函数,它同时调用了pygame.quit()
和sys.exit()
函数。这是一种语法糖,这样我们就不必记住调用这两个函数,只需要调用一个函数即可。
检查特定事件,并将事件发布到 Pygame 的事件队列
def checkForQuit(): for event in pygame.event.get(QUIT): # get all the QUIT events terminate() # terminate if any QUIT events are present for event in pygame.event.get(KEYUP): # get all the KEYUP events if event.key == K_ESCAPE: terminate() # terminate if the KEYUP event was for the Esc key pygame.event.post(event) # put the other KEYUP event objects back
checkForQuit()
函数将检查QUIT
事件(或用户是否按下了 Esc 键),然后调用terminate()
函数。但这有点棘手,需要一些解释。
Pygame 内部有自己的列表数据结构,它会在创建 Event 对象时将其附加到其中。这个数据结构称为事件队列。当调用pygame.event.get()
函数而不带参数时,整个列表将被返回。但是,你可以传递一个常量,比如QUIT
给pygame.event.get()
,这样它就只会返回内部事件队列中的QUIT
事件(如果有的话)。其余的事件将保留在事件队列中,以便下次调用pygame.event.get()
时使用。
你应该注意,Pygame 的事件队列只能存储最多 127 个 Event 对象。如果你的程序不经常调用pygame.event.get()
,并且队列填满了,那么发生的任何新事件都不会被添加到事件队列中。
第 123 行从 Pygame 的事件队列中提取了一个QUIT
事件列表并返回它们。如果事件队列中有任何QUIT
事件,程序将终止。
第 125 行从事件队列中提取所有KEYUP
事件,并检查它们是否是 Esc 键。如果其中一个事件是,那么程序将终止。但是,除了 Esc 键之外,可能还有其他键的KEYUP
事件。在这种情况下,我们需要将KEYUP
事件放回 Pygame 的事件队列中。我们可以使用pygame.event.post()
函数来实现这一点,该函数将传递给它的 Event 对象添加到 Pygame 事件队列的末尾。这样,当第 70 行调用pygame.event.get()
时,非 Esc 键的KEYUP
事件仍将存在。否则,对checkForQuit()
的调用将“消耗”所有的KEYUP
事件,这些事件将永远不会被处理。
pygame.event.post()
函数也很方便,如果您希望程序将 Event 对象添加到 Pygame 事件队列中。
创建棋盘数据结构
def getStartingBoard(): # Return a board data structure with tiles in the solved state. # For example, if BOARDWIDTH and BOARDHEIGHT are both 3, this function # returns [[1, 4, 7], [2, 5, 8], [3, 6, None]] counter = 1 board = [] for x in range(BOARDWIDTH): column = [] for y in range(BOARDHEIGHT): column.append(counter) counter += BOARDWIDTH board.append(column) counter -= BOARDWIDTH * (BOARDHEIGHT - 1) + BOARDWIDTH - 1 board[BOARDWIDTH-1][BOARDHEIGHT-1] = None return board
getStartingBoard()
数据结构将创建并返回一个表示“已解决”棋盘的数据结构,其中所有编号的瓷砖都是有序的,空白瓷砖位于右下角。这与内存拼图游戏中的棋盘数据结构一样,都是使用嵌套的for
循环完成的。
但是,请注意,第一列不会是[1, 2, 3]
,而是[1, 4, 7]
。这是因为瓷砖上的数字是横向增加 1,而不是纵向增加。沿着列向下,数字按照棋盘宽度的大小增加(存储在BOARDWIDTH
常量中)。我们将使用counter
变量来跟踪应放在下一个瓷砖上的数字。当列中的瓷砖编号完成时,我们需要将counter
设置为下一列开始的数字。
不跟踪空白位置
def getBlankPosition(board): # Return the x and y of board coordinates of the blank space. for x in range(BOARDWIDTH): for y in range(BOARDHEIGHT): if board[x][y] == None: return (x, y)
每当我们的代码需要找到空白空间的 XY 坐标时,我们可以创建一个函数,通过遍历整个棋盘来找到空白空间的坐标,而不是在每次滑动后跟踪空白空间的位置。None
值在棋盘数据结构中用于表示空白空间。getBlankPosition()
中的代码简单地使用嵌套的for
循环来找到棋盘上的空白空间。
通过更新棋盘数据结构进行移动
def makeMove(board, move): # This function does not check if the move is valid. blankx, blanky = getBlankPosition(board) if move == UP: board[blankx][blanky], board[blankx][blanky + 1] = board[blankx][blanky + 1], board[blankx][blanky] elif move == DOWN: board[blankx][blanky], board[blankx][blanky - 1] = board[blankx][blanky - 1], board[blankx][blanky] elif move == LEFT: board[blankx][blanky], board[blankx + 1][blanky] = board[blankx + 1][blanky], board[blankx][blanky] elif move == RIGHT: board[blankx][blanky], board[blankx - 1][blanky] = board[blankx - 1][blanky], board[blankx][blanky]
board
参数中的数据结构是一个表示所有瓷砖位置的二维列表。每当玩家进行移动时,程序都需要更新此数据结构。发生的情况是,瓷砖的值与空白空间的值交换。
makeMove()
函数不必返回任何值,因为board
参数是作为其参数传递的列表引用。这意味着我们在此函数中对board
所做的任何更改都将应用于传递给makeMove()
的列表值。(您可以在invpy.com/references
上查看引用的概念。)
何时不使用断言
def isValidMove(board, move): blankx, blanky = getBlankPosition(board) return (move == UP and blanky != len(board[0]) - 1) or \ (move == DOWN and blanky != 0) or \ (move == LEFT and blankx != len(board) - 1) or \ (move == RIGHT and blankx != 0)
isValidMove()
函数接收一个棋盘数据结构和玩家想要进行的移动。如果移动是可能的,则返回值为True
,如果不可能,则返回值为False
。例如,您不能连续一百次将瓷砖向左滑动,因为最终空白空间将位于边缘,没有更多的瓷砖可以向左滑动。
移动是否有效取决于空白空间的位置。此函数调用getBlankPosition()
来找到空白位置的 X 和 Y 坐标。第 173 至 176 行是一个带有单个表达式的return
语句。在前三行的末尾的\
斜杠告诉 Python 解释器这不是代码行的结尾(即使它在行的末尾)。这将使我们能够将“代码行”分成多行以使其看起来漂亮,而不是只有一行非常长且难以阅读。
因为括号中的这个表达式是由或运算符连接的,只需要其中一个为True
整个表达式就为True
。每个部分都检查预期的移动是什么,然后看空白空间的坐标是否允许该移动。
获取一个不那么随机的移动
def getRandomMove(board, lastMove=None): # start with a full list of all four moves validMoves = [UP, DOWN, LEFT, RIGHT] # remove moves from the list as they are disqualified if lastMove == UP or not isValidMove(board, DOWN): validMoves.remove(DOWN) if lastMove == DOWN or not isValidMove(board, UP): validMoves.remove(UP) if lastMove == LEFT or not isValidMove(board, RIGHT): validMoves.remove(RIGHT) if lastMove == RIGHT or not isValidMove(board, LEFT): validMoves.remove(LEFT) # return a random move from the list of remaining moves return random.choice(validMoves)
在游戏开始时,我们从解决的、有序的状态开始,通过随机滑动瓷砖来创建拼图。为了决定我们应该滑动哪个方向,我们将调用我们的getRandomMove()
函数。通常我们可以使用random.choice()
函数,并传递一个元组(UP, DOWN, LEFT, RIGHT)
,让 Python 简单地随机选择一个方向值。但是滑动拼图游戏有一个小限制,阻止我们选择纯随机数。
如果你有一个滑动拼图,将一个瓷砖向左滑动,然后将一个瓷砖向右滑动,你最终会得到与开始时完全相同的棋盘。进行相反的滑动是毫无意义的。此外,如果空白空间位于右下角,那么不可能将瓷砖向上或向左滑动。
getRandomMove()
中的代码将考虑这些因素。为了防止函数选择上次移动的值,函数的调用者可以为lastMove
参数传递一个方向值。第 181 行从存储在validMoves
变量中的所有四个方向值的列表开始。如果lastMove
值(如果未设置为None
)则从validMoves
中删除。根据空白空间是否在棋盘的边缘,第 184 到 191 行将从lastMove
列表中删除其他方向值。
在lastMove
中剩下的值中,使用random.choice()
随机选择一个值并返回。
将瓷砖坐标转换为像素坐标
def getLeftTopOfTile(tileX, tileY): left = XMARGIN + (tileX * TILESIZE) + (tileX - 1) top = YMARGIN + (tileY * TILESIZE) + (tileY - 1) return (left, top)
getLeftTopOfTile()
函数将棋盘坐标转换为像素坐标。对于传入的棋盘 XY 坐标,该函数计算并返回该棋盘空间左上角的像素 XY 坐标。
从像素坐标转换为棋盘坐标
def getSpotClicked(board, x, y): # from the x & y pixel coordinates, get the x & y board coordinates for tileX in range(len(board)): for tileY in range(len(board[0])): left, top = getLeftTopOfTile(tileX, tileY) tileRect = pygame.Rect(left, top, TILESIZE, TILESIZE) if tileRect.collidepoint(x, y): return (tileX, tileY) return (None, None)
getSpotClicked()
函数与getLeftTopOfTile()
相反,它将像素坐标转换为棋盘坐标。第 205 和 206 行的嵌套循环遍历了每个可能的 XY 棋盘坐标,如果传入的像素坐标在棋盘上的空间内,则返回这些棋盘坐标。由于所有的瓷砖都有在TILESIZE
常量中设置的宽度和高度,我们可以创建一个表示棋盘空间的 Rect 对象,方法是获取棋盘空间左上角的像素坐标,然后使用collidepoint()
Rect 方法来查看像素坐标是否在该 Rect 对象的区域内。
如果传入的像素坐标不在任何棋盘空间上,则返回值为(None, None)
。
绘制瓷砖
def drawTile(tilex, tiley, number, adjx=0, adjy=0): # draw a tile at board coordinates tilex and tiley, optionally a few # pixels over (determined by adjx and adjy) left, top = getLeftTopOfTile(tilex, tiley) pygame.draw.rect(DISPLAYSURF, TILECOLOR, (left + adjx, top + adjy, TILESIZE, TILESIZE)) textSurf = BASICFONT.render(str(number), True, TEXTCOLOR) textRect = textSurf.get_rect() textRect.center = left + int(TILESIZE / 2) + adjx, top + int(TILESIZE / 2) + adjy DISPLAYSURF.blit(textSurf, textRect)
drawTile()
函数将在棋盘上绘制一个带编号的瓷砖。tilex
和tiley
参数是瓷砖的棋盘坐标。数字参数是瓷砖编号的字符串(如'3'
或'12'
)。adjx
和adjy
关键字参数用于对瓷砖位置进行微小调整。例如,将5
传递给adjx
会使瓷砖出现在棋盘上tilex
和tiley
空间的右侧 5 像素处。将-10
传递给adjx
会使瓷砖出现在空间的左侧 10 像素处。
当我们需要在滑动中间绘制瓷砖时,这些调整值将非常方便。如果在调用drawTile()
时没有传递这些参数的值,则默认设置为0
。这意味着它们将正好在由tilex
和tiley
给出的棋盘空间上。
Pygame 绘图函数只使用像素坐标,因此第 217 行首先将tilex
和tiley
的棋盘坐标转换为像素坐标,我们将把它们存储在变量left
和top
中(因为getLeftTopOfTile()
返回左上角的坐标)。我们使用pygame.draw.rect()
调用绘制瓦片的背景方块,同时在需要调整瓦片位置的情况下,将adjx
和adjy
的值添加到left
和top
中。
然后,第 219 到 222 行创建了具有数字文本的表面对象。一个用于表面对象的 Rect 对象被定位,然后用于将表面对象 blit 到显示表面。drawTile()
函数不调用pygame.display.update()
函数,因为调用drawTile()
的人可能会想在将它们显示在屏幕上之前为棋盘的其余部分绘制更多的瓦片。
在屏幕上显示文本
def makeText(text, color, bgcolor, top, left): # create the Surface and Rect objects for some text. textSurf = BASICFONT.render(text, True, color, bgcolor) textRect = textSurf.get_rect() textRect.topleft = (top, left) return (textSurf, textRect)
makeText()
函数处理创建用于在屏幕上定位文本的表面和 Rect 对象。我们可以只调用makeText()
而不是每次想在屏幕上制作文本时都进行所有这些调用。这节省了我们程序中需要输入的数量。(尽管drawTile()
自己调用render()
和get_rect()
,因为它通过中心点而不是左上角点定位文本表面对象,并使用透明背景颜色。)
绘制棋盘
def drawBoard(board, message): DISPLAYSURF.fill(BGCOLOR) if message: textSurf, textRect = makeText(message, MESSAGECOLOR, BGCOLOR, 5, 5) DISPLAYSURF.blit(textSurf, textRect) for tilex in range(len(board)): for tiley in range(len(board[0])): if board[tilex][tiley]: drawTile(tilex, tiley, board[tilex][tiley])
这个函数处理绘制整个棋盘和所有的瓦片到DISPLAYSURF
显示表面对象。第 234 行的fill()
方法完全覆盖了以前在显示表面对象上绘制的任何东西,这样我们就可以从头开始。
第 235 到 237 行处理在窗口顶部绘制消息。我们用它来显示“生成新的谜题…”和其他我们想要在窗口顶部显示的文本。请记住,if
语句条件认为空字符串是False
值,因此如果消息设置为''
,那么条件就是False
,第 236 和 237 行将被跳过。
接下来,嵌套的for
循环用于通过调用drawTile()
函数将每个瓦片绘制到显示表面对象上。
绘制棋盘的边框
left, top = getLeftTopOfTile(0, 0) width = BOARDWIDTH * TILESIZE height = BOARDHEIGHT * TILESIZE pygame.draw.rect(DISPLAYSURF, BORDERCOLOR, (left - 5, top - 5, width + 11, height + 11), 4)
第 244 到 247 行绘制了瓦片周围的边框。边框的左上角将位于棋盘坐标(0, 0)处的瓦片的左上角的左侧 5 像素和上侧 5 像素。边框的宽度和高度是根据棋盘的宽度和高度(存储在BOARDWIDTH
和BOARDHEIGHT
常量中)乘以瓦片的大小(存储在TILESIZE
常量中)计算的。
我们在第 247 行绘制的矩形将有 4 像素的厚度,所以我们将边框向左和向上移动 5 像素,以便线的厚度不会重叠在瓦片上。我们还将宽度和长度增加 11(这 11 个像素中的 5 个是为了补偿将矩形向左和向上移动)。
绘制按钮
DISPLAYSURF.blit(RESET_SURF, RESET_RECT) DISPLAYSURF.blit(NEW_SURF, NEW_RECT) DISPLAYSURF.blit(SOLVE_SURF, SOLVE_RECT)
最后,我们在屏幕的一侧绘制按钮。这些按钮的文本和位置永远不会改变,这就是为什么它们在main()
函数的开头被存储在常量变量中的原因。
动画化瓦片滑动
def slideAnimation(board, direction, message, animationSpeed): # Note: This function does not check if the move is valid. blankx, blanky = getBlankPosition(board) if direction == UP: movex = blankx movey = blanky + 1 elif direction == DOWN: movex = blankx movey = blanky - 1 elif direction == LEFT: movex = blankx + 1 movey = blanky elif direction == RIGHT: movex = blankx - 1 movey = blanky
我们的瓦片滑动动画代码需要计算的第一件事是空白空间在哪里,移动瓦片在哪里。第 255 行的注释提醒我们,调用slideAnimation()
的代码应确保传递给方向参数的滑动是有效的移动。
空白空间的坐标来自于对getBlankPosition()
的调用。根据这些坐标和滑动的方向,我们可以找出瓦片将滑动的 XY 棋盘坐标。这些坐标将存储在movex
和movey
变量中。
copy()
表面方法
# prepare the base surface drawBoard(board, message) baseSurf = DISPLAYSURF.copy() # draw a blank space over the moving tile on the baseSurf Surface. moveLeft, moveTop = getLeftTopOfTile(movex, movey) pygame.draw.rect(baseSurf, BGCOLOR, (moveLeft, moveTop, TILESIZE, TILESIZE))
Surface 对象的copy()
方法将返回一个新的 Surface 对象,其上绘制了相同的图像。但它们是两个独立的 Surface 对象。调用copy()
方法后,如果我们使用blit()
或 Pygame 绘图函数在一个 Surface 对象上绘制,它不会改变另一个 Surface 对象上的图像。我们将这个副本存储在第 273 行的baseSurf
变量中。
接下来,我们在将要滑动的板块上绘制另一个空白空间。这是因为当我们绘制滑动动画的每一帧时,我们将在baseSurf
Surface 对象的不同部分上绘制滑动板块。如果我们没有在baseSurf
Surface 上擦除移动的板块,那么当我们绘制滑动板块时,它仍然会在那里。在这种情况下,baseSurf
Surface 将如下所示:
然后当我们在其上绘制“9”板块向上滑动时,它会是这个样子:
通过注释掉第 276 行并运行程序,您可以自行查看。
for i in range(0, TILESIZE, animationSpeed): # animate the tile sliding over checkForQuit() DISPLAYSURF.blit(baseSurf, (0, 0)) if direction == UP: drawTile(movex, movey, board[movex][movey], 0, -i) if direction == DOWN: drawTile(movex, movey, board[movex][movey], 0, i) if direction == LEFT: drawTile(movex, movey, board[movex][movey], -i, 0) if direction == RIGHT: drawTile(movex, movey, board[movex][movey], i, 0) pygame.display.update() FPSCLOCK.tick(FPS)
为了绘制滑动动画的帧,我们必须在显示 Surface 上绘制baseSurf
Surface,然后在动画的每一帧上,将滑动板块绘制得越来越接近其最终位置,即原始空白空间的位置。相邻两个板块之间的间距与单个板块的大小相同,我们将其存储在TILESIZE
中。代码使用for
循环从0
到TILESIZE
。
通常情况下,这意味着我们会将板块绘制为 0 像素,然后在下一帧绘制为 1 像素,然后 2 像素,然后 3 像素,依此类推。每一帧将花费 1/30 秒。如果将TILESIZE
设置为80
(就像本书中的程序在第 12 行所做的那样),那么滑动一个板块将需要超过两秒半,这实际上有点慢。
因此,我们将使for
循环每帧从0
到TILESIZE
迭代几个像素。跳过的像素数存储在animationSpeed
中,在调用slideAnimation()
时传入。例如,如果animationSpeed
设置为8
,常量TILESIZE
设置为80
,那么for
循环和range(0, TILESIZE, animationSpeed)
将将i
变量设置为值0
,8
,16
,24
,32
,40
,48
,56
,64
,72
。(不包括80
,因为range()
函数的第二个参数是到达但不包括的。)这意味着整个滑动动画将在 10 帧内完成,这意味着它在 10/30 秒内完成(三分之一秒),因为游戏以 30 FPS 运行。
第 282 到 289 行确保我们以正确的方向绘制滑动的板块(基于direction
变量的值)。动画完成后,函数返回。请注意,当动画正在进行时,用户创建的任何事件都不会被处理。这些事件将在下一次执行到main()
函数的第 70 行或checkForQuit()
函数中的代码时处理。
创建一个新的拼图
def generateNewPuzzle(numSlides): # From a starting configuration, make numSlides number of moves (and # animate these moves). sequence = [] board = getStartingBoard() drawBoard(board, '') pygame.display.update() pygame.time.wait(500) # pause 500 milliseconds for effect
generateNewPuzzle()
函数将在每个新游戏开始时调用。它将通过调用getStartingBoard()
创建一个新的板数据结构,然后随机打乱它。generateNewPuzzle()
的前几行获取板然后将其绘制到屏幕上(冻结半秒钟以让玩家看到新鲜的板片刻)。
lastMove = None for i in range(numSlides): move = getRandomMove(board, lastMove) slideAnimation(board, move, 'Generating new puzzle...', int(TILESIZE / 3)) makeMove(board, move) sequence.append(move) lastMove = move return (board, sequence)
numSlides
参数将告诉函数要进行多少次这些随机移动。执行随机移动的代码是在第 305 行调用getRandomMove()
来获取移动本身,然后调用slideAnimation()
在屏幕上执行动画。因为执行滑动动画实际上并不会更新板数据结构,我们通过在第 307 行调用makeMove()
来更新板。
我们需要跟踪每个随机移动,以便玩家稍后可以点击“解决”按钮,并让程序撤销所有这些随机移动。(“通过使用愚蠢的代码变得聪明”部分讨论了我们为什么以及如何这样做。)所以移动被附加到第 308 行的sequence
移动列表中。
然后我们将随机移动存储在名为lastMove
的变量中,这将在下一次迭代中传递给getRandomMove()
。这可以防止下一个随机移动撤销我们刚刚执行的随机移动。
所有这些需要发生numSlides
次,所以我们将第 305 行到 309 行放在一个for
循环中。当棋盘被打乱后,我们返回棋盘数据结构,以及在其上进行的随机移动的列表。
动画化棋盘重置
def resetAnimation(board, allMoves): # make all of the moves in allMoves in reverse. revAllMoves = allMoves[:] # gets a copy of the list revAllMoves.reverse() for move in revAllMoves: if move == UP: oppositeMove = DOWN elif move == DOWN: oppositeMove = UP elif move == RIGHT: oppositeMove = LEFT elif move == LEFT: oppositeMove = RIGHT slideAnimation(board, oppositeMove, '', int(TILESIZE / 2)) makeMove(board, oppositeMove)
当玩家点击“重置”或“解决”时,滑动拼图游戏程序需要撤消对棋盘所做的所有移动。幻灯片的方向值列表将作为参数传递给allMoves
参数。
第 315 行使用列表切片来创建allMoves
列表的副本。记住,如果你在:
之前不指定数字,那么 Python 会假定切片应该从列表的开头开始。如果你在:
之后不指定数字,那么 Python 会假定切片应该一直到列表的末尾。所以allMoves[:]
创建了整个allMoves
列表的切片。这样可以创建实际列表的副本存储在revAllMoves
中,而不仅仅是列表引用的副本。(详情请参阅invpy.com/references
。)
为了撤消allMoves
中的所有移动,我们需要按相反的顺序执行allMoves
中的移动。有一个名为reverse()
的列表方法,它会颠倒列表中项目的顺序。我们在第 316 行调用这个方法来颠倒revAllMoves
列表的顺序。
在第 318 行的for
循环遍历方向值的列表。记住,我们需要相反的移动,所以从第 319 行到 326 行的if
和elif
语句设置了oppositeMove
变量中的正确方向值。然后我们调用slideAnimation()
执行动画,以及makeMove()
来更新棋盘数据结构。
if __name__ == '__main__': main()
就像在记忆拼图游戏中一样,在执行所有def
语句以创建所有函数之后,我们调用main()
函数来开始程序的主要部分。
这就是滑动拼图程序的全部内容!但让我们谈谈在这个游戏中出现的一些一般编程概念。
时间与内存的权衡
当然,有几种不同的方法可以编写滑动拼图游戏,使其看起来和行为方式完全相同,尽管代码不同。一个任务的程序可能有许多不同的编写方式。最常见的区别是在执行时间和内存使用之间进行权衡。
通常,程序运行得越快,就越好。这对于需要进行大量计算的程序尤其如此,无论是科学天气模拟器还是需要绘制大量详细的 3D 图形的游戏。同时,尽可能少地使用内存也是很好的。程序使用的变量越多,列表越大,它所占用的内存就越多。(你可以在invpy.com/profiling
找到如何测量程序的内存使用和执行时间。)
现在,这本书中的程序还不够大且复杂,不需要担心节约内存或优化执行时间。但随着你成为更有技巧的程序员,这可能需要考虑的事情。
例如,考虑getBlankPosition()
函数。这个函数需要时间来运行,因为它需要遍历所有可能的棋盘坐标来找到空白空间的位置。相反,我们可以只有一个blankspacex
和blankspacey
变量,它们将具有这些 XY 坐标,这样我们就不必每次想知道它在哪里时都要遍历整个棋盘。(我们还需要代码,每当进行移动时更新blankspacex
和blankspacey
变量。这段代码可以放在makeMove()
中。)使用这些变量会占用更多内存,但它们会节省执行时间,使您的程序运行更快。
另一个例子是,我们在SOLVEDBOARD
变量中保留了一个解决状态的棋盘数据结构,以便我们可以将当前棋盘与SOLVEDBOARD
进行比较,以查看玩家是否已经解决了谜题。每次我们想要进行这个检查时,我们可以调用getStartingBoard()
函数并将返回的值与当前棋盘进行比较。然后我们就不需要SOLVEDBOARD
变量了。这会为我们节省一点内存,但是我们的程序会花更长的时间运行,因为它每次进行这个检查时都会重新创建解决状态的棋盘数据结构。
然而,有一件事您必须记住。编写可读性强的代码是一项非常重要的技能。可读性强的代码是易于理解的代码,尤其是对于没有编写代码的程序员。如果另一个程序员可以查看您程序的源代码并且毫不费力地弄清楚它的作用,那么该程序就是非常易读的。可读性很重要,因为当您想要修复错误或添加新功能到您的程序时(错误和新功能总是会出现),那么拥有一个易读的程序会使这些任务变得更加容易。
没有人在乎几个字节
还有一件事可能在这本书中似乎有点愚蠢,但许多人会对此感到困惑。您应该知道,使用像x
或num
这样的短变量名,而不是像blankx
或numSlides
这样更长、更具描述性的变量名,在程序实际运行时并不会节省内存。使用这些更长的变量名更好,因为它们会使您的程序更易读。
您可能会想出一些聪明的技巧,以节省一些内存。一个技巧是,当您不再需要一个变量时,您可以重新使用该变量名称以用于不同的目的,而不仅仅是使用两个不同命名的变量。
尽量避免这种诱惑。通常,这些技巧会降低代码的可读性,并使调试程序变得更加困难。现代计算机有数十亿字节的内存,而在程序中节省几个字节并不值得为了人类程序员更加混乱而使代码更加混乱。
没有人在乎几百万纳秒
同样,有时您可以以某种方式重新排列代码,使其稍微快一些,减少几个纳秒。这些技巧通常也会使代码更难阅读。考虑到在您阅读这句话所花费的时间内已经过去了数十亿纳秒,程序执行时间节省几个纳秒并不会被玩家注意到。
总结
除了使用 Surface 对象的copy()
方法之外,本章没有介绍任何记忆迷题游戏没有使用的新 Pygame 编程概念。只要了解一些不同的概念,您就可以创建完全不同的游戏。
为了练习,您可以从invpy.com/buggy/slidepuzzle
下载 Sliding Puzzle 程序的有错误的版本。
第五章:模拟
原文:
inventwithpython.com/pygame/chapter5.html
译者:飞龙
如何玩模拟
模拟是 Simon 游戏的克隆版。屏幕上有四个彩色按钮。按钮以某种随机模式亮起,然后玩家必须按照正确的顺序重复这个模式。每次玩家成功模拟模式,模式就会变得更长。玩家尽可能长时间地匹配模式。
模拟源代码
可以从以下网址下载此源代码:invpy.com/simulate.py
。如果出现任何错误消息,请查看错误消息中提到的行号,并检查代码中是否有任何拼写错误。您还可以将代码复制粘贴到invpy.com/diff/simulate
的 Web 表单中,以查看您的代码与书中代码之间的差异。
您可以从以下网址下载此程序使用的四个声音文件:
# Simulate (a Simon clone) # By Al Sweigart [[email protected]](/cdn-cgi/l/email-protection) # http://inventwithpython.com/pygame # Creative Commons BY-NC-SA 3.0 US import random, sys, time, pygame from pygame.locals import * FPS = 30 WINDOWWIDTH = 640 WINDOWHEIGHT = 480 FLASHSPEED = 500 # in milliseconds FLASHDELAY = 200 # in milliseconds BUTTONSIZE = 200 BUTTONGAPSIZE = 20 TIMEOUT = 4 # seconds before game over if no button is pushed. # R G B WHITE = (255, 255, 255) BLACK = ( 0, 0, 0) BRIGHTRED = (255, 0, 0) RED = (155, 0, 0) BRIGHTGREEN = ( 0, 255, 0) GREEN = ( 0, 155, 0) BRIGHTBLUE = ( 0, 0, 255) BLUE = ( 0, 0, 155) BRIGHTYELLOW = (255, 255, 0) YELLOW = (155, 155, 0) DARKGRAY = ( 40, 40, 40) bgColor = BLACK XMARGIN = int((WINDOWWIDTH - (2 * BUTTONSIZE) - BUTTONGAPSIZE) / 2) YMARGIN = int((WINDOWHEIGHT - (2 * BUTTONSIZE) - BUTTONGAPSIZE) / 2) # Rect objects for each of the four buttons YELLOWRECT = pygame.Rect(XMARGIN, YMARGIN, BUTTONSIZE, BUTTONSIZE) BLUERECT = pygame.Rect(XMARGIN + BUTTONSIZE + BUTTONGAPSIZE, YMARGIN, BUTTONSIZE, BUTTONSIZE) REDRECT = pygame.Rect(XMARGIN, YMARGIN + BUTTONSIZE + BUTTONGAPSIZE, BUTTONSIZE, BUTTONSIZE) GREENRECT = pygame.Rect(XMARGIN + BUTTONSIZE + BUTTONGAPSIZE, YMARGIN + BUTTONSIZE + BUTTONGAPSIZE, BUTTONSIZE, BUTTONSIZE) def main(): global FPSCLOCK, DISPLAYSURF, BASICFONT, BEEP1, BEEP2, BEEP3, BEEP4 pygame.init() FPSCLOCK = pygame.time.Clock() DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT)) pygame.display.set_caption('Simulate') BASICFONT = pygame.font.Font('freesansbold.ttf', 16) infoSurf = BASICFONT.render('Match the pattern by clicking on the button or using the Q, W, A, S keys.', 1, DARKGRAY) infoRect = infoSurf.get_rect() infoRect.topleft = (10, WINDOWHEIGHT - 25) # load the sound files BEEP1 = pygame.mixer.Sound('beep1.ogg') BEEP2 = pygame.mixer.Sound('beep2.ogg') BEEP3 = pygame.mixer.Sound('beep3.ogg') BEEP4 = pygame.mixer.Sound('beep4.ogg') # Initialize some variables for a new game pattern = [] # stores the pattern of colors currentStep = 0 # the color the player must push next lastClickTime = 0 # timestamp of the player's last button push score = 0 # when False, the pattern is playing. when True, waiting for the player to click a colored button: waitingForInput = False while True: # main game loop clickedButton = None # button that was clicked (set to YELLOW, RED, GREEN, or BLUE) DISPLAYSURF.fill(bgColor) drawButtons() scoreSurf = BASICFONT.render('Score: ' + str(score), 1, WHITE) scoreRect = scoreSurf.get_rect() scoreRect.topleft = (WINDOWWIDTH - 100, 10) DISPLAYSURF.blit(scoreSurf, scoreRect) DISPLAYSURF.blit(infoSurf, infoRect) checkForQuit() for event in pygame.event.get(): # event handling loop if event.type == MOUSEBUTTONUP: mousex, mousey = event.pos clickedButton = getButtonClicked(mousex, mousey) elif event.type == KEYDOWN: if event.key == K_q: clickedButton = YELLOW elif event.key == K_w: clickedButton = BLUE elif event.key == K_a: clickedButton = RED elif event.key == K_s: clickedButton = GREEN if not waitingForInput: # play the pattern pygame.display.update() pygame.time.wait(1000) pattern.append(random.choice((YELLOW, BLUE, RED, GREEN))) for button in pattern: flashButtonAnimation(button) pygame.time.wait(FLASHDELAY) waitingForInput = True else: # wait for the player to enter buttons if clickedButton and clickedButton == pattern[currentStep]: # pushed the correct button flashButtonAnimation(clickedButton) currentStep += 1 lastClickTime = time.time() if currentStep == len(pattern): # pushed the last button in the pattern changeBackgroundAnimation() score += 1 waitingForInput = False currentStep = 0 # reset back to first step elif (clickedButton and clickedButton != pattern[currentStep]) or (currentStep != 0 and time.time() - TIMEOUT > lastClickTime): # pushed the incorrect button, or has timed out gameOverAnimation() # reset the variables for a new game: pattern = [] currentStep = 0 waitingForInput = False score = 0 pygame.time.wait(1000) changeBackgroundAnimation() pygame.display.update() FPSCLOCK.tick(FPS) def terminate(): pygame.quit() sys.exit() def checkForQuit(): for event in pygame.event.get(QUIT): # get all the QUIT events terminate() # terminate if any QUIT events are present for event in pygame.event.get(KEYUP): # get all the KEYUP events if event.key == K_ESCAPE: terminate() # terminate if the KEYUP event was for the Esc key pygame.event.post(event) # put the other KEYUP event objects back def flashButtonAnimation(color, animationSpeed=50): if color == YELLOW: sound = BEEP1 flashColor = BRIGHTYELLOW rectangle = YELLOWRECT elif color == BLUE: sound = BEEP2 flashColor = BRIGHTBLUE rectangle = BLUERECT elif color == RED: sound = BEEP3 flashColor = BRIGHTRED rectangle = REDRECT elif color == GREEN: sound = BEEP4 flashColor = BRIGHTGREEN rectangle = GREENRECT origSurf = DISPLAYSURF.copy() flashSurf = pygame.Surface((BUTTONSIZE, BUTTONSIZE)) flashSurf = flashSurf.convert_alpha() r, g, b = flashColor sound.play() for start, end, step in ((0, 255, 1), (255, 0, -1)): # animation loop for alpha in range(start, end, animationSpeed * step): checkForQuit() DISPLAYSURF.blit(origSurf, (0, 0)) flashSurf.fill((r, g, b, alpha)) DISPLAYSURF.blit(flashSurf, rectangle.topleft) pygame.display.update() FPSCLOCK.tick(FPS) DISPLAYSURF.blit(origSurf, (0, 0)) def drawButtons(): pygame.draw.rect(DISPLAYSURF, YELLOW, YELLOWRECT) pygame.draw.rect(DISPLAYSURF, BLUE, BLUERECT) pygame.draw.rect(DISPLAYSURF, RED, REDRECT) pygame.draw.rect(DISPLAYSURF, GREEN, GREENRECT) def changeBackgroundAnimation(animationSpeed=40): global bgColor newBgColor = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) newBgSurf = pygame.Surface((WINDOWWIDTH, WINDOWHEIGHT)) newBgSurf = newBgSurf.convert_alpha() r, g, b = newBgColor for alpha in range(0, 255, animationSpeed): # animation loop checkForQuit() DISPLAYSURF.fill(bgColor) newBgSurf.fill((r, g, b, alpha)) DISPLAYSURF.blit(newBgSurf, (0, 0)) drawButtons() # redraw the buttons on top of the tint pygame.display.update() FPSCLOCK.tick(FPS) bgColor = newBgColor def gameOverAnimation(color=WHITE, animationSpeed=50): # play all beeps at once, then flash the background origSurf = DISPLAYSURF.copy() flashSurf = pygame.Surface(DISPLAYSURF.get_size()) flashSurf = flashSurf.convert_alpha() BEEP1.play() # play all four beeps at the same time, roughly. BEEP2.play() BEEP3.play() BEEP4.play() r, g, b = color for i in range(3): # do the flash 3 times for start, end, step in ((0, 255, 1), (255, 0, -1)): # The first iteration in this loop sets the following for loop # to go from 0 to 255, the second from 255 to 0. for alpha in range(start, end, animationSpeed * step): # animation loop # alpha means transparency. 255 is opaque, 0 is invisible checkForQuit() flashSurf.fill((r, g, b, alpha)) DISPLAYSURF.blit(origSurf, (0, 0)) DISPLAYSURF.blit(flashSurf, (0, 0)) drawButtons() pygame.display.update() FPSCLOCK.tick(FPS) def getButtonClicked(x, y): if YELLOWRECT.collidepoint( (x, y) ): return YELLOW elif BLUERECT.collidepoint( (x, y) ): return BLUE elif REDRECT.collidepoint( (x, y) ): return RED elif GREENRECT.collidepoint( (x, y) ): return GREEN return None if __name__ == '__main__': main()
通常的起始工作
# Simulate (a Simon clone) # By Al Sweigart [[email protected]](/cdn-cgi/l/email-protection) # http://inventwithpython.com/pygame # Creative Commons BY-NC-SA 3.0 US import random, sys, time, pygame from pygame.locals import * FPS = 30 WINDOWWIDTH = 640 WINDOWHEIGHT = 480 FLASHSPEED = 500 # in milliseconds FLASHDELAY = 200 # in milliseconds BUTTONSIZE = 200 BUTTONGAPSIZE = 20 TIMEOUT = 4 # seconds before game over if no button is pushed. # R G B WHITE = (255, 255, 255) BLACK = ( 0, 0, 0) BRIGHTRED = (255, 0, 0) RED = (155, 0, 0) BRIGHTGREEN = ( 0, 255, 0) GREEN = ( 0, 155, 0) BRIGHTBLUE = ( 0, 0, 255) BLUE = ( 0, 0, 155) BRIGHTYELLOW = (255, 255, 0) YELLOW = (155, 155, 0) DARKGRAY = ( 40, 40, 40) bgColor = BLACK XMARGIN = int((WINDOWWIDTH - (2 * BUTTONSIZE) - BUTTONGAPSIZE) / 2) YMARGIN = int((WINDOWHEIGHT - (2 * BUTTONSIZE) - BUTTONGAPSIZE) / 2)
在这里,我们设置了通常的常量,用于以后可能要修改的事物,例如四个按钮的大小,按钮用于的颜色阴影(当按钮亮起时使用的明亮颜色)以及玩家在游戏超时之前必须按下序列中的下一个按钮的时间。
设置按钮
# Rect objects for each of the four buttons YELLOWRECT = pygame.Rect(XMARGIN, YMARGIN, BUTTONSIZE, BUTTONSIZE) BLUERECT = pygame.Rect(XMARGIN + BUTTONSIZE + BUTTONGAPSIZE, YMARGIN, BUTTONSIZE, BUTTONSIZE) REDRECT = pygame.Rect(XMARGIN, YMARGIN + BUTTONSIZE + BUTTONGAPSIZE, BUTTONSIZE, BUTTONSIZE) GREENRECT = pygame.Rect(XMARGIN + BUTTONSIZE + BUTTONGAPSIZE, YMARGIN + BUTTONSIZE + BUTTONGAPSIZE, BUTTONSIZE, BUTTONSIZE)
就像滑动拼图游戏中的“重置”、“解决”和“新游戏”按钮一样,模拟游戏有四个矩形区域和处理玩家在这些区域内点击时的代码。程序将需要 Rect 对象来表示四个按钮的区域,以便可以在它们上调用collidepoint()
方法。第 36 至 39 行设置了这些 Rect 对象的适当坐标和大小。
main()
函数
def main(): global FPSCLOCK, DISPLAYSURF, BASICFONT, BEEP1, BEEP2, BEEP3, BEEP4 pygame.init() FPSCLOCK = pygame.time.Clock() DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT)) pygame.display.set_caption('Simulate') BASICFONT = pygame.font.Font('freesansbold.ttf', 16) infoSurf = BASICFONT.render('Match the pattern by clicking on the button or using the Q, W, A, S keys.', 1, DARKGRAY) infoRect = infoSurf.get_rect() infoRect.topleft = (10, WINDOWHEIGHT - 25) # load the sound files BEEP1 = pygame.mixer.Sound('beep1.ogg') BEEP2 = pygame.mixer.Sound('beep2.ogg') BEEP3 = pygame.mixer.Sound('beep3.ogg') BEEP4 = pygame.mixer.Sound('beep4.ogg')
main()
函数将实现程序的大部分内容,并在需要时调用其他函数。通常的 Pygame 设置函数被调用来初始化库,创建一个 Clock 对象,创建一个窗口,设置标题,并创建一个 Font 对象,用于在窗口上显示分数和说明。这些函数调用创建的对象将存储在全局变量中,以便它们可以在其他函数中使用。但它们基本上是常量,因为其中的值从不改变。
第 55 至 58 行将加载声音文件,以便模拟可以在玩家点击每个按钮时播放声音效果。pygame.mixer.Sound()
构造函数将返回一个 Sound 对象,我们将其存储在变量BEEP1
到BEEP4
中,这些变量在第 42 行被设置为全局变量。
本程序中使用的一些局部变量
# Initialize some variables for a new game pattern = [] # stores the pattern of colors currentStep = 0 # the color the player must push next lastClickTime = 0 # timestamp of the player's last button push score = 0 # when False, the pattern is playing. when True, waiting for the player to click a colored button: waitingForInput = False
pattern
变量将是一个颜色值列表(YELLOW
,RED
,BLUE
或GREEN
),用于跟踪玩家必须记住的模式。例如,如果模式的值是[RED, RED, YELLOW, RED, BLUE, BLUE, RED, GREEN]
,那么玩家首先必须点击红色按钮两次,然后是黄色按钮,然后是红色按钮,依此类推,直到最后的绿色按钮。当玩家完成每一轮时,一个新的随机颜色将被添加到列表的末尾。
currentStep
变量将跟踪玩家必须点击的模式列表中的颜色。如果currentStep
是0
,pattern
是[GREEN, RED, RED, YELLOW]
,那么玩家必须点击绿色按钮。如果他们点击其他按钮,代码将导致游戏结束。
有一个TIMEOUT
常量,使玩家在一定时间内点击模式中的下一个按钮,否则代码会导致游戏结束。为了检查自上次按钮点击以来是否已经过了足够的时间,lastClickTime
变量需要跟踪玩家上次点击按钮的时间。(Python 有一个名为 time 的模块和一个time.time()
函数来返回当前时间。这将在后面解释。)
也许很难相信,score
变量跟踪得分。难以置信!
我们的程序还有两种模式。要么程序正在为玩家播放按钮的模式(在这种情况下,waitingForInput
设置为False
),要么程序已经完成了模式的播放,并正在等待用户按正确顺序点击按钮(在这种情况下,waitingForInput
设置为True
)。
绘制棋盘和处理输入
while True: # main game loop clickedButton = None # button that was clicked (set to YELLOW, RED, GREEN, or BLUE) DISPLAYSURF.fill(bgColor) drawButtons() scoreSurf = BASICFONT.render('Score: ' + str(score), 1, WHITE) scoreRect = scoreSurf.get_rect() scoreRect.topleft = (WINDOWWIDTH - 100, 10) DISPLAYSURF.blit(scoreSurf, scoreRect) DISPLAYSURF.blit(infoSurf, infoRect)
第 68 行是主游戏循环的开始。clickedButton
在每次迭代开始时将被重置为None
。如果在此迭代期间点击了按钮,那么clickedButton
将被设置为与按钮匹配的颜色值之一(YELLOW
、RED
、GREEN
或BLUE
)。
fill()
方法在第 70 行被调用,以重新绘制整个显示表面,这样我们就可以从头开始绘制。四个彩色按钮是通过调用drawButtons()
(稍后解释)来绘制的。然后在 73 到 76 行创建了得分的文本。
还将有文本告诉玩家他们当前的得分是多少。与 51 行对指示文本的render()
方法不同,得分的文本会发生变化。它起初是'Score: 0'
,然后变成'Score: 1'
,然后变成'Score: 2'
,依此类推。这就是为什么我们在游戏循环内部的 73 行调用render()
方法来创建新的 Surface 对象。由于指示文本(“按照模式匹配…”)永远不会改变,所以我们只需要在 50 行游戏循环外部调用一次render()
。
检查鼠标点击
checkForQuit() for event in pygame.event.get(): # event handling loop if event.type == MOUSEBUTTONUP: mousex, mousey = event.pos clickedButton = getButtonClicked(mousex, mousey)
第 80 行快速检查是否有任何QUIT
事件,然后第 81 行是事件处理循环的开始。任何鼠标点击的 XY 坐标将存储在mousex
和mousey
变量中。如果鼠标点击在四个按钮之一上,那么我们的getButtonClicked()
函数将返回被点击的按钮的颜色对象(否则返回None
)。
检查键盘按键
elif event.type == KEYDOWN: if event.key == K_q: clickedButton = YELLOW elif event.key == K_w: clickedButton = BLUE elif event.key == K_a: clickedButton = RED elif event.key == K_s: clickedButton = GREEN
第 85 到 93 行检查是否有任何KEYDOWN
事件(当用户在键盘上按键时创建)。Q、W、A 和 S 键对应按钮,因为它们在键盘上呈正方形排列。
Q 键位于四个键盘键的左上方,就像屏幕上的黄色按钮位于左上方一样,所以我们将按下 Q 键与点击黄色按钮相同。我们可以通过将clickedButton
变量设置为常量变量YELLOW
中的值来实现这一点。我们也可以对其他三个键做同样的操作。这样,用户可以用鼠标或键盘玩模拟游戏。
游戏循环的两种状态
if not waitingForInput: # play the pattern pygame.display.update() pygame.time.wait(1000) pattern.append(random.choice((YELLOW, BLUE, RED, GREEN))) for button in pattern: flashButtonAnimation(button) pygame.time.wait(FLASHDELAY) waitingForInput = True
程序可以处于两种不同的“模式”或“状态”。当waitingForInput
为False
时,程序将显示模式的动画。当waitingForInput
为True
时,程序将等待用户选择按钮。
第 97 到 105 行将涵盖程序显示模式动画的情况。由于这是在游戏开始或玩家完成模式时完成的,第 101 行将向模式列表添加一个随机颜色,使模式变长一步。然后第 102 到 104 行循环遍历模式列表中的每个值,并调用flashButtonAnimation()
使该按钮发光。在所有按钮都发光完成后,程序将waitingForInput
变量设置为True
。
弄清楚玩家是否按下了正确的按钮
else: # wait for the player to enter buttons if clickedButton and clickedButton == pattern[currentStep]: # pushed the correct button flashButtonAnimation(clickedButton) currentStep += 1 lastClickTime = time.time()
如果waitingForInput
是True
,那么第 106 行的else
语句中的代码将执行。第 108 行检查玩家是否在游戏循环的这次迭代中点击了一个按钮,以及该按钮是否是正确的。currentStep
变量跟踪模式列表中玩家下一个应该点击的按钮的索引。
例如,如果模式设置为[YELLOW, RED, RED],并且currentStep
变量设置为0
(就像玩家刚开始游戏时一样),那么玩家应该点击的正确按钮将是pattern[0]
(黄色按钮)。
如果玩家点击了正确的按钮,我们希望通过调用flashButtonAnimation()
来闪烁玩家点击的按钮,然后增加currentStep
到下一步,然后更新lastClickTime
变量到当前时间。(time.time()
函数返回自 1970 年 1 月 1 日以来的秒数的浮点值,因此我们可以用它来跟踪时间。)
if currentStep == len(pattern): # pushed the last button in the pattern changeBackgroundAnimation() score += 1 waitingForInput = False currentStep = 0 # reset back to first step
第 114 到 119 行位于从第 106 行开始的else
语句内。如果执行在该else
语句内部,我们知道玩家点击了一个按钮,而且这是正确的按钮。第 114 行检查是否这是模式列表中的最后一个正确的按钮,通过检查存储在currentStep
中的整数是否等于模式列表中的值数量。
如果这是True
,那么我们希望通过调用changeBackgroundAnimation()
来改变背景颜色。这是让玩家知道他们已经完全正确输入整个模式的简单方法。分数增加,currentStep
设置回0
,waitingForInput
变量设置为False
,这样在游戏循环的下一次迭代中,代码将向模式列表添加一个新的颜色值,然后闪烁按钮。
elif (clickedButton and clickedButton != pattern[currentStep]) or (currentStep != 0 and time.time() - TIMEOUT > lastClickTime):
如果玩家没有点击正确的按钮,第 121 行的elif
语句处理了玩家点击错误按钮或者玩家等待太久没有点击按钮的情况。无论哪种情况,我们都需要显示“游戏结束”动画并开始新游戏。
elif
语句的条件中的(clickedButton and clickedButton != pattern[currentStep])
部分检查是否点击了一个按钮,并且是错误的按钮。您可以将此与第 108 行的if
语句的条件clickedButton and clickedButton == pattern[currentStep]
进行比较,如果玩家点击了一个按钮,并且这是正确的按钮,则评估为True
。
第 121 行elif
条件的另一部分是(currentStep != 0 and time.time() - TIMEOUT > lastClickTime)
。这处理确保玩家没有“超时”的情况。请注意,该条件的这一部分有两个由and
关键字连接的表达式。这意味着and
关键字的两侧都需要评估为True
。
为了“超时”,它不能是玩家的第一个按钮点击。但一旦他们开始点击按钮,他们必须快速点击按钮,直到他们输入整个模式(或者点击了错误的模式并得到了“游戏结束”)。如果currentStep != 0
是True
,那么我们知道玩家已经开始点击按钮。
时代时间
此外,为了“超时”,当前时间(由time.time()
返回)减去四秒(因为4
存储在TIMEOUT
中)必须大于上次点击按钮的时间(存储在lastClickTime
中)。time.time() - TIMEOUT > lastClickTime
之所以有效,是因为时代时间的工作原理。时代时间(也称为 Unix 时代时间)是自 1970 年 1 月 1 日以来的秒数。这个日期被称为 Unix 时代。
例如,当我从交互式 shell 中运行time.time()
(不要忘记首先导入 time 模块),它看起来像这样:
>>> import time >>> time.time() 1320460242.118
这个数字的意思是,time.time()
函数被调用的时刻距离 1970 年 1 月 1 日午夜已经超过了 1,320,460,242 秒。(这相当于 2011 年 11 月 4 日晚上 7 点 30 分 42 秒。您可以在invpy.com/epochtime
学习如何将 Unix 纪元时间转换为常规英文时间)
如果我稍后从交互式 shell 中调用time.time()
,可能会看起来像这样:
>>> time.time() 1320460261.315
从 Unix 纪元的午夜开始的 1320460261.315 秒是 2011 年 11 月 4 日晚上 7 点 31 分 01 秒。(实际上,如果您想要精确的话,是 7 点 31 分 0.315 秒。)
如果我们必须处理字符串,处理时间将会很困难。如果我们只有字符串值'7:30:42 PM'
和'7:31:01 PM'
进行比较,很难知道已经过去了 19 秒。但是使用纪元时间,只需要减去整数1320460261.315 - 1320460242.118
,得到19.197000026702881
。这个值是这两个时间之间的秒数。(额外的0.000026702881
来自于使用浮点数进行数学运算时发生的非常小的舍入误差。它们只会偶尔发生,通常太微小而不值得关注。您可以在invpy.com/roundingerrors
了解更多关于浮点数舍入误差的信息。)
回到第 121 行,如果time.time() - TIMEOUT > lastClickTime
的值为True
,那么自time.time()
被调用并存储在lastClickTime
以来已经过去了 4 秒以上。如果值为False
,则已经过去了不到 4 秒。
# pushed the incorrect button, or has timed out gameOverAnimation() # reset the variables for a new game: pattern = [] currentStep = 0 waitingForInput = False score = 0 pygame.time.wait(1000) changeBackgroundAnimation()
如果玩家点击了错误的按钮或者超时了,程序应该播放“游戏结束”的动画,然后重置变量以开始新游戏。这包括将pattern
列表设置为空列表,currentStep
设置为0
,waitingForInput
设置为False
,然后将score
设置为0
。稍作暂停,然后设置新的背景颜色,以提示玩家新游戏的开始,新游戏将在游戏循环的下一次迭代中开始。
将面板绘制到屏幕上
pygame.display.update() FPSCLOCK.tick(FPS)
就像其他游戏程序一样,在游戏循环中最后要做的事情是将显示 Surface 对象绘制到屏幕上,并调用tick()
方法。
同样的terminate()
函数
def terminate(): pygame.quit() sys.exit() def checkForQuit(): for event in pygame.event.get(QUIT): # get all the QUIT events terminate() # terminate if any QUIT events are present for event in pygame.event.get(KEYUP): # get all the KEYUP events if event.key == K_ESCAPE: terminate() # terminate if the KEYUP event was for the Esc key pygame.event.post(event) # put the other KEYUP event objects back
terminate()
和checkForQuit()
函数在滑动拼图章节中被使用和解释过,所以我们将跳过再次描述它们。
重复使用常量变量
def flashButtonAnimation(color, animationSpeed=50): if color == YELLOW: sound = BEEP1 flashColor = BRIGHTYELLOW rectangle = YELLOWRECT elif color == BLUE: sound = BEEP2 flashColor = BRIGHTBLUE rectangle = BLUERECT elif color == RED: sound = BEEP3 flashColor = BRIGHTRED rectangle = REDRECT elif color == GREEN: sound = BEEP4 flashColor = BRIGHTGREEN rectangle = GREENRECT
根据传递给颜色参数的 Color 值的不同,声音、明亮闪光的颜色和闪光的矩形区域也会有所不同。第 151 到 166 行根据color
参数中的值设置了三个本地变量:sound
、flashColor
和rectangle
。
闪烁按钮的动画
origSurf = DISPLAYSURF.copy() flashSurf = pygame.Surface((BUTTONSIZE, BUTTONSIZE)) flashSurf = flashSurf.convert_alpha() r, g, b = flashColor sound.play()
闪烁按钮的动画过程很简单:在每一帧动画上,首先绘制正常的面板,然后在上面绘制闪烁的按钮的明亮颜色版本。明亮颜色的 alpha 值在动画的第一帧开始时为0
,然后在每一帧后慢慢增加,直到完全不透明,明亮颜色版本完全覆盖了正常按钮颜色。这将使它看起来像按钮慢慢变亮。
变亮是动画的第一部分。第二部分是按钮变暗。这是用相同的代码完成的,只是在每一帧中,alpha 值不是增加,而是减少。随着 alpha 值越来越低,覆盖在上面的明亮颜色将变得越来越不可见,直到只剩下原始的板子和暗淡的颜色可见。
要在代码中执行此操作,第 168 行创建显示 Surface 对象的副本并将其存储在origSurf
中。第 169 行创建一个新的 Surface 对象,大小与单个按钮相同,并将其存储在flashSurf
中。在flashSurf
上调用convert_alpha()
方法,以便 Surface 对象可以在其上绘制透明颜色(否则,我们使用的 Color 对象中的 alpha 值将被忽略并自动假定为 255)。在您自己的游戏程序中,如果您在使颜色透明度工作时遇到问题,请确保已在任何具有透明颜色的 Surface 对象上调用了convert_alpha()
方法。
第 171 行创建名为r
,g
和b
的单独局部变量,用于存储存储在flashColor
中的元组的各个 RGB 值。这只是一些语法糖,使该函数中的其余代码更容易阅读。在开始执行按钮闪烁动画之前,第 172 行将播放该按钮的声音效果。声音效果开始播放后,程序执行会继续进行,因此声音将在按钮闪烁动画期间播放。
for start, end, step in ((0, 255, 1), (255, 0, -1)): # animation loop for alpha in range(start, end, animationSpeed * step): checkForQuit() DISPLAYSURF.blit(origSurf, (0, 0)) flashSurf.fill((r, g, b, alpha)) DISPLAYSURF.blit(flashSurf, rectangle.topleft) pygame.display.update() FPSCLOCK.tick(FPS) DISPLAYSURF.blit(origSurf, (0, 0))
请记住,为了执行动画,我们首先要用从0
到255
的递增 alpha 值绘制flashSurf
以执行动画的变亮部分。然后为了执行变暗,我们希望 alpha 值从255
到0
。我们可以使用以下代码执行:
for alpha in range(0, 255, animationSpeed): # brightening checkForQuit() DISPLAYSURF.blit(origSurf, (0, 0)) flashSurf.fill((r, g, b, alpha)) DISPLAYSURF.blit(flashSurf, rectangle.topleft) pygame.display.update() FPSCLOCK.tick(FPS) for alpha in range(255, 0, -animationSpeed): # dimming checkForQuit() DISPLAYSURF.blit(origSurf, (0, 0)) flashSurf.fill((r, g, b, alpha)) DISPLAYSURF.blit(flashSurf, rectangle.topleft) pygame.display.update() FPSCLOCK.tick(FPS)
但请注意,for
循环内的代码处理绘制帧并且彼此相同。如果我们像上面那样编写代码,那么第一个for
循环将处理动画的变亮部分(其中 alpha 值从0
到255
),第二个for
循环将处理动画的变暗部分(其中 alpha 值从255
到0
)。请注意,对于第二个for
循环,range()
调用的第三个参数是一个负数。
每当我们有相同的代码时,我们可能可以缩短我们的代码,这样我们就不必重复它。这就是我们在第 173 行的for
循环中所做的,它为第 174 行的range()
调用提供了不同的值:
for start, end, step in ((0, 255, 1), (255, 0, -1)): # animation loop for alpha in range(start, end, animationSpeed * step):
在第 173 行的for
循环的第一次迭代中,start
设置为0
,end
设置为255
,step
设置为1
。这样,当执行第 174 行的for
循环时,它调用range(0, 255, animationSpeed)
。 (请注意,animationSpeed * 1
与animationSpeed
相同。将一个数字乘以1
会给我们相同的数字。)
然后,第 174 行的for
循环执行并执行变亮动画。
在第 173 行的for
循环的第二次迭代(始终有两次且仅有两次此内部for
循环的迭代)中,start
设置为255
,end
设置为0
,step
设置为-1
。当执行第 174 行的for
循环时,它调用range(255, 0, -animationSpeed)
。 (请注意,animationSpeed * -1
评估为-animationSpeed
,因为将任何数字乘以-1
都会返回该数字的负形式。)
这样,我们就不必有两个单独的for
循环,并重复其中的所有代码。以下是再次在第 174 行的for
循环内的代码:
checkForQuit() DISPLAYSURF.blit(origSurf, (0, 0)) flashSurf.fill((r, g, b, alpha)) DISPLAYSURF.blit(flashSurf, rectangle.topleft) pygame.display.update() FPSCLOCK.tick(FPS) DISPLAYSURF.blit(origSurf, (0, 0))
我们检查任何QUIT
事件(以防用户在动画期间尝试关闭程序),然后将origSurf
Surface 贴到显示 Surface 上。然后我们通过调用fill()
(提供我们在第 171 行得到的颜色的r
,g
,b
值和for
循环在alpha
变量中设置的 alpha 值)来绘制flashSurf
Surface。然后将flashSurf
Surface 贴到显示 Surface 上。
然后,为了使显示 Surface 显示在屏幕上,第 179 行调用了pygame.display.update()
。为了确保动画不会以计算机可以绘制的速度播放,我们通过调用tick()
方法添加了短暂的暂停。(如果要看到闪烁动画播放得非常慢,请将低数字(如 1 或 2)作为tick()
的参数,而不是FPS
。)
绘制按钮
def drawButtons(): pygame.draw.rect(DISPLAYSURF, YELLOW, YELLOWRECT) pygame.draw.rect(DISPLAYSURF, BLUE, BLUERECT) pygame.draw.rect(DISPLAYSURF, RED, REDRECT) pygame.draw.rect(DISPLAYSURF, GREEN, GREENRECT)
每个按钮只是一个特定颜色的矩形放在特定位置,我们只需调用pygame.draw.rect()
四次来在显示表面上绘制按钮。我们使用的 Color 对象和 Rect 对象永远不会改变,这就是为什么我们将它们存储在像YELLOW
和YELLOWRECT
这样的常量变量中。
背景变化动画
def changeBackgroundAnimation(animationSpeed=40): global bgColor newBgColor = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) newBgSurf = pygame.Surface((WINDOWWIDTH, WINDOWHEIGHT)) newBgSurf = newBgSurf.convert_alpha() r, g, b = newBgColor for alpha in range(0, 255, animationSpeed): # animation loop checkForQuit() DISPLAYSURF.fill(bgColor) newBgSurf.fill((r, g, b, alpha)) DISPLAYSURF.blit(newBgSurf, (0, 0)) drawButtons() # redraw the buttons on top of the tint pygame.display.update() FPSCLOCK.tick(FPS) bgColor = newBgColor
每当玩家完成正确输入整个模式时,背景颜色变化动画就会发生。在从第 198 行开始的每次循环迭代中,整个显示表面都必须重新绘制(与越来越不透明的新背景颜色混合,直到背景完全被新颜色覆盖)。循环的每次迭代所做的步骤是:
- 第 200 行用旧的背景颜色(存储在
bgColor
中)填充整个显示表面(存储在DISPLAYSURF
中)。 - 第 202 行用不同的 Surface 对象(存储在
newBgSurf
中)填充新背景颜色的 RGB 值(并且每次迭代时 alpha 透明度值都会改变,因为这是第 198 行上的for
循环所做的)。 - 第 203 行然后将
newBgSurf
Surface 绘制到DISPLAYSURF
中的显示表面上。我们之所以没有直接在DISPLAYSURF
上绘制半透明的新背景颜色,是因为fill()
方法只会替换表面上的颜色,而blit()
方法会混合颜色。 - 现在我们已经按照自己的意愿设置了背景,我们将在第 205 行调用
drawButtons()
来在其上绘制按钮。 - 第 207 和 208 行只是将显示表面绘制到屏幕上并添加了一个暂停。
changeBackgroundAnimation()
函数开头有一个global
语句的原因是bgColor
变量,因为这个函数通过第 209 行的赋值语句修改了变量的内容。任何函数都可以读取全局变量的值,而不需要指定global
语句。
如果该函数为全局变量分配一个值而没有global
语句,那么 Python 认为该变量是一个局部变量,只是恰好与全局变量同名。main()
函数使用bgColor
变量,但不需要为它添加全局语句,因为它只读取bgColor
的内容,main()
函数从不为bgColor
分配新值。这个概念在invpy.com/global
上有更详细的解释。
游戏结束动画
def gameOverAnimation(color=WHITE, animationSpeed=50): # play all beeps at once, then flash the background origSurf = DISPLAYSURF.copy() flashSurf = pygame.Surface(DISPLAYSURF.get_size()) flashSurf = flashSurf.convert_alpha() BEEP1.play() # play all four beeps at the same time, roughly. BEEP2.play() BEEP3.play() BEEP4.play() r, g, b = color for i in range(3): # do the flash 3 times
下一行(下面的第 223 行)的for
循环的每次迭代都会执行一次闪烁。为了完成三次闪烁,我们将所有代码放在一个具有三次迭代的for
循环中。如果您想要更多或更少的闪烁,那么请更改传递给第 222 行的range()
的整数。
for start, end, step in ((0, 255, 1), (255, 0, -1)):
第 223 行上的for
循环与第 173 行上的完全相同。start
、end
和step
变量将在下一个for
循环(第 224 行)中用于控制alpha
变量的变化。如果需要刷新自己对这些循环的工作原理的话,请重新阅读“按钮闪烁动画”部分。
# The first iteration in this loop sets the following for loop # to go from 0 to 255, the second from 255 to 0. for alpha in range(start, end, animationSpeed * step): # animation loop # alpha means transparency. 255 is opaque, 0 is invisible checkForQuit() flashSurf.fill((r, g, b, alpha)) DISPLAYSURF.blit(origSurf, (0, 0)) DISPLAYSURF.blit(flashSurf, (0, 0)) drawButtons() pygame.display.update() FPSCLOCK.tick(FPS)
这个动画循环与“背景变化动画”部分中的以前的闪烁动画代码相同。存储在origSurf
中的原始 Surface 对象被绘制在显示表面上,然后flashSurf
(上面涂上新的闪烁颜色)被 blitted 到显示表面上。背景颜色设置完成后,按钮在第 232 行上方绘制。最后,通过调用pygame.display.update()
将显示表面绘制到屏幕上。
第 226 行上的for
循环调整了每帧动画使用的颜色的 alpha 值(起初增加,然后减少)。
从像素坐标转换为按钮
def getButtonClicked(x, y): if YELLOWRECT.collidepoint( (x, y) ): return YELLOW elif BLUERECT.collidepoint( (x, y) ): return BLUE elif REDRECT.collidepoint( (x, y) ): return RED elif GREENRECT.collidepoint( (x, y) ): return GREEN return None if __name__ == '__main__': main()
getButtonClicked()
函数简单地接受 XY 像素坐标并返回值YELLOW
、BLUE
、RED
或GREEN
,如果其中一个按钮被点击,或者如果 XY 像素坐标不在四个按钮之上则返回None
。
显式胜于隐式
您可能已经注意到getButtonClicked()
的代码以在第 247 行以return None
语句结束。这可能看起来像一个奇怪的输入,因为如果所有函数根本没有return
语句,它们都会返回None
。我们本来可以完全省略第 47 行,程序仍然会以完全相同的方式工作。那么为什么要写它呢?
通常当一个函数到达结尾并隐式地返回None
值(也就是说,没有明确的return
语句表明它返回None
)调用它的代码并不关心返回值。所有函数调用都必须返回一个值(这样它们才能被计算为某个值并成为表达式的一部分),但我们的代码并不总是使用返回值。
例如,想想print()
函数。从技术上讲,这个函数返回None
值,但我们从来不关心它:
>>> spam = print('Hello') Hello >>> spam == None True >>>
然而,当getButtonClicked()
返回None
时,这意味着传递给它的坐标不在四个按钮之上。为了清楚地表明在这种情况下从getButtonClicked()
返回了值None
,我们在函数末尾有了return None
行。
为了使您的代码更易读,最好让您的代码明确(也就是说,明确地陈述某事,即使它可能是显而易见的),而不是隐含的(也就是说,让阅读代码的人知道它的工作方式,而不是直接告诉他们)。事实上,“显式胜于隐式”是 Python Koans 中的一个。
Koans 是一组关于如何编写良好代码的小格言。在 Python 交互式 shell 中有一个彩蛋(也就是一个小小的隐藏惊喜),如果你尝试导入一个名为this
的模块,它会显示“Python 禅宗”Koans。在交互式 shell 中试一试:
>>> import this The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren't special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one-- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless you're Dutch. Now is better than never. Although never is often better than *right* now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea -- let's do more of those!
如果您想了解这些个别 Koans 的更多含义,请访问invpy.com/zen
。