原文地址:https://realpython.com/asteroids-game-python/
教程的电子文档:下载
模块pygame的官方网址:https://www.pygame.org/docs/index.html
获取源代码: 单击此处获取您将在本教程中使用 Pygame使用Python 构建 Asteroids 游戏的源代码。

目录

  • 演示:Python 中的小行星游戏
  • 项目概况
  • 先决条件
  • 第 1 步:Pygame 设置
    • 蟒蛇项目
    • 游戏代码
  • 第 2 步:输入处理
  • 第 3 步:图像
  • 第 4 步:控制游戏对象
    • 高级行为
    • 游戏对象类
    • 控制速度
  • 第 5 步:飞船
    • 创建一个类
    • 旋转飞船
    • 加速宇宙飞船
    • 在屏幕周围环绕对象
  • 第 6 步:小行星
    • 创建一个类
    • 随机化位置
    • 移动小行星
    • 与宇宙飞船相撞
  • 第 7 步:子弹
    • 创建一个类
    • 射击子弹
    • 包裹子弹
    • 与小行星相撞
  • 第 8 步:分裂小行星
  • 第 9 步:播放声音
  • 第 10 步:结束游戏
  • 结论
  • 下一步

您是否想创建自己的电脑游戏,但又太喜欢 Python 而不想放弃它而从事游戏开发者的职业?有一个解决方案!使用 Pygame 模块,您可以使用惊人的 Python 技能来创建从基本到非常复杂的游戏。下面,您将通过制作 Asteroids 游戏的克隆来学习如何使用 Pygame!

在本教程中,您将学习如何构建一个完整的游戏,包括:

  • 加载图像并在屏幕上显示
  • 处理用户输入以控制游戏
  • 根据游戏逻辑****移动物体
  • 检测物体之间的碰撞
  • 在屏幕上显示文本
  • 播放声音

让我们开始吧!

演示:Python 中的小行星游戏

您将制作的游戏是经典街机游戏Asteroids的克隆版。在其中,您可以控制宇宙飞船并射击小行星。如果你的飞船与小行星相撞,你就输了。如果你击落所有小行星,你就赢了!

项目概况

您的 Python 小行星游戏将以单个宇宙飞船为特色。飞船可以左右旋转,也可以向前加速。当它不加速时,它将继续以它的速度移动。飞船也可以发射子弹。

游戏将使用以下键映射:

钥匙 行动
Right 向右旋转飞船
Left 向左旋转飞船
Up 加速飞船前进
Space 射击
Esc 退出游戏

游戏中还将有六颗大行星。当一颗子弹击中一颗大小行星时,它会分裂成两个中等大小的小行星。当一颗子弹击中一颗中等大小的小行星时,它会分裂成两个小行星。小行星不会分裂,但会被子弹摧毁。

当小行星与飞船相撞时,飞船将被摧毁,游戏以失败告终。当所有小行星都消失时,游戏将以胜利告终!

该项目将分为十个步骤:

  1. 为 Python 项目设置 Pygame
  2. 在游戏中处理输入
  3. 加载图像并在屏幕上显示
  4. 创建具有图像、位置和一些逻辑的游戏对象
  5. 移动宇宙飞船
  6. 移动小行星并检测与宇宙飞船的碰撞
  7. 射击子弹和摧毁小行星
  8. 将小行星分裂成更小的行星
  9. 播放声音
  10. 处理游戏结束

每个步骤都将提供指向所有必要资源的链接。

先决条件

要构建您的 Asteroids 游戏,您需要一些更高级的 Python 元素。您应该已经熟悉语言本身以及类、继承和回调等概念。如果您需要更新有关这些主题的知识,请查看我们的Python 3 中的面向对象编程 (OOP)。

游戏还会使用矢量来表示位置和方向,以及一些矢量操作来移动屏幕上的元素。Pygame 将处理大部分数学运算,本教程将解释所有必要的概念。但是,如果您想了解更多,那么您可以查看Vector Addition。

在pygame的文档,如果你想深入了解一些概念可能是有用的,但你会发现你需要在本教程中知道的一切。

第 1 步:Pygame 设置

在此步骤结束时,您将拥有一个使用 Pygame 的小型 Python 项目。它将显示一个带有标题的窗口,填充为蓝色。这将是您的 Asteroids 游戏的第一步。您不需要任何特定的游戏开发工具。您最喜欢的文本编辑器和命令行就足够了。

蟒蛇项目

要组织您的项目,首先要为其创建一个文件夹:

$ mkdir awesome_pygame_project
$ cd awesome_pygame_project

与任何 Python 项目一样,您还应该为 Asteroids 游戏创建一个虚拟环境。您可以在Python Virtual Environments: A Primer 中阅读有关虚拟环境的更多信息。

完成后,创建一个requirements.txt文件并添加 Pygame 依赖项。对于本项目,建议您使用最新版本,这将使您的 Asteroids 游戏在 Linux 和 macOS 上无缝运行。您的文件应如下所示:

pygame==2.0.0

接下来,安装依赖项:

(venv) $ python -m pip install -r requirements.txt

您可以通过运行以下命令来检查 Pygame 是否安装正确:

(venv) $ python -m pygame.examples.aliens

如果一切顺利,那么您应该会看到一个包含 Pygame Aliens 游戏的窗口。

游戏代码

现在是时候开始处理您自己的代码了!通常,Pygame 程序的结构如下所示:

 initialize_pygame()while True:handle_input()process_game_logic()draw_game_elements()

第 3 行开始一个循环,称为游戏循环。此循环的每次迭代生成游戏的单个帧,通常执行以下操作:

  1. 输入处理: 收集并处理诸如按下按钮、鼠标运动和 VR 控制器位置之类的输入。根据游戏的不同,它可能会导致对象改变它们的位置、创建新对象、请求结束游戏等等。
  2. 游戏逻辑: 这是大多数游戏机制实现的地方。在这里,应用物理规则,检测和处理碰撞,人工智能完成其工作,等等。这部分还负责检查玩家是否赢了或输了游戏。
  3. 绘图: 如果游戏还没有结束,那么这就是将在屏幕上绘制框架的地方。它将包括当前在游戏中并且对玩家可见的所有项目。

Pygame 程序的一般结构并不复杂,您可能可以将其置于基本循环中。但是,考虑到您将来可能会扩展您的 Asteroids 游戏,最好将所有这些操作封装在一个 Python 类中。

创建一个类意味着你需要为你的游戏选择一个名字,但“Asteroids”已经被采用了。“太空摇滚”怎么样?

创建一个space_rocks目录,并在其中创建一个名为game.py. 您将在此处放置 Asteroids 游戏的主要类:SpaceRocks. 该文件应如下所示:

 import pygameclass SpaceRocks:def __init__(self):self._init_pygame()self.screen = pygame.display.set_mode((800, 600))def main_loop(self):while True:self._handle_input()self._process_game_logic()self._draw()def _init_pygame(self):pygame.init()pygame.display.set_caption("Space Rocks")def _handle_input(self):passdef _process_game_logic(self):passdef _draw(self):self.screen.fill((0, 0, 255))pygame.display.flip()

下面是代码中发生的事情,一步一步:

  • 第 1 行 导入Pygame 模块以访问其所有惊人的功能。
  • 第 3 行创建SpaceRocks类。
  • 第 4 行SpaceRocks类的构造函数,它是放置初始化 Pygame 所需的任何方法的完美位置。实际的 Pygame 初始化发生在_init_pygame(). 稍后您将了解有关此方法的更多信息。
  • 第 6 行创建了一个显示表面。Pygame 中的图像由表面表示。这里有一些关于它们的信息:
    • 表面可以相互绘制,使您可以从简单的图片创建复杂的场景。
    • 每个 Pygame 项目都有一个特殊的表面。该表面代表屏幕,并且最终将显示给玩家。所有其他表面都必须在某个时候绘制在这个表面上。否则,它们将不会显示。
    • 要创建显示表面,您的程序使用pygame.display.set_mode(). 您传递给此方法的唯一参数是屏幕的大小,由两个值的元组表示:宽度和高度。在这种情况下,Pygame 将创建一个宽度为800像素、高度为600像素的屏幕。
  • 第 8 行是上面讨论的游戏循环。对于每一帧,它包含相同的三个步骤:
    1. 第 10 行包含输入处理。
    2. 第 11 行包含游戏逻辑。
    3. 第 12 行包含绘图。
  • 第 14 行定义了一个名为 _init_pygame()的方法。这是 Pygame 的一次性初始化发生的地方。该方法做了两件事:
    1. 15行pygame.init()。这一行代码负责设置 Pygame 的惊人功能。每次使用 Pygame 时,都应该pygame.init()在程序开始时调用,以确保框架能够正常工作。
    2. 第 16 行使用pygame.display.set_caption()设置标题.,标题将是您的游戏名称:Space Rocks
  • 第 18 和 21 行定义了_handle_input()_process_game_logic()。它们暂时是空的,但在接下来的部分中,您将添加一些代码以使您的游戏更有趣。
  • 第 24 行定义了_draw(). 为你的游戏创建一个模板,在屏幕上显示内容。它逐步来绘制屏幕的内容,它分两步完成:
    1. 第 25 行使用screen.fill(). 该方法采用具有三个值的元组,代表三种基色:红色、绿色和蓝色。每个颜色值介于0和之间255,代表其强度。在此示例中,元组 (0, 0, 255) 表示颜色将仅由蓝色组成,没有红色或绿色的痕迹。
    2. 第 26 行使用pygame.display.flip()更新屏幕内容。因为您的游戏最终会显示移动对象,所以您将在每一帧调用此方法来更新显示。因此,您需要在每一帧都用颜色填充屏幕,因为该方法将清除前一帧生成的内容。

