Python-PyGame-和树莓派游戏开发教程-二-

132 阅读21分钟

Python、PyGame 和树莓派游戏开发教程(二)

原文:Python, PyGame and Raspberry Pi Game Development

协议:CC BY-NC-SA 4.0

八、放在一起:井字棋

在我们开始研究 PyGame 以及如何创建街机风格的游戏之前,我们应该后退一步,将前几章中介绍的内容放入一个简单的 ASCII 控制台游戏井字棋中,这是一个两人游戏。

规则

对于那些以前没有玩过井字棋的人,这里有一些规则:

在一张纸上画一个有九个正方形的棋盘,人们通常是这样做的,先画两条互相平行的水平线,再画两条互相平行但垂直于水平线的垂直线,就像一个散列符号:#(图 8-1 )。

img/435550_2_En_8_Fig1_HTML.png

图 8-1。

井字棋棋盘的布局

第一个玩家使用令牌 X,第二个玩家使用令牌 o。每个玩家从 X 开始,将他们的令牌放在棋盘上的一个盒子中。一个槽只能接受一个令牌!如图 8-2 所示,当玩家放置的代币形成水平、垂直或对角线三排时,游戏结束。

img/435550_2_En_8_Fig2_HTML.png

图 8-2。

井字棋棋盘上的获胜线

程序布局

该计划将分为以下几个部分:

  • 变量声明和初始化–创建变量并赋予它们初始值

  • 显示一条欢迎信息——简单的文字说明程序做什么以及如何玩游戏

  • 展示公告板

  • 听取玩家的意见——他们希望将棋子放在棋盘的什么位置

  • 测试输入的有效性——不断询问玩家输入是否无效

  • 将棋子放在棋盘上

  • 检查玩家是否赢了——如果他们赢了,显示祝贺信息并结束游戏

  • 如果还有空位可以放代币,跳回“显示牌”

我们将在这个程序中使用 while 循环和 if 语句。while 循环将在仍然有空位或者没有人赢的时候继续玩游戏。if 语句将用于确定我们是否有赢家或者玩家的输入是否有效。

变量

我们需要一个地方来存储程序运行时的数据,并需要决定我们将使用什么变量。表 8-1 显示了哪些变量将被声明以及它们将如何被使用。

表 8-1。

声明的变量

|

可变的

|

使用

| | --- | --- | | | 最初包含字符 1 到 9,这将包含 X 和 O 标记在棋盘上的位置,并将用于在屏幕上绘制棋盘 | | 电流令牌 | 当前令牌,即当前玩家,将包含 X 或 O | | winnington | 其中一个控制变量将用于在所有玩家回合中保持游戏进行。当设置为 X 或 O 时,程序将退出 | | 槽已填充 | 有可能没有人会赢得这场比赛——这有时被称为猫的游戏。在这种情况下,我们需要一种方法来退出 while 循环,如果没有其他移动可以进行的话。每当玩家移动一步,第二个控制变量就会增加 |

游戏

在“pygamebook”文件夹中创建一个名为“ch8”的新文件夹。在“ch8”中创建一个名为“tictactoe.py”的新文件在 Python IDLE 中打开文件,并输入以下文本。我将在进行过程中添加注释来帮助说明代码在做什么。

#!/usr/bin/python
#
# Program:      Tic-Tac-Toe Example
# Author:       Sloan Kelly

头信息很有用,因为它可以快速识别这个程序或脚本的目的是什么,以及是谁写的。

board = ['1', '2', '3', '4', '5', '6', '7', '8', '9']

currentToken = 'X'
winningToken = "
slotsFilled = 0

程序使用的变量被声明和初始化。“board”变量包含一个字符串数组,其中包含符号 1–9。这些将用于两个原因:向玩家显示他们可以输入什么数字,其次允许程序确定某个位置是否被令牌占用。

将“currentToken”设置为第一个玩家的令牌,将“winningToken”和“slotsFilled”分别设置为空字符串(“”)和 0 的默认值。后两个变量用于控制游戏,并确保游戏在没有赢家和棋盘上有空位的情况下继续进行。

print ("Tic-Tac-Toe by Sloan Kelly")
print ("Match three lines vertically, horizontally or diagonally")
print ("X goes first, then O")

将向玩家显示关于该程序的一些基本信息。它让他们知道程序的名字,谁创作的,以及一些基本的游戏规则。

while winningToken == " and slotsFilled < 9:

一个 sentinel while 循环的例子,当没有人赢并且有空位要填补时,保持游戏运行。

    print("\n")
    print("%s|%s|%s" % (board[0], board[1], board[2]))
    print("-+-+-")
    print("%s|%s|%s" % (board[3], board[4], board[5]))
    print("-+-+-")
    print("%s|%s|%s" % (board[6], board[7], board[8]))

向玩家展示棋盘。随着时间的推移,棋盘上的条目将填满 X 和 O,但在第一轮,棋盘包含符号 1 到 9。然后,播放器将输入这个数字,我们必须将它向下转换一位,因为在 Python 中,数组索引从 0 开始,而不是从 1 开始。

此外,不要忘记你的缩进!

    pos = -1
    while (pos == -1):

当玩家选择了一个无效的位置值时,这个 while 循环会将玩家留在循环中。

        pos = int(input("\n%s's turn. Where to? : " % currentToken))
        if pos < 1 or pos > 9:
            pos = -1
            print ("Invalid choice! 1-9 only.")

提示玩家输入,然后通过确保输入介于 1 和 9 之间来验证它。否则,显示一条错误消息,并将“pos”变量设置回–1(无效输入),这将使播放器保持在 while 循环中,直到它们输入正确的值。

        pos = pos – 1

移动“位置”,使其位于“电路板”阵列的 0–8 范围内。

        if board[pos] == 'X' or board[pos] == 'O':
            pos = -1
            print("That spot has already been taken by %s! Try again" % board[pos])

检查棋盘上位置“pos”处的数值是否被玩家获取,如果是,显示警告。

    board[pos] = currentToken
    slotsFilled = slotsFilled + 1

否则,将索引“pos”处的板设置为当前令牌,并增加“slotsFilled”变量。请注意,这两行位于 while 循环之外,因为此时已经验证了“pos”变量。

    row1 = board[0] == currentToken and board[1] == currentToken and board[2] == currentToken
    row2 = board[3] == currentToken and board[4] == currentToken and board[5] == currentToken
    row3 = board[6] == currentToken and board[7] == currentToken and board[8] == currentToken

为了使这个程序更整洁,我将棋盘、列和对角线检查分成了多行代码。第一组决定行的状态。

    col1 = board[0] == currentToken and board[3] == currentToken and board[6] == currentToken
    col2 = board[1] == currentToken and board[4] == currentToken and board[7] == currentToken
    col3 = board[2] == currentToken and board[5] == currentToken and board[8] == currentToken

第二组决定列的状态。

    diag1 = board[0] == currentToken and board[4] == currentToken and board[8] == currentToken
    diag2 = board[2] == currentToken and board[4] == currentToken and board[6] == currentToken

最后一组决定了对角线的状态。

    row = row1 or row2 or row3
    col = col1 or col2 or col3
    diag = diag1 or diag2

这些组被组合成单个变量,以使 if 检查更容易。

    if (row or col or diag):

如果玩家获得了一行或一列或一条对角线,他们就赢了,游戏进入结束游戏状态。

        print("\n")
        print("%s|%s|%s" % (board[0], board[1], board[2]))
        print("-+-+-")
        print("%s|%s|%s" % (board[3], board[4], board[5]))
        print("-+-+-")
        print("%s|%s|%s" % (board[6], board[7], board[8]))

再次显示棋盘,显示获胜的玩家。

        print("Congratulations %s! You won!!" % currentToken)
        winningToken = currentToken

显示“祝贺您!”消息并设置获胜令牌。记住——这是主(顶部)while 循环使用的标记控制变量之一。如果将其设置为非空值,也就是说,我们将其设置为“currentToken”的内容,则主循环结束。

    if currentToken == 'X':
        currentToken = 'O'
    else:
        currentToken = 'X'

如果游戏还在玩,当前的代币需要换成相反的。如果当前令牌是 X,我们交换 O,反之亦然。

if slotsFilled == 9 and winningToken == ":
    print("No one won :( Better luck next time, players!")

我们最后的 if-check 在主循环之外,如果两个玩家都没有赢,它会显示一条消息。

保存并运行

保存并运行程序。如果您想从命令行运行该程序,您需要在终端中找到该文件夹,例如:

$ cd ~
$ cd pygamebook
$ cd ch8

然后输入 chmod 命令以确保程序可以执行:

$ chmod +x tictactoe.py

最后,输入以下内容来运行游戏:

$ ./tictactoe.py

如果你想在空闲状态下运行游戏,按键盘上的 F5 键或者从“运行”菜单中选择“运行模块”。

结论

这不是我们的第一个 2D 图形游戏,但它是我们的第一个游戏!用 Python 写游戏的温和介绍。我们使用了本书前几章提到的结构来构建这个游戏。尽管它们很简单,但是这些小的构建块——变量、循环、条件和容器——可以帮助我们构建复杂的软件。

九、PyGame 基础介绍

PyGame 是一个免费的 Python 框架,它提供了用于编写视频游戏的模块。它建立在简单的 DirectMedia 层库(SDL)之上,该层库提供了对声音和视觉元素的简单访问。

在这一节中,我们将看到如何设置 PyGame,以及在我们未来的程序中会用到的一些元素。Python 语言不包含 PyGame,因此在使用之前必须导入框架。

导入 PyGame 框架

在 Python 中,通过“import”关键字导入模块。要导入 PyGame,您需要在脚本的顶部添加下面一行,在 hash-bang 之后:

import pygame, os, sys
from pygame.locals import *

第一行导入 PyGame 模块及其对象,以及 OS 和系统模块。import 关键字不直接在当前符号表中输入 pygame、os 和 sys 中定义的对象的名称。它只输入模块名。要访问每个模块的元素,我们必须使用模块名,这就是为什么我们必须编写 pygame.locals。第二行说明我们将从 pygame 框架导入常量,就好像它们是在本地定义的一样。在这种情况下,我们不需要给每个常量加上前缀“pygame”“from”关键字是 import 关键字的变体,它允许我们导入模块元素,就好像它们是在我们的(本地)代码库中定义的一样。

正在初始化 PyGame

在使用框架中的任何对象之前,您必须首先初始化它。我们还希望将更新限制在每秒 30 帧,因此我们添加了一个 fpsClock 变量,并将其初始化为每秒 30 帧。

