使用 Python 和 Pygame 制作游戏:第一章到第三章

216 阅读1小时+

第一章:安装 Python 和 Pygame

原文:inventwithpython.com/pygame/chapter1.html

译者:飞龙

协议:CC BY-NC-SA 4.0

开始之前您应该知道的事情

在阅读本书之前,如果您了解一些 Python 编程知识(或者知道如何使用 Python 之外的其他语言进行编程),可能会有所帮助;但是即使您没有,您仍然可以阅读本书。编程并不像人们想象的那么难。如果您遇到问题,可以在线阅读免费书籍“使用 Python 发明自己的电脑游戏”inventwithpython.com,或者在 Invent with Python 维基 inventwithpython.com/wiki 上查找您觉得困惑的主题。

在阅读本书之前,您不需要知道如何使用 Pygame 库。下一章是关于 Pygame 的所有主要特性和功能的简要教程。

如果您还没有阅读第一本书并在计算机上安装了 Python 和 Pygame,安装说明在本章中。如果您已经安装了这两者,那么您可以跳过本章。

下载和安装 Python

在我们开始编程之前,您需要在计算机上安装名为 Python 解释器的软件。(在这里,您可能需要向成年人寻求帮助。)解释器是一个程序,它能理解您将用 Python 语言编写(或者说,打出来)的指令。没有解释器,您的计算机将无法运行 Python 程序。我们从现在开始将“Python 解释器”简称为“Python”。

Python 解释器软件可以从 Python 编程语言的官方网站 www.python.org 下载。您可能需要他人的帮助来下载和安装 Python 软件。安装方式略有不同,取决于您的计算机操作系统是 Windows、Mac OS X 还是 Ubuntu 等 Linux 操作系统。您还可以在网上找到人们在计算机上安装 Python 软件的视频,网址是 invpy.com/installing。

Windows 说明

当您访问 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 的一些额外信息,网址是 docs.python.org/dev/using/m…

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 浏览器中,转到网址 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 的视频教程可在本书网站 invpy.com/videos 上找到。

如何使用本书

“使用 Python 和 Pygame 制作游戏”与其他编程书籍不同,因为它专注于几个游戏程序的完整源代码。本书不是教授编程概念,然后让您自己想出如何使用这些概念制作程序,而是向您展示一些程序,然后解释它们是如何组合在一起的。

一般来说,你应该按顺序阅读这些章节。在这些游戏中有许多概念会反复使用,它们只在它们首次出现的第一个游戏中详细解释。但如果有一个你觉得有趣的游戏,可以直接跳到那一章。如果你超前了,可以随时回头阅读之前的章节。

特色程序

每章都专注于一个游戏程序,并解释代码的不同部分是如何工作的。通过逐行从本书中输入代码来复制这些程序非常有帮助。

然而,你也可以从本书的网站下载源代码文件。在网页浏览器中,访问网址 invpy.com/source,并按照说…

下载图形和声音文件

虽然你可以直接输入本书中的代码,但你需要从 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 工具将显示书中源代码与你输入的源代码之间的任何差异。这是一种查找程序中任何打错字的简单方法。

复制和粘贴文本是一项非常有用的计算机技能,特别是对于计算机编程。本书的网站上有关复制和粘贴的视频教程,请访问 invpy.com/copypaste。

在线 diff 工具位于此网页:invpy.com/diff/pygame…

更多信息链接请访问 invpy.com

关于编程,有很多东西可以学习。但你现在不需要学习所有这些。在这本书中有几个地方,你可能想学习这些额外的细节和解释,但如果我把它们包括在这本书中,那么会增加很多页。如果这本更大更重的书意外地掉在你身上,这些额外的页的重量会压垮你,导致死亡。相反,我在这本书中包含了“更多信息”链接,你可以在这本书的网站上查看。你不必阅读这些额外的信息来理解这本书中的任何内容,但如果你好奇的话,它们就在那里。这些(以及其他)链接已经被缩短,并以 invpy.com 开头。

所有这些“更多信息”链接中的信息也可以从 invpy.com/pygamemorei… 下载。

即使这本书并不危险地沉重,请不要让它无论如何掉在你身上。

第二章:Pygame 基础

原文:inventwithpython.com/pygame/chapter2.html

译者:飞龙

协议:CC BY-NC-SA 4.0

就像 Python 自带了几个模块,比如randommathtime,为你的程序提供了额外的功能一样,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语句,导入了pygamesys模块,以便我们的程序可以使用它们中的函数。所有与图形、声音和其他 Pygame 功能相关的函数都在pygame模块中。