这可能看起来像很多额外的步骤,但现在您的代码结构良好,并且具有具有描述性名称的方法。下次您需要更改与绘图相关的内容时,您将知道使用_draw(). 添加输入处理,您将修改_handle_input(),依此类推。

注意: 通常,您会在课程开始时将屏幕大小和颜色等变量提取为常量。但是,在几个步骤中,您将用图像替换颜色,并且不会在此方法之外使用屏幕大小。因此,您可以保留这些值。

接下来,在您的space_rocks文件夹中创建一个文件__main__.py。此文件将负责创建游戏的新实例并通过运行main_loop(). 它应该是这样的:

from game import SpaceRocksif __name__ == "__main__":space_rocks = SpaceRocks()space_rocks.main_loop()

您的项目结构现在应如下所示:

awesome_pygame_project/
|
├── space_rocks/
|   ├── __main__.py
|   └── game.py
|
└── requirements.txt

继续运行游戏:

(venv) $ python space_rocks

你会看到一个蓝色背景的窗口:

恭喜,你刚刚创建了一个 Pygame 项目!但是,此时没有退出条件,因此您仍然必须在命令行中使用Ctrl+C来退出它。这就是为什么您接下来将学习输入处理的原因。

第 2 步:输入处理

此时,您拥有了游戏的主循环,可以填充逻辑了。在此步骤结束时,您还将有一个脚手架来开始插入用户控件。

Pygame 中的大多数输入处理发生在事件循环中。在每一帧中,您的程序都可以获得自上一帧以来发生的事件的集合。这包括鼠标移动、按键等。然后,可以一一处理这些事件。在 Pygame 中,获取该集合的方法是pygame.event.get()

您现在需要的事件是pygame.QUIT. 当有人请求程序结束,通过点击它发生关闭或按Alt+F4在Windows和Linux或Cmd+W在MacOS。在space_rocks/game.py中重写修改SpaceRocks._handle_input()

def _handle_input(self):for event in pygame.event.get():if event.type == pygame.QUIT:quit()

继续并测试它。运行游戏并单击角落的小X或使用适当的快捷方式。就像您期望的那样,窗口将关闭。

但是你可以更进一步。最终,您的游戏将仅由键盘控制,而不是鼠标。按自定义键关闭窗口怎么样?

Pygame 中还有其他类型的事件,其中之一是按键事件。它由一个pygame.KEYDOWN常数表示。每个这样的事件都有关于被按下的键的信息存储在event.key属性中。您可以在Pygame 文档中检查不同键的常量。在本例中,要通过按 关闭游戏Esc,您将使用pygame.K_ESCAPE

再次修改_handle_input()方法:

def _handle_input(self):for event in pygame.event.get():if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE):quit()

现在,当您按下Esc 时,您的游戏也会关闭。

您已设法显示一个窗口并正确关闭它。但是窗口仍然充满了单一的颜色。接下来,您将学习如何加载图像并将其显示在屏幕上。

第 3 步:图像

此时,您有一个可以通过按键关闭的游戏窗口。在此步骤结束时,您将在该窗口中显示一个图像。
尽管您可以仅使用彩色矩形和其他简单形状来制作电脑游戏,但使用图像会使其更具吸引力。在计算机游戏开发中,图像通常称为精灵。当然,游戏使用更多类型的资源,如声音、字体、动画等。这些资源统称为assets(资产)

随着游戏的发展,保持适当的结构很重要。因此,首先创建一个名为assets的文件夹,并在其中创建另一个名为sprites文件夹. 这就是您将放置游戏使用的所有精灵的地方。

接下来,下载空间背景的图像并将其放入assets/sprites文件夹中。您可以点击以下链接下载源代码:

获取源代码: 单击此处获取您将在本教程中使用 Pygame使用Python 构建 Asteroids 游戏的源代码。

此外,由于图像将在您的程序中多次加载,因此最好将此功能提取到单独文件中的单独方法中。创建一个名为的文件space_rocks/utils.py,该文件将保留所有可重用的方法。然后实现图片加载:

 from pygame.image import loaddef load_sprite(name, with_alpha=True):path = f"assets/sprites/{name}.png"loaded_sprite = load(path)if with_alpha:return loaded_sprite.convert_alpha()else:return loaded_sprite.convert()

这是发生的事情:

  • 第 1 行导入了一个被调用的方法load(),稍后读取图像将需要该方法。
  • 第 4 行创建一个图像路径,假设它存储在assets/sprites目录中并且它是一个 PNG 文件。这样,您以后只需要提供精灵的名称,用到了f字符串(教程)的功能。
  • 第 5 行 使用load(). 此方法返回一个表面Surface,它是 Pygame 用来表示图像的对象。您可以稍后在屏幕上(或其他表面,如果需要)绘制它。
  • 第 8 行和第 10 行将图像转换为更适合屏幕的格式以加快绘图过程。这是使用convert_alpha()或完成的convert(),具体取决于您是否要使用透明度。

注意: 通常,您可以只convert_alpha()用于所有类型的图像,因为它也可以处理没有透明像素的图像。但是,绘制透明图像比绘制非透明图像要慢一些。

由于计算机游戏都与性能有关,因此您将通过选择正确的图像类型和提高游戏速度来练习优化游戏,即使只是提高一点点。

您的项目结构现在如下所示:

awesome_pygame_project/
|
├── assets/
|   |
│   └── sprites/
│       └── space.png
|
├── space_rocks/
│   ├── __main__.py
│   ├── game.py
│   └── utils.py
|
└── requirements.txt

现在您的程序可以加载图像,是时候将蓝色背景更改为更有趣的内容了。编辑space_rocks/game.py文件:

import pygamefrom utils import load_spriteclass SpaceRocks:def __init__(self):self._init_pygame()self.screen = pygame.display.set_mode((800, 600))self.background = load_sprite("space", False)def main_loop(self):while True:self._handle_input()self._process_game_logic()self._draw()def _init_pygame(self):pygame.init()pygame.display.set_caption("Space Rocks")def _handle_input(self):for event in pygame.event.get():if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE):quit()def _process_game_logic(self):passdef _draw(self):self.screen.blit(self.background, (0, 0))pygame.display.flip()

要在 Pygame 中在另一个表面上显示一个表面,您需要调用要blit()在其上绘图的表面。这个方法有两个参数:

  1. 您要绘制的Surface
  2. 要绘制的点

请记住,在 Pygame 中,坐标系从左上角开始。x 轴从左到右,y 轴从上到下:

如您所见,向上的向量将具有负的 y 坐标。

传递给的坐标blit()作为两个值给出:XY。它们表示操作后曲面左上角所在的点:

如您所见,左上角由 blit 坐标移动以计算正确位置。

在您的情况下,新背景与屏幕大小相同(800×600像素),因此坐标将为(0, 0),代表屏幕的左上角。这样,背景图像将覆盖整个屏幕。

现在运行你的程序,你会看到一个带有背景图像的屏幕:

你的游戏现在有一个非常漂亮的背景图像,但那里还没有发生任何事情。让我们通过添加一些对象来改变这一点。

第 4 步:控制游戏对象

此时,您的程序显示一小块宇宙的背景图像,您将在那里进行 Asteroids 游戏。现在有点空,所以在本节中,您将填写它。您将创建一个表示其他可绘制游戏对象的类,并使用它来显示宇宙飞船和小行星。

高级行为

您已经使用过表面Surface,但 Pygame 还提供了另一个类,Sprite,它旨在作为可见对象的基类。它包含一些有用的方法,但您可能还会遇到一些限制。

一个限制是游戏对象不仅仅是一个精灵。它包含额外的数据,比如它的方向和速度。它还需要更高级的行为,例如射击子弹或播放声音。大多数这些附加信息和行为不是由Sprite类提供的,因此您需要自己添加它。

另一个问题是 Pygame 从左上角开始绘制精灵。在您的游戏中,为了移动和旋转对象而存储对象的中心位置可能更容易。在这种情况下,您必须实现一种方法来根据 Pygame 的要求将该位置转换为左上角。

最后,虽然 Pygame 已经有检测图像之间重叠的方法,但它们可能不适用于检测对象之间的碰撞。可旋转的宇宙飞船或小行星可能不会填满整个图像,而是填满其中的圆形区域。在这种情况下,碰撞应该只考虑那个圆形区域,而不是精灵的整个表面。否则,您可能会得到错误的结果:

在此示例中,精灵发生碰撞,但游戏对象不会发生碰撞。

这实际上是Sprite该类可以提供帮助的地方,因为您可使用pygame.sprite.collide_circle(). 此方法使用以其表面为中心的圆来检测两个精灵之间的碰撞。但是,检测圆的碰撞并不是一个非常复杂的过程,您可以自己实现。