pygame.init()
fpsClock = pygame.time.Clock()
surface = pygame.display.set_mode((800, 600))

第一行初始化 PyGame。第二行创建一个对象的实例,并将该值存储在“fpsClock”中。对象是一个类的实例。我们将在面向对象部分详细讨论这一点。Python 中的一切都是对象,这也是这门语言的魅力所在;但是现在,我们只能说你可以创建你自己的数据类型。这些用户定义的数据类型称为“类”

第三行创建了一个表面,我们可以在上面绘制图像(背景和精灵)。set_mode()方法采用两个参数,以像素为单位指定表面的宽度和高度。在本例中,我们创建了一个 800 × 600 像素的表面。

在我们在屏幕上画画之前清理屏幕是一个好习惯。因此,我们将创建一个包含背景的红色、绿色和蓝色成分的元组,而不是凭空提取数字。屏幕上的像素由红色、绿色和蓝色组合而成。这些比率决定了显示什么颜色。例如,( 0,0,0)是黑色,( 255,255,255)是白色。元组按顺序表示组成颜色的红色、绿色和蓝色组合。所以,(255,0,0)是红色的,(0,0,255)是蓝色的。

background = pygame.Color(100, 149, 237) # cornflower blue

在这个例子中,我选择了矢车菊蓝,因为它不是你经常看到的颜色,所以当窗口出现时,你就知道程序已经工作了。

主循环

一些程序,特别是那些从命令行运行的程序,倾向于执行一系列任务并退出。对于大多数窗口环境程序和游戏来说,这是不正确的。这些程序保持活动状态,直到用户明确退出。在执行过程中,它们执行所谓的主循环。这包含一系列重复执行直到程序结束的语句。主循环是

while True:

这将程序保留在内存中,因为它在条件为“真”时执行循环。因为条件实际上为“真”,所以循环将总是执行。

    surface.fill(background)

在屏幕上画任何东西之前,我们首先清理表面。这将删除之前的内容,让我们重新开始。

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

PyGame 为我们提供了来自窗口管理器的事件:按键、按钮点击和窗口关闭请求。当我们收到窗口关闭请求(“退出”)时,我们将停止 PyGame 并退出应用。在循环过程中会发生许多事件,这些事件保存在一个我们可以迭代的列表中。因此,我们必须检查每个事件,看看它是什么类型,然后采取行动。在我们的基本框架中,我们只检查“退出”事件。

    pygame.display.update()
    fpsClock.tick(30)

pygame.display.update()方法重绘屏幕。当你在屏幕上放置对象时,它会被拉到一个叫做后台缓冲区的内存区域。当调用 update 时,这个后台缓冲区变得可见,并且当前显示数据的缓冲区(前台缓冲区)成为后台缓冲区。这允许平滑移动并减少闪烁。

在“pygamebook”文件夹中创建一个名为“ch9”的文件夹。将本章中已经介绍过的代码保存到一个名为“firstwindow.py”的新文件中。当运行程序时,你应该会看到一个矢车菊蓝色的窗口出现(图 9-1 )。

img/435550_2_En_9_Fig1_HTML.jpg

图 9-1。

显示矢车菊蓝色背景的 PyGame 窗口

单击右上角的“X”按钮关闭窗口。

大多数现代视频卡有两个内存区域;两者都用于向用户显示图像,但是一次只显示一个。这种技术被称为双缓冲,如图 9-2 所示。

img/435550_2_En_9_Fig2_HTML.jpg

图 9-2。

现代图形适配器中的双缓冲

从用户的角度来看,他们看到监视器上可见的项目。但是在幕后,程序正在绘制到后台缓冲区。随着电子手指的轻弹,用户看到了后台缓冲区中的图像。图 9-3 显示了发生的情况。

img/435550_2_En_9_Fig3_HTML.jpg

图 9-3。

前台和后台缓冲区的内容

剧院已经使用这种技术来隐藏场景的变化。当演员们在舞台上时,幕布后面一个新的场景正在上演。当演员的场景结束时,幕布打开,新的场景展现出来。

最后,我们希望将更新限制在每秒 30 帧。为此,我们调用 fpsClock.tick(30)。这确保了我们在游戏中获得一致的时间。这是时钟可以运行的最大值,但不是最小值。您可能会在游戏中执行复杂的计算,这可能会降低帧速率。当你开始编写比本文中介绍的更复杂的游戏时,你需要意识到这一点。

图像和表面

PyGame 使用表面将图像绘制到屏幕上。它使用图像模块从磁盘加载图像文件。然后将它们转换为内部格式并存储在曲面对象中,以备后用。您将为主屏幕创建至少一个曲面对象。这将是你要在其上绘制精灵和其他图像的对象。

我们执行主绘图的表面是后台缓冲区。然后,我们通过调用 update()方法将这个后台缓冲区呈现给屏幕。

创建图像