请注意,当导入pygame模块时,还会自动导入pygame模块中的所有模块,如pygame.imagespygame.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循环,以及一个称为“主游戏循环”的注释。游戏循环(也称为主循环)是一个循环,其中代码执行三件事:

  1. 处理事件。

  2. 更新游戏状态。

  3. 将游戏状态绘制到屏幕上。

游戏状态简单地指的是游戏程序中所有变量的一组值。在许多游戏中,游戏状态包括跟踪玩家健康和位置的变量的值,任何敌人的健康和位置,标记在棋盘上的标记,得分,或者轮到谁了。每当发生像玩家受到伤害(降低他们的健康值)或敌人移动到某个地方,或者游戏世界发生了什么事情,我们就说游戏状态已经改变了。

如果你玩过一个可以保存的游戏,那么“保存状态”就是你保存时的游戏状态。在大多数游戏中,暂停游戏会阻止游戏状态的改变。

由于游戏状态通常是响应事件(如鼠标点击或键盘按键)或时间流逝而更新的,游戏循环会不断地每秒检查和重新检查是否有新事件发生。主循环中有代码来查看已经创建了哪些事件(使用 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 值。

因为你可以使用每个三原色的0255的任意组合,这意味着 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)

透明颜色

当你透过一个有深红色色调的玻璃窗户看时,所有背后的颜色都会被添加上红色色调。你可以通过在颜色值中添加第四个0255的整数值来模仿这种效果。

这个值被称为 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 有两种表示矩形区域的方法(就像有两种表示颜色的方法一样)。第一种是四个整数的元组:

  1. 左上角的 X 坐标。

  2. 左上角的 Y 坐标。

  3. 矩形的宽度(以像素为单位)。

  4. 矩形的高度(以像素为单位)。

第二种方法是作为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) - 多边形是由平面边组成的形状。surfacecolor参数告诉函数在哪个表面上绘制多边形,以及用什么颜色绘制它。

pointlist参数是一个点的元组或列表(即,XY 坐标的两个整数元组或列表)。多边形是通过在每个点和元组中的下一个点之间画线来绘制的。然后从最后一个点画一条线到第一个点。你也可以传递一个点的列表而不是一个点的元组。

width参数是可选的。如果你不传递它,那么绘制的多边形将被填充,就像我们屏幕上的绿色多边形被填充一样。如果你为width参数传递一个整数值,那么只会绘制多边形的轮廓。这个整数表示多边形轮廓的宽度。传递1width参数会得到一个细的多边形,而传递41020会得到更厚的多边形。如果你为width参数传递整数0,多边形将被填充(就像如果你完全不传递width参数一样)。

所有的pygame.draw绘图函数都有可选的width参数,它们的工作方式与pygame.draw.polygon()width参数相同。也许width参数的更好名称应该是thickness,因为该参数控制你绘制的线条有多厚。

  • pygame.draw.line(surface, color, start_point, end_point, width) - 这个函数在start_pointend_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复制到DISPLAYSURFblit()有两个参数。第一个是源 Surface 对象,它将被复制到DISPLAYSURF Surface 对象上。第二个参数是一个包含 X 和 Y 值的二元组,表示图像应该被 blit 到的左上角的位置。

如果catxcaty设置为100200catImg的宽度为125,高度为79,则此blit()调用将复制此图像到DISPLAYSURF,使catImg的左上角位于 XY 坐标(100, 200),右下角的 XY 坐标位于(225, 279)。

请注意,您不能将内容 blit 到当前“锁定”的 Surface 上(例如当从中创建了 PixelArray 对象但尚未删除时)。

游戏循环的其余部分只是改变catxcatydirection变量,使猫在窗口周围移动。还有一个调用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()

使文本显示在屏幕上有六个步骤:

  1. 创建一个pygame.font.Font 对象。(就像第 12 行一样)

  2. 使用 Font 对象的render()方法在其上绘制文本创建一个 Surface 对象。(第 13 行)

  3. 通过调用 Surface 对象的get_rect()方法从 Surface 对象创建一个 Rect 对象。(第 14 行) 这个 Rect 对象将正确设置为呈现的文本的宽度和高度,但 top 和 left 属性将为0

  4. 通过改变 Rect 对象的属性来设置 Rect 对象的位置。在第 15 行,我们将 Rect 对象的中心设置为 200, 150。

  5. 使用pygame.display.set_mode()返回的 Surface 对象将带有文本的 Surface 对象贴到上面。 (第 19 行)

  6. 调用pygame.display.update()使显示 Surface 出现在屏幕上。(第 24 行)