考虑到这些问题,很快就会发现内置的 PygameSprite类需要增强,而不是简单地单独使用。就您的游戏而言,Pygame 精灵提供了一些有用的功能。

为游戏对象实现自定义类可能是个好主意。这应该给你更多的控制权并帮助你理解一些概念,因为你将自己实现它们。

游戏对象类

在本节中,将介绍GameObject类。它将封装所有其他游戏对象的一些通用行为和数据。代表特定对象(如宇宙飞船)的类将从它继承并用它们自己的行为和数据扩展它。如果您想更新类和继承的知识,请查看Python 3 中的面向对象编程 (OOP)。

GameObject类将存储以下数据:

  • position: 2D 屏幕上对象中心的一个点
  • sprite 用于显示对象的图像
  • radius 表示对象位置周围碰撞区域的值
  • velocity 用于移动的值

这是游戏对象的图形表示:

在前面的实例中用load_sprite()方法将sprite装载到表面。radius是指示从物体的中心到碰撞区的边缘的像素数量的整数。但是,position本身和velocity将需要一种新类型:向量

向量类似于元组。在 2D 世界(如游戏中的世界)中,向量由两个值表示,分别表示 x 坐标和 y 坐标。这些坐标可以指向一个位置,但它们也可以表示给定方向上的运动或加速度。向量可以通过相加、相减甚至相乘来快速更新精灵的位置。您可以在二维空间中的向量中阅读有关向量的更多信息。

由于向量在游戏中非常有用,Pygame 已经为它们提供了一个类:Vector2pygame.math模块中。它提供了一些附加功能,例如计算向量之间的距离以及添加或减去向量。这些功能将使您的游戏逻辑更容易实现。

在该space_rocks目录中,创建一个名为models.py. 现在,它将存储GameObject类,但稍后您将添加小行星、子弹和宇宙飞船的类。该文件应如下所示:

 from pygame.math import Vector2class GameObject:def __init__(self, position, sprite, velocity):self.position = Vector2(position)self.sprite = spriteself.radius = sprite.get_width() / 2self.velocity = Vector2(velocity)def draw(self, surface):blit_position = self.position - Vector2(self.radius)surface.blit(self.sprite, blit_position)def move(self):self.position = self.position + self.velocitydef collides_with(self, other_obj):distance = self.position.distance_to(other_obj.position)return distance < self.radius + other_obj.radius

这是一个细分:

  • 第 1 行导入前面提到的Vector2类。
  • 第 3 行创建了GameObject类,您将使用它来表示 Space Rocks 中的所有游戏对象。
  • 第 4 行GameObject类的构造函数。它需要三个参数:
    1. position: 物体的中心
    2. sprite: 用于绘制此对象的图像
    3. velocity: 每帧更新对象的position的速度
  • 第 5 行和第 8 行确保 positionvelocity将始终表示为向量以供将来计算,即使将元组传递给构造函数也是如此。你通过调用Vector2()构造函数来做到这一点。如果给它一个元组,那么它会从中创建一个新的向量。如果给它一个向量,那么它将创建该向量的副本。
  • 第 7 行计算sprite图像radius宽度的一半。在这个程序中,游戏精灵对象将始终是具有透明背景的正方形。你也可以使用图像的高度——它没有区别。
  • 第 10 行定义了draw(),它将sprite对象绘制在作为参数传递的表面上。
  • 第 11 行计算 blitting 图像的正确位置。下面更详细地描述该过程。请注意,Vector2()构造函数接收单个数字而不是元组。在这种情况下,它将对两个参数值使用该数字。所以,Vector2(self.radius)是的等价Vector2((self.radius, self.radius))
  • 第 12 行使用新计算的 blit 位置将对象的精灵放置在给定表面上的正确位置。
  • 第 14 行定义了move(). 它将更新游戏对象的位置。
  • 第 15行将速度与位置相加并得到更新后的位置向量。Pygame 使操作向量变得简单,允许您像数字一样添加它们。
  • 第 17 行定义了collides_with()用于检测碰撞的方法。
  • 第 18 行使用计算两个对象之间的距离Vector2.distance_to()
  • 第 19 行检查该距离是否小于对象半径的总和。如果是这样,物体就会发生碰撞。

请注意,您的游戏对象有一个中心位置,但blit()需要一个左上角。因此,必须通过向量来计算 blit 位置移动对象的实际位置:

该过程发生在draw().

您可以通过添加一艘宇宙飞船和一颗小行星来测试这一点。首先,将飞船和小行星图像复制到assets/sprites.:

您的项目结构应如下所示:

awesome_pygame_project/
|
├── assets/
|   |
│   └── sprites/
│       ├── asteroid.png
│       ├── space.png
│       └── spaceship.png
|
├── space_rocks/
│   ├── __main__.py
│   ├── game.py
│   ├── models.py
│   └── utils.py
|
└── requirements.txt

现在修改space_rocks/game.py文件:

import pygamefrom models import GameObject
from utils import load_spriteclass SpaceRocks:def __init__(self):self._init_pygame()self.screen = pygame.display.set_mode((800, 600))self.background = load_sprite("space", False)self.spaceship = GameObject((400, 300), load_sprite("spaceship"), (0, 0))self.asteroid = GameObject((400, 300), load_sprite("asteroid"), (1, 0))def main_loop(self):while True:self._handle_input()self._process_game_logic()self._draw()def _init_pygame(self):pygame.init()pygame.display.set_caption("Space Rocks")def _handle_input(self):for event in pygame.event.get():if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE):quit()def _process_game_logic(self):self.spaceship.move()self.asteroid.move()def _draw(self):self.screen.blit(self.background, (0, 0))self.spaceship.draw(self.screen)self.asteroid.draw(self.screen)pygame.display.flip()

两个对象都使用坐标放置在屏幕中间(400, 300)。两个对象的位置都将使用_process_game_logic() 更新每一帧,并且将使用_draw() 绘制它们。

运行这个程序,你会看到一颗小行星向右移动,一艘宇宙飞船静止在屏幕中间:

您还可以通过在collides_with()末尾临时添加一行来进行测试:

print("Collides:", self.spaceship.collides_with(self.asteroid))

在命令行中,您会注意到该方法最初打印True,True因为小行星覆盖了飞船。后来,随着小行星进一步向右移动,它开始打印False

控制速度

现在屏幕上有移动的对象,是时候考虑您的游戏在具有不同处理器的不同机器上的表现了。有时它会跑得更快,有时它会跑得更慢。

因此,小行星(以及很快的子弹)将以不同的速度移动,使游戏有时更容易有时更难。那不是你想要的。您想要的是让您的游戏以固定的每秒帧数 (FPS) 运行

幸运的是,Pygame 可以解决这个问题。它提供了一个pygame.time.Clock带有tick()方法的类。此方法将等待足够长的时间以匹配作为参数传递的所需 FPS 值。

继续更新space_rocks/game.py

import pygamefrom models import GameObject
from utils import load_spriteclass SpaceRocks:def __init__(self):self._init_pygame()self.screen = pygame.display.set_mode((800, 600))self.background = load_sprite("space", False)== self.clock = pygame.time.Clock() ==self.spaceship = GameObject((400, 300), load_sprite("spaceship"), (0, 0))self.asteroid = GameObject((400, 300), load_sprite("asteroid"), (1, 0))def main_loop(self):while True:self._handle_input()self._process_game_logic()self._draw()def _init_pygame(self):pygame.init()pygame.display.set_caption("Space Rocks")def _handle_input(self):for event in pygame.event.get():if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE):quit()def _process_game_logic(self):self.spaceship.move()self.asteroid.move()def _draw(self):self.screen.blit(self.background, (0, 0))self.spaceship.draw(self.screen)self.asteroid.draw(self.screen)pygame.display.flip()== self.clock.tick(60) ==

如果您现在运行游戏,那么小行星的移动速度可能与最初不同。但是,您现在可以确定该速度将保持不变,即使在具有超快处理器的计算机上也是如此。那是因为您的游戏将始终以 60 FPS 运行。您还可以试验传递给tick()不同值以查看差异。

您刚刚学习了如何在屏幕上显示和移动对象。现在您可以为您的游戏添加一些更高级的逻辑。

第 5 步:飞船

此时,您应该有一个用于一般可绘制和可移动游戏对象的类。在这一步结束时,您将使用它来创建一个可控的宇宙飞船。

您在上一步中创建的类GameObject包含一些可以被不同游戏对象重用的通用逻辑。但是,每个游戏对象也将实现自己的逻辑。例如,宇宙飞船有望旋转和加速。它也会发射子弹。

创建一个类

宇宙飞船的图像已经在space_rocks/assets您在步骤 4 中添加的目录中。但是,之前它是在主游戏文件中使用的,现在您需要在其中一个模型中加载它。为此,请更新space_rocks/models.py文件中的导入部分:

from pygame.math import Vector2from utils import load_sprite

现在您可以在同一个文件中创建继承自GameObjectSpaceship类:

class Spaceship(GameObject):def __init__(self, position):super().__init__(position, load_sprite("spaceship"), Vector2(0))

在这一点上它没有做很多事情——它只是GameObject用一个特定的图像和零速度调用构造函数。但是,您很快就会添加更多功能。

要使用这个新类,首先需要导入它。space_rocks/game.py像这样更新文件中的导入:

import pygamefrom models import Spaceship
from utils import load_sprite