在大多数情况下,您会希望在第三方产品中创建图像,例如位于 www.gimp.org 的开源 GIMP (GNU 图像处理程序)。GIMP 是与 Photoshop 齐名的专业级图形程序。如果你像我一样,在职业生涯的大部分时间里都在使用 Photoshop,你可能会发现一开始使用 GIMP 有点令人沮丧——这不是应用的错!只要放松,你就能像在 Photoshop 中一样创建图像!任何允许您生成 BMP、PNG 和 JPG 图像的图像创建程序都可以。附录中有一个清单。如果你被图片困扰,这本书的网站( http://sloankelly.net )上有一些(糟糕的)图片可以帮助你。有些图片是通过 GPL (GNU 公共许可证)成为 SpriteLib 的一部分;这意味着这些图像可以免费用于商业和非商业用途。

加载图像

Python 使用表面在屏幕上绘制图像。当你把一幅图像装入内存时,它被放在一个特殊的表面上。例如,加载一个名为“car.png”的图像:

image = pygame.image.load('car.png')

这将把图像加载到内存中,并在“图像”中放置一个对新加载对象的引用

绘制图像

图像绘制在 PyGame 表面上。记得在我们的骨骼游戏中,我们创建了一个用于在屏幕上绘制图像的表面。要绘制图像:

surface.blit(image, (0, 0))

其中“surface”是表面实例,“image”是您要在屏幕上绘制的图像。第二个参数是您希望在屏幕上绘制图像的位置。

屏幕坐标和分辨率

屏幕或监视器是计算机系统的主要输出设备。有两种不同类型的屏幕:阴极射线管(CRT)和液晶显示器(LCD)。后者越来越便宜,因此更受欢迎,还是因为它受欢迎而更便宜?计算机以给定的分辨率向监视器输出图像。分辨率意味着“多少像素?往下多少像素?”物理屏幕分辨率以像素为单位。像素这个词是像素的简称。您的电脑有多种分辨率可供选择,从 320×240 像素到 2560×1600 像素甚至更高。

计算机内部的图形卡与 CPU 一起在显示器上产生图像。对于较新的图形卡,图形处理器单元(GPU)放置在卡上,以提高系统的 3D 功能-通过提供更高的分辨率、特殊效果和更好的帧速率,使游戏更加逼真。

分辨率定义了图像在屏幕上的显示细节。列数(水平轴)和行数(垂直轴)定义了应用可用的像素数。在下面的例子中,显示了 1920×1080 分辨率的屏幕地图。不管你的显示器运行的分辨率是多少,原点在左上角,它的坐标总是(0,0)。参见图 9-4 。

img/435550_2_En_9_Fig4_HTML.png

图 9-4。

1920×1080 显示器的屏幕坐标

精灵表

Sprite sheets 通常用于将角色动画的所有帧保存在一个图像上。这个名字来自于一个精灵,用计算机术语来说,是一个在游戏中用作化身的小图像。图 9-5 显示了一个子画面示例。

img/435550_2_En_9_Fig5_HTML.jpg

图 9-5。

四图像子画面

这个 sprite 表包含四个图像:两个空间入侵者角色的两帧动画。当我们想要在屏幕上绘制字符时,我们选择使用 sprite 工作表的哪个单元格。单元格由精灵的高度和宽度决定。在这种情况下,我们有 32×32 像素的精灵,这意味着我们的精灵表是 64×64 像素,因为我们有 2×2 个精灵。

PyGame 允许我们显示我们想要显示的图像的一部分。因此,举例来说,如果我们只想显示第一个入侵者的第二帧(图像的右上角),我们可以使用这样的行:

surface.blit(image, (0, 0), (32, 0, 32, 32))

第三个参数,即包含四个值的元组,是我们希望在(0,0)处显示的图像区域。元组表示要显示的图像的(x,y,width,height)。

完整列表

本章中程序的完整列表如下所示:

import pygame, os, sys
from pygame.locals import *

pygame.init()
fpsClock = pygame.time.Clock()
surface = pygame.display.set_mode((800, 600))

background = pygame.Color(100, 149, 237) # cornflower blue

while True:
    surface.fill(background)

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

    pygame.display.update()
    fpsClock.tick(30)

结论

本章介绍了我们将在每个游戏中使用的基本循环,以及如何初始化 PyGame。调用 pygame.display 对象上的 set_mode()并返回一个将被用作后台缓冲区的表面,我们的所有图像都将显示在该表面上。

使用 image.load()方法将图像加载到内存中,并使用其 blit()方法在图面上绘制图像。图像可以包含多个形状,这些形状被称为 sprite sheets。通过指定要绘制的框架的矩形,可以绘制 sprite 工作表的单个框架。

十、设计你的游戏

在我们开始编写我们的第一个游戏之前,我们要放慢一点速度。在开始任何一个项目之前,无论是家装、旅行、还是游戏编程,你都应该坐下来规划你想做的事情。

这通常包括采取以下步骤:

  • 初始概念

  • 功能规格

  • 程序设计

  • 编码

  • 试验

  • 循环

编码和测试往往携手并进;您将编写一些代码,然后测试它。从编程的角度来看,这个循环占据了你游戏开发的大部分时间。

初始概念

我们关心的是这里的小项目。在一个更正式的环境中,这需要走到所有相关人员(利益相关者)身边,问他们想从计划中得到什么。在我们的例子中,它是一个电子游戏。你可能会和两三个人一起工作,这部分倾向于集思广益:

  • 这将是一场赛车游戏

  • 带着武器

  • 还有陷阱!可以设陷阱!

这些想法都存储在一个文档中;Google Drive 非常适合这类工作,因为它支持开发人员之间的协作。

一旦你有了所有的需求,你就可以进入功能需求了。请记住,所有这些文档都是“活的”,因为它们可以更改。后续文档/代码需要更新以反映这些更改。

最初的概念不断重复,这些文档形成了所谓的游戏设计文档或简称为 GDD。

样机研究

作为游戏设计初始阶段的一部分,你作为程序员可能会被要求做一些概念验证工作,称为原型。这是一个粗略的草图,描述了游戏的一部分可能会是什么样子。例如,在一个纸牌游戏中,它可能是一个弃牌动画,或者是玩家死亡时的屏幕抖动。

作为原型阶段的一部分而生成的代码并不一定会进入生产阶段,也就是说,你的游戏已经上市了。这种情况有时确实会发生,所以你应该总是尽量让你的代码尽可能的干净。

功能规格

功能规范采用第一阶段收集的需求,并删除所有围绕它们的“无用”语言。他们制定了一系列关于游戏的规则,这些规则可以传递给编码人员去执行。例如,我们的赛车游戏可以发射武器,所以我们的功能需求可能有一个“武器”部分和一个“陷阱”部分。

这些部分进一步将需求分解成程序员可以带走和实现的小块。连同程序设计,这形成了所谓的技术设计文件 (TDD)。请参见以下示例。

武器发射

玩家可以用机枪向另一个玩家 开火。每个玩家每秒钟最多可以射击十次。如果按住枪超过 2 秒,枪就会开始发热。这将启动一个“热量”计数器。在热量计数器达到 5 秒后,枪将不再可以发射。一旦玩家释放了开火按钮,这把枪还需要 5 秒钟才能冷却下来。

这也给了艺术家一些线索;他们必须展示枪加热和冷却的过程。

程序设计

如您所见,每一步都提炼了前一步的信息。程序设计采用功能需求,并将它们分解成程序员可以理解和实现的模块。程序员可能会采用这些模块,并进一步细化它们,制作更小的模块。

这里的总体目标是把一个问题分解,直到你有很多更小、更容易解决的问题。这听起来违反直觉:把一个问题变成多个问题。“泡杯茶”是一个更大的问题。这可以被分解成这样的小问题:

  • 煮水壶

  • 将茶包放入杯中

  • 将开水放入杯中

  • 等等。等等。

从编程的角度来看,你将需求(游戏的基本思想)通过功能需求(玩家如何与游戏互动——游戏环境如何工作)带到程序设计中,在程序设计中,你将这些功能需求和从编程的角度来看需要做的事情联系起来。

现在,这有点像一个第 22 条军规。你需要有经验,知道如何获取这些需求,并弄清楚它们如何成为程序设计。

编码

有时被称为过程中有趣的部分。这是想法开始形成的地方;添加图形,并使用代码在屏幕上移动它们。如果你还记得开篇的章节,程序本身是这样的:

程序=数据+算法

这些数据被称为模型,由算法处理。用于操纵数据的算法称为控制器,用于将项目呈现给显示器的算法是视图的一部分。在面向对象编程中,这种模式被称为模型视图控制器。

在本文中,我们将尽量保持模型、视图和控制器的分离,通过控制器进行通信,如图 10-1 所示。

img/435550_2_En_10_Fig1_HTML.png

图 10-1。

模型视图控制器模式

MVC 模式非常符合我们的“程序=数据+算法”的说法。控制器用代码操纵模型。反过来,视图读取模型的数据来呈现数据。可以有许多不同的视图来呈现不同的数据。

在图 10-2 所示的例子中,我们看到游戏的主视图以全尺寸显示玩家和敌人的精灵,而较小的雷达视图显示玩家和敌人相对于整个游戏世界的大致位置。有一个主视图控制器和一个雷达视图控制器。两个控制器都可以访问相同的数据:玩家和敌人的位置。

img/435550_2_En_10_Fig2_HTML.jpg

图 10-2。

一种显示游戏中相同物体的两种视图的游戏

在主游戏区显示外星人和玩家飞船的代码与它们在雷达视图中的显示方式不同。尽管他们有一个共同点:他们使用相同的数据。玩家的模型也用于显示(在另一个视图中)剩余的生命数、分数以及玩家可以使用的智能炸弹的数量。

虽然在面向对象的章节之前,我们不会被正式介绍 MVC 模式,但是我们将在前面的游戏(砖块和蛇)中使用这种模式的精神。

测试

在开发过程中,您将不断地测试您的代码。每次你实现(编码)一个新的例程,你都要测试它,以确保它做了你为它设定的事情。你怎么知道它在做正确的事情?您拥有“需求”和“功能规范”形式的文档,以确保您期望发生的事情确实发生了。

从编程的角度来看,在编码级别有两种类型的测试:白盒测试和黑盒测试。前者依次检查每个代码步骤,并确保它们按预期执行。后者将每个独立的模块视为一个黑盒。数据进去,结果出来。

循环

正如我之前提到的,游戏设计文档或 GDD 是一个“活的”文档。开发游戏的人会在游戏被创造出来的时候继续玩游戏。这叫做测试。这种游戏测试会导致反馈循环,可能会改变原始设计的元素。你会发现让游戏变得‘有趣’的东西变得很累。通过在开发过程中迭代设计,你可以做一些小的改变来改善你最初的概念。

结论

尽管您不会总是为需求和功能规范创建单独的文档,但是将您的想法写下来仍然是一个好主意。即使它只是提醒你什么需要编程,什么艺术需要创造。如果你仍然不热衷于写作,不要忘记一幅画胜过千言万语。

说到编程,在你把手放在键盘上开始打字之前要三思。你必须问自己的最大问题是,“我希望用我将要写的代码实现什么?”在你开始打字之前,你应该对你的目标有一个清晰的想法。

最后,但肯定不是最不重要的,是测试。永远,永远,永远测试你的代码!

十一、游戏项目:打砖块

在这一章我们将回顾砖块,我们的第一个游戏项目。对于没玩过这个游戏的人来说,你在屏幕下方控制一个球棒(图 11-1 )。你的上方有一堆砖块,你必须用球击碎所有的砖块。

听起来很简单,但是在这个项目中,我们将学习

img/435550_2_En_11_Fig1_HTML.jpg

图 11-1。

主砖块播放屏幕

  • 球员运动

  • 自动(非玩家)移动

  • 冲突检出

  • 显示图像

主要框架

我们将在这一节打下主要框架,让你对整个游戏的结构有一个大致的了解。为了让我们的第一个游戏简单,不会有任何中间画面,比如闪屏、菜单、暂停屏等等。

框架中将有占位符注释,指示在整个项目过程中将添加新行的点。

#!/usr/bin/python
import pygame, os, sys
from pygame.locals import *

pygame.init()
fpsClock = pygame.time.Clock()
mainSurface = pygame.display.set_mode((800, 600))
pygame.display.set_caption('Bricks')

black = pygame.Color(0, 0, 0)

# bat init
# ball init
# brick init

while True:
    mainSurface.fill(black)
    # brick draw
    # bat and ball draw
    # events
    for event in pygame.event.get():

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

    # main game logic
    # collision detection

    pygame.display.update()
    fpsClock.tick(30)

在“pygamebook”文件夹中创建一个名为“bricks”的新文件夹。将文件保存在那里,并将其命名为“bricks.py”。

形象

游戏中使用了三幅图像,都可以从该书网站的参考资料部分下载( http://sloankelly.net )。如果您不想使用这些图像,您可以创建自己的图像。然而,该游戏为每个图像假定了以下尺寸。参见图 11-2 至 11-4 。

img/435550_2_En_11_Fig4_HTML.jpg

图 11-4。

Brick.png 31×16 像素

img/435550_2_En_11_Fig3_HTML.jpg

图 11-3。

Bat.png 55×11 像素

img/435550_2_En_11_Fig2_HTML.jpg

图 11-2。

Ball.png 8×8 像素

移动球棒

用户使用他们的鼠标控制球棒。我们通过忽略鼠标上 y 轴的变化,将移动限制在 x 轴上。球棒也被限制只能在屏幕范围内移动。在比赛过程中,球棒必须留在比赛场地(屏幕)内。

Bat 初始化

在框架中找到以下代码行:

# bat init

在那一行下面,添加几个空行给你一些空间。键入以下内容:

bat = pygame.image.load('bat.png')

我们的 bat 作为一个名为‘bat’的曲面加载到内存中。不需要叫这个,但是把你的变量叫做有意义的东西是有意义的。例如,你也可以称它为“batImage”或“batSprite”。

playerY = 540

我们的玩家的移动被限制在 x 轴上,所以他们在屏幕上总是在 540 像素的高度。这相当接近底部;请记住,随着 y 轴值的增加,您会在屏幕上移动得更远。

batRect = bat.get_rect()

蝙蝠的矩形将在我们以后的碰撞检测计算中使用。

mousex, mousey = (0, playerY)

我们给鼠标坐标一个默认值。注意我们在这里使用了一个元组。我们也可以这样写这一行:

mousex = 0
mousey = playerY

这占了两行,并没有暗示我们的价值观是什么;它们在屏幕上代表蝙蝠在 2D 空间的坐标。

画蝙蝠

每次执行主循环时,我们在一行中清除主表面,它已经包含在主循环中:

mainSurface.fill(black)

这用黑色填充了主表面,这样我们就可以在上面画其他的东西了!向下滚动到这一行:

# bat and ball draw

并在其后添加下面一行:

mainSurface.blit(bat, batRect)

保存并运行游戏。你看到了什么?球棒应该在屏幕的左上方。但是为什么会这样呢?答案就在“batRect”再看一下“batRect”的初始化:

batRect = bat.get_rect()

这将包含球棒的尺寸:

(0,0,55,11)

这意味着图像将在(0,0)处绘制。是时候移动球棒了。

移动球棒

移动球棒分两步完成:

  • 捕获鼠标输入

  • 在新位置绘制蝙蝠图像

向下滚动到标有

# events

将下面的代码改为:

for event in pygame.event.get():
    if event.type == QUIT:
        pygame.quit()
        sys.exit()
    elif event.type == MOUSEMOTION:
        mousex, mousey = event.pos
        if (mousex < 800 - 55):
             batRect.topleft = (mousex, playerY)
        else:
             batRect.topleft = (800 - 55, playerY)

那是许多标签!注意制表符的位置,否则你的代码将不起作用。

事件

事件是由 Windows 管理器生成的,无论是在 Microsoft Windows、Mac OS 下,还是在 Linux 操作系统下的 X-Windows 管理器,比如在 Raspberry Pi 上运行的那个。应用于当前活动窗口的事件由系统传递给它进行处理。您只需要检查想要对其执行操作的事件。在这个游戏中,我们只对检查这个感兴趣:

  • 用户关闭窗口

  • 移动鼠标的用户

  • 用户点击鼠标按钮(稍后)

退出事件

根据需要,每个事件都作为带有附加参数的事件类型传递。对于 QUIT 事件,没有附加参数。QUIT 只是给应用一个关闭的信号,我们通过退出 PyGame 和程序本身来完成。

鼠标移动事件

当用户移动鼠标时,信息从硬件(鼠标、物理接口、一些控制器芯片)通过一些低级操作系统驱动程序传递到当前活动的应用。在这种情况下,我们的游戏。随之而来的是鼠标的位置以及任何被按下的按钮。与所有事件一样,只有当事件发生时(在这种情况下,鼠标被移动),才会传递此消息。

鼠标移动的事件类型是“MOUSEMOTION ”,有一个名为“pos”的参数,其中包含鼠标的位置。“pos”是一个包含鼠标位置的 x 和 y 坐标的元组。

新的 x 坐标被限制在屏幕的范围内,然后赋给“batRect”变量的“topleft”属性。

保存并运行程序。蝙蝠现在会随着鼠标的移动而移动。如果没有,或者出现错误,请检查您的代码。这可能是一个迷路或丢失的标签。

移动球

移动球完全是用代码完成的,不需要用户输入,除了第一次点击鼠标按钮让事情滚动,如果你原谅这个双关语的话。

球初始化

球的初始化看起来非常类似于球棒的初始化。在代码中找到这一行:

# ball init

在下面添加以下几行:

ball = pygame.image.load('ball.png')
ballRect = ball.get_rect()
ballStartY = 200
ballSpeed = 3
ballServed = False
bx, by = (24, ballStartY)
sx, sy = (ballSpeed, ballSpeed)
ballRect.topleft = (bx, by)

前两行加载图像并捕获它的矩形。接下来的两行设置了起始 y 坐标和速度的默认值。在后面的代码中,变量“ballServed”用于确定球是否被发球。剩下的几行设置了球的初始位置和速度。

向下滚动代码到

# bat and ball draw

添加以下行在屏幕上绘制球:

mainSurface.blit(ball, ballRect)

保存并运行游戏。现在你会在屏幕的左上角看到球。如果您不知道,请对照上面写的代码行检查您的代码。打字错误或错别字是很常见的,即使是经验丰富的程序员也不例外!

球运动

球的移动是通过将球的速度加到当前位置来实现的。这是物理方程式:

速度=距离/时间

我们如何用代码做到这一点?向下滚动到这样一行

# main game logic

计算距离的公式是

距离=速度×时间

因为我们的速率固定为每秒 30 帧,所以我们将每 1/30 秒向当前位置添加一次速度。这意味着 1 秒钟后,我们的球将会运行

30 × 3 = 90 像素

所以,球的实际速度是每秒 90 像素。

就在“主游戏逻辑”注释行之后,添加以下代码并运行游戏:

bx += sx
by += sy
ballRect.topleft = (bx, by)

这里引入了一个新的符号。+=运算符用于将运算符左侧的值与右侧的值相加,并将总和放入运算符左侧的变量中。是 bx = bx + sx 的简称。还有其他简短形式的操作符,如–=(减)、×=(乘)和/=(除),它们遵循我们为+=概述的规则。球现在将缓慢地从屏幕的左上角沿对角线移动到右下角。如果它击中了球棒会怎么样?当它到达屏幕的末端时会发生什么?没什么;球正好穿过球棒,掠过屏幕边缘。

让我们补救这种情况。首先,我们将把球夹在屏幕区域的范围内。我们的屏幕尺寸是 800×600 像素。我们的球的大小是 8×8 像素。我们将使用一些布尔逻辑来确定,从球的位置,如果它击中了边缘。如果是这样,我们将逆转速度。这意味着在下一个循环中,球将向相反的方向移动,如图 11-5 所示。

img/435550_2_En_11_Fig5_HTML.png

图 11-5。

球击中侧壁,显示沿 x 轴方向反转

图 11-5 显示了碰撞的两个阶段:检测和响应。检测——两个物体相撞了吗?反应——我们该怎么办?在这种情况下,我们检测球是否接触到屏幕的外部边缘,我们的反应是将球反射回它来的方向。

检测确定两个物体是否接触过

反应是两个物体碰撞时执行的动作

在焊球位置更新代码后添加一到两行空行,并添加以下内容:

if (by <= 0):
    by = 0
    sy *= -1

球的 y 坐标与 0 进行比较,0 是显示屏上最上面的一行像素。记住屏幕左上方是(0,0),最下方是最大尺寸;在我们的例子中,就是(800,600)。这段代码将确保屏幕最顶端的边界反射球。球只在 y 轴上反射,因为我们碰到了屏幕的垂直边界,在这种情况下是顶部边缘。

对屏幕底部做同样的操作。在这种情况下,我们必须从最底部的数字中减去球的大小。我们的球是 8×8 像素,所以这意味着我们必须减去 8。请记住,当我们在屏幕上绘制图像时,我们是从图像的左上角开始绘制的:

if (by >= 600 - 8):
    by = 600 - 8
    sy *= -1

屏幕的侧面将反映在 x 轴上,而不是 y 轴上:

if (bx <= 0):
    bx = 0
    sx *= -1

这将在左侧边缘反射球(当 x 为 0 时)。最后,当我们在右边时(当 x 是 800–8 或 792 时),我们将进行反思:

if (bx >=800 - 8):
    bx = 800 - 8
    sx *= -1

保存并运行游戏。现在你会看到球在屏幕上反弹。但它仍然会穿过蝙蝠。我们需要在游戏中添加更多的代码,让它与球棒发生碰撞,这样它就会在屏幕上弹起。

球棒和球的碰撞

球棒和球碰撞的工作方式类似于检查屏幕底部的碰撞。我们将使用 rect 类的 colliderect 方法来确定是否发生了冲突。

在您键入的最后一个代码后添加几个空行,并添加

if ballRect.colliderect(batRect):
    by = playerY - 8
    sy *= -1

colliderect 接受单个参数,该参数表示我们要对其进行碰撞检查的矩形。colliderect 方法根据矩形是否相交返回布尔值“真”或“假”。见图 11-6 。

img/435550_2_En_11_Fig6_HTML.png

图 11-6。

显示接触、不接触和相交的碰撞矩形

左上角的图像显示,当两个矩形接触时,colliderect 将返回“True”。右上方的图像显示,当两个矩形不接触时,colliderect 将返回“False”。

下面两张图片显示了球棒和球相交时发生的情况。Colliderect 将返回“True ”,因为两个矩形接触,但在代码中,我们必须向上移动球的位置,使它们不接触。这阻止了任何异常现象的发生;如果你从侧面击球,球会进入球棒内部!通过替换球来接触球棒的顶部,我们绕过了这个问题,这条线:

by = playerY - 8

是解决问题的方法。保存并运行代码,你就可以用球棒把球击回屏幕。

发球

到目前为止,我们只是在比赛开始时发球。我们希望将发球限制在用户点击鼠标左键的时候。首先,如果球没有被发球,我们将停止球的运动。找到线:

# main game logic

您应该会看到下面的这几行:

bx += sx
by += sy
ballRect.topleft = (bx, by)

将这几行改为

if ballServed:
    bx += sx
    by += sy
    ballRect.topleft = (bx, by)

保存并运行游戏将显示球停留在左上角。

要让它动起来,我们必须将“ballServed”改为“True”为了做到这一点,我们必须响应玩家点击鼠标左键。这在代码的事件部分。向上滚动到事件部分,在最后一个“elif”块后添加以下行:

elif event.type == MOUSEBUTTONUP and not ballServed:
    ballServed = True

MOUSEBUTTONUP 测试鼠标上的任何按钮是否被“按下”。所以,真的,右键也可以。我们还测试了 ballServed 已经为“真”的情况。如果球已经发了,我们不需要再发一次。

砖墙

我们快到了!这个谜题的最后一块是玩家必须摧毁的砖墙。如本节开头的屏幕截图所示,我们将在屏幕中央排列砖块。

找到代码中的以下行:

# brick init

添加以下行,列与上一行的井号(#)对齐:

brick = pygame.image.load('brick.png')
bricks = []

再一次,我们加载一个图像,我们将使用它作为砖块。然后,我们创建一个空列表,在其中存储每块砖的位置。

for y in range(5):
    brickY = (y * 24) + 100
    for x in range(10):
        brickX = (x * 31) + 245
        width = brick.get_width()
        height = brick.get_height()
        rect = Rect(brickX, brickY, width, height)
        bricks.append(rect)

我们的砖块排成五排,每排十块。我们将砖块位置存储在“砖块”列表中。我们的砖块位置存储为 Rect 实例,因为这将使以后的碰撞检测更容易。

向下滚动找到这行代码:

# brick draw

在后面添加以下几行:

for b in bricks:
    mainSurface.blit(brick, b)

保存并运行游戏。你现在会看到砖墙。再一次,你会注意到碰撞不起作用,所以球只是穿过墙壁。我们将在最后一节中解决这个问题。

砖块和球碰撞

我们的球棒和球在移动,我们的砖墙在展示。我们在这个项目中的倒数第二个任务是在球击中砖块时摧毁它们。这类似于球击中球棒,除了我们将移走被击中的砖块。幸运的是,PyGame 在 Rect 类上提供了一个名为 collidelist()的方法。

向下滚动源代码并找到

# collision detection

你会记得我们的砖块只是一列长方形。collidelist()方法获取矩形列表,并返回命中的两个矩形的索引。我们将使用球的矩形作为测试的左侧,并将砖块变量作为函数的参数:

brickHitIndex = ballRect.collidelist(bricks)
if brickHitIndex >= 0:
    hb = bricks[brickHitIndex]

捕获与 ballRect 矩形相交的砖块中包含的砖块矩形的索引。通俗地说,就是找出球碰到了哪块砖。如果没有击中砖块,该方法返回–1。所以,我们只对大于或等于零的值感兴趣。请记住,在 Python 中,列表从元素零(0)开始,而不是从元素 1 开始。

    mx = bx + 4
    my = by + 4
    if mx > hb.x + hb.width or mx < hb.x:
        sx *= -1
    else:
        sy *= -1

然后我们计算球的矩形的中点,因为球是一个 8×8 的图像,所以它向内 4 个像素,向下 4 个像素。然后我们用被撞砖块的宽度来测试。如果球在宽度之外,那么球是从侧面被击中的。否则,球会击中砖块的顶部或底部。我们通过改变球的速度来相应地偏转球。

    del (bricks[brickHitIndex])

因为我们击中了砖块,所以我们将它从列表中移除。

保存并运行游戏。当球击中砖块时,砖块将被移除,球将因击中而反弹。那么,点击屏幕底部呢?

出界

当球击中屏幕底部时,它应该被标记为出界。目前,我们还没有做到这一点,球只是从底部反弹。

向下滚动源代码,找到这样一行

# main game logic

您将看到这段代码:

if (by >= 600 - 8):
    by = 600 - 8
    sy *= -1

替换为

if (by >= 600 - 8):
    ballServed = False
    bx, by = (24, ballStartY)
    ballSpeed = 3
    sx, sy = (ballSpeed, ballSpeed)
    ballRect.topleft = (bx, by)

当球击中屏幕底部时,“发球”标志被重置为“假”,表示球没有被发球。因为球还没发,就不更新了。该代码还将球的位置和速度重置为初始值。

保存并运行完整的游戏,点击任何鼠标按钮发球,并使用鼠标移动。

结论

你已经写了你的第一个游戏!这个游戏真正展示了 Python 和 PyGame 的威力,因为像这样的游戏包含以下内容:

  • 鼠标移动

  • 自动球运动

  • 冲突

  • 砖块毁坏

  • 边界检查

这都可以在大约 120 行代码中实现。

现在我们已经有了第一个游戏,我们将花一些时间来学习更多关于 Python 语言的知识。

十二、用户定义的函数

用户定义的函数允许你打包和命名几行代码,并在整个程序中重用这些代码行。你所要做的就是调用你赋予你的函数的名字。

什么是函数?

Python 中的一个函数可以用来执行一个简单的任务,因此它只是一个助记符或给一组行的特殊名称。您还可以选择将值作为参数发送到函数中,或者从函数中返回值。一个函数只能返回一个值,但是这个值可以是一个元组。

函数的格式

下面的简单函数在被调用时显示“Hello world ”:

def sayHello():
    print("Hello, world!")

sayHello()

使用 def 关键字定义函数。该函数由它的名称和括号“(”和“)”内的可选参数组成。

因为它是一个 Python 块,所以第一行以冒号结尾,组成块的行缩进一个制表符。

作为一个卑微的任务/记忆装置

简单来说,函数可以用作记忆或替换你将反复使用的多行代码。例如,如果您想要显示一个框,您可能想要使用如下内容:

def drawBox():
    print("+--------+")
    print("|        |")
    print("+--------+")

drawBox()
print("Between two boxes")
drawBox()

这段代码的输出是

+--------+
|        |
+--------+
Between two boxes
+--------+
|        |
+--------+

当我们想画一个盒子时,我们现在有了一致性。当我们调用 drawBox()时,每个盒子看起来都和其他盒子一样。

函数允许你重复使用代码

这就是函数的力量:它们允许所谓的代码重用。代码重用意味着您可以在应用中多次使用同一个代码块。如果您出于任何原因需要更改该函数,任何调用它的代码都将获得更改后的版本。

函数的另一个目标是让你调用的地方更容易阅读。一个代码块应该执行一个任务,而不是多个任务。写程序的时候,要考虑这些断点应该出现在哪里。这些应该是你的功能。

例如,您被要求从键盘读入温度,并将它们写入文件,计算平均值、最大值和最小值,并将它们存储在单独的文件中。您可以编写名为

  • getTemperatures()

  • 写入温度()

  • calcAverage()

  • calcMinimum()

  • calcMaximum()

  • writeStats()

然后这些将从主程序中以正确的顺序被调用。

发送参数

拥有一个可以在多个地方重复执行的代码块当然很好,但是有一点限制。如果每次调用它的时候都想改变一些值呢?参数(或自变量)可用于为您的函数提供更多信息。例如,您要绘制的框的宽度和高度。考虑以下函数:

def drawBox(width, height):

drawBox()方法有两个参数:一个名为 width,另一个名为 height。这些参数从调用行传递到函数中(稍后会看到)。这些只是我们使用的名字,以便我们可以在函数体中以有意义的方式引用参数。

    if width < 0:
        width = 3

这些框是在基于字符的显示器上绘制的,因此,我们可以拥有的最小宽度是三个字符;这是因为我们在每个角上使用“+”字符,并用“–”表示水平线。

    if height < 3:
        height = 3

我们对身高也有类似的限制。我们的最小高度是三,因为我们必须有两条水平线和至少一条包含“|”、一些空格和“|”的线来表示盒子的垂直线。

    width = width - 2

不管我们的宽度是多少,它都长了两个字符!这是因为每行都以“|”开始和结束。因此,字符数为宽度–2(两个“|”字符)。

    print("+" + "-" * width + "+")

我们的顶行是固定的,因为它包含由“+”表示的角块。我们还使用 Python 方便的字符串算法来生成字符串行;“+”用于将两个字符串连接(相加)在一起,而“*”用于将一个字符串与一个数字相乘,以将一个字符重复一定的次数。

    for y in range(3, height + 1):
        print("|" + " " * width + "|")

for 循环遍历从“3”到高度加 1 的每个值。请记住,范围是从起始值到比您想要的数字小一的值。同样,我们使用字符串算法来生成我们的行。

    print("+" + "-" * width + "+")

我们通过画出盒子的底部来结束这个功能。

要调用该函数,您可以使用函数的名称,然后传入我们想要使用的参数:

drawBox(5, 4)

您必须知道每个参数的用途,这就是为什么将参数命名为易于识别的名称是个好主意。在这个例子中,如果盒子的宽度是 5,高度是 4,那么它的输出将是

+---+
|   |
|   |
+---+

默认参数值

可以指定每个参数的默认值。这意味着如果用户不想指定一个参数的值,他们不必这样做。假设我们想要默认宽度和高度为 3。将函数定义更改为

def drawBox(width = 3, height = 3):

如果我们只是想要一个 3×3 的盒子,我们可以这样做:

drawBox()

这会将默认值分配给宽度和高度。假设我们想要指定宽度而不指定高度。让我们创建一个 5×3 的矩形:

drawBox(5)

默认值必须是传递给函数的最右边的参数。下列函数签名是有效的,因为第一个带有默认值的参数后面的所有参数也被赋予默认值:

def drawBox(width, height = 10)
def drawSprite(sprite, width = 32, height = 32, transparency = 1)

以下函数签名无效:

def drawBox(width = 5, height)
def drawSprite(sprite = None, width, height, transparency = 1)

带有默认值的参数后面的所有参数也必须有默认值!

命名参数

如果我们只是想要一个默认的宽度,但是我们想要指定一个高度呢?这很简单;只需传入您想要为其指定值的参数的名称:

drawBox(height = 10)

这会画出一个 3×10 的盒子。宽度将默认为 3,因为它没有被赋值。这种技术称为命名参数,允许您通过名称指定参数。在其他语言中,可选参数(具有默认值的参数)必须放在参数列表的末尾。在 Python 中,可以使用命名参数来指定全部或部分可选参数。

返回值

函数的主要用途之一是从提供的参数生成新值。让我们先来看一个微不足道的例子,将两个数字相加:

def add(first, second):
    return first + second

print(add(10, 5))

该函数通常用“def”关键字和函数名来定义。该函数有两个参数“第一”和“第二”

组成函数体的唯一一行是

return num1 + num2

“return”关键字获取其右侧的任何值,并将其传递回调用行。在我们的示例中,调用行是以下打印语句:

print(add(10, 5))

“第一个”被赋值为 10,“第二个”被赋值为 5。两者相加后返回。然后“print”关键字显示返回值。因为它是一个整数值,所以这是一个微不足道的任务,它只显示结果:

15

但是我们可以添加的不仅仅是整数值:

print(add('sloan ', 'kelly'))
print(add(3.14, 1.61))
print(add((1,2,3), (4,5,6)))

我们能加在一起的任何东西都可以使用这个功能。我们已经看到,Python 将从函数中返回我们想要的任何东西,这可能取决于传递的参数值是如何确定的。

返回元组

元组可以作为完整的元组返回,也可以作为单独的元素值返回。在下面的示例中,元组被返回并打印到屏幕上:

def getPlayerPosition():
    return (10, 5)
print (getPlayerPosition())

输出是

(10, 5)

我们还可以在调用函数时将元组分解成单独的变量,例如:

def getPlayerPosition():
    return (10, 5)

x, y = getPlayerPosition()

print ("Player x is", x)
print ("Player y is", y)

这将显示

Player x is 10
Player y is 5

访问全局变量

全局变量通常被认为是糟糕的编程实践。

它们会导致代码中的错误或 bug,因为跟踪每个全局变量何时被访问(值被读取)以及每次被更改(值被写入)都需要时间。

函数可以毫无问题地读取全局变量,如下例所示:

num = 5
def printNum():
    print(num)

printNum()

如果我们改变函数内部的值呢?然后会发生什么?

num = 5
def changeNum():
    num = 10

print(num)
changeNum()
print(num)

现在,输出是

5
5

为什么会这样呢?嗯,为了防止不好的事情在你的程序中发生,Python 有一个防故障技术来防止全局值被写入,除非你明确地说它们可以被写入。要在函数中将变量标记为“可写”,请添加 global 和全局变量的名称,如下所示:

num = 5
def changeNum():
    global num
    num = 10

print(num)
changeNum()
print(num)

添加了 global 关键字和全局变量的名称后,对 printNum 中“num”全局变量的任何更改都将被应用。该程序的输出现在将是

5
10

函数的真实示例

函数可以包含自己的变量。这些变量被称为函数的局部变量。它们不能被功能之外的任何东西看到或操纵。变量的这种隐藏被称为变量作用域。我们已经看到,全局变量可以在任何地方访问。对于局部变量,它们只对函数可见,并且只在函数执行时存在。

我们可以为砖块游戏重写一些代码来使用函数。我将把它作为一个练习,让读者将代码的其他部分转换成函数。我们将创建一个函数来加载砖块图像并设置砖块位置。

打开包含“砖块”游戏代码的 Python 文件。现在,您的代码应该有一个类似这样的区域:

# brick init
brick = pygame.image.load('brick.png')
bricks = []
for y in range(5):
    brickY = (y * 24) + 100
    for x in range(10):
        brickX = (x * 31) + 245
        width = brick.get_width()
        height = brick.get_height()
        rect = Rect(brickX, brickY, width, height)
        bricks.append(rect)

拆下管路

brick = pygame.image.load('brick.png')

并替换为

brick = None

将剩余的几行改为

def createBricks(pathToImg, rows, cols):
    global brick

该函数将接受三个参数。第一个是我们将用来画砖的图像文件的路径。第二个和第三个参数是我们想要的砖块的行数和列数。我们的砖块位置存储在名为“砖块”的列表中,图像名为“砖块”我们将在名为 brick 的文件顶部创建一个全局变量。这保持了我们对砖块的印象。

    brick = pygame.image.load(pathToImg)
    bricks = []
    for y in range(rows):
        brickY = (y * 24) + 100
        for x in range(cols):
            brickX = (x * 31) + 245
            width = brick.get_width()
            height = brick.get_height()
            rect = Rect(brickX, brickY, width, height)
            bricks.append(rect)
    return bricks

现在,向下滚动到主循环开始处的这一行之前:

现在将这一行添加到“while True”的正上方:

bricks = createBricks('brick.png', 5, 10)

我们将砖块数据列表直接返回到我们的“砖块”变量中。这意味着我们不需要更早地创建一个变量,也不需要在函数中添加一个全局行。

谨慎使用全局变量!

全局变量可以而且应该避免,我们将在本书中看到如何避免。与其现在教那些技术,把水搅浑,不如让这种违规溜走,享受我们的第一场比赛!

保存并运行游戏。它应该像以前一样工作,但酷的是现在你可以很容易地改变砖的行数和列数,只需改变传递给“createBricks”的参数

结论

在本章中,我们探索了代码重用的第一个 Python 例子:函数。函数允许我们编写执行单一任务的宏程序,例如,显示精灵、保存游戏或设置游戏屏幕。

通过给函数名后括号中列出的形式变量(参数)赋值,可以向函数传递附加信息。每个参数都应该有一个名称来描述它们的用途,例如,“playerData”、“width”、“enemySprite”等。

有时函数并不需要所有的参数,您可以为每个参数添加默认值。如果有多个默认值,而您只想指定一个或两个值,那么在调用函数时也可以指定一个命名参数。

十三、文件输入和输出

能够从磁盘上保存和加载文件是游戏开发的一个重要部分。等级、玩家精灵等资产。,是从存储在磁盘上的文件中加载的。进度会保存到磁盘上,以便玩家可以从上次玩的地方继续游戏。

在这一节中,我们将了解文件输入和输出的基础知识,并介绍一种存储有序数据的方法,如我们在第七章中介绍的字典容器。

要保存和加载数据,您的脚本必须导入“os”(操作系统的缩写)模块来访问磁盘上的文件。

从磁盘读取文件

该程序从磁盘读取程序的源代码,并将内容显示在屏幕上:

import os

f = open('readmyself.py', 'r')
for line in f:
    print(line)

f.close() # ALWAYS close a file that you open

open 关键字的第一个参数是我们想要访问的文件。第二个参数是我们希望访问文件的模式:

  • r '–读取文件的内容

  • w '–将数据写入文件

  • ' a '–将数据追加(添加到现有文件的末尾)到文件中

对于 read,缺省值是“r ”,因此我们可以在这个实例中省略这个参数。最后,这仅适用于文本模式。这意味着如果我们传递一个' \n ',它将被转换为平台特定的行尾。在 UNIX 和 Raspbian 上是' \n ',但在 Windows 上是' \r\n '。

您可以将“b”添加到访问模式参数中(例如,“rb”或“wb”),以指定二进制模式。这种模式通常用于图像或复杂的保存数据。

open 关键字返回一个文件对象。我们可以用它从文件中读取信息,或者写出数据,这取决于我们想做什么。

不要忘记在你打开的任何文件上调用 close()。

在“pygamebook”文件夹中的“ch13”文件夹中,将程序保存为“readmyself.py ”,然后运行它。程序将显示内容,但它会在每行代码之间添加空行:

import os

f = open('readmyself.py', 'r')

for line in f:

    print(line)

f.close()

它们不在文件中,那么它们来自哪里呢?嗯,在磁盘上,每一行都以一个换行符' \n '结束,print 关键字添加了自己的换行符,使得这些空行。

要解决这个问题,您可以添加。rstrip('\n ')到每个打印,就像这样:

print(line.rstrip('\n'))

函数的作用是:返回一个字符串的副本,其中所有指定的字符都已从字符串的末尾删除。默认情况下,这都是空白字符,但在这种情况下,我们只想去掉'换行'字符。

将数据写入文件

将文本写入文件使用 file 对象的 write 方法。下一个程序获取一个高分列表,并将其写入一个文本文件。

players = ['Anna,10000', 'Barney,9000', 'Jane,8000', 'Fred,7000']

该列表包含以逗号分隔的运动员姓名及其分数。

f = open('highscores.txt', 'w')

该文件以“写入”模式打开,因为我们正在向该文件发送数据。该文件的名称可以是您想要的任何名称,但它应该是有意义的名称。甚至不一定要以. txt 结尾。

for p in players:
    f.write(p + '\n')

列表中的所有值都被循环,并且 File 对象的 write 方法被调用,列表项后跟一个“\n”。如果我们不包括这一点,文件将把所有的名字和分数混杂在一行中。

f.close()

你一定要记得在用完文件后把它关上。当我写一个读/写文件时,我总是先写开始和结束行,然后写我想对文件做什么。这意味着我永远不会忘记关闭文件。

在磁盘上找到“highscores.txt”文件,并输入以下命令:

$ more highscores.txt

您应该会看到以下输出:

Anna,10000
Barney,9000
Jane,8000
Fred,7000

虽然这是我们想要的,但是数据的内部结构是错误的。我们通常不会将玩家的名字和他们的分数存储为一个字符串。相反,我们使用某种容器。

向文件中读写容器

有两种方法可以将复杂数据读写到文件中。将举例说明的第一种方法是手动编写自己的格式。第二个是使用 JSON 格式来组织我们的数据,以便在文件中维护结构。

将内存中的数据写入文件称为序列化,将数据从文件读回内存称为反序列化。将数据写入磁盘的代码称为序列化程序,从磁盘读取数据的代码称为反序列化程序。我们将研究如何编写我们自己的序列化器和反序列化器,然后使用 Python 提供的 JSON 库来简化复杂数据的读写。

将数据从内存写入文件称为串行化

从文件中读取数据到内存被称为反序列化

通常,当您拥有专有的数据结构或格式时,或者如果您想要混淆(扰乱和混淆)您正在存储的内容以掩饰您正在做的事情,防止潜在的黑客攻击您的游戏时,您将编写自己的序列化方法。

编写自己的序列化程序

玩家和他们的分数是相关的,但是不应该一起存储在一个字符串中。相反,高分表将是一个字典,包含玩家的姓名(键)和他们的分数(值):

players = { 'Anna': 10000, 'Barney': 9000, 'Jane': 8000, 'Fred': 7000 }

我们可以使用“for”关键字遍历字典中的值,并依次获得每个元素的键。有了密钥,我们可以像这样解锁值:

for p in players:
    print(p, players[p])

这将显示以下(几乎熟悉的)输出:

Anna 10000
Barney 9000
Jane 8000
Fred 7000

创建一个名为“serializer.py”的新程序,并输入以下代码:

def serialize(fileName, players):
    f = open(fileName, 'w')

    for p in players:
        f.write(p + ',' + str(players[p]) + '\n')

    f.close()

序列化方法有两个参数。第一个是高分表将被写入的文件的名称,第二个是包含球员姓名和分数的字典。将分数包装在 str()函数中会将值转换为字符串,这样我们就可以使用字符串串联(将两个或更多的字符串加在一起)。

players = { 'Anna': 10000, 'Barney': 9000, 'Jane': 8000, 'Fred': 7000 }
serialize('highscores.txt', players)

“玩家”字典是在调用 serialize 函数的上面创建的——也不需要在函数中添加“global ”,因为代码不会改变“玩家”字典,我们将它作为参数传递。

这为我们提供了之前的格式,因为相同的信息被写入文件:

Anna,10000
Barney,9000
Jane,8000
Fred,7000

现在,我们如何从文件中将数据读回内存呢?

编写自己的反序列化程序

反序列化有一个转折,因为数据是字符串格式的——我们毕竟是在写入字符串文件——并且名称和分数由逗号(,)分隔。逗号分隔的值非常常见,有一个名为“split()”的函数可以更容易地分隔字符串值。拆分字符串会返回字符串数组:

‘我的,弦,这里’会分裂成[‘我的’,‘弦’,‘这里’]

为了确保我们的分数存储在正确的数据类型中,使用了“int()”函数。将所有这些放在一起,我们的反序列化函数如下所示:

def deserialize(fileName, players):
    f = open(fileName, 'r')

    for entry in f:
        split = entry.split(',')
        name = split[0]
        score = int(split[1])

        players[name] = score

该函数有两个参数;第一个是包含高分数据的文件名,第二个是玩家的字典。

从文件中读入每一行,并使用逗号(,)作为分隔符调用 split()函数。这将把数值分成球员姓名和分数。向字典中添加一个条目,其中名称是键,整数版本的分数是值。

players = { }
deserialize('highscores.txt', players)
print(players)

“玩家”变量被设置为空白字典。调用函数并显示内容:

{'Anna': 10000, 'Barney': 9000, 'Jane': 8000, 'Fred': 7000}

数据

JSON 代表 JavaScript 对象符号,是系统序列化和反序列化数据以便存储或通过网络传输的常用方法。JSON 对象的格式非常类似于 Python 字典的格式。事实上,它们几乎完全相同。这是格式化为 JSON 字符串的高分表:

{"Anna": 10000, "Barney": 9000, "Jane": 8000, "Fred": 7000}

怪异,对吧!?

Python 提供了“json”模块,通过“json”对象的“dump()”和“load()”方法使读取和写入 JSON 对象变得更加容易。

要使用 JSON,您必须将下面一行添加到程序的顶部,同时导入其余的内容:

import json

JSON 序列化

JSON 序列化在一行中完成。回顾一下之前的 high score 序列化程序,我们可以重写“serialize()”函数:

import json

def serialize(fileName, players):
    f = open(fileName, 'w')
    json.dump(players, f)
    f.close()

我们不必写出自己的格式,而是让“json”对象来完成繁重的工作。“dump()”方法将对象作为 JSON 格式的字符串写出到文件“f”中,不管它是什么。

players = { 'Anna': 10000, 'Barney': 9000, 'Jane': 8000, 'Fred': 7000 }
serialize('jsonhiscore.txt', players)

调用“serialize()”方法的部分不会更改;它仍然传入两个值,但是这次我改变了文件的位置。方便的东西功能!

要查看文件的内容:

$ more jsonhiscore.txt

这将显示以下内容:

{"Anna": 10000, "Barney": 9000, "Jane": 8000, "Fred": 7000}

JSON 反串行化器

' deserialize()'函数将略有变化,因为我们将返回' player '字典,所以我们不需要将它作为参数传入。“deserialize()”方法程序如下所示:

import json

def deserialize(fileName):
    f = open(fileName, 'r')
    players = json.load(f)
    f.close()

    return players

“json”对象上的“load()”方法在文件句柄中被调用。该函数获取文件的字符串内容,并构建适当的 Python 数据结构。这个函数的输出存储在变量“players”中,并返回给调用者。

players = deserialize('jsonhiscore.txt')
print (players)

在函数调用站点,我们可以看到“deserialize()”方法丢失了一个参数,但获得了一个返回值。返回值是一个字典,由“print()”的输出来演示:

{'Anna': 10000, 'Barney': 9000, 'Jane': 8000, 'Fred': 7000}

处理错误

文件访问有时可能是一个棘手的行为,因为文件可能会被系统锁定(病毒检查程序),或者您期望的文件可能不存在。为了解决这个问题,你可以使用结构化错误处理(简称 SEH)。你的程序不会崩溃,但是你应该优雅地处理这个事件。

在“ch13”文件夹中创建一个名为“filenotfound.py”的新程序。它演示了一个可以用来确定文件是否存在的函数。该函数尝试读取文件。如果成功,函数返回 True,否则返回 False:

import os

def fileExists(fileName):
    try:
        f = open(fileName, 'r')
        f.close()
        return True
    except IOError:
        return False

我们想要“尝试”执行的代码被放在“try”块中。如果出现问题,就会运行“except”中的代码。“try”块中的代码一旦遇到问题就会停止,因此,如果您有大量的处理,其中一些代码可能无法执行,因此最好使“try”块尽可能短。

print (fileExists('filenotfound.py'))
print (fileExists('this-does-not-exist.txt'))

这个程序的输出是

True
False

结论

现在,您应该了解如何读取和写入文件。完成后记得关闭文件。不要让文件打开超过必要的时间;打开它,做你需要做的,然后尽快关闭它。

序列化是将内存中变量的内容写入磁盘上文件的过程。写入磁盘的代码被称为串行化器。反序列化是读取磁盘上文件的内容并从中构造内存中对象的过程。从磁盘读取数据的代码被称为解串器

您可以编写自己的序列化/反序列化方法,但是使用像 JSON 这样的预定格式来执行这些操作通常更容易。

磁盘访问有时容易出错,因为您调用的是操作系统。有时文件可能正在使用中,您将无法访问它。确保使用结构化错误处理或简称 SEH 来安全地访问文件。

十四、面向对象编程简介

到目前为止,我们一直将 Python 作为结构化语言使用。每行一个接一个地执行。如果我们想重用代码,我们就创建函数。还有一种编程方式叫做面向对象编程。在面向对象编程中,我们创建小对象,这些小对象不仅保存我们的数据,还将操作(我们想用这些数据做的事情)与数据本身组合在一起。面向对象编程(简称 OOP)的主要特点是

  • 包装

  • 抽象

  • 遗产

  • 多态性

接下来的两章将涵盖 OOP 的基础知识,以及如何在你的游戏中使用它。在这一章中,我们将使用许多新术语。这是对主题的简要概述,所以不要觉得必须快速浏览一遍,请慢慢来。

类别和对象

“类”是一个抽象事物的定义。“类”定义了可以对“实例”的数据(属性)采取的方法(动作)类定义可以和 Python 游戏的其他部分写在同一个文件中。然而,更常见的是将类放在它们自己的文件中。

存储在文件中的函数和类定义被称为模块。我们之前已经使用模块将额外的功能导入到我们的游戏中,例如 pygame、os 和 sys。

一个类的“实例”被称为“对象”用户定义类的“实例”很像“5”是一个整数的实例,或者“Hello,World”是一个字符串的实例。“integer”和“string”都是抽象概念,“5”和“Hello,World”分别是它们的实例。

OOP 允许你把你的程序分割成独立的包,就像我们对函数所做的那样,但是所有与类相关的数据和代码都存储在一起。

包装

封装是关于数据隐私的。类的内容——它的状态——是私有的,只有类内部的代码可以访问。

包含在类中的数据被称为私有字段。字段是变量,只能由拥有它们的类直接更改和读取。

字段也可以被暴露,尽管在 Java、C#和 C++这样的语言中,这通常是不被允许的。相反,内部字段隐藏在名为getter(用于获取数据)和setter的方法后面,用于给字段赋值。在任一情况下,字段也被称为属性

向其他人公开的函数称为公共方法。这些允许外部代码与类交互。

抽象

除了封装,你还想让你的类尽可能简单。你不希望使用它的人不得不做一些复杂的步骤,或者太了解你的类的内部工作来使用它。

这就是抽象的由来。要打开游戏控制台并开始玩游戏,请按下电源按钮。这是一个简单的界面——按钮——它执行许多步骤:执行一个称为 POST(开机自检)的自检,从 BIOS 加载代码,然后启动操作系统。你所要做的就是按一个按钮。

遗产

有时你会开始编写一个类,并意识到它从另一个类复制了相当多的代码。事实上,大部分代码与另一个类相同。如果有办法共享代码就好了。有!这叫做继承,它允许一个类从另一个类派生。这样,您只需编写从基类更改而来的特定代码。说到这里,一个父类被称为基类,一个使用另一个作为基础的类被称为子类派生类

多态性

多态来自希腊语,意思是多种形状。在 OOP 中,有时需要改变子类。多态性可以与继承携手并进。例如,我们可能有一个形状类,圆形、正方形和三角形都是从它派生出来的。Shape 类有一个 draw() 方法,其他类实现在屏幕上绘制不同的形状。

为什么要用 OOP?

OOP 允许我们创建代码

  • 数据隐藏

  • 可重复使用的

  • 更容易分别编码和测试

数据隐藏

信息存储在类中,而不是在程序中传递数据,或者更糟的是拥有全局数据。这些类中保存的数据只能通过该类公开的方法来访问。这些方法组成了接口,也就是你的游戏中其他代码是如何访问这个类的。

可重复使用的

就像函数一样,类可以被多个游戏重用。经过多年的编程,你可以建立一个相当大的类库。这些类中的每一个都可以在后续项目中使用。

更容易分别编码和测试

在一个较大的项目中,工作量可以在开发人员之间分配。随着工作量的划分,程序员可以编写类,并独立于游戏的其他部分测试它们。通过分别编写和测试这些类,您增加了可重用性的机会,因为这些类不相互依赖,可以独立工作。

球课

让我们以一个我们以前见过的物体为例:一个球。球可以用它的大小、形状和颜色来描述。这些是它的属性。在游戏世界中,我们不能对球做太多事情,但我们能做的是更新它的位置,检查碰撞,并在屏幕上绘制它。这些动作被称为方法。

在“pygamebook”中创建一个名为“ch14”的新文件夹将“砖块”项目中的“ball.png”图像复制到该文件夹中。在文件夹内创建一个名为“BallClass.py”的新文件。将以下几行添加到文件的顶部,告诉 shell 在哪里可以找到 Python 可执行文件,以及我们将需要哪些模块:

#!/usr/bin/python
import pygame, os, sys
from pygame.locals import *

在 Python 中,我们会这样描述 ball 类:

class Ball:

使用 class 关键字定义一个类。你必须给你的类一个名字。简短而有意义的东西是完美的,但要避免复数。如果你有一个项目的集合(比如球),使用 BallCollection 而不是 balls 作为类的名称。

    x = 0
    y = 200
    speed = (4, 4)
    img = pygame.image.load('ball.png')

这些变量被称为“成员字段”,它们是基于每个对象存储的。这意味着每个对象为每个字段获得一个单独的内存位。在我们的 Ball 类中,我们有四个这样的成员字段:一个用于 x 和 y 平面上的坐标、球的速度,一个用于球的图像。

    def update(self, gameTime):
        pass

方法被定义为带有 def 关键字、方法/函数名和参数列表的函数。主要区别是使用“self”关键字作为参数列表的第一个条目。

前面我提到过成员字段是针对每个对象的。使用“self”关键字是因为 Python 传入了对用于该操作的对象的引用。尽管每个对象的数据不同,但代码却是一样的。它由该类的所有实例共享。这意味着 ball 类的所有实例都使用更新 Ball 的同一段代码。

即使没有其他参数,也必须始终将“self”关键字作为方法参数列表中的第一个参数。

类方法的参数列表中的第一个参数总是“self”

这里有一个新的关键字,这不是 OOP 的一部分,但在这个例子中是至关重要的。我们制作了一个有效的存根。这意味着我们班做的不多。这些方法都不执行任何合理的操作,但是因为 Python 不能有空块,所以我们必须使用' pass '关键字。这在 C 风格的语言中相当于“{ }”。

    def hasHitBrick(self, bricks):
        return False

如果球击中了砖块,这个方法将返回 true。在我们的存根代码中,我们总是返回 False。

    def hasHitBat(self, bat):
        return False

我们测试球是否击中球棒的存根方法:

    def draw(self, gameTime, surface):
        surface.blit(self.img, (self.x, self. y))

这不是一个存根,因为我们确切地知道这将如何实现。我们使用主表面将图像传送到屏幕上正确的 x 和 y 坐标上。要访问对象的成员字段,我们必须使用“self”关键字。属于当前对象的属性和方法通过“self”后跟一个点(“.”来访问)后跟属性或方法。当调用方法时,不要传入“self”,Python 会为您处理。“self”只放在方法声明的参数列表中。

if __name__ == '__main__':

Python 知道每个模块的名称——记住,包含函数和/或类定义的 Python 文件是一个模块——它正在运行,因为它是不带'的文件名。py '扩展名。

使用以下方法之一执行 Python 脚本时:

$ ./myprogram.py
$ python3 myprogram.py

入口文件有一个特殊的名称,因此入口点文件的名称不是“myprogram”,而是“main”。我们可以利用这一点,因为这意味着我们可以将类放在单独的文件中;根据需要导入它们;更重要的是,单独测试它们。

这就是 OOP 的美妙之处:你可以用小对象,孤立地测试它们,然后将它们组合成一个更大的程序。

简单地说,这个“if”语句检查这是否是我们程序的主入口点,如果是,它将运行下面的代码块。如果不是,下面的代码块将被忽略。当我们在其他程序中使用' Ball '类时,我们不必删除这段代码,因为它会被忽略。

    pygame.init()
    fpsClock = pygame.time.Clock()
    surface = pygame.display.set_mode((800, 600))

创建类的实例

这是我们几乎标准的 PyGame 初始化代码。我们初始化 PyGame 并创建一个时钟来将我们的游戏固定在每秒 30 帧。我们创建一个 800×600 像素的表面。

    ball = Ball()

要创建一个类的实例,这就是所需要的:将该类的一个新实例分配给一个名称,就像将一个数字分配给一个名称一样。主要的区别在于赋值语句末尾的括号。这允许将参数传递给一个称为构造函数的特殊方法。稍后我们会看到 Python 中的构造函数是什么样子的。

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

我们使用了与 Bricks 程序中相同的代码来确保我们监听系统事件,尤其是当那些事件告诉我们关闭窗口时。

通过调用 ball 对象的“update()”方法来更新球的位置。此方法的实现将编码如下:请记住,目前它只包含“通过”:

        ball.update(fpsClock)

我们的显示更新从这一行开始:

        surface.fill((0, 0, 0))

清除屏幕以便绘图。我们在这里不用费心创建颜色,只要传入一个表示红色、绿色和蓝色组件的元组(全零是黑色)对于我们的测试代码就足够了。

        ball.draw(fpsClock, surface)

在这一行中,我们在前面几行创建的球对象上调用 draw()方法。尽管方法签名有三个参数(self、gameTime 和 surface),但我们没有显式地传入“self”这是作为 ball 类的“Ball”实例在我的 Python 中传递的。

        pygame.display.update()
        fpsClock.tick(30)

最后,我们更新显示,将后台缓冲区翻转到前台缓冲区,反之亦然。我们也滴答时钟以确保稳定的每秒 30 帧。

Ball update()方法

当我们运行程序时,它不会做太多;事实上,它只是将球画在游戏屏幕的左上角。返回到球的 update()方法,并将其更改为如下所示:

    def update(self, gameTime):
        sx = self.speed[0]
        sy = self.speed[1]

我们不能直接给元组赋值,所以我们将把值复制到局部变量中;它也节省了我们打字的时间。我们可以稍后重新分配元组。

        self.x += sx
        self.y += sy

        if (self.y <= 0):
            self.y = 0
            sy = sy * -1
        if (self.y >= 600 - 8):
            self.y = 600 - 8
            sy = sy * -1
        if (self.x <= 0):
            self.x = 0
            sx = sx * -1
        if (self.x >=800 - 8):
            self.x = 800 - 8
            sx = sx * -1

        self.speed = (sx, sy)

对“sx”和“sy”的任何更改都将被重新分配到“速度”成员字段。

保存并运行程序。你应该看到球在屏幕上弹跳。

构造器

构造函数是一种特殊的方法,在实例化对象时调用。该方法不是使用带有对象、点和方法名的传统调用方法来调用的。当你创建球的时候,你实际上一直在调用构造函数:

ball = Ball()

虽然您没有显式创建构造函数,但是 Python 会为您创建一个。它不包含任何代码,看起来像这样(永远不要这样做,不值得;让 Python 在幕后为您创建一个即可):

def __init__(self):
    pass

名称前后的双下划线,如 init,是 Python 使用的特殊方法名称。当你想做一些不同于默认行为的事情时,你可以用你自己的方法覆盖默认方法。Python 将这些名称描述为“神奇的”,因此你永远不应该发明自己的名称,而应该只在文档中使用它们。比如当我们想创建自己的构造函数时。

在 Python 中,构造函数方法被称为 init。它至少接受一个参数,即“self”关键字。在我们的球类中,我们将创建自己的构造函数。从类中删除所有这些行:

x = 0
y = 24
speed = (4, 4)
img = pygame.image.load('ball.png')

替换为

    def __init__(self, x, y, speed, imgPath):
        self.x = x
        self.y = y
        self.speed = speed
        self.img = pygame.image.load(imgPath)

请注意,我们必须添加“自我”当我们读取或写入值到成员字段时。当我们在构造函数中时也是如此。向下滚动源代码到球初始化行,并将其更改为

    ball = Ball(0, 200, (4, 4), 'ball.png')

这将把用于球图形的起始坐标、速度和图像文件传递给所创建的球实例。与函数一样,将值传递给构造函数的能力非常强大,并且允许在许多情况下使用您的对象。

固体

这一切意味着什么?在面向对象语言中,我们创建了一个类来表示我们的球。我们不关心这个类内部发生了什么,只要它做我们期望它做的事情。虽然我们将自己编写本书中的类,但我们可以将这项工作外包给其他开发人员,给他们一个规范或接口来编写代码。例如,所有动作对象都必须有一个接受 FPS 时钟的 update()方法。

类描述了分别描述和执行抽象数据结构的动作的属性和方法。有一个首字母缩略词描述了对象设计的五个原则。对于我们的游戏,我们将努力坚持这些原则:

  • 单一责任

  • 开闭原理

  • 利斯科夫替代

  • 界面分离

  • 依赖性倒置

这些的首字母拼成实心的。虽然在你所有的游戏中使用这些技术并不重要,但是你应该努力使你的类在某种程度上遵守下面几节列出的原则。如果你愿意,你可以跳过这一步,直接进入结论部分。

单一责任

每个类都应该有一个单独的职责,并且这个职责应该包含在类中。换句话说,您有一个球类,它的功能应该包装在该类中。你不应该在同一个类中实现额外的功能,比如 Bat。为每个项目创建一个单独的类。如果你有很多空间入侵者,你只需要创建一个入侵者类,但是你可以创建一个 InvaderCollection 类来包含所有的入侵者。

开闭原理

你的类应该被彻底的测试(提示:name ==' main '),并且应该被关闭以防止进一步的扩展。进去修复错误是可以的,但是你现有的类不应该增加额外的功能,因为那会引入新的错误。您可以通过两种方式之一实现这一点:扩展或组合。

使用扩展,您可以扩展基类并更改方法的现有功能。通过组合,您可以将旧类封装在新类中,并使用相同的接口来改变调用者与内部类的交互方式。类接口就是可以在类上执行的方法(动作)的列表。

利斯科夫替代

这是迄今为止所有坚实的原则中最棘手的。这个原则背后的思想是,当扩展一个类时,子类的行为应该和它所扩展的类没有什么不同。这也称为类的可替代性。

界面分离

接口分离意味着您应该针对接口而不是实现进行编码。在其他 OOP 语言中有其他方法可以实现这一点,但是 Python 使用了一种叫做 Duck Typing 的东西。

在某些编程语言中,如 Java、C#和 C++,对象的类型用于确定它是否合适。然而,在 Python 中,适用性是由方法或属性的存在而不是对象的类型决定的。

如果它走路像鸭子,叫声像鸭子,那它就是鸭子

Python 会尝试调用具有相同名称和参数的对象上的方法,即使它们不是同一个对象。以这个示例程序为例。我们创建两个类:Duck 和 Person。每个类都有一个名为 Quack()的方法。观察 makeItQuack()函数中发生的情况。传递的参数调用它的 Quack()方法

class Duck:
    def Quack(self):
        print ("Duck quack!")

class Person:
    def Quack(self):
        print ("Person quack!")

def makeItQuack(duck):
    duck.Quack()

duck = Duck()
person = Person()

makeItQuack(duck)
makeItQuack(person)

当我们创建 add()函数来把两个东西加在一起时,我们以前见过鸭子打字;整数、实数、字符串和元组都可以使用,因为它们都可以使用加号('+')运算符相加。

依赖性倒置

最后是依赖倒置。依赖倒置是一种解耦形式,其中较高级别的模块(类)不应该依赖于较低级别的模块(类)。相反,它们都应该依赖于抽象。第二,抽象不应该依赖于细节。细节应该依赖于抽象。让我们创建一个例子来更好地说明这一点。

class Alien(object):
    def __init__(self):
        self.x = 0
        self.y = 0

    def update(self):
        self.x = self.x + 5

    def draw(self):
        print("%d, %d" % (self.x, self.y))
alien1 = Alien()
alien1.update()
alien1.draw()

Alien 类打破了开放/封闭原则,因为它对扩展是封闭的;如果我们想要一个对角线移动的外星人,我们必须创造一个新的职业。我们需要的是另一个类来计算外星人的新位置,就像这样:

class Strafe(object):
    def update(self, obj):
        obj.x = obj.x + 5

我们有一个单独的类来表示游戏中的每个外星人是如何在屏幕上移动的。这些类可以在创建外来对象时传递给它。假设我们想对角移动一个外星人:

class Diagonal(object):
    def update(self, alien):
        obj.x = obj.x + 5
        obj.y = obj.y + 5

移动类钢鞭和对角线不需要知道他们正在移动什么,只要他们有称为“x”和“y”的字段。类似地,外星人类不需要知道钢鞭和对角线类做什么,只要他们有 update()方法。

class Alien(object):
    def __init__(self, movement):
        self.x = 0
        self.y = 0
        self.movement = movement

    def update(self):
        self.movement.update(self)

    def draw(self):
        print("%d, %d" % (self.x, self.y))

class Strafe(object):
    def update(self, obj):
        obj.x = obj.x + 5

class Diagonal(object):
    def update(self, obj):
        obj.x = obj.x + 5
        obj.y = obj.y + 5

alien1 = Alien(Strafe())
alien2 = Alien(Diagonal())

alien1.update()
alien1.update()

alien2.update()
alien2.update()

alien1.draw()
alien2.draw()

为每个移动方法创建单独的类似乎有点过分,但这确实意味着在这个例子中,您不必为每个移动方法创建一个新的 alien 类。例如,如果你想添加一个垂直移动,只需添加几行代码就可以了。事实上,移动职业可以从世界另一端的玩家那里获取信息,而外星人职业永远不需要知道。

结论

这是对 OOP 的简短介绍。至此,您应该了解以下内容:

  • 属性是成员字段,包含描述类的数据。

  • 方法是属于一个类的函数,对该类执行操作。

  • 自我是用来指代的。

  • 创建对象实例时,可以使用构造函数来初始化成员字段。

  • Python 使用鸭式打字;当你看到一只像鸭子一样走路、像鸭子一样游泳、像鸭子一样嘎嘎叫的鸟……那就是鸭子。

作为练习,创建一个名为 BatClass 的新空白文件,并实现一个名为 Bat 的类。您可以使用砖块游戏中的代码作为起点。