pygame.font.Font()构造函数的参数是要使用的字体文件的字符串和字体大小的整数(以点为单位,就像文字处理器测量字体大小一样)。在第 12 行,我们传递了'freesansbold.ttf'(这是 Pygame 附带的字体)和整数32(32 点大小的字体)。

有关使用其他字体的更多信息,请参见invpy.com/usingotherfonts

render()方法调用的参数是要渲染的文本字符串,一个布尔值,用于指定是否要使用抗锯齿(本章后面将解释),文本的颜色和背景的颜色。如果要透明背景,则在方法调用中省略背景颜色参数。

抗锯齿

抗锯齿是一种图形技术,通过在边缘添加一点模糊来使文本和形状看起来不那么方块。使用抗锯齿绘制需要更多的计算时间,因此尽管图形可能看起来更好,但程序可能运行得更慢(但只是一点点)。

如果您放大一个有锯齿的线和一个抗锯齿的线,它们看起来像这样:

要使 Pygame 的文本使用抗锯齿,只需将render()方法的第二个参数传递为Truepygame.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

译者:飞龙

协议:CC BY-NC-SA 4.0

如何玩记忆迷题

在记忆迷题游戏中,几个图标被白色方框覆盖。每种图标有两个。玩家可以点击两个方框,看看它们后面是什么图标。如果图标匹配,那么这些方框将保持打开状态。当棋盘上的所有方框都被打开时,玩家获胜。为了给玩家一个提示,方框在游戏开始时会迅速打开一次。

嵌套的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 = ( 6060, 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模块中没有函数,但其中有几个常量变量,我们想要使用,比如MOUSEMOTIONKEYUPQUIT。使用这种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 行选择的BOARDWIDTHBOARDHEIGHT的值导致了一个有奇数个框的板(比如如果宽度为 3,高度为 5),那么总会有一个多余的图标没有配对。这将在程序的后面导致错误,并且可能需要大量的调试工作才能找出错误的真正来源是在程序的一开始。事实上,只是为了好玩,试着注释掉断言,这样它就不会运行,然后将BOARDWIDTHBOARDHEIGHT常量都设置为奇数。当你运行程序时,它会立即显示在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 行,我们设置了BOARDWIDTHBOARDHEIGHT常量。

断言确保这种情况永远不会发生。如果我们的代码将崩溃,我们希望它在检测到某些严重错误时尽快崩溃,否则该错误可能直到程序的后期才会显现。尽早崩溃!

每当程序中有一些条件必须始终始终始终为True时,您都要添加assert语句。经常崩溃!您不必过度使用assert语句,但是经常使用assert语句可以在检测错误的真正来源时大有裨益。尽早崩溃,经常崩溃!

(在您的代码中。比如说,不是骑马时。)

使源代码看起来漂亮

#            R    G    B
GRAY     = (100, 100, 100)
NAVYBLUE = ( 6060, 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 中的颜色由一个包含从0255的三个整数的元组表示。这三个整数代表颜色中红色、绿色和蓝色的数量,这就是为什么这些元组被称为 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)。

元组与列表,不可变与可变

你可能已经注意到ALLCOLORSALLSHAPES变量是元组而不是列表。我们何时使用元组,何时使用列表?它们之间的区别是什么?

元组和列表在所有方面都是相同的,除了两点:元组使用括号而不是方括号,并且元组中的项不能被修改(但列表中的项可以被修改)。我们通常称列表为可变的(可以被更改),元组为不可变的(不能被更改)。

要尝试更改列表和元组中的值,请查看以下代码示例:

>>> 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()函数外持续存在。我们将FPSCLOCKDISPLAYSURF变量标记为全局,因为它们在程序中的其他几个函数中使用。(更多信息请参阅invpy.com/scope。)

有四条简单的规则来确定变量是局部的还是全局的:

  1. 如果函数开头有一个变量的全局语句,那么该变量是全局的。

  2. 如果函数中的变量名与全局变量同名,并且函数从未为变量分配值,那么该变量就是全局变量。

  3. 如果函数中的变量名与全局变量同名,并且函数确实为变量分配了一个值,那么该变量是局部变量。

  4. 如果在函数中没有与函数中的变量同名的全局变量,那么该变量显然是局部变量。

通常希望避免在函数内部使用全局变量。函数应该像程序中的迷你程序,具有特定的输入(参数)和输出(返回值)。但是读取和写入全局变量的函数具有额外的输入和输出。由于在调用函数之前全局变量可能已在许多地方被修改,因此跟踪涉及全局变量中设置错误值的错误可能会很棘手。

将函数作为一个单独的迷你程序,不使用全局变量,可以更容易地找到代码中的错误,因为函数的参数是明确已知的。它还使得更改函数中的代码更容易,因为如果新函数与相同的参数一起工作并给出相同的返回值,它将自动与程序的其余部分一样工作,就像旧函数一样。

基本上,使用全局变量可能会使编写程序变得更容易,但它们通常会使调试变得更加困难。

在本书中的游戏中,全局变量主要用于永远不会改变的全局常量,但需要先调用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) 当玩家点击棋盘上的图标时,程序需要跟踪这是否是点击的一对图标中的第一个图标还是第二个图标。如果firstSelectionNone,则点击是在第一个图标上,我们将 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事件(即先前按下鼠标按钮,现在释放按钮)的情况下,鼠标光标的位置应该存储在mousexmousey变量中。如果这是一个MOUSEBUTTONUP事件,mouseClicked也应该设置为True