您可能注意到已经删除对GameObject类的导入。那是因为GameObject用作基类被其他类继承。您不应直接使用它,而应导入代表实际游戏对象的类。

这意味着上一步的小行星将停止工作,但这不是什么大问题。您很快就会添加一个代表小行星的适当类。在那之前,你应该专注于宇宙飞船。

继续编辑SpaceRocks类,使其看起来像这样:

 class SpaceRocks:def __init__(self):self._init_pygame()self.screen = pygame.display.set_mode((800, 600))self.background = load_sprite("space", False)self.clock = pygame.time.Clock()self.spaceship = Spaceship((400, 300))def main_loop(self):while True:self._handle_input()self._process_game_logic()self._draw()def _init_pygame(self):pygame.init()pygame.display.set_caption("Space Rocks")def _handle_input(self):for event in pygame.event.get():if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE):quit()def _process_game_logic(self):self.spaceship.move()def _draw(self):self.screen.blit(self.background, (0, 0))self.spaceship.draw(self.screen)pygame.display.flip()self.clock.tick(60)

发生了两件事:

  1. 在第 7 行中,您用Spaceship类替换了基类GameObject
  2. __init__()_process_game_logic() 中删除了所有引用self.asteroid._draw()

如果你现在运行你的游戏,那么你会在屏幕中间看到一艘宇宙飞船:

这些更改尚未添加任何新行为,但现在您有了一个可以扩展的类。

旋转飞船

默认情况下,飞船面朝上,朝向屏幕顶部。您的玩家应该能够左右旋转它。幸运的是,Pygame 有用于旋转精灵的内置方法,但有一个小问题。

一般来说,图像旋转是一个复杂的过程,需要重新计算新图像中的像素。在重新计算期间,有关原始像素的信息会丢失,并且图像会稍微变形。随着每次旋转,变形变得越来越明显。

因此,将原始精灵存储在Spaceship类中并拥有另一个精灵可能是更好的主意,该精灵将在每次飞船旋转时更新。

对于这种工作方法,您需要知道飞船旋转的角度。这可以通过两种方式完成:

  1. 将角度保持为浮点值并在旋转期间更新它。
  2. 保持表示飞船面向方向的向量并使用该向量计算角度。

两种方式都不错,但在继续之前您需要选择一种方式。由于飞船的位置和速度已经是向量,所以使用另一个向量来表示方向是有意义的。这将使添加向量和稍后更新位置变得更加简单。幸运的是,Vector2类可以很容易地旋转,结果不会变形。

首先,在space_rocks/models.py文件中创建一个UP常量向量。稍后您将使用它作为参考:

UP = Vector2(0, -1)

请记住,Pygame 的 y 轴是从上到下的,因此负值实际上指向上方:

接下来修改Spaceship类:

class Spaceship(GameObject):MANEUVERABILITY = 3

MANEUVERABILITY值决定了你的飞船可以旋转多快。您之前了解到 Pygame 中的向量可以旋转,该值表示您的飞船的方向在每一帧旋转的角度(以度为单位)。使用较大的数字将使飞船旋转得更快,而较小的数字将允许对旋转进行更精细的控制。

接下来,Spaceship通过修改构造函数为类添加一个方向:

def __init__(self, position):# Make a copy of the original UP vectorself.direction = Vector2(UP)super().__init__(position, load_sprite("spaceship"), Vector2(0))

方向向量最初将与UP向量相同。但是,稍后会对其进行修改,因此您需要创建它的副本。

接下来,您需要在Spaceship名为的类中创建一个新方法rotate()

def rotate(self, clockwise=True):sign = 1 if clockwise else -1angle = self.MANEUVERABILITY * signself.direction.rotate_ip(angle)

此方法将通过顺时针或逆时针旋转来改变方向。Vector2类的rotate_ip()方法将它旋转到给定的角度(以度为单位)。在此操作期间,向量的长度不会改变。您可以从使用旋转矩阵的旋转点中了解更多关于 2D 矢量旋转背后的高级数学知识。

剩下的就是更新Spaceship. 为此,您首先需要 import rotozoom,它负责缩放和旋转图像:

from pygame.math import Vector2
from pygame.transform import rotozoomfrom utils import load_sprite

然后,您可以重写类Spaceship中的draw()方法:

 def draw(self, surface):angle = self.direction.angle_to(UP)rotated_surface = rotozoom(self.sprite, angle, 1.0)rotated_surface_size = Vector2(rotated_surface.get_size())blit_position = self.position - rotated_surface_size * 0.5surface.blit(rotated_surface, blit_position)

以下是分步分解:

  • 第 2 行使用类的angle_to()方法Vector2计算一个向量需要旋转的角度,以便与另一个向量指向相同的方向。这使得将宇宙飞船的方向转换为以度为单位的旋转角度变得轻而易举。
  • 第 3 行使用rotozoom() 旋转精灵。它需要原始图像、它应该旋转的角度以及应该应用于精灵的比例。在这种情况下,您不想更改大小,因此将比例保持为1.0
  • 第 4 行和第 5 行使用rotated_surface 的大小重新计算 blit 位置。该过程描述如下。
  • 第 5 行包含rotated_surface_size * 0.5操作。这是你可以用 Pygame 中的向量做的另一件事。当您将一个向量乘以一个数字时,它的所有坐标都将乘以该数字。结果,乘以0.5将返回原始长度一半的向量。
  • 第 6 行使用新计算的 blit 位置将图像放在屏幕上。

请注意,rotozoom()返回一个带有旋转图像的新表面。但是,为了保留原始精灵的所有内容,新图像可能具有不同的大小。在这种情况下,Pygame 将添加一些额外的透明背景:

新图像的大小可能与原始图像的大小显着不同。这就是为什么draw()重新计算 rotated_surface的 blit 位置的原因。请记住,blit()从左上角开始,因此要使旋转图像居中,您还需要将 blit 位置移动图像大小的一半。

现在您需要添加输入处理。但是,事件循环在这里不会完全起作用。事件发生时会被记录下来,但您需要不断检查是否按下了某个键。毕竟,只要你按下Up,飞船就应该加速,而你按下Left或Right时,它应该不断旋转。

您可以为每个键创建一个标志,在按键被按下时设置它,并在它被释放时重置。但是,还有更好的方法。

键盘的当前状态存储在 Pygame 中,可以使用pygame.key.get_pressed(). 它返回一个字典,其中键是键常量(就像pygame.K_ESCAPE你之前使用的那样),值是TrueFalse指示键是否被按下。

知道了这一点,您可以编辑space_rocks/game.py文件并更新类的_handle_input()方法SpaceRocks。您需要用于箭头键的常量是pygame.K_RIGHTpygame.K_LEFT

def _handle_input(self):for event in pygame.event.get():if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE):quit()is_key_pressed = pygame.key.get_pressed()if is_key_pressed[pygame.K_RIGHT]:self.spaceship.rotate(clockwise=True)elif is_key_pressed[pygame.K_LEFT]:self.spaceship.rotate(clockwise=False)

现在,当您按下箭头键时,您的飞船将左右旋转:

如您所见,飞船正确旋转。然而,它仍然不动。你接下来会解决这个问题。

加速宇宙飞船

在本节中,您将为您的飞船添加加速度。请记住,根据 Asteroids 的游戏机制,飞船只能向前移动。

在你的游戏中,当你按下Up 时,飞船的速度会增加。当您松开按键时,飞船将保持其当前速度,但不应再加速。所以为了让它慢下来,你必须把飞船转过来再按Up一次。

这个过程可能看起来有点复杂,所以在你继续之前,这里有一个简短的回顾:

  • direction 是一个向量,描述了飞船指向的位置。
  • velocity 是一个向量,描述飞船每帧移动的位置。
  • ACCELERATION 是一个常数,描述了飞船每帧可以加速的速度。

您可以通过将direction向量乘以ACCELERATION值并将结果添加到当前来计算速度的变化velocity。这仅在引擎开启时发生——即当玩家按下 时Up。飞船的新位置是通过将当前速度与飞船当前位置相加来计算的。无论引擎状态如何,每帧都会发生这种情况。

知道了这一点,您可以将ACCELERATION值添加到Spaceship类中:

class Spaceship(GameObject):MANEUVERABILITY = 3ACCELERATION = 0.25

然后,在Spaceship类中创建accelerate()

def accelerate(self):self.velocity += self.direction * self.ACCELERATION

现在您可以在SpaceRocks_handle_input()添加输入处理。与旋转类似,这将检查键盘的当前状态,而不是按键事件。Up键的常数是pygame.K_UP

def _handle_input(self):for event in pygame.event.get():if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE):quit()is_key_pressed = pygame.key.get_pressed()if is_key_pressed[pygame.K_RIGHT]:self.spaceship.rotate(clockwise=True)elif is_key_pressed[pygame.K_LEFT]:self.spaceship.rotate(clockwise=False)if is_key_pressed[pygame.K_UP]:self.spaceship.accelerate()

继续测试这个。运行你的游戏,旋转飞船,然后打开引擎:

你的飞船现在可以移动和旋转了!但是,当它到达屏幕边缘时,它只会继续移动。这是你应该解决的问题!

在屏幕周围环绕对象

这个游戏的一个重要元素是确保游戏对象不会离开屏幕。您可以让它们从边缘反弹回来,也可以让它们重新出现在屏幕的相对边缘。在本项目中,您将实现后者。

首先Vector2space_rocks/utils.py文件中导入类:

from pygame.image import load
from pygame.math import Vector2

接下来,wrap_position()在同一个文件中创建:

 def wrap_position(position, surface):x, y = positionw, h = surface.get_size()return Vector2(x % w, y % h)

通过在第 4 行使用模运算符,您可以确保位置永远不会离开给定表面的区域。在您的游戏中,该表面将成为屏幕。

导入这个新方法space_rocks/models.py

from pygame.math import Vector2
from pygame.transform import rotozoomfrom utils import load_sprite, wrap_position

现在您可以在GameObject类中更新move()

def move(self, surface):self.position = wrap_position(self.position + self.velocity, surface)

请注意,使用wrap_position()并不是此处唯一的更改。您还为此方法添加了一个新参数surface。那是因为您需要知道位置应该环绕的区域。记住也要更新SpaceRocks类中的方法调用:

def _process_game_logic(self):self.spaceship.move(self.screen)

现在你的飞船重新出现在屏幕的另一边。

移动和旋转飞船的逻辑已准备就绪。但是这艘船仍然独自在空荡荡的空间里。是时候添加一些小行星了!

第 6 步:小行星

此时,您有一个可以在屏幕上移动的宇宙飞船。在这一步结束时,您的游戏还将显示一些小行星。此外,您将实现飞船和小行星之间的碰撞。

创建一个类

与 类似Spaceship,您将首先创建一个从GameObject继承的子类Asteroid. space_rocks/models.py像这样编辑文件:

class Asteroid(GameObject):def __init__(self, position):super().__init__(position, load_sprite("asteroid"), (0, 0))

就像以前一样,您首先GameObject使用特定图像调用构造函数。您在前面的步骤之一中添加了图像。

接下来,在space_rocks/game.py中导入新类:

import pygamefrom models import Asteroid, Spaceship
from utils import load_sprite

最后,在同一个文件中编辑类SpaceRocks的构造函数以创建六个小行星:

def __init__(self):self._init_pygame()self.screen = pygame.display.set_mode((800, 600))self.background = load_sprite("space", False)self.clock = pygame.time.Clock()self.asteroids = [Asteroid((0, 0)) for _ in range(6)]self.spaceship = Spaceship((400, 300))

现在您有更多的游戏对象,在SpaceRocks的类中创建一个辅助方法返回所有这些对象是个好主意。此方法随后将由绘图和移动逻辑使用。这样,您可以稍后引入新类型的游戏对象并仅修改此单一方法,或者您可以在必要时从该组中排除一些对象。

调用这个方法_get_game_objects()

def _get_game_objects(self):return [*self.asteroids, self.spaceship]

现在通过编辑在_process_game_logic()单个循环中移动所有游戏对象使用它:

def _process_game_logic(self):for game_object in self._get_game_objects():game_object.move(self.screen)

这同样适用于_draw()

def _draw(self):self.screen.blit(self.background, (0, 0))for game_object in self._get_game_objects():game_object.draw(self.screen)pygame.display.flip()self.clock.tick(60)

现在运行你的游戏,你应该会看到一个带有小行星的屏幕:

不幸的是,所有的小行星都堆积在屏幕的一个角落。

嗯,这在意料之中,因为所有小行星的位置都是(0, 0),代表左上角。您可以通过在屏幕上设置随机位置来更改此设置。

随机化位置

要生成随机位置,您必须向space_rocks/utils.py文件添加一些导入:

import randomfrom pygame.image import load
from pygame.math import Vector2

然后,在同一个文件中创建一个get_random_position()调用的方法:

def get_random_position(surface):return Vector2(random.randrange(surface.get_width()),random.randrange(surface.get_height()),)

这将在给定表面上生成一组随机坐标并将结果作为Vector2实例返回。

接下来,在space_rocks/game.py文件中导入这个方法:

import pygamefrom models import Asteroid, Spaceship
from utils import get_random_position, load_sprite

现在用于get_random_position()将所有六颗小行星放置在随机位置。修改SpaceRocks类的构造函数:

def __init__(self):self._init_pygame()self.screen = pygame.display.set_mode((800, 600))self.background = load_sprite("space", False)self.clock = pygame.time.Clock()self.asteroids = [Asteroid(get_random_position(self.screen)) for _ in range(6)]self.spaceship = Spaceship((400, 300))

现在,当你运行游戏时,你会在屏幕上看到一个漂亮的、随机分布的小行星:

这看起来好多了,但有一个小问题:小行星和宇宙飞船是在同一区域生成的。添加碰撞后,这将导致玩家在开始游戏后立即失败。那将是非常不公平的!

这个问题的一个解决方案是检查位置是否离飞船太近,如果是,则生成一个新的位置,直到找到一个有效的位置。

首先创建一个常量,表示必须保持为空的区域。一个250像素值就足够了:

class SpaceRocks:MIN_ASTEROID_DISTANCE = 250

现在您可以修改SpaceRocks该类的构造函数以确保您的玩家始终有机会获胜:

def __init__(self):self._init_pygame()self.screen = pygame.display.set_mode((800, 600))self.background = load_sprite("space", False)self.clock = pygame.time.Clock()self.asteroids = []self.spaceship = Spaceship((400, 300))for _ in range(6):while True:position = get_random_position(self.screen)if (position.distance_to(self.spaceship.position)> self.MIN_ASTEROID_DISTANCE):breakself.asteroids.append(Asteroid(position))

在循环中,您的代码会检查小行星的位置是否大于最小小行星距离。如果不是,则循环再次运行,直到找到这样的位置。

再次运行程序,所有小行星都不会与飞船重叠:

您可以多次运行游戏,以确保每次飞船周围都有一些空闲空间。

移动小行星

目前,您的程序在随机位置显示了六颗小行星,您可以通过移动它们来增加一些趣味!与位置类似,小行星的速度也应该是随机的,不仅在方向上,而且在值上。

首先在space_rocks/utils.py文件中创建一个get_random_velocity()调用的方法:

def get_random_velocity(min_speed, max_speed):speed = random.randint(min_speed, max_speed)angle = random.randrange(0, 360)return Vector2(speed, 0).rotate(angle)

该方法将生成介于min_speedmax_speed之间的随机值以及介于 0 和 360 度之间的随机角度。然后它将创建一个具有该值的向量,并按该角度旋转。

因为小行星的速度无论放在哪里都应该是随机的,所以我们直接在Asteroid类中使用这个方法。从更新space_rocks/models.py文件中的导入开始:

from pygame.math import Vector2
from pygame.transform import rotozoom
from utils import get_random_velocity, load_sprite, wrap_position

请注意,您正在一个地方设置随机位置,在其他地方设置随机速度。那是因为位置应该只对你开始的六颗小行星是随机的,所以它被设置在space_rocks/game.py文件中,游戏初始化的地方。但是,每个小行星的速度都是随机的,因此您可以在Asteroid类的构造函数中设置它。

然后在Asteroid类的构造函数中使用新方法:

def __init__(self, position):super().__init__(position, load_sprite("asteroid"), get_random_velocity(1, 3))

请注意,该方法使用 的最小值1。那是因为小行星应该总是在移动,至少有一点。

再次运行游戏以查看移动的小行星:

您还可以在屏幕上移动飞船。不幸的是,当它遇到小行星时,什么也没有发生。是时候添加一些碰撞了。

与宇宙飞船相撞

这个游戏的一个非常重要的部分是你的飞船被小行星碰撞摧毁的可能性。您可以使用步骤 4 中GameObject.collides_with()介绍的方法检查碰撞。所有你需要做的就是为每个小行星调用这个方法。

像这样编辑SpaceRocks类中的_process_game_logic()方法:

def _process_game_logic(self):for game_object in self._get_game_objects():game_object.move(self.screen)if self.spaceship:for asteroid in self.asteroids:if asteroid.collides_with(self.spaceship):self.spaceship = Nonebreak

如果任何一颗小行星与飞船相撞,那么飞船就会被摧毁。在这个游戏中,您将通过设置self.spaceshipNone来表示这一点。

请注意,self.spaceship在循环的开头还有一个检查。那是因为,当飞船被摧毁时,没有理由检查与它的任何碰撞。此外,检测与None物体的碰撞会导致错误。

现在宇宙飞船的值可能为None,重要的是在SpaceRocks类中_get_game_objects()更新以避免试图渲染或移动被摧毁的宇宙飞船:

def _get_game_objects(self):game_objects = [*self.asteroids]if self.spaceship:game_objects.append(self.spaceship)return game_objects

输入处理也是如此:

def _handle_input(self):for event in pygame.event.get():if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE):quit()is_key_pressed = pygame.key.get_pressed()if self.spaceship:if is_key_pressed[pygame.K_RIGHT]:self.spaceship.rotate(clockwise=True)elif is_key_pressed[pygame.K_LEFT]:self.spaceship.rotate(clockwise=False)if is_key_pressed[pygame.K_UP]:self.spaceship.accelerate()

你现在可以运行你的游戏,看到飞船在与小行星碰撞后消失了:

您的飞船现在可以四处飞行并在与小行星相撞时被摧毁。你已经准备好让小行星也被摧毁。