一旦我们处理了所有事件,存储在mousexmouseymouseClicked中的值将告诉我们玩家给了我们任何输入。现在我们应该更新游戏状态并将结果绘制到屏幕上。

检查鼠标光标在哪个方块上

        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()是如何做到的将在后面解释。现在我们只需要知道,如果mousexmousey坐标在一个方块上,函数将返回 XY 棋盘坐标的元组,并存储在boxxboxy中。如果鼠标光标不在任何方块上(例如,如果它在棋盘的一侧或在方块之间的间隙中),那么函数将返回元组(None, None),并且boxxboxy都将存储None

我们只关心boxxboxy中没有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()函数(稍后解释)将检索图标的形状和颜色值。(这些值将是ALLCOLORSALLSHAPES元组中的一个值。)

处理不匹配的图标对

                    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()来播放“游戏获胜”动画,然后稍微暂停一下,让玩家沉浸在胜利中,然后重置mainBoardrevealedBoxes中的数据结构,以开始新游戏。

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) )

棋盘数据结构只是一个元组的列表列表,每个元组有两个值:一个是图标的形状,一个是图标的颜色。但是创建这个数据结构有点复杂。我们需要确保棋盘上的盒子数量与图标数量完全一样,并且确保每种类型只有两个图标。

首先要做的是创建一个包含每种形状和颜色的可能组合的列表。回想一下,我们在 ALLCOLORSALLSHAPES 中有每种颜色和形状的列表,所以在第 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。)让我们举个例子。如果列表的长度是20groupSize参数是8,那么range(0, len(theList), groupSize)将评估为range(0, 20, 8)。这将使i变量在for循环的三次迭代中分别取值0816

在第 160 行的列表切片 theList[i:i + groupSize]创建了添加到result列表中的列表。在每次i0816groupSize8)的迭代中,这个列表切片表达式将是theList[0:8],然后在第二次迭代时是theList[8:16],然后在第三次迭代时是theList[16:24]

请注意,即使在我们的示例中theList的最大索引是19theList[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()函数将在给定boxxboxy参数中的空间上绘制一个图标(具有指定的shapecolor)。每种可能的形状都有不同的 Pygame 绘图函数调用集,因此我们必须有一大堆ifelif语句来区分它们。(这些语句在第 187 到 198 行。)

可以通过调用leftTopCoordsOfBox()函数获得框的左边和顶部边缘的 X 和 Y 坐标。框的宽度和高度都在BOXSIZE常量中设置。然而,许多形状绘制函数调用也使用框的中点和四分点。我们可以计算这个并将其存储在变量quarterhalf中。我们可以很容易地使用代码int(BOXSIZE * 0.25)代替变量quarter,但这样代码变得更易读,因为更容易理解quarter的含义,而不是int(BOXSIZE * 0.25)

这些变量是语法糖的一个例子。语法糖是指我们添加的代码,本来可以用另一种方式编写(可能使用更少的实际代码和变量),但确实使源代码更易于阅读。常量变量是语法糖的一种形式。预先计算一个值并将其存储在变量中是另一种类型的语法糖。(例如,在getRandomizedBoard()函数中,我们可以很容易地将第 140 行和第 141 行的代码合并为一行代码。但将其分开阅读更容易。)我们不需要额外的quarterhalf变量,但是拥有它们使得代码更易于阅读。易于阅读的代码易于调试和将来升级。

    # 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()函数,传递8boxes中的列表。函数返回的列表的列表将存储在名为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循环的每次迭代中,color1color2的值将在第 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 的版本。尝试运行程序,找出错误在哪里,以及程序为什么会那样运行。