第 7 步:子弹

在这一点上,你有一些随机放置和移动的小行星和一艘可以四处移动并避开它们的宇宙飞船。在这一步结束时,你的飞船也将能够通过射击子弹来保护自己。

创建一个类

首先将项目符号的图像添加到assets/sprites. 您可以点击以下链接下载源代码或在本教程的开始位置从本地下载:

获取源代码: 单击此处获取您将在本教程中使用 Pygame使用Python 构建 Asteroids 游戏的源代码。

您的项目结构应如下所示:

awesome_pygame_project/
|
├── assets/
|   |
│   └── sprites/
│       ├── asteroid.png
│       ├── bullet.png
│       ├── space.png
│       └── spaceship.png
|
├── space_rocks/
│   ├── __main__.py
│   ├── game.py
│   ├── models.py
│   └── utils.py
|
└── requirements.txt

然后编辑文件space_rocks/models.py通过创建一个继承自GameObjectBullet类:

class Bullet(GameObject):def __init__(self, position, velocity):super().__init__(position, load_sprite("bullet"), velocity)

就像以前一样,这只会调用GameObject具有特定精灵的构造函数。然而,这次速度将是一个必需的参数,因为子弹必须移动。

接下来,您应该添加一种跟踪子弹的方法,类似于您对小行星所做的。编辑space_rocks/game.py文件中SpaceRocks类的构造函数:

def __init__(self):self._init_pygame()self.screen = pygame.display.set_mode((800, 600))self.background = load_sprite("space", False)self.clock = pygame.time.Clock()self.asteroids = []self.bullets = []self.spaceship = Spaceship((400, 300))for _ in range(6):while True:position = get_random_position(self.screen)if (position.distance_to(self.spaceship.position)> self.MIN_ASTEROID_DISTANCE):breakself.asteroids.append(Asteroid(position))

子弹的处理方式应与其他游戏对象相同,因此请编辑 SpaceRocks 中的_get_game_object()方法:

def _get_game_objects(self):game_objects = [*self.asteroids, *self.bullets]if self.spaceship:game_objects.append(self.spaceship)return game_objects

该子弹列表是存在的,但它现在是空的。你可以解决这个问题。

射击子弹

射击子弹有一个小问题。子弹存储在主游戏对象中,由SpaceRocks类表示。但是,射击逻辑应该由飞船决定。知道如何创建新子弹的是宇宙飞船,但它是存储子弹并随后为子弹设置动画的游戏。本Spaceship类需要一种方式来通知SpaceRocks类子弹已创建并应该跟踪。

要解决此问题,您可以向Spaceship类中添加回调函数。该函数将由SpaceRocks类在飞船初始化时提供。飞船每次创建子弹时,都会初始化一个Bullet对象,然后调用回调。回调会将子弹添加到游戏存储的所有子弹列表中。

首先向space_rocks/models.py文件中的Spaceship类的构造函数添加一个回调:

def __init__(self, position, create_bullet_callback):self.create_bullet_callback = create_bullet_callback# Make a copy of the original UP vectorself.direction = Vector2(UP)super().__init__(position, load_sprite("spaceship"), Vector2(0))

您还需要子弹速度的值:

class Spaceship(GameObject):MANEUVERABILITY = 3ACCELERATION = 0.25BULLET_SPEED = 3

接下来,在Spaceship类中创建一个shoot()调用的方法:

def shoot(self):bullet_velocity = self.direction * self.BULLET_SPEED + self.velocitybullet = Bullet(self.position, bullet_velocity)self.create_bullet_callback(bullet)

首先计算子弹的速度。子弹总是向前射,所以你用飞船的方向乘以子弹的速度。因为宇宙飞船不一定静止不动,所以你将它的速度加到子弹的速度上。这样,如果飞船移动得非常快,您就可以制造高速子弹。

然后Bullet使用刚刚计算的速度在与飞船相同的位置创建类的实例。最后,通过回调方法将子弹添加到游戏中的所有子弹中。

现在飞船创建时添加回调参数。子弹存储为列表,回调唯一要做的就是向该列表添加新项目。因此,该append()方法应该可以胜任。编辑space_rocks/game.py文件中SpaceRocks类的构造函数:

def __init__(self):self._init_pygame()self.screen = pygame.display.set_mode((800, 600))self.background = load_sprite("space", False)self.clock = pygame.time.Clock()self.asteroids = []self.bullets = []self.spaceship = Spaceship((400, 300), self.bullets.append)for _ in range(6):while True:position = get_random_position(self.screen)if (position.distance_to(self.spaceship.position)> self.MIN_ASTEROID_DISTANCE):breakself.asteroids.append(Asteroid(position))

您需要添加的最后一件事是输入处理。子弹应仅在Space按下时生成,因此您可以使用事件循环。Space的常数是pygame.K_SPACE

修改SpaceRocks类中的_handle_input()方法:

def _handle_input(self):for event in pygame.event.get():if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE):quit()elif (self.spaceshipand event.type == pygame.KEYDOWNand event.key == pygame.K_SPACE):self.spaceship.shoot()is_key_pressed = pygame.key.get_pressed()if self.spaceship:if is_key_pressed[pygame.K_RIGHT]:self.spaceship.rotate(clockwise=True)elif is_key_pressed[pygame.K_LEFT]:self.spaceship.rotate(clockwise=False)if is_key_pressed[pygame.K_UP]:self.spaceship.accelerate()

请注意,新的输入处理还会检查飞船是否存在。否则,你将遇到在一个None对象调用shoot()的错误。

现在运行你的游戏并发射一些子弹:

你的飞船终于可以射击了!但是,有子弹不会离开屏幕的问题。

包裹子弹

目前,所有游戏对象都环绕在屏幕上。这包括子弹。然而,由于这种包裹,屏幕很快就被四面八方的子弹填满了。这可能会使游戏变得有点太简单了!

您可以通过仅对子弹禁用环绕来解决此问题。在文件space_rocks/models.py中的Bullet类的添加move()方法来覆盖父类中的方法,子类的方法参数应该与父类中的方法参数一致吗?

def move(self, surface):self.position = self.position + self.velocity

这样子弹就不会环绕屏幕。但是,它们也不会被破坏。相反,他们将继续飞入宇宙的无限深渊。很快,你的子弹列表将包含数千个元素,并且每帧都会处理所有这些元素,从而导致你的游戏性能下降。

为避免这种情况,您的游戏应在子弹离开屏幕后立即将其移除。更新SpaceRocks``space_rocks/game.py文件中类的_process_game_logic()方法:

 def _process_game_logic(self):for game_object in self._get_game_objects():game_object.move(self.screen)if self.spaceship:for asteroid in self.asteroids:if asteroid.collides_with(self.spaceship):self.spaceship = Nonebreakfor bullet in self.bullets[:]:if not self.screen.get_rect().collidepoint(bullet.position):self.bullets.remove(bullet)

请注意,不是使用原始列表self.bullets,而是self.bullets[:]在第 11 行中创建了它的副本。这是因为在迭代列表时从列表中删除元素可能会导致错误。

Pygame 中的表面有一个get_rect()方法可以返回一个表示其面积的矩形。反过来,该矩形有一个collidepoint()方法,矩形中包含一个点该方法返回True,否则返回False。使用这两种方法,您可以检查项目符号是否已离开屏幕,如果是,则将其从列表中删除。

与小行星相撞

你的子弹仍然缺少一个关键元素:摧毁小行星的能力!您将在本节中解决该问题。

像这样更新SpaceRocks类的_process_game_logic()方法:

 def _process_game_logic(self):for game_object in self._get_game_objects():game_object.move(self.screen)if self.spaceship:for asteroid in self.asteroids:if asteroid.collides_with(self.spaceship):self.spaceship = Nonebreakfor bullet in self.bullets[:]:for asteroid in self.asteroids[:]:if asteroid.collides_with(bullet):self.asteroids.remove(asteroid)self.bullets.remove(bullet)breakfor bullet in self.bullets[:]:if not self.screen.get_rect().collidepoint(bullet.position):self.bullets.remove(bullet)

现在,每当检测到子弹和小行星之间发生碰撞时,两者都会从游戏中移除。请注意,就像之前在项目符号循环中一样,这里不使用原始列表。相反,您使用[:]第 11 行和第 12 行创建副本。

如果您现在运行游戏并在射击时瞄准良好的目标,那么您应该能够摧毁一些小行星:

你的飞船终于可以保护自己了!然而,游戏中只有六个大目标。接下来,您将使其更具挑战性。

第 8 步:分裂小行星

在这一点上,你有一个宇宙飞船、小行星和子弹的游戏。在这一步结束时,你的小行星会在被子弹击中时分裂。一颗大的小行星会变成两颗中的,一颗中的会变成两颗小的,小的会消失。

小行星的大小将由一个数字表示:

小行星大小 小行星类型
3 大小行星
2 中等小行星
1 小行星

每次撞击小行星时,都会产生两颗较小的小行星。大小为 1 的小行星除外,因为它不应产生任何新的小行星。

小行星的大小也将决定其精灵的大小,从而决定其半径。换句话说,小行星将按如下比例缩放:

小行星大小 小行星尺度 描述
3 1 默认精灵和半径
2 0.5 默认精灵和半径的一半
1 0.25 默认精灵和半径的四分之一

这可能看起来有点复杂,但您只需几行代码即可完成。重写文件space_rocks/models.pyAsteroid类的构造函数:

def __init__(self, position, size=3):self.size = sizesize_to_scale = {3: 1,2: 0.5,1: 0.25,}scale = size_to_scale[size]sprite = rotozoom(load_sprite("asteroid"), 0, scale)super().__init__(position, sprite, get_random_velocity(1, 3))

此方法将为小行星分配一个大小,使用默认值3,代表一个大的小行星。它还将使用 rotozoom()缩放原始精灵。你以前用它来旋转宇宙飞船。如果角度为 0 且比例为 0 以外的任何值,则此方法也可用于缩放。在此示例中,size_to_scale查找表包含不同大小的比例:

尺寸 规模
3 1
2 0.5
1 0.25

最后,您将缩放后的精灵传递给GameObject类的构造函数,该类的构造函数将负责根据新图像大小计算半径。
您的新逻辑需要小行星才能创建新的小行星。情况类似于飞船和子弹,所以可以使用类似的解决方案:回调方法。

Asteroid再次更新类的构造函数:

def __init__(self, position, create_asteroid_callback, size=3):self.create_asteroid_callback = create_asteroid_callbackself.size = sizesize_to_scale = {3: 1,2: 0.5,1: 0.25,}scale = size_to_scale[size]sprite = rotozoom(load_sprite("asteroid"), 0, scale)super().__init__(position, sprite, get_random_velocity(1, 3))

现在你可以在同一个类中创建一个split()调用的方法:

def split(self):if self.size > 1:for _ in range(2):asteroid = Asteroid(self.position, self.create_asteroid_callback, self.size - 1)self.create_asteroid_callback(asteroid)

这将在与当前位置相同的位置创建两个新的小行星。它们中的每一个都会有一个稍小的尺寸。只有当当前的小行星是中型或大型行星时,这种逻辑才会发生。

现在您可以在SpaceRocks类的构造函数中为每个新创建的小行星添加回调。就像在宇宙飞船的情况下一样,您将使用append()正确列表的方法:

def __init__(self):self._init_pygame()self.screen = pygame.display.set_mode((800, 600))self.background = load_sprite("space", False)self.clock = pygame.time.Clock()self.asteroids = []self.bullets = []self.spaceship = Spaceship((400, 300), self.bullets.append)for _ in range(6):while True:position = get_random_position(self.screen)if (position.distance_to(self.spaceship.position)> self.MIN_ASTEROID_DISTANCE):breakself.asteroids.append(Asteroid(position, self.asteroids.append))

别忘记,split()在小行星被子弹击中时应该被调用。更新SpaceRocks类的_process_game_logic()方法:

def _process_game_logic(self):for game_object in self._get_game_objects():game_object.move(self.screen)if self.spaceship:for asteroid in self.asteroids:if asteroid.collides_with(self.spaceship):self.spaceship = Nonebreakfor bullet in self.bullets[:]:for asteroid in self.asteroids[:]:if asteroid.collides_with(bullet):self.asteroids.remove(asteroid)self.bullets.remove(bullet)asteroid.split()breakfor bullet in self.bullets[:]:if not self.screen.get_rect().collidepoint(bullet.position):self.bullets.remove(bullet)

如果你现在运行你的游戏并击落一些小行星,那么你会注意到,它们并没有立即消失,而是分裂成更小的小行星:

您刚刚实现了游戏的整个逻辑!飞船可以移动,与小行星碰撞后会被摧毁,它会发射子弹,然后小行星会分裂成更小的行星。但此刻游戏是无声的。你接下来会处理这个问题。

第 9 步:播放声音

此时,您的程序会显示所有游戏对象并处理它们之间的交互。在这一步结束时,您的游戏也会播放声音。

在第7步,飞船配备了武器。然而,那武器是完全沉默的。这在物理学方面非常准确,因为声音不会在真空中传播(“在太空中没有人能听到你的尖叫声”)。尽管如此,在您的游戏中使用声音会使其更具吸引力。

首先,创建一个assets/sounds目录并在那里添加激光声音。您可以点击以下链接下载源代码或在教程开始处从本地下载:

获取源代码: 单击此处获取您将在本教程中使用 Pygame使用Python 构建 Asteroids 游戏的源代码。

您的项目结构应如下所示:

awesome_pygame_project/
|
├── assets/
|   |
│   ├── sounds/
│   │   └── laser.wav
|   |
│   └── sprites/
│       ├── asteroid.png
│       ├── bullet.png
│       ├── space.png
│       └── spaceship.png
|
├── space_rocks/
│   ├── __main__.py
│   ├── game.py
│   ├── models.py
│   └── utils.py
|
└── requirements.txt

现在您需要加载文件。在 Pygame 中,声音由模块中的Sound类表示pygame.mixer。尽管您将在此游戏中仅使用一种声音,但您可能希望稍后添加更多声音。这就是为什么您将创建一个用于加载声音的辅助方法,类似于您为精灵创建的方法。

首先,Soundspace_rocks/utils.py文件中导入类:

import randomfrom pygame.image import load
from pygame.math import Vector2
from pygame.mixer import Sound

接下来,在同一个文件中创建一个load_sound()调用的方法:

def load_sound(name):path = f"assets/sounds/{name}.wav"return Sound(path)

该方法具有与load_sprite() 类似的逻辑。它将假定声音始终位于assets/sounds目录中并且它是一个 WAV 文件。

您现在可以在space_rocks/models.py文件中导入这个新方法:

from pygame.math import Vector2
from pygame.transform import rotozoomfrom utils import get_random_velocity, load_sound, load_sprite, wrap_position

然后在Spaceship类的构造函数中加载声音:

def __init__(self, position, create_bullet_callback):self.create_bullet_callback = create_bullet_callbackself.laser_sound = load_sound("laser")# Make a copy of the original UP vectorself.direction = Vector2(UP)super().__init__(position, load_sprite("spaceship"), Vector2(0))

最后,你应该在飞船发射时播放声音。更新shoot()

def shoot(self):bullet_velocity = self.direction * self.BULLET_SPEED + self.velocitybullet = Bullet(self.position, bullet_velocity)self.create_bullet_callback(bullet)self.laser_sound.play()

现在运行游戏,你每次射击都会听到声音。

您刚刚学会了如何在 Pygame 中处理音频文件!剩下的就是在游戏结束时显示一条消息。

第 10 步:结束游戏

此时,您的游戏几乎完成了,包括输入处理、交互、图像甚至声音。在此步骤结束时,您还将在屏幕上显示游戏状态。

许多游戏在游戏过程中和结束后都会显示一些附加信息。这可以是剩余生命值的数量、护盾等级、弹药数量、任务的总分等。在这个游戏中,您将显示游戏的状态。

如果宇宙飞船被小行星摧毁,则You lost!屏幕上应显示消息。但是如果所有的小行星都消失了,而飞船还在,那么你应该显示You won!

Pygame 没有任何用于绘制文本的高级工具,这意味着程序员需要做更多的工作。渲染文本由具有透明背景的表面表示。您可以像处理精灵一样操作该表面,例如使用blit(). 表面本身是使用字体创建的。

在 Pygame 中处理文本的完整过程如下所示:

  1. **创建字体:**由字体pygame.font.Font类表示。您可以使用自定义字体文件,也可以使用默认字体。对于这个游戏,你会做后者。
  2. **用文本创建一个表面:**这是使用Font.render(). 在本教程后面,您将了解有关该方法的更多信息。现在,知道它创建了一个带有渲染文本和透明背景的表面就足够了。
  3. **将表面显示到屏幕上:**与 Pygame 中的任何其他表面一样,只有将文本显示到屏幕上或最终将显示在屏幕上的另一个表面上时,文本才会可见。

您的字体将呈现一种颜色。在步骤 1 中,您使用三个值创建了一种颜色:红色、绿色和蓝色。在本节中,您将改用Color类。首先将其导入space_rocks/utils.py文件:

import randomfrom pygame import Color
from pygame.image import load
from pygame.math import Vector2
from pygame.mixer import Sound

然后,在同一个文件中创建一个print_text()方法:

 def print_text(surface, text, font, color=Color("tomato")):text_surface = font.render(text, True, color)rect = text_surface.get_rect()rect.center = Vector2(surface.get_size()) / 2surface.blit(text_surface, rect)

这是发生了什么:

  • 第 1 行是您的方法的声明。它需要一个来渲染文本的表面、文本本身、字体和颜色。本Color类提供了很多预定义的颜色,您可以在找到的pygame的资料库。您的方法将使用名为"tomato" 的默认颜色。
  • 第 2 行使用render() 创建文本曲面。它的第一个参数是需要呈现的文本。第二个是抗锯齿标志。将其设置为True将平滑渲染文本的边缘。最后一个参数是文本的颜色。
  • 第 4 行获得一个矩形,该矩形表示带有文本的表面区域。该矩形是Rect类的一个实例,可以轻松移动和对齐。您可以在文档中阅读有关对齐的更多信息。
  • 第 5行将center矩形的属性设置为屏幕中间的一个点。该点是通过将屏幕大小除以 2来计算的。此操作可确保您的文本显示在屏幕中央。
  • 第 7行在屏幕上绘制文本。请注意,这一次,您将一个矩形而不是一个点传递给blit()。在这种情况下,该方法将取给定矩形的左上角,并在那里开始块传输过程。

现在您可以在space_rocks/game.py文件中导入此方法:

import pygamefrom models import Asteroid, Spaceship
from utils import get_random_position, load_sprite, print_text

现在你需要创建一个字体。您还应该存储将显示的消息。编辑SpaceRocks类的构造函数:

def __init__(self):self._init_pygame()self.screen = pygame.display.set_mode((800, 600))self.background = load_sprite("space", False)self.clock = pygame.time.Clock()self.font = pygame.font.Font(None, 64)self.message = ""self.asteroids = []self.bullets = []self.spaceship = Spaceship((400, 300), self.bullets.append)for _ in range(6):while True:position = get_random_position(self.screen)if (position.distance_to(self.spaceship.position)> self.MIN_ASTEROID_DISTANCE):breakself.asteroids.append(Asteroid(position, self.asteroids.append))

Font类的构造函数接受两个参数:

  1. 字体文件的名称,其中None表示将使用默认字体
  2. 字体的大小(以像素为单位)

需要正确设置消息的内容。当飞船被摧毁时,将其设置为"You lost!"。当所有小行星都被摧毁时,将其设置为"You won!"。编辑SpaceRocks类的_process_game_logic()方法:

def _process_game_logic(self):for game_object in self._get_game_objects():game_object.move(self.screen)if self.spaceship:for asteroid in self.asteroids:if asteroid.collides_with(self.spaceship):self.spaceship = Noneself.message = "You lost!"breakfor bullet in self.bullets[:]:for asteroid in self.asteroids[:]:if asteroid.collides_with(bullet):self.asteroids.remove(asteroid)self.bullets.remove(bullet)asteroid.split()breakfor bullet in self.bullets[:]:if not self.screen.get_rect().collidepoint(bullet.position):self.bullets.remove(bullet)if not self.asteroids and self.spaceship:self.message = "You won!"

您需要做的最后一件事是在屏幕上实际显示消息。更新SpaceRocks类的_draw()方法:

def _draw(self):self.screen.blit(self.background, (0, 0))for game_object in self._get_game_objects():game_object.draw(self.screen)if self.message:print_text(self.screen, self.message, self.font)pygame.display.flip()self.clock.tick(60)

继续并测试它。开始游戏并将飞船撞向小行星:

游戏正确显示一条消息You lost!

现在付出更多的努力并尝试摧毁所有的小行星。如果您设法做到了这一点,那么您应该会看到一个胜利屏幕:

在这一步中,您学习了如何在屏幕上显示文本消息。这是本教程的最后一步。您的游戏现已完成!

结论

恭喜,您刚刚使用 Python 构建了 Asteroids 游戏的克隆!使用 Pygame,您的 Python 知识可以直接转化为游戏开发项目。

在本教程中,您学习了如何:

  • 加载图像并将其显示在屏幕上
  • 为您的游戏添加输入处理
  • 用 Python实现游戏逻辑碰撞检测
  • 播放声音
  • 在屏幕上显示文本

您经历了设计游戏、构建文件、导入和使用资产以及编写逻辑的整个过程。您可以将所有这些知识用于您所有令人惊叹的未来项目!

单击下面的链接下载此项目的代码,并在构建游戏时按照以下步骤操作:

获取源代码: 单击此处获取您将在本教程中使用 Pygame使用Python 构建 Asteroids 游戏的源代码。

下一步

您使用 Python 编写的 Asteroids 游戏已经完成,但您可以添加许多功能。以下是一些帮助您入门的想法:

  • 限制飞船的最大速度。
  • 当小行星被摧毁时播放声音。
  • 为宇宙飞船添加一个护盾,使其能够在一次碰撞中幸存下来。
  • 记录最高分。
  • 让子弹也摧毁飞船并将它们包裹在屏幕上,使游戏变得更加困难!

你还能想出什么其他想法来扩展这个项目?有创意,玩得开心!在这种情况下,正如他们所说,空间是极限

使用 Python 和 Pygame 构建小行星游戏相关推荐

  1. Python 用pygame 做一个游戏的开始界面(小白第一篇博客)

    Python 用pygame 做一个游戏的开始界面(小白第一篇博客) 主要功能实现 本篇文章主要是实现了一个游戏开始界面的两个功能: 1,将鼠标放到"开始游戏"或"结束游 ...

  2. 【Python】Pygame模块设计游戏

    Pygame是一个跨平台Python模块,专为电子游戏设计,包含图像.声音.建立在SDL基础上,允许实时电子游戏研发而无需被低级语言(如机器语言和汇编语言)束缚. Pygame的历史 Pygame是一 ...

  3. python下俄罗斯方块的游戏设计_[源码和文档分享]基于Python的PyGame的俄罗斯方块游戏设计与实现...

    摘 要 近年来,随着游戏产业的突飞猛进,游戏玩家的技术也是与日俱增,当你看见游戏高手完美的表演时,你是否想过我也能达到那种水平,本程序用Python语言编写俄罗斯方块,左侧显示正在运行的游戏,右边显示 ...

  4. 初入Python(一) Pygame贪吃蛇游戏的编写与改进

    贪吃蛇游戏是一款简单耐玩的休闲益智类游戏,利用pygame可以实现轻松编写,不需要辅佐图片等等元素,可以直接利用涂色方块表示,吃果子变长的原理也很容易实现,将body分为一块一块,每块有自己的位置属性 ...

  5. pip install pygame_使用 Python 和 Pygame 模块构建一个游戏框架!

    这系列的第一篇通过创建一个简单的骰子游戏来探究 Python.现在是来从零制作你自己的游戏的时间. 在我的这系列的第一篇文章 中, 我已经讲解如何使用 Python 创建一个简单的.基于文本的骰子游戏 ...

  6. pygame为游戏添加背景_万能的Python和Pygame模块构建一个游戏框架

    通过创建一个简单的骰子游戏来探究 Python.现在是来从零制作你自己的游戏的时间. 在我的这系列的第一篇文章中, 我已经讲解如何使用 Python 创建一个简单的.基于文本的骰子游戏.这次,我将展示 ...

  7. python pygame模块怎么写游戏_使用 Python 和 Pygame 模块构建一个游戏框架

    这系列的第一篇通过创建一个简单的骰子游戏来探究 Python.现在是来从零制作你自己的游戏的时间. 在我的这系列的第一篇文章 中, 我已经讲解如何使用 Python 创建一个简单的.基于文本的骰子游戏 ...

  8. pygame为游戏添加背景_为游戏添加背景使用Python和Pygame模块构建一个游戏框架

    这系列的第一篇通过创建一个简单的骰子游戏来探究 Python.现在是来从零制作你自己的游戏的时间. 在我的这系列的第一篇文章 中, 我已经讲解如何使用 Python 创建一个简单的.基于文本的骰子游戏 ...

  9. 使用Python 3和Pygame构建游戏:第1部分

    总览 许多开发人员进入软件开发是因为他们想开发游戏. 并非每个人都可以成为专业的游戏开发人员,但是每个人都可以出于娱乐目的甚至可能自己的利益而制作自己的游戏. 在这个由五部分组成的系列文章中,我将向您 ...

最新文章

  1. 奇妙的二叉树:Huffman的贡献
  2. mac brew 安装
  3. Solr中Field常用属性
  4. C# - linq查询现有的DataTable
  5. 贵州2021高考体考成绩查询,2021年贵州体育专业考试成绩查询网址:http://www.eaagz.org.cn/...
  6. 轩逸车联网功能怎么用_北斗已建设完成,那“北斗导航”怎么用?“短报文功能”怎么用?...
  7. windows 下 git 禁用 CRLF 转换 LF
  8. python语言入门t_Python基础学习
  9. 计算机病案管理系统,TWQ病案管理软件
  10. @import与link方式的区别
  11. Unity容器中AOP应用示例程序
  12. iOS 关于本地持久化存储的探讨
  13. 数据结构视频教程 -《[猎豹网校]数据结构与算法_C语言》
  14. LGP993使用心得和Android手机使用建议
  15. 填充因子设置的一般性准则和指导
  16. 统计java代码行数_统计项目代码行数
  17. 为静态照片添加动画表情的iOS应用MugLife来了,网友惊呼「这技术等着被收购吧」
  18. 迷失lost结局什么意思_《lost》《迷失》大结局,没看懂的进,详细讲解
  19. MySQL级联优缺点_【Mysql】外键级联与级联的劣势
  20. yocto(二)——bitbake工作流程

热门文章

  1. 大学生充实自己生活的方法
  2. AndroidKiller报.smali文件丢失问题解决(关闭Android Studio的Instant Run)
  3. dbpedia知识图谱java_中文通用百科知识图谱(CN-DBpedia)
  4. 看了 web.dev 的 631 篇博客,我总结了这些内容
  5. js破解 中国国际航空公司登录
  6. 逆向工程实验——pre6(汇编、Android逆向、RSA算法破解)
  7. 从底层操作系统到容器云平台:OpenCloudOS与秒云构筑完美兼容链
  8. MATLAB的取整函数与取余函数
  9. Cookie被禁用,如何传递session id?
  10. 用DOS命令格式化U盘