使用 Arcade 用 Python 构建平台游戏

举报
Yuchuan 发表于 2021/10/25 22:24:55 2021/10/25
【摘要】 阅读本教程后,您现在可以: 安装Pythonarcade库 创建基本的2D 游戏结构 查找可用的游戏艺术品和其他资产 使用平铺地图编辑器构建平台地图 定义玩家动作、游戏奖励和障碍 使用键盘和操纵杆输入控制您的播放器 播放游戏动作的音效 使用视口滚动游戏屏幕以保持您的玩家在视野中 添加标题、说明和暂停屏幕 在屏幕上移动非玩家游戏元素

目录

对于许多视频游戏玩家来说,编写游戏的诱惑是学习计算机编程的主要原因。但是,构建 2D 平台游戏,例如Lode RunnerPitfall!超级马里奥兄弟,如果没有适当的工具或指导,可能会让您感到沮丧。幸运的是,Pythonarcade库使许多程序员可以使用 Python 创建 2D 游戏!

如果您还没有听说过它,该arcade是一个现代 Python 框架,用于制作具有引人入胜的图形和声音的游戏。面向对象并为 Python 3.6 及更高版本构建,arcade为您提供一套现代工具,用于打造出色的游戏体验,包括平台游戏。

在本教程结束时,您将能够:

  • 安装Pythonarcade
  • 创建基本的2D 游戏结构
  • 查找可用的游戏艺术品和其他资产
  • 使用平铺地图编辑器构建平台地图
  • 定义玩家动作、游戏奖励障碍
  • 使用键盘操纵杆输入控制您的播放器
  • 播放游戏动作的音效
  • 使用视口滚动游戏屏幕以保持您的玩家在视野中
  • 添加标题说明暂停屏幕
  • 在屏幕上移动非玩家游戏元素

本教程假设您对编写 Python 程序基本的了解。您还应该熟悉使用该arcade并熟悉面向对象的 Python,它在arcade.

您可以通过单击以下链接下载本教程的所有代码、图像和声音:

安装 Python arcade

您可以arcade使用pip以下命令安装及其依赖项:

$ python -m pip install arcade

完整的安装说明适用于WindowsMacLinux。如果您愿意,您甚至可以arcade 直接从源代码安装。

本教程arcade始终使用 Python 3.9 和2.5.5。

设计游戏

在开始编写任何代码之前,制定一个计划是有益的。由于您的目标是编写 2D 平台游戏,因此最好准确定义是什么使游戏成为平台游戏。

什么是平台游戏?

平台游戏与其他类型游戏的区别有以下几个特点

  • 玩家在游戏场地的各个平台之间跳跃、攀爬。
  • 平台通常具有不平坦的地形和不均匀的高度放置。
  • 障碍物放置在玩家的路径上,必须克服才能达到目标。

这些只是平台游戏的最低要求,您可以随意添加其他您认为合适的功能,包括:

  • 多级难度增加
  • 整个游戏都有奖励
  • 多人生活
  • 破坏游戏障碍的能力

本教程中制定的游戏计划包括增加难度和奖励。

游戏故事

所有好的游戏都有一些背景故事,即使它很简单:

您的游戏受益于将玩家采取的行动与某个总体目标联系起来的故事。

在本教程中,游戏故事涉及一位名叫 Roz 的太空旅行者,他坠毁在一个外星世界。在他们的飞船坠毁之前,罗兹被抛到了一边,现在需要找到他们的太空船,修理它,然后回家。

为此,Roz 必须从他们当前的位置移动到每个级别的出口,这使他们更接近船。一路上,罗兹可以收集硬币,用于修复损坏的工艺。由于罗兹被逐出飞船,他们没有任何武器,因此必须避开任何危险的障碍物。

虽然这个故事可能看起来很傻,但它的重要目的是为您的关卡和角色的设计提供信息。这有助于您在实现功能时做出决定:

  • 由于罗兹没有武器,因此无法射击可能出现的敌人。
  • Roz 坠毁在一个外星世界,所以敌人可能无处不在。
  • 由于行星是外星,重力可能不同,这可能会影响 Roz 的跳跃和移动能力。
  • Roz 需要修理他们损坏的飞船,这需要收集物品才能这样做。目前,硬币可用,但其他物品可能稍后可用。

在设计游戏时,您可以根据自己的喜好让故事变得简单或复杂。

游戏机制

考虑到粗略的设计,您还可以开始计划如何控制游戏玩法。在游戏场地中移动 Roz 需要一种方法来控制几种不同的动作:

  • LeftRight在平台上移动
  • UpDown在平台之间爬梯子
  • 跳跃以收集硬币、躲避敌人或在平台之间移动

传统上,玩家使用四个箭头键控制方向移动和Space跳跃。您也可以使用键如IJKLIJKMWASD如果你愿意的话。

您也不仅限于键盘输入。该arcade库包括对操纵杆和游戏控制器的支持,稍后您将对其进行探索。一旦操纵杆连接到您的计算机,您可以通过检查操纵杆的 X 轴和 Y 轴的位置来移动 Roz,并通过检查特定的按钮按下来跳跃。

游戏资产

现在您已经了解了游戏应该如何运作,您需要就游戏的外观和声音做出一些决定。用于显示乐谱的图像、精灵、声音甚至文本统称为资产。他们在玩家眼中定义了你的游戏。创建它们可能是一项挑战,与编写实际游戏代码相比,花费的时间甚至更多。

您可以下载免费或低成本的资产以在您的游戏中使用,而不是创建自己的资产。许多艺术家和设计师提供精灵、背景、字体、声音和其他内容供游戏制作者使用。以下是一些音乐、声音和艺术来源,您可以搜索有用的内容:

Source Sprites Artwork Music Sound Effects
OpenGameArt.org X X X X
Kenney.nl X X X X
Game Art 2D X X
ccMixter X X
Freesound X X

对于本教程中概述的游戏,您将使用由Kenney.nl创建的免费地图图块图像和精灵。可下载源代码中提供的音效是作者使用MuseScoreAudacity创建的。

注意:如果您决定使用其他人拥有或创建的游戏资产,请务必阅读、理解并遵守所有者指定的任何许可要求。许可可能需要支付费用或添加适当的归属,并且可能对您的游戏施加许可限制。如果您有任何疑问,请咨询法律专业人士。

开始编写代码之前的最后一步是决定如何构建和存储所有内容。

定义程序结​​构

因为视频游戏由图形和声音资产以及代码组成,所以组织您的项目很重要。妥善组织游戏资产和代码将使您能够对游戏的设计或行为进行有针对性的更改,同时最大限度地减少对其他游戏方面的影响。

该项目使用以下结构:

arcade_platformer/
|
├── arcade_platformer/
|
├── assets/
|   |
│   ├── images/
|   |   |
│   │   ├── enemies/
|   |   |
│   │   ├── ground/
|   |   |
│   │   ├── HUD/
|   |   |
│   │   ├── items/
|   |   |
│   │   ├── player/
|   |   |
│   │   └── tiles/
|   |
│   └── sounds/
|
└── tests/

在项目的根文件夹下有以下子文件夹:

  • arcade_platformer 包含游戏的所有 Python 代码。
  • assets 包含您所有的游戏图像、字体、声音和瓷砖地图。
  • tests 包含您可以选择编写的任何测试。

虽然还有一些其他的游戏决策需要做出,但这足以开始编写代码。您将首先定义arcade可以构建平台游戏的基本代码结构!

在 Python 中定义游戏结构 arcade

您的游戏使用arcade. 为此,您需要基于 定义一个新类arcade.Window,然后覆盖该类中的方法以更新和呈现您的游戏图形。

这是一个完成的游戏可能是什么样子的基本框架。随着游戏的进行,您将建立在这个骨架上:

 1"""
 2Arcade Platformer
 3
 4Demonstrating the capabilities of arcade in a platformer game
 5Supporting the Arcade Platformer article
 6at https://realpython.com/platformer-python-arcade/
 7
 8All game artwork from www.kenney.nl
 9Game sounds and tile maps by author
10"""
11
12import arcade
13
14class Platformer(arcade.Window):
15    def __init__(self):
16        pass
17
18    def setup(self):
19        """Sets up the game for the current level"""
20        pass
21
22    def on_key_press(self, key: int, modifiers: int):
23        """Processes key presses
24
25        Arguments:
26            key {int} -- Which key was pressed
27            modifiers {int} -- Which modifiers were down at the time
28        """
29
30    def on_key_release(self, key: int, modifiers: int):
31        """Processes key releases
32
33        Arguments:
34            key {int} -- Which key was released
35            modifiers {int} -- Which modifiers were down at the time
36        """
37
38    def on_update(self, delta_time: float):
39        """Updates the position of all game objects
40
41        Arguments:
42            delta_time {float} -- How much time since the last call
43        """
44        pass
45
46    def on_draw(self):
47        pass
48
49if __name__ == "__main__":
50    window = Platformer()
51    window.setup()
52    arcade.run()

这种基本结构几乎提供了构建 2D 平台游戏所需的一切:

  • 12个线 进口arcade库。

  • 第 14 行定义了用于运行整个游戏的类。调用此类的方法来更新游戏状态、处理用户输入以及在屏幕上绘制项目。

  • 第 15 行定义了.__init__(),它初始化了游戏对象。您可以在此处添加代码来处理应仅在游戏首次启动时执行的操作。

  • 第 18 行定义了.setup(),它设置游戏开始播放。您向此方法添加可能需要在整个游戏中重复的代码。例如,这是在成功时初始化新级别或在失败时重置当前级别的好地方。

  • 第 22 行和第 30 行定义了.on_key_press().on_key_release(),它们允许您独立处理键盘输入。arcade分别处理按键按下和按键释放,这有助于避免键盘自动重复出现问题。

  • 第 38 行定义了.on_update(),您可以在其中更新游戏状态和其中的所有对象。这是处理对象之间的碰撞、播放大多数音效、更新分数和动画精灵的地方。这个方法是你游戏中一切实际发生的地方,所以这里通常有很多代码。

  • 第 46 行定义了.on_draw(),其中绘制了游戏中显示的所有内容。与 相比.on_update(),此方法通常只包含几行代码。

  • 第 49 到 52 行定义了游戏的主要入口点。这是您:

    • window根据第 13 行定义的类创建游戏对象
    • 通过调用设置游戏 window.setup()
    • 通过调用开始游戏循环 arcade.run()

这种基本结构适用于大多数 Pythonarcade游戏。

注意:在可下载的材料中,可以在arcade_platformer/01_game_skeleton.py.

随着本教程的进展,您将充实这些方法中的每一个,并添加新的方法来实现您的游戏功能。

添加初始游戏功能

开始游戏的第一件事就是打开游戏窗口。到本节结束时,您的游戏将如下所示:

第一次运行游戏。

您可以在以下位置查看游戏骨架的更改arcade_platformer/02_open_game_window.py

11import arcade
12import pathlib
13
14# Game constants
15# Window dimensions
16SCREEN_WIDTH = 1000
17SCREEN_HEIGHT = 650
18SCREEN_TITLE = "Arcade Platformer"
19
20# Assets path
21ASSETS_PATH = pathlib.Path(__file__).resolve().parent.parent / "assets"
22
23class Platformer(arcade.Window):
24    def __init__(self) -> None:
25        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
26
27        # These lists will hold different sets of sprites
28        self.coins = None
29        self.background = None
30        self.walls = None
31        self.ladders = None
32        self.goals = None
33        self.enemies = None
34
35        # One sprite for the player, no more is needed
36        self.player = None
37
38        # We need a physics engine as well
39        self.physics_engine = None
40
41        # Someplace to keep score
42        self.score = 0
43
44        # Which level are we on?
45        self.level = 1
46
47        # Load up our sounds here
48        self.coin_sound = arcade.load_sound(
49            str(ASSETS_PATH / "sounds" / "coin.wav")
50        )
51        self.jump_sound = arcade.load_sound(
52            str(ASSETS_PATH / "sounds" / "jump.wav")
53        )
54        self.victory_sound = arcade.load_sound(
55            str(ASSETS_PATH / "sounds" / "victory.wav")
56        )

这是一个细分:

  • 第 11 和 12 行导入您需要的arcadepathlib库。

  • 第 16 到 18 行定义了几个游戏窗口常量,用于稍后打开游戏窗口。

  • 第 21 行保存assets文件夹的路径,使用当前文件的路径作为基础。由于您将在整个游戏中使用这些资产,因此了解它们的位置至关重要。使用pathlib可确保您的路径在 Windows、Mac 或 Linux 上正常工作。

  • 第 25 行通过.__init__()使用super()上面第 16 到 18 行定义的常量调用父类的方法来设置您的游戏窗口。

  • 第 28 到 33 行定义了六个不同的精灵列表来保存游戏中使用的各种精灵。在这里声明和定义这些并不是绝对必要的,因为它们稍后将在.setup(). 声明对象属性是C++Java等语言的保留。每个级别都有一组不同的对象,它们填充在.setup()

    • coins 是 Roz 可以在整个游戏中找到的收藏品。

    • background 对象仅出于视觉兴趣而呈现,不与任何内容交互。

    • walls是 Roz 无法通过的物体。这些包括实际的墙壁和 Roz 行走和跳跃的平台。

    • ladders 是允许 Roz 爬上或下的物体。

    • goals 是 Roz 必须找到才能进入下一个级别的对象。

    • enemies是 Roz 在整个游戏中必须避免的对象。与敌人接触将结束游戏。

  • 第 36 行声明了玩家对象,它将在.setup().

  • 第 39 行声明了一个用于管理运动和碰撞的物理引擎

  • 第 42 行定义了一个变量来跟踪当前分数。

  • 第 45 行定义了一个变量来跟踪当前游戏级别。

  • 第 48 行到第 56 行使用ASSETS_PATH之前定义的常量来定位和加载用于收集硬币、跳跃和完成每个级别的声音文件。

如果您愿意,可以在此处添加更多内容,但请记住,这.__init__()仅在游戏首次启动时运行。

注意:上面提到的声音在可下载的材料中提供。您可以按照提供的方式使用它们,也可以根据需要替换自己的声音。

Roz 需要能够在游戏世界中行走、跳跃和攀爬。管理这种情况发生的时间和方式是物理引擎的工作。

什么是物理引擎?

在大多数平台游戏中,用户使用操纵杆或键盘移动玩家。他们可能会让玩家跳下平台或让玩家离开平台。一旦玩家在半空中,用户不需要做任何其他事情就可以让他们掉到较低的平台上。控制玩家可以走的位置以及他们跳下或离开平台后如何跌倒由物理引擎处理。

在游戏中,物理引擎提供作用于玩家和其他游戏对象的物理力的近似值。这些力可能会影响或影响游戏对象的运动,包括跳跃、攀爬、坠落和阻挡运动。

Python 中包含三个物理引擎arcade

  1. arcade.PhysicsEngineSimple是一个非常基本的引擎,用于处理单个玩家精灵和墙壁精灵列表的移动和交互。这对于自上而下的游戏很有用,其中重力不是一个因素。

  2. arcade.PhysicsEnginePlatformer是为平台游戏量身定制的更复杂的引擎。除了基本运动之外,它还提供重力将物体拉到屏幕底部。它还为玩家提供了一种跳跃和爬梯子的方法。

  3. arcade.PymunkPhysicsEngine建立在Pymunk之上,Pymunk是一个使用Chipmunk库的 2D 物理库。Pymunk 为arcade应用程序提供了极其逼真的物理计算。

在本教程中,您将使用arcade.PhysicsEnginePlatformer.

为了正确设置arcade.PhysicsEnginePlatformer,您必须提供玩家精灵以及包含玩家与之交互的墙壁和梯子的两个精灵列表。由于墙壁和梯子因关卡而异,因此在设置关卡之前,您无法正式定义物理引擎,这发生在.setup().

说到层次,你如何定义这些?与大多数事情一样,完成工作的方法不止一种。

构建游戏关卡

回到视频游戏仍然分布在软盘上的时候,很难存储游戏所需的所有游戏级别数据。许多游戏制造商求助于编写代码来创建关卡。虽然这种方法可以节省磁盘空间,但使用命令式代码生成游戏关卡会限制您以后修改或扩充它们的能力。

随着存储空间变得更便宜,游戏通过将更多资产存储在数据文件中来获得优势,这些文件由代码读取和处理。现在可以在不更改游戏代码的情况下创建和修改游戏关卡,这使得美术师和游戏设计师无需了解底层代码即可做出贡献。这种声明式的关卡设计方法在设计和开发游戏时提供了更大的灵活性。

声明式游戏关卡设计的缺点是不仅需要定义数据,还需要存储数据。幸运的是,有一个工具可以做到这两者,而且它与arcade.

Tiled是一个开源的 2D 游戏关卡编辑器,可以生成 Python 可以读取和使用的文件arcade。Tiled 允许您创建称为tileset的图像集合,用于创建定义游戏每个级别的tile 地图。您可以使用 Tiled 为自上而下、等距和横向卷轴游戏创建瓷砖地图,包括您的游戏关卡:

第一级街机平台游戏的基本设计

Tiled 附带了一组很棒的文档和一个很棒的介绍教程。为了让您开始并希望激发您对更多内容的兴趣,接下来您将逐步完成创建第一个地图关卡的步骤。

下载和启动平铺

在运行 Tiled 之前,您需要下载它。撰写本文时的当前版本是 Tiled 版本 1.4.3,它以多种格式适用于 Windows、Mac 和 Linux。下载时,请考虑通过捐赠来支持其持续维护。

下载 Tiled 后,您可以第一次启动它。您将看到以下窗口:

Tiled,平台游戏编辑器,首次启动时

单击“新建地图”为您的第一级创建图块地图。将出现以下对话框:

在 Tiled 中创建新的瓦片地图

这些默认的图块地图属性非常适合平台游戏,代表游戏的最佳选项arcade。以下是您可以选择的其他选项的快速细分:

  • 方向指定地图的显示和编辑方式。
    • 正交地图是方形的,用于自上而下和平台游戏。arcade最适合正交贴图。
    • 等距贴图将视点移动到游戏场地的非方形角度,提供 2D 世界的伪 3D 视图。交错等轴测图指定地图的顶部边缘是视图的顶部边缘。
    • 六边形地图对每个地图图块使用六边形而不是正方形(尽管 Tiled 在编辑器中显示正方形)。
  • 切片图层格式指定地图在磁盘上的存储方式。使用zlib 进行压缩有助于节省磁盘空间。
  • 图块渲染顺序指定图块在文件中的存储方式以及最终它们如何由游戏引擎呈现。
  • 地图大小设置要存储的地图的大小,以图块为单位。将地图指定为Infinite 会告诉 Tiled 根据所做的编辑确定最终大小。
  • 图块大小指定每个图块的大小(以像素为单位)。如果您使用的是来自外部来源的图稿,请将其设置为该组中图块的大小。本教程提供的图稿使用尺寸为 128 × 128 像素的方形精灵。这意味着每个图块由大约 16,000 个像素组成,它们可以存储在磁盘和内存中,必要时可以提高游戏性能。

单击另存为以保存级别。由于这是游戏资产,请将其另存为arcade_platformer/assets/platform_level_01.tmx.

图块地图由一组放置在特定地图图层上的图块组成。要开始为关卡定义瓦片地图,您必须首先定义要使用的瓦片集以及它们出现的图层。

创建图块集

用于创建关卡的图块包含在图块集中。图块集与图块地图相关联,并提供定义关卡所需的所有精灵图像。

您可以使用位于 Tiled 窗口右下角的Tileset视图定义一个tileset 并与之交互:

图块集在 Tiled 中的位置

点击New Tileset按钮来定义这个关卡的tileset。Tiled 会显示一个对话框,询问有关要创建的新图块集的一些信息:

在 Tiled 中创建新的图块集

您的新图块集有以下选项:

  • 名称是您的图块集的名称。叫这个arcade_platformer
  • 类型指定如何定义图块集:
    • 图像集合表示每个图块都包含在磁盘上的一个单独的图像中。您应该选择此选项,因为arcade最适合单个平铺图像。
    • 基于 Tileset Image表示将所有的图块组合成一个单独的大图像,Tiled 需要处理该图像来定位每个单独的图像。仅当您使用的资产需要时才选择此选项。
  • Embed in Map告诉 Tiled 将图块集存储在图块地图中。保持未选中状态,因为您将在多个图块地图中保存和使用图块集作为单独的资源。

单击另存为并将其另存assets/arcade_platformer.tsx. 要在未来的瓦片地图上重用此瓦片集,请选择“地图” →“添加外部瓦片集”以包含它。

定义图块集

你的新图块集最初是空的,所以你需要用图块填充它。为此,您可以找到平铺图像并将它们添加到集合中。每个图像的尺寸应与您在创建瓷砖地图时定义的瓷砖尺寸相同。

此示例假设您已下载本教程的游戏资产。您可以通过单击以下链接来执行此操作:

或者,您可以下载Platformer Pack Redux (360 Assets)并将PNG文件夹的内容移动到您的arcade-platformer/assets/images文件夹中。回想一下,您的图块地图位于 下arcade-platformer/assets,因为这在以后很重要。

在工具栏上,单击蓝色加号 ( +) 或选择Tileset → Add Tiles开始该过程。您将看到以下对话框:

将图块添加到 Tiled 中的图块集

从这里,导航到下面列出的文件夹以将指定的资源添加到您的图块集:

Folder File
arcade-platformer/assets/images/ground/Grass All Files
arcade-platformer/assets/images/HUD hudHeart_empty.png
hudHeart_full.png
hudHeart_half.png
hudX.png
arcade-platformer/assets/images/items coinBronze.png
coinGold.png
coinSilver.png
flagGreen_down.png
flagGreen1.png
flagGreen2.png
arcade-platformer/assets/images/tiles doorOpen_mid.png
doorOpen_top.png
grass.png
ladderMid.png
ladderTop.png
signExit.png
signLeft.png
signRight.png
torch1.png
torch2.png
water.png
waterTop_high.png
waterTop_low.png

添加完文件后,您的图块集应如下所示:

在 Tiled 中设置的填充磁贴

如果您没有看到所有贴,请单击工具栏上的动态包裹磁贴按钮以显示所有贴。

使用Ctrl+SFile → Save从菜单中保存您的新图块集并返回到您的图块地图。您将在 Tiled 界面的右下方看到新的tileset,可用于定义您的tile map!

定义地图层

关卡中的每个项目都有特定的用途:

  • 地面和墙壁定义了玩家可以移动的位置和方式。
  • 硬币和其他收藏品可以获得积分并解锁成就。
  • 梯子允许玩家爬到新的平台,但不会阻止移动。
  • 背景项目提供视觉兴趣并且可以提供信息。
  • 敌人为玩家提供了躲避障碍。
  • 目标提供了在关卡中移动的理由。

这些不同的项目类型中的每一个都需要不同的处理arcade。因此,在 Tiled 中定义它们时将它们分开是有意义的。Tiled 允许您通过使用地图图层来做到这一点。通过在不同的地图图层上放置不同的项目类型并分别处理每个图层,您可以以不同的方式跟踪和处理每种类型的精灵。

要定义一个层,首先打开Tiled 屏幕右上角的Layers视图:

平铺中的图层视图

默认图层已设置并选中。ground通过单击图层重命名该图层,然后在左侧Name的“属性”视图中更改。或者,您可以双击名称直接在“图层”面板中进行编辑:

在 Tiled 中更改图层名称

该层将包含您的地砖,包括玩家无法穿过的墙。

创建新图层不仅需要定义图层名称,还需要定义图层类型。Tiled 提供了四种类型的图层:

  1. 图块图层允许您将图块集中的图块放置到地图上。放置仅限于网格位置,并且必须按照定义放置图块。
  2. 对象图层允许您在地图上放置诸如收藏品或触发器之类的对象。对象可能是来自瓦片地图的瓦片或自由绘制的形状,它们可能是可见的,也可能是不可见的。每个对象都可以自由定位、缩放和旋转。
  3. 图像图层允许您将图像放置在地图上以用作背景或前景图像。
  4. 图层组允许您将图层收集到组中以便于地图管理。

在本教程中,您将使用对象图层在地图上放置硬币,并为其他所有图层放置瓷砖。

要创建新的平铺层,请在Layers视图中单击New Layer,然后选择Tile Layer

在 Tiled 中创建一个新的地图层

创建一个名为三个新的图块层laddersbackgroundgoal

接下来,创建一个名为的新对象层coins来保存您的收藏品:

在 Tiled 中创建一个新的对象地图层

您可以使用图层视图底部的箭头按钮按您喜欢的任何顺序排列图层。现在你可以开始布置你的关卡了!

设计关卡

在《经典游戏设计》一书中,作者兼游戏开发者 Franz Lanzinger 为经典游戏设计定义了八项规则。以下是前三个规则:

  1. 把事情简单化。
  2. 立即开始游戏。
  3. 从简单到困难的斜坡难度。

同样,资深游戏开发者史蒂夫·古德温 (Steve Goodwin) 在他的书《精炼游戏开发》中谈到了平衡游戏。他强调,良好的游戏平衡从第 1 级开始,“应该是第一个开发的,最后一个完成的”。

考虑到这些想法,以下是设计平台游戏关卡的一些指南:

  1. 游戏的第一级应该向用户介绍基本的游戏功能和控制。
  2. 使最初的障碍易于克服。
  3. 使第一个收藏品不可能错过,而后来的收藏品更难获得。
  4. 在用户学会导航之前,不要引入需要技巧才能克服的障碍。
  5. 在用户学会克服障碍之前不要引入敌人。

下面详细介绍了根据这些准则设计的第一级。在可下载的材料中,可以在以下位置找到完整的关卡设计assets/platform_level_01.tmx

第一级街机平台游戏的基本设计

玩家从左边开始,然后向右移动,由指向右边的箭头指示。当玩家向右移动时,他们会找到一枚铜币,这会增加他们的分数。后来发现第二枚铜币悬在空中,这向玩家表明硬币可能在任何地方。然后玩家找到一个金币,它具有不同的点值。

然后玩家爬上斜坡,这表明他们上方有更多的世界。山顶是最后的金币,他们必须跳下去才能拿到。山的另一边是出口,也有标示。

这个简单的关卡有助于向用户展示如何移动和跳跃。它表明世界上有值得积分的收藏品。它还显示提供信息或装饰且玩家不与之交互的物品,例如箭头标志、出口标志和草丛。最后,它向他们展示了目标的样子。

完成第一个关卡的设计工作后,您现在可以在 Tiled 中构建它。

构建关卡

在放置硬币和目标之前,您需要知道如何到达那里。所以首先要定义的是地面的位置。在 Tiled 中选择您的切片地图后,选择ground要构建的图层。

注意:在瓷砖地图上放置瓷砖时,请确保选择了正确的图层。否则,arcade将无法正确处理您的物品。

从您的grassCenter图块集中,选择图块。然后,单击图块地图底行的任何网格以将该图块设置到位:

在 Tiled 中设置第一个地面瓷砖

对于第一个图块集,您可以拖过底行以将所有内容设置为grassCenter。然后,选择grassMid瓷砖以在第二行绘制关卡的草地顶部:

在 Tiled 中放置草方块

继续使用草方块建造关卡,从地球的一半开始建造一个两格高的山丘。在右边缘留出四格的空间,为玩家提供下山的空间以及出口标志和出口门户。

接下来,切换到goal图层并将出口门户瓷砖放置在最右侧边缘的一个瓷砖中:

将目标放在 Tiled 中

基本平台和目标到位后,您可以放置​​一些背景项目。切换到background图层,在左侧放置一个箭头以指示玩家要去哪里,并在门户旁边放置一个退出标志。您还可以在地图上的任何位置放置草丛:

在 Tiled 中放置背景项目

现在您可以定义放置硬币的位置。切换到您的coins图层以执行此操作。请记住,这是一个对象层,因此您不仅限于在网格上放置硬币。选择铜币并将其放置在靠近起始箭头的位置。将第二枚铜币放在稍微靠右一点、再高一点的地方:

在 Tiled 中的水平面上放置青铜硬币对象

用两枚金币重复此过程,将一枚放在山前,一枚放在山顶,至少比山顶高出三格:

在 Tiled 中的关卡上放置金币对象

当玩家收集不同的硬币时,它们应获得不同的分值。有几种方法可以做到这一点,但在本教程中,您将设置一个自定义属性来跟踪每个硬币的点值。

定义自定义属性

使用对象层的好处之一是能够在该层上的对象上设置自定义属性。自定义属性由您定义并代表您希望的任何值。在这种情况下,您将使用它们来指定图层上每个硬币的点值。

硬币层后,按S开始选择对象。然后右键单击您放置的第一枚铜币,并从上下文菜单中选择对象属性以查看其属性:

在 Tiled 中查看对象属性

预定义的对象属性显示在对象属性视图的顶部,而自定义属性显示在下方。目前没有自定义属性,因此您需要添加一个。单击“对象属性”视图底部的蓝色加号以添加新的自定义属性:

向 Tiled 中的对象添加新的自定义属性

您可以定义自定义属性的名称和类型。在这种情况下,您将属性设置为 anint并将名称设置为point_value

注意:虽然自定义属性名称points似乎是更好的选择,但arcade在确定碰撞时在内部使用该属性名称来定义精灵的形状。

定义自定义属性后,您可以在“对象属性”视图中设置其值:

设置自定义属性的值

对关卡中的每个硬币执行相同的步骤,将10铜币和金币的值设置20为 。不要忘记保存关卡,因为接下来您将学习如何将其读入arcade.

阅读游戏关卡

在 Tiled 中定义游戏关卡很棒,但除非您可以将其读入arcade,否则它不是很有用。幸运的是,arcade本机支持读取 Tiled tile maps 和处理图层。完成后,您的游戏将如下所示:

显示 Roz 球员的第一个游戏关卡

读取您的游戏关卡完全在.setup(). 可以在文件中找到此代码arcade_platformer/03_read_level_one.py

注意:如果您随着文章的进行输入代码,则代码块中显示的行号可能与代码中的行号不匹配。

在可能的情况下,添加了额外的上下文,使您能够找到正确的行来添加新代码。

首先,添加更多常量:

# Game constants
# Window dimensions
SCREEN_WIDTH = 1000
SCREEN_HEIGHT = 650
SCREEN_TITLE = "Arcade Platformer"

# Scaling constants
MAP_SCALING = 1.0

# Player constants
GRAVITY = 1.0
PLAYER_START_X = 65
PLAYER_START_Y = 256

这些常量定义了地图的比例因子以及玩家的起始位置和世界中的重力强度。这些常量用于定义中的级别.setup()

def setup(self) -> None:
    """Sets up the game for the current level"""

    # Get the current map based on the level
    map_name = f"platform_level_{self.level:02}.tmx"
    map_path = ASSETS_PATH / map_name

    # What are the names of the layers?
    wall_layer = "ground"
    coin_layer = "coins"
    goal_layer = "goal"
    background_layer = "background"
    ladders_layer = "ladders"

    # Load the current map
    game_map = arcade.tilemap.read_tmx(str(map_path))

    # Load the layers
    self.background = arcade.tilemap.process_layer(
        game_map, layer_name=background_layer, scaling=MAP_SCALING
    )
    self.goals = arcade.tilemap.process_layer(
        game_map, layer_name=goal_layer, scaling=MAP_SCALING
    )
    self.walls = arcade.tilemap.process_layer(
        game_map, layer_name=wall_layer, scaling=MAP_SCALING
    )
    self.ladders = arcade.tilemap.process_layer(
        game_map, layer_name=ladders_layer, scaling=MAP_SCALING
    )
    self.coins = arcade.tilemap.process_layer(
        game_map, layer_name=coin_layer, scaling=MAP_SCALING
    )

    # Set the background color
    background_color = arcade.color.FRESH_AIR
    if game_map.background_color:
        background_color = game_map.background_color
    arcade.set_background_color(background_color)

    # Create the player sprite if they're not already set up
    if not self.player:
        self.player = self.create_player_sprite()

    # Move the player sprite back to the beginning
    self.player.center_x = PLAYER_START_X
    self.player.center_y = PLAYER_START_Y
    self.player.change_x = 0
    self.player.change_y = 0

    # Load the physics engine for this map
    self.physics_engine = arcade.PhysicsEnginePlatformer(
        player_sprite=self.player,
        platforms=self.walls,
        gravity_constant=GRAVITY,
        ladders=self.ladders,
    )

首先,您使用当前级别构建当前瓦片地图的名称。格式字符串{self.level:02}产生一个两位数的级别编号,并允许您定义多达九十九个不同的地图级别。

接下来,使用pathlib语法定义地图的完整路径。这允许arcade正确定位您的所有游戏资源。

接下来,定义您将很快使用的图层名称。确保它们与您在 Tiled 中定义的图层名称匹配。

现在您打开切片地图,以便您可以处理先前命名的图层。该函数arcade.tilemap.process_layer()接受许多参数,但您将只提供其中三个:

  1. game_map,它包含层要被处理
  2. 要读取和处理的层的名称
  3. 应用于图块的任何缩放

arcade.tilemap.process_layer()返回一个SpriteList填充了Sprite代表图层中图块的对象。为图块定义的任何自定义属性(例如图层中point_value的图块)coins都与 一起存储Sprite在名为 的字典中.properties。稍后您将看到如何访问它们。

您还可以设置关卡的背景颜色。您可以在 Tiled 中使用Map → Map Properties并定义Background Color属性来定义自己的背景颜色。如果未在 Tiled 中设置背景颜色,则使用预定义的.FRESH_AIR颜色。

接下来,检查是否已经创建了播放器。如果您调用.setup()以重新启动关卡或移至下一个关卡,则可能会出现这种情况。如果没有,则调用一个方法来创建玩家精灵(稍后会详细介绍)。如果有玩家,则将玩家放置到位并确保它不会移动。

最后,您可以定义要使用的物理引擎,传入以下参数:

  1. 玩家精灵
  2. ASpriteList围墙
  3. 一个常数定义的重力
  4. ASpriteList包含梯子

墙壁决定了玩家可以移动的位置以及何时可以跳跃,而梯子则可以实现攀爬。重力常数控制玩家下落的速度或速度。

当然,现在运行此代码是行不通的,因为您仍然需要定义播放器。

定义播放器

到目前为止,您的游戏中缺少的一件事是玩家:

显示 Roz 球员的第一个游戏关卡

在 中.setup(),您调用了一个方法.create_player_sprite()来定义播放器(如果播放器尚不存在)。您在单独的方法中创建播放器精灵有两个主要原因:

  1. 它将播放器中的任何更改与.setup().
  2. 它有助于简化游戏设置代码。

在任何游戏中,精灵可以是静态的动画的。静态精灵不会随着游戏的进行而改变它们的外观,例如代表您的地砖、背景物品和硬币的精灵。相比之下,动画精灵会随着游戏的进行而改变它们的外观。为了增加一些视觉趣味,您将使您的播放器精灵动画化。

在 Python 中arcade,您可以通过为每个动画序列(例如攀爬或行走)定义一个称为纹理的图像列表来创建动画精灵。随着游戏的进行,arcade从正在动画的序列列表中选择要显示的下一个纹理。当到达列表的末尾arcade时,从头开始。通过仔细挑选纹理,您可以在动画精灵中创建运动错觉:

动画 Roz 角色的纹理选择

由于您的播放器精灵执行许多不同的活动,因此您需要为以下各项提供纹理列表:

  • 站立,面向左右
  • 向右和向左走
  • 爬上爬下梯子

您可以为每个活动提供任意数量的纹理。如果你不想要一个动作动画,你可以提供一个单一的纹理。

该文件arcade_platformer/04_define_player.py包含 的定义.create_player_sprite(),它定义了动画播放器精灵。将此方法放在Platformer下面的类中.setup()

def create_player_sprite(self) -> arcade.AnimatedWalkingSprite:
    """Creates the animated player sprite

    Returns:
        The properly set up player sprite
    """
    # Where are the player images stored?
    texture_path = ASSETS_PATH / "images" / "player"

    # Set up the appropriate textures
    walking_paths = [
        texture_path / f"alienGreen_walk{x}.png" for x in (1, 2)
    ]
    climbing_paths = [
        texture_path / f"alienGreen_climb{x}.png" for x in (1, 2)
    ]
    standing_path = texture_path / "alienGreen_stand.png"

    # Load them all now
    walking_right_textures = [
        arcade.load_texture(texture) for texture in walking_paths
    ]
    walking_left_textures = [
        arcade.load_texture(texture, mirrored=True)
        for texture in walking_paths
    ]

    walking_up_textures = [
        arcade.load_texture(texture) for texture in climbing_paths
    ]
    walking_down_textures = [
        arcade.load_texture(texture) for texture in climbing_paths
    ]

    standing_right_textures = [arcade.load_texture(standing_path)]

    standing_left_textures = [
        arcade.load_texture(standing_path, mirrored=True)
    ]

    # Create the sprite
    player = arcade.AnimatedWalkingSprite()

    # Add the proper textures
    player.stand_left_textures = standing_left_textures
    player.stand_right_textures = standing_right_textures
    player.walk_left_textures = walking_left_textures
    player.walk_right_textures = walking_right_textures
    player.walk_up_textures = walking_up_textures
    player.walk_down_textures = walking_down_textures

    # Set the player defaults
    player.center_x = PLAYER_START_X
    player.center_y = PLAYER_START_Y
    player.state = arcade.FACE_RIGHT

    # Set the initial texture
    player.texture = player.stand_right_textures[0]

    return player

对于您的游戏,您可以在 Roz 行走和攀爬时为其设置动画,但当他们只是站着不动时则不然。每个动画都有两个单独的图像,您的首要任务是定位这些图像。您可以通过单击以下链接下载本教程中使用的所有资产和源代码:

或者,您可以创建一个名为的文件夹assets/images/player来存储用于绘制 Roz 的纹理。然后,在Platformer Pack Redux (360 Assets)您之前下载的存档中,找到该PNG/Players/128x256/Green文件夹,并将其中的所有图像复制到您的新assets/images/player文件夹中。

这个包含玩家纹理的新路径在texture_path. 使用此路径,您可以使用列表推导式f 字符串格式为每个纹理资源创建完整路径名。

拥有这些路径允许您arcade.load_texture()使用更多列表推导来创建纹理列表。由于 Roz 可以左右行走,您可以为每个方向定义不同的列表。图像显示 Roz 指向右侧,因此您可以mirrored在定义 Roz 面向左行走或站立的纹理时使用该参数。向上或向下移动梯子看起来相同,因此这些列表的定义相同。

即使只有一个常设纹理,您仍然需要将它放在一个列表中,以便正确arcade处理AnimatedSprite

所有真正辛苦的工作现在都完成了。您创建实际的AnimatedWalkingSprite,指定要使用的纹理列表。接下来,设置 Roz 的初始位置和方向以及要显示的第一个纹理。最后,在方法结束时返回完全构造的精灵。

现在你有一个初始地图和一个玩家精灵。如果您运行此代码,您应该看到以下内容:

初始播放测试导致黑屏。

嗯,这不是很有趣。那是因为虽然您已经创建了所有内容,但您当前并未更新或绘制任何内容。是时候解决这个问题了!

更新和绘图

更新游戏状态发生在 中.on_update()arcade大约每秒调用 60 次。此方法处理以下操作和事件:

  • 移动玩家和敌人的精灵
  • 检测与敌人或收藏品的碰撞
  • 更新分数
  • 动画精灵

简而言之,使您的游戏可玩的一切都发生在.on_update(). 更新完所有内容后,arcade调用.on_draw()将所有内容渲染到屏幕上。

游戏逻辑与游戏显示的这种分离意味着您可以在游戏中自由添加或修改功能,而不会影响显示游戏的代码。事实上,因为大部分游戏逻辑发生在 中.on_update(),你的.on_draw()方法往往很短。

您可以arcade_platformer/05_update_and_draw.py在可下载的材料中找到以下所有代码。添加.on_draw()到您的Platformer班级:

def on_draw(self) -> None:
    arcade.start_render()

    # Draw all the sprites
    self.background.draw()
    self.walls.draw()
    self.coins.draw()
    self.goals.draw()
    self.ladders.draw()
    self.player.draw()

在强制调用 之后arcade.start_render(),调用.draw()所有精灵列表,然后是玩家精灵。请注意绘制项目的顺序。您应该从最靠后的精灵开始,然后继续前进。现在,当您运行代码时,它应该如下所示:

真正的初始播放测试屏幕绘制到窗口中。

唯一缺少的是正确放置玩家精灵。为什么?因为动画精灵需要更新以选择合适的纹理来显示并在屏幕上正确放置,而您还没有更新任何内容。这是它的样子:

def on_update(self, delta_time: float) -> None:
    """Updates the position of all game objects

    Arguments:
        delta_time {float} -- How much time since the last call
    """

    # Update the player animation
    self.player.update_animation(delta_time)

    # Update player movement based on the physics engine
    self.physics_engine.update()

    # Restrict user movement so they can't walk off screen
    if self.player.left < 0:
        self.player.left = 0

    # Check if we've picked up a coin
    coins_hit = arcade.check_for_collision_with_list(
        sprite=self.player, sprite_list=self.coins
    )

    for coin in coins_hit:
        # Add the coin score to our score
        self.score += int(coin.properties["point_value"])

        # Play the coin sound
        arcade.play_sound(self.coin_sound)

        # Remove the coin
        coin.remove_from_sprite_lists()

    # Now check if we're at the ending goal
    goals_hit = arcade.check_for_collision_with_list(
        sprite=self.player, sprite_list=self.goals
    )

    if goals_hit:
        # Play the victory sound
        self.victory_sound.play()

        # Set up the next level
        self.level += 1
        self.setup()

为了确保你的游戏在运行恒定的速度不管实际的帧速率,.on_update()需要一个float称为参数delta_time,它表示自上次更新的时间。

首先要做的是为玩家精灵设置动画。根据玩家的移动,.update_animation()自动选择要使用的正确纹理。

接下来,您更新所有可以移动的东西的移动。由于您在 中定义了物理引擎.setup(),因此让它处理运动是有意义的。但是,物理引擎会让玩家跑出游戏地图的左侧,因此您还需要采取措施防止这种情况发生。

重要提示:请确保AnimatedSprite.update_animation()在 之前致电PhysicsEnginePlatformer.update()。通过首先更新精灵,您可以确保物理引擎将根据当前精灵设置而不是前一帧的精灵设置进行操作。

现在玩家已经移动了,你检查他们是否与硬币相撞。如果是这样,这算作收集硬币,因此您可以使用point_value您在 Tiled 中定义的自定义属性来增加玩家的分数。然后您播放声音并从游戏场中取出硬币。

您还可以检查玩家是否达到了最终目标。如果是这样,您播放胜利声音,增加级别,然后.setup()再次调用以加载下一张地图并重置其中的玩家。

但是用户如何达到最终目标呢?物理引擎将确保 Roz 不会从地板上掉下来并且可以跳跃,但它实际上并不知道将 Roz 移动到何处或何时跳跃。这是用户应该决定的事情,您需要为他们提供一种方法来做到这一点。

移动玩家精灵

在电脑游戏的早期,唯一可用的输入设备是键盘。即使在今天,许多游戏——包括这个游戏——仍然提供键盘控制。

可以通过多种方式使用键盘移动播放器。有许多不同的流行键盘排列,包括:

当然,还有许多其他键盘排列可供选择。

由于您需要允许 Roz 在所有四个方向上移动以及跳跃,因此在此游戏中,您将使用箭头和 IJKL 键进行移动,使用空格键进行跳跃:

所有键盘输入arcade都由.on_key_press()和处理.on_key_release()。您可以在 中找到通过键盘使 Roz 移动的代码arcade_platformer/06_keyboard_movement.py

首先,您需要两个新常量:

23# Player constants
24GRAVITY = 1.0
25PLAYER_START_X = 65
26PLAYER_START_Y = 256
27PLAYER_MOVE_SPEED = 10
28PLAYER_JUMP_SPEED = 20

这些常数控制 Roz 移动的速度。PLAYER_MOVE_SPEED控制他们向左、向右和上下梯子的移动。PLAYER_JUMP_SPEED表示 Roz 能跳多高。通过将这些值设置为常量,您可以调整它们以在测试期间拨入正确的游戏玩法。

您在 中使用这些常量.on_key_press()

def on_key_press(self, key: int, modifiers: int) -> None:
    """Arguments:
    key -- Which key was pressed
    modifiers -- Which modifiers were down at the time
    """

    # Check for player left or right movement
    if key in [arcade.key.LEFT, arcade.key.J]:
        self.player.change_x = -PLAYER_MOVE_SPEED
    elif key in [arcade.key.RIGHT, arcade.key.L]:
        self.player.change_x = PLAYER_MOVE_SPEED

    # Check if player can climb up or down
    elif key in [arcade.key.UP, arcade.key.I]:
        if self.physics_engine.is_on_ladder():
            self.player.change_y = PLAYER_MOVE_SPEED
    elif key in [arcade.key.DOWN, arcade.key.K]:
        if self.physics_engine.is_on_ladder():
            self.player.change_y = -PLAYER_MOVE_SPEED

    # Check if player can jump
    elif key == arcade.key.SPACE:
        if self.physics_engine.can_jump():
            self.player.change_y = PLAYER_JUMP_SPEED
            # Play the jump sound
            arcade.play_sound(self.jump_sound)

这段代码包含三个主要组成部分:

  1. 您可以通过检查IJKL 排列中的LeftRight箭头以及JL键来处理水平移动。然后,您可以.change_x适当地设置该属性。

  2. 您可以通过检查UpDown箭头以及IK键来处理垂直移动。但是,由于 Roz 只能在梯子上上下移动,因此您.is_on_ladder()在上下移动之前验证使用。

  3. 你通过Space钥匙处理跳跃。为了防止 Roz 在半空中跳跃,您可以使用 来检查 Roz 是否可以跳跃.can_jump(),它True仅在 Roz 站在墙上时返回。如果是这样,您将播放器向上移动并播放跳跃声音。

当您松开按键时,Roz 应该停止移动。你在.on_key_release()

def on_key_release(self, key: int, modifiers: int) -> None:
    """Arguments:
    key -- The key which was released
    modifiers -- Which modifiers were down at the time
    """

    # Check for player left or right movement
    if key in [
        arcade.key.LEFT,
        arcade.key.J,
        arcade.key.RIGHT,
        arcade.key.L,
    ]:
        self.player.change_x = 0

    # Check if player can climb up or down
    elif key in [
        arcade.key.UP,
        arcade.key.I,
        arcade.key.DOWN,
        arcade.key.K,
    ]:
        if self.physics_engine.is_on_ladder():
            self.player.change_y = 0

此代码遵循与以下类似的模式.on_key_press()

  1. 您检查是否有任何水平移动键被释放。如果是,则 Roz'schange_x设置为 0。
  2. 您检查垂直移动键是否被释放。同样,由于 Roz 需要在梯子上上下移动,因此您也需要在.is_on_ladder()这里检查。如果没有,玩家可以跳跃然后按下并释放Up,让 Roz 悬在半空中!

请注意,您不需要检查跳跃键是否被释放。

好的,现在你可以移动 Roz 了,但是为什么 Roz 只是向右走出窗户呢?你需要一种方法来让 Roz 在他们四处移动时在游戏世界中可见,这就是视口的用武之地。

滚动视口

早期的视频游戏将游戏玩法限制在一个窗口,这是玩家的整个世界。然而,现代视频游戏世界可能太大而无法容纳在一个很小的游戏窗口中。大多数游戏都实现了滚动视图,它向玩家展示了游戏世界的一部分。在 Python 中arcade,这种滚动视图称为视口。它本质上是一个矩形,用于定义您在游戏窗口中显示的游戏世界的哪一部分:

您可以在 下的可下载材料中找到此代码arcade_platformer/07_scrolling_view.py

要实现滚动视图,您需要根据 Roz 的当前位置定义视口。当 Roz 靠近游戏窗口的任何边缘时,您可以沿行进方向移动视口,以便 Roz 舒适地停留在屏幕上。您还确保视口不会滚动到可见世界之外。为此,您需要了解以下几点:

  • 在视口滚动之前,Roz 可以移动到游戏窗口边缘多近?这称为边距,每个窗口边缘的边距可能不同。
  • 当前视口现在在哪里?
  • 你的游戏地图有多宽?
  • 罗兹现在在哪里?

首先,您将边距定义为代码顶部的常量:

# Player constants
GRAVITY = 1.0
PLAYER_START_X = 65
PLAYER_START_Y = 256
PLAYER_MOVE_SPEED = 10
PLAYER_JUMP_SPEED = 20

# Viewport margins
# How close do we have to be to scroll the viewport?
LEFT_VIEWPORT_MARGIN = 50
RIGHT_VIEWPORT_MARGIN = 300
TOP_VIEWPORT_MARGIN = 150
BOTTOM_VIEWPORT_MARGIN = 150

注意之间的差异LEFT_VIEWPORT_MARGINRIGHT_VIEWPORT_MARGIN。这允许 Roz 更接近左边而不是右边。这样,当 Roz 向右移动时,用户有更多时间查看障碍物并对障碍物做出反应。

视口是一个与游戏窗口具有相同宽度和高度的矩形,它们是常量SCREEN_WIDTHSCREEN_HEIGHT。因此,要完整地描述视口,您只需要知道左下角的位置。通过改变这个角,视口将对 Roz 的移动做出反应。.setup()在您将 Roz 移动到关卡的开头之后,您可以在游戏对象中跟踪这个角落并在 中定义它:

# Move the player sprite back to the beginning
self.player.center_x = PLAYER_START_X
self.player.center_y = PLAYER_START_Y
self.player.change_x = 0
self.player.change_y = 0

# Reset the viewport
self.view_left = 0
self.view_bottom = 0

对于本教程,由于每个级别都从同一个位置开始,因此视口的左下角也始终从同一位置开始。

您可以通过将游戏地图中包含的瓷砖数量乘以每个瓷砖的宽度来计算游戏地图的宽度。您在阅读每张地图并在.setup()以下位置设置背景颜色后计算:

# Set the background color
background_color = arcade.color.FRESH_AIR
if game_map.background_color:
    background_color = game_map.background_color
arcade.set_background_color(background_color)

# Find the edge of the map to control viewport scrolling
self.map_width = (
    game_map.map_size.width - 1
) * game_map.tile_size.width

1game_map.map_size.width平铺使用的平铺索引的校正中减去。

最后,您可以通过检查 中的任何位置属性随时了解 Roz 所在的位置self.player

以下是您如何使用所有这些信息在 中滚动视口.update()

  1. 更新 Roz 的位置后,您计算它们是否在四个边中任何一个边距的距离内。
  2. 如果是这样,您将视口向那个方向移动 Roz 在边距内的量。

您可以将此代码放在类的单独方法中,Platformer以便更轻松地进行更新:

def scroll_viewport(self) -> None:
    """Scrolls the viewport when the player gets close to the edges"""
    # Scroll left
    # Find the current left boundary
    left_boundary = self.view_left + LEFT_VIEWPORT_MARGIN

    # Are we to the left of this boundary? Then we should scroll left.
    if self.player.left < left_boundary:
        self.view_left -= left_boundary - self.player.left
        # But don't scroll past the left edge of the map
        if self.view_left < 0:
            self.view_left = 0

    # Scroll right
    # Find the current right boundary
    right_boundary = self.view_left + SCREEN_WIDTH - RIGHT_VIEWPORT_MARGIN

    # Are we to the right of this boundary? Then we should scroll right.
    if self.player.right > right_boundary:
        self.view_left += self.player.right - right_boundary
        # Don't scroll past the right edge of the map
        if self.view_left > self.map_width - SCREEN_WIDTH:
            self.view_left = self.map_width - SCREEN_WIDTH

    # Scroll up
    top_boundary = self.view_bottom + SCREEN_HEIGHT - TOP_VIEWPORT_MARGIN
    if self.player.top > top_boundary:
        self.view_bottom += self.player.top - top_boundary

    # Scroll down
    bottom_boundary = self.view_bottom + BOTTOM_VIEWPORT_MARGIN
    if self.player.bottom < bottom_boundary:
        self.view_bottom -= bottom_boundary - self.player.bottom

    # Only scroll to integers. Otherwise we end up with pixels that
    # don't line up on the screen.
    self.view_bottom = int(self.view_bottom)
    self.view_left = int(self.view_left)

    # Do the scrolling
    arcade.set_viewport(
        left=self.view_left,
        right=SCREEN_WIDTH + self.view_left,
        bottom=self.view_bottom,
        top=SCREEN_HEIGHT + self.view_bottom,
    )

这段代码看起来有点混乱,所以看一个具体的例子可能会很有用,比如当 Roz 向右移动并且你需要滚动视口时会发生什么。这是您将完成的代码:

# Scroll right
# Find the current right boundary
right_boundary = self.view_left + SCREEN_WIDTH - RIGHT_VIEWPORT_MARGIN

# Are we right of this boundary? Then we should scroll right.
if self.player.right > right_boundary:
    self.view_left += self.player.right - right_boundary
    # Don't scroll past the right edge of the map
    if self.view_left > self.map_width - SCREEN_WIDTH:
        self.view_left = self.map_width - SCREEN_WIDTH

以下是关键变量的一些示例值:

  • Roz 向右移动,将他们的self.player.right属性设置为710
  • 视口尚未更改,self.view_left当前也是0
  • 常数SCREEN_WIDTH1000
  • 常数RIGHT_VIEWPORT_MARGIN300

首先,计算 的值right_boundary,这决定了 Roz 是否在视口右边缘的边距内:

  • 可见视口的右边缘是self.view_left + SCREEN_WIDTH,即1000
  • 减去RIGHT_VIEWPORT_MARGIN从这个给你right_boundary700

接下来,检查 Roz 是否超出了right_boundary. 由于self.player.right > right_boundaryis True,您需要移动视口,因此您可以计算将其移动多远:

  • 计算self.player.right - right_boundary10,这是 Roz 移动到右边距的距离。
  • 由于视口矩形从左侧测量,加上这self.view_left使它10

但是,您不想将视口移出世界的边缘。如果视口一直向右滚动,它的左边缘将是一个小于地图宽度的全屏宽度:

  • 检查是否self.view_left > self.map_width - SCREEN_WIDTH
  • 如果是这样,只需设置self.view_left为该值即可限制视口移动。

您对左边界执行相同的步骤序列。还检查顶部和底部边缘以进行更新self.view_bottom。更新两个视图变量后,最后要做的就是使用arcade.set_viewport().

由于您将此代码放在单独的方法中,因此请在 末尾调用它.on_update()

if goals_hit:
    # Play the victory sound
    self.victory_sound.play()

    # Set up the next level
    self.level += 1
    self.setup()

# Set the viewport, scrolling if necessary
self.scroll_viewport()

有了这个,你的游戏视图应该跟随 Roz 向左、向右、向上或向下移动,永远不要让他们离开屏幕!

就是这样 - 你有一个平台游戏!现在是添加一些额外功能的时候了!

添加额外功能

除了通过越来越复杂的平台添加关卡外,您还可以添加许多其他功能,让您的游戏脱颖而出。本教程将介绍其中的一些,包括:

  • 保持屏幕上的分数
  • 使用操纵杆或游戏控制器控制 Roz
  • 添加标题、结束游戏、帮助和暂停屏幕
  • 自动移动敌人和平台

由于您已经在滚动视图中看到了它的运行情况,让我们从在屏幕上添加运行分数开始。

屏幕分数

您已经在 中跟踪玩家的分数self.score,这意味着您需要做的就是在屏幕上绘制它。您可以.on_draw()使用arcade.draw_text()以下方法处理:

在屏幕上显示分数。

您可以在arcade_platformer/08_on_screen_score.py.

绘制分数的代码出现在 的底部.on_draw(),就在self.player.draw()调用之后。您最后绘制分数,因此它始终可见于其他所有内容:

def on_draw(self) -> None:
    arcade.start_render()

    # Draw all the sprites
    self.background.draw()
    self.walls.draw()
    self.coins.draw()
    self.goals.draw()
    self.ladders.draw()
    self.player.draw()

    # Draw the score in the lower left
    score_text = f"Score: {self.score}"

    # First a black background for a shadow effect
    arcade.draw_text(
        score_text,
        start_x=10 + self.view_left,
        start_y=10 + self.view_bottom,
        color=arcade.csscolor.BLACK,
        font_size=40,
    )
    # Now in white, slightly shifted
    arcade.draw_text(
        score_text,
        start_x=15 + self.view_left,
        start_y=15 + self.view_bottom,
        color=arcade.csscolor.WHITE,
        font_size=40,
    )

首先,您构建显示当前分数的字符串。这是后续调用将显示的内容arcade.draw_text()。然后在屏幕上绘制实际文本,传入以下参数:

  • 要绘制的文本
  • start_xstart_y坐标指示从哪里开始绘制文本
  • color绘制文本
  • font_size要使用的入点

通过将start_xstart_y参数基于视口属性self.view_leftself.view_bottom,您可以确保分数始终显示在窗口中的相同位置,即使视口移动时也是如此。

您第二次绘制相同的文本,但稍微移动并使用较浅的颜色以提供一些对比。

有更多选项可用于arcade.draw_text(),包括指定粗体或斜体文本以及使用特定于游戏的字体。查看文档以根据自己的喜好自定义文本。

操纵杆和游戏控制器

平台游戏与操纵杆和游戏控制器配合得非常好。控制板、摇杆和无数按钮为您提供了许多机会来最终控制屏幕上的角色。添加操纵杆控制可帮助您的游戏脱颖而出。

与键盘控制不同,没有要覆盖的特定操纵杆方法。相反,arcade提供了一个函数来设置操纵杆并公开变量和方法pyglet来读取实际操纵杆和按钮的状态。你在你的游戏中使用以下这些子集:

  • arcade.get_joysticks()返回连接到系统的操纵杆列表。如果此列表为空,则不存在操纵杆。
  • joystick.x并分别joystick.y返回水平和垂直方向的摇杆偏转状态。这些float值的范围从 -1.0 到 1.0,需要转换为对您的游戏有用的值。
  • joystick.buttons返回指定控制器上所有按钮状态的布尔值列表。如果按下了按钮,则其值将为True

有关可用操纵杆变量和方法的完整列表,请查看pyglet文档

可以在arcade_platformer/09_joystick_control.py.

在您的玩家可以使用操纵杆之前,您需要验证游戏.__init__()方法中是否附加了操纵杆。加载游戏声音后会出现此代码:

# Check if a joystick is connected
joysticks = arcade.get_joysticks()

if joysticks:
    # If so, get the first one
    self.joystick = joysticks[0]
    self.joystick.open()
else:
    # If not, flag it so we won't use it
    print("There are no Joysticks")
    self.joystick = None

首先,您使用 枚举所有连接的操纵杆arcade.get_joysticks()。如果找到,第一个保存为self.joystick. 否则,您设置self.joystick = None.

检测和定义操纵杆后,您可以读取它以提供对 Roz 的控制。.on_update()在任何其他检查之前,您在 , 顶部执行此操作:

def on_update(self, delta_time: float) -> None:
    """Updates the position of all game objects

    Arguments:
        delta_time {float} -- How much time since the last call
    """

    # First, check for joystick movement
    if self.joystick:
        # Check if we're in the dead zone
        if abs(self.joystick.x) > DEAD_ZONE:
            self.player.change_x = self.joystick.x * PLAYER_MOVE_SPEED
        else:
            self.player.change_x = 0

        if abs(self.joystick.y) > DEAD_ZONE:
            if self.physics_engine.is_on_ladder():
                self.player.change_y = self.joystick.y * PLAYER_MOVE_SPEED
            else:
                self.player.change_y = 0

        # Did the user press the jump button?
        if self.joystick.buttons[0]:
            if self.physics_engine.can_jump():
                self.player.change_y = PLAYER_JUMP_SPEED
                # Play the jump sound
                arcade.play_sound(self.jump_sound)

    # Update the player animation
    self.player.update_animation(delta_time)

在读取操纵杆之前,首先确保连接了操纵杆。

所有静止的操纵杆都围绕中心值或零值波动。因为joystick.xjoystick.y返回float值,这些波动可能导致返回值略高于或低于零,这将转化为 Roz 在没有任何操纵杆输入的情况下非常轻微地移动。

为了解决这个问题,游戏设计师定义了一个包含这些小波动的操纵杆死区。对此死区joystick.x或其joystick.y内的任何更改都将被忽略。您可以通过首先DEAD_ZONE在代码顶部定义一个常量来实现死区:

# Viewport margins
# How close do we have to be to scroll the viewport?
LEFT_VIEWPORT_MARGIN = 50
RIGHT_VIEWPORT_MARGIN = 300
TOP_VIEWPORT_MARGIN = 150
BOTTOM_VIEWPORT_MARGIN = 150

# Joystick control
DEAD_ZONE = 0.1

现在您可以检查操纵杆是否移动超过DEAD_ZONE。如果没有,则忽略操纵杆输入。否则,您将操纵杆值乘以PLAYER_MOVE_SPEED移动 Roz。这允许玩家根据推动操纵杆的距离来更慢或更快地移动 Roz。请记住,在允许他们上下移动之前,您仍然必须检查 Roz 是否在梯子上。

接下来,你处理跳跃。如果按下操纵杆上的第一个按钮,也就是A我的游戏手柄上的按钮,您会将其解释为跳跃命令,并使 Roz 以相同的方式跳跃Space

就是这样!现在,您可以使用操作系统连接并支持的任何操纵杆来控制 Roz!

标题和其他屏幕

刚开始没有介绍的游戏会让您的用户感到被抛弃。除非他们已经知道该怎么做,否则在没有标题画面或基本说明的情况下直接从第 1 级开始游戏可能会令人不安。您可以arcade使用views解决这个问题。

一个视图arcade代表您想要向用户显示的任何内容,无论是静态文本、关卡之间的过场动画还是实际游戏本身。视图基于类arcade.View,可用于向用户显示信息以及允许他们玩您的游戏:

对于此游戏,您将定义三个单独的视图:

  1. 标题视图允许用户开始游戏或查看帮助屏幕。
  2. 说明视图向用户显示背景故事和基本控件。
  3. 当用户暂停游戏时显示暂停视图

为了让一切变得无缝,您首先需要将您的游戏转换为视图,所以您现在就来处理吧!

这 PlatformerView

修改现有游戏以无缝使用视图需要三个单独的代码更改。您可以在 中的可下载材料中找到这些更改arcade_platformer/10_view_conversion.py。您可以通过单击以下链接下载本教程中使用的所有材料和代码:

第一个是对您的Platformer班级进行单行更改:

class PlatformerView(arcade.View):
    def __init__(self) -> None:
        super().__init__()

为了保持命名的一致性,您需要更改类的名称以及基类的名称。在功能上,PlatformerView该类包含与原始Platformer类相同的方法。

第二个变化是在.__init__(),您不再传递常量SCREEN_WIDTHSCREEN_HEIGHT, 或SCREEN_TITLE。这是因为您的PlatformerView类现在基于arcade.View,它不使用这些常量。您的super()电话也会更改以反映这一点。

为什么不再需要这些常量了?视图不是窗口,因此无需传入这些arcade.Window参数。那么你在哪里定义游戏窗口的大小和外观呢?

这发生在文件底部的最终更改中__main__

if __name__ == "__main__":
    window = arcade.Window(
        width=SCREEN_WIDTH, height=SCREEN_HEIGHT, title=SCREEN_TITLE
    )
    platform_view = PlatformerView()
    platform_view.setup()
    window.show_view(platform_view)
    arcade.run()

您显式地创建一个arcade.Window用于显示您的视图。然后创建PlatformerView对象,调用.setup(),并使用window.show_view(platformer_view)它来显示它。一旦它可见,您就可以像以前一样运行您的游戏。

这些更改应该不会导致游戏玩法发生任何功能上的变化,因此在对此进行测试之后,您就可以添加标题视图了。

标题视图

任何游戏的标题视图都应该稍微展示一下游戏,并允许玩家在闲暇时开始游戏。虽然动画标题页是可能的,但在本教程中,您将创建一个带有简单菜单的静态标题视图,以允许用户启动游戏或查看帮助屏幕:

可以在 中找到此代码arcade_platformer/11_title_view.py

创建标题视图首先为其定义一个新类:

class TitleView(arcade.View):
    """Displays a title screen and prompts the user to begin the game.
    Provides a way to show instructions and start the game.
    """

    def __init__(self) -> None:
        super().__init__()

        # Find the title image in the images folder
        title_image_path = ASSETS_PATH / "images" / "title_image.png"

        # Load our title image
        self.title_image = arcade.load_texture(title_image_path)

        # Set our display timer
        self.display_timer = 3.0

        # Are we showing the instructions?
        self.show_instructions = False

标题视图显示一个简单的静态图像。

注意:此处使用的标题图像仅在可下载材料中提供。您可以按照提供的方式使用它,也可以根据需要替换自己的。

您可以使用self.display_timerself.show_instructions属性使一组说明在屏幕上闪烁。这在.on_update()您在TitleView类中创建的 中处理:

def on_update(self, delta_time: float) -> None:
    """Manages the timer to toggle the instructions

    Arguments:
        delta_time -- time passed since last update
    """

    # First, count down the time
    self.display_timer -= delta_time

    # If the timer has run out, we toggle the instructions
    if self.display_timer < 0:

        # Toggle whether to show the instructions
        self.show_instructions = not self.show_instructions

        # And reset the timer so the instructions flash slowly
        self.display_timer = 1.0

回想一下,该delta_time参数告诉您自上次调用 以来已经过去了多长时间.on_update()。每次.on_update()被调用时,你减去delta_timeself.display_timer。当它通过零时,您切换self.show_instructions并重置计时器。

那么这个是如何控制显示指令的呢?这一切都发生在.on_draw()

def on_draw(self) -> None:
    # Start the rendering loop
    arcade.start_render()

    # Draw a rectangle filled with our title image
    arcade.draw_texture_rectangle(
        center_x=SCREEN_WIDTH / 2,
        center_y=SCREEN_HEIGHT / 2,
        width=SCREEN_WIDTH,
        height=SCREEN_HEIGHT,
        texture=self.title_image,
    )

    # Should we show our instructions?
    if self.show_instructions:
        arcade.draw_text(
            "Enter to Start | I for Instructions",
            start_x=100,
            start_y=220,
            color=arcade.color.INDIGO,
            font_size=40,
        )

绘制背景图像后,检查是否self.show_instructions设置。如果是这样,您可以使用 绘制说明文本arcade.draw_text()。否则,你什么也不画。由于每秒.on_update()切换self.show_instructions一次的值,这会使文本在屏幕上闪烁。

指令要求玩家点击EnterI,因此您需要提供一个.on_key_press()方法:

def on_key_press(self, key: int, modifiers: int) -> None:
    """Resume the game when the user presses ESC again

    Arguments:
        key -- Which key was pressed
        modifiers -- What modifiers were active
    """
    if key == arcade.key.RETURN:
        game_view = PlatformerView()
        game_view.setup()
        self.window.show_view(game_view)
    elif key == arcade.key.I:
        instructions_view = InstructionsView()
        self.window.show_view(instructions_view)

如果用户按下Enter,您将创建一个PlatformerView名为的对象game_view,调用game_view.setup()并显示该视图以开始游戏。如果用户按下I,您将创建一个InstructionsView对象(更多内容见下文)并显示它。

最后,您希望标题屏幕是用户看到的第一件事,因此您也要更新您的__main__部分:

if __name__ == "__main__":
    window = arcade.Window(
        width=SCREEN_WIDTH, height=SCREEN_HEIGHT, title=SCREEN_TITLE
    )
    title_view = TitleView()
    window.show_view(title_view)
    arcade.run()

现在,指令视图是什么?

说明查看

显示用户游戏说明可以像完整游戏一样复杂,也可以像标题屏幕一样轻巧:

在这种情况下,您的说明视图与标题屏幕非常相似:

  • 显示带有游戏说明的预生成图像。
  • 允许玩家在按下 时开始游戏Enter
  • 如果玩家按下 返回标题画面Esc

由于没有定时器,所以只需要实现三个方法:

  1. .__init__() 加载说明图像
  2. .on_draw() 绘制图像
  3. .on_key_press() 处理用户输入

您可以在arcade_platformer/12_instructions_view.py以下位置找到此代码:

class InstructionsView(arcade.View):
    """Show instructions to the player"""

    def __init__(self) -> None:
        """Create instructions screen"""
        super().__init__()

        # Find the instructions image in the image folder
        instructions_image_path = (
            ASSETS_PATH / "images" / "instructions_image.png"
        )

        # Load our title image
        self.instructions_image = arcade.load_texture(instructions_image_path)

    def on_draw(self) -> None:
        # Start the rendering loop
        arcade.start_render()

        # Draw a rectangle filled with the instructions image
        arcade.draw_texture_rectangle(
            center_x=SCREEN_WIDTH / 2,
            center_y=SCREEN_HEIGHT / 2,
            width=SCREEN_WIDTH,
            height=SCREEN_HEIGHT,
            texture=self.instructions_image,
        )

    def on_key_press(self, key: int, modifiers: int) -> None:
        """Start the game when the user presses Enter

        Arguments:
            key -- Which key was pressed
            modifiers -- What modifiers were active
        """
        if key == arcade.key.RETURN:
            game_view = PlatformerView()
            game_view.setup()
            self.window.show_view(game_view)

        elif key == arcade.key.ESCAPE:
            title_view = TitleView()
            self.window.show_view(title_view)

有了这个,您现在可以向您的播放器显示标题屏幕和说明,并允许他们在屏幕之间移动。

但是如果有人在玩你的游戏并且电话响了怎么办?让我们看看如何使用视图来实现暂停功能。

暂停视图

实现暂停功能需要您编写两个新功能:

  1. 一个可以暂停和取消暂停游戏的按键
  2. 一种表示游戏暂停的方法

当用户暂停时,他们会看到如下所示的内容:

您可以在arcade_platformer/13_pause_view.py.

PlatformerView.on_keypress()在检查跳转键后,您将按键添加到 中:

# Check if we can jump
elif key == arcade.key.SPACE:
    if self.physics_engine.can_jump():
        self.player.change_y = PLAYER_JUMP_SPEED
        # Play the jump sound
        arcade.play_sound(self.jump_sound)

# Did the user want to pause?
elif key == arcade.key.ESCAPE:
    # Pass the current view to preserve this view's state
    pause = PauseView(self)
    self.window.show_view(pause)

当玩家点击 时Esc,游戏会创建一个新PauseView对象并显示它。由于PlatformerView将不再主动显示,因此它无法处理任何方法调用,例如.on_update().on_draw()。这有效地阻止了游戏运行。

需要注意的一件事是创建新PauseView对象的行。这里传入的self是对当前PlatformerView对象的引用。记住这一点,因为这在以后很重要。

现在您可以创建新PauseView类。这个类TitleViewInstructionView您已经实现的和类非常相似。最大的区别在于视图显示的内容。不是完全覆盖游戏屏幕的图形,而是PauseView显示覆盖有半透明层的活动游戏屏幕。在这一层上绘制的文字表示游戏已暂停,而背景则向用户显示暂停的位置。

定义暂停视图从定义类及其.__init__()方法开始:

class PauseView(arcade.View):
    """Shown when the game is paused"""

    def __init__(self, game_view: arcade.View) -> None:
        """Create the pause screen"""
        # Initialize the parent
        super().__init__()

        # Store a reference to the underlying view
        self.game_view = game_view

        # Store a semitransparent color to use as an overlay
        self.fill_color = arcade.make_transparent_color(
            arcade.color.WHITE, transparency=150
        )

在这里,.__init__()接受一个名为 的参数game_view。这是对PlatformerView您在创建PauseView对象时传递的游戏的引用。您需要将此引用存储在其中,self.game_view因为您稍后将使用它。

要创建半透明图层效果,您还需要创建一种半透明颜色,用于填充屏幕PauseView.on_draw()

def on_draw(self) -> None:
    """Draw the underlying screen, blurred, then the Paused text"""

    # First, draw the underlying view
    # This also calls start_render(), so no need to do it again
    self.game_view.on_draw()

    # Now create a filled rect that covers the current viewport
    # We get the viewport size from the game view
    arcade.draw_lrtb_rectangle_filled(
        left=self.game_view.view_left,
        right=self.game_view.view_left + SCREEN_WIDTH,
        top=self.game_view.view_bottom + SCREEN_HEIGHT,
        bottom=self.game_view.view_bottom,
        color=self.fill_color,
    )

    # Now show the Pause text
    arcade.draw_text(
        "PAUSED - ESC TO CONTINUE",
        start_x=self.game_view.view_left + 180,
        start_y=self.game_view.view_bottom + 300,
        color=arcade.color.INDIGO,
        font_size=40,
    )

请注意,您在PlatformerView此处使用了对当前对象的保存引用。首先通过调用显示游戏的当前状态self.game_view.on_draw()。由于self.game_view仍在内存中且处于活动状态,因此这是完全可以接受的。只要self.game_view.on_update()从未被调用,您将始终在按下暂停键时绘制游戏的静态视图。

接下来,您绘制一个覆盖整个窗口的矩形,用 中定义的半透明颜色填充.__init__()。由于这是在游戏绘制对象之后发生的,所以看起来好像迷雾笼罩在游戏上。

为了清楚地表明游戏已暂停,您最终通过在屏幕上显示一条消息来告知用户这一事实。

取消Esc暂停游戏使用与暂停相同的按键,因此您必须处理它:

def on_key_press(self, key: int, modifiers: int) -> None:
    """Resume the game when the user presses ESC again

    Arguments:
        key -- Which key was pressed
        modifiers -- What modifiers were active
    """
    if key == arcade.key.ESCAPE:
        self.window.show_view(self.game_view)

这是保存self.game_view参考的最终原因。当玩家Esc再次按下时,您需要从上次停止的地方重新激活游戏。而不是创建一个新的PlatformerView,您只需显示您之前保存的已经激活的视图。

使用这些技术,您可以实现任意数量的视图。一些扩展的想法包括:

  • 游戏结束时的游戏概览
  • 在关卡之间转换并允许过场动画的关卡结束视图
  • 如果玩家选择重新启动关卡,则会显示一个特殊的重新启动屏幕
  • 广受欢迎的老板键,为工作中的玩家提供电子表格覆盖

所有的选择都是你的!

移动敌人和平台

让屏幕上的东西自动移动并不像听起来那么困难。您不是根据玩家输入移动对象,而是根据内部和游戏状态移动对象。您将实现两种不同类型的运动:

  1. 在密闭区域内自由移动的敌人
  2. 在固定路径上移动的平台

您将探索让敌人先移动。

敌人运动

您可以在arcade_platformer/14_enemies.py和中的可下载材料中找到本节的代码assets/platform_level_02.tmx。它会告诉你如何让你的游戏像这样:

在你让敌人移动之前,你必须有一个敌人。在本教程中,您将在代码中定义敌人,这需要一个敌人类:

class Enemy(arcade.AnimatedWalkingSprite):
    """An enemy sprite with basic walking movement"""

    def __init__(self, pos_x: int, pos_y: int) -> None:
        super().__init__(center_x=pos_x, center_y=pos_y)

        # Where are the player images stored?
        texture_path = ASSETS_PATH / "images" / "enemies"

        # Set up the appropriate textures
        walking_texture_path = [
            texture_path / "slimePurple.png",
            texture_path / "slimePurple_move.png",
        ]
        standing_texture_path = texture_path / "slimePurple.png"

        # Load them all now
        self.walk_left_textures = [
            arcade.load_texture(texture) for texture in walking_texture_path
        ]

        self.walk_right_textures = [
            arcade.load_texture(texture, mirrored=True)
            for texture in walking_texture_path
        ]

        self.stand_left_textures = [
            arcade.load_texture(standing_texture_path, mirrored=True)
        ]
        self.stand_right_textures = [
            arcade.load_texture(standing_texture_path)
        ]

        # Set the enemy defaults
        self.state = arcade.FACE_LEFT
        self.change_x = -PLAYER_MOVE_SPEED // 2

        # Set the initial texture
        self.texture = self.stand_left_textures[0]

将敌人定义为一个类遵循与 Roz 相似的模式。基于arcade.AnimatedWalkingSprite,敌人继承了一些基本功能。和 Roz 一样,你需要采取以下步骤:

  • 定义动画时要使用的纹理。
  • 定义精灵最初应该面对的方向。
  • 定义它应该移动多快。

通过让敌人以 Roz 一半的速度移动,您可以确保 Roz 跑得更快。

现在您需要创建敌人并将其放置在屏幕上。由于每个关卡在不同的地方可能有不同的敌人,创建一个PlatformerView方法来处理这个:

def create_enemy_sprites(self) -> arcade.SpriteList:
    """Creates enemy sprites appropriate for the current level

    Returns:
        A Sprite List of enemies"""
    enemies = arcade.SpriteList()

    # Only enemies on level 2
    if self.level == 2:
        enemies.append(Enemy(1464, 320))

    return enemies

创建一个SpriteList来容纳你的敌人确保你可以以与其他屏幕对象类似的方式管理和更新你的敌人。虽然此示例显示单个敌人放置在单个级别的硬编码位置,但您可以编写代码来处理不同级别的多个敌人或从数据文件中读取敌人放置信息。

.setup()在创建播放器精灵之后和设置视口之前,您可以在 中调用此方法:

# Move the player sprite back to the beginning
self.player.center_x = PLAYER_START_X
self.player.center_y = PLAYER_START_Y
self.player.change_x = 0
self.player.change_y = 0

# Set up our enemies
self.enemies = self.create_enemy_sprites()

# Reset the viewport
self.view_left = 0
self.view_bottom = 0

现在您的敌人已创建,您可以在更新玩家后立即更新它们.on_update()

# Update the player animation
self.player.update_animation(delta_time)

# Are there enemies? Update them as well
self.enemies.update_animation(delta_time)
for enemy in self.enemies:
    enemy.center_x += enemy.change_x
    walls_hit = arcade.check_for_collision_with_list(
        sprite=enemy, sprite_list=self.walls
    )
    if walls_hit:
        enemy.change_x *= -1

arcade物理引擎不会自动管理敌人的行动,所以你必须手动处理它。您还需要检查墙壁是否碰撞,如果敌人与墙壁碰撞,则逆转敌人的运动。

您还需要检查 Roz 是否与您的任何敌人发生碰撞。在检查 Roz 是否捡到硬币后执行此操作:

for coin in coins_hit:
    # Add the coin score to our score
    self.score += int(coin.properties["point_value"])

    # Play the coin sound
    arcade.play_sound(self.coin_sound)

    # Remove the coin
    coin.remove_from_sprite_lists()

# Has Roz collided with an enemy?
enemies_hit = arcade.check_for_collision_with_list(
    sprite=self.player, sprite_list=self.enemies
)

if enemies_hit:
    self.setup()
    title_view = TitleView()
    window.show_view(title_view)

此代码与硬币碰撞检查的开始方式相同,只是您查找 Roz 和 之间的碰撞self.enemies。但是,如果您与任何敌人相撞,游戏就结束了,因此唯一需要检查的是是否至少击中了一个敌人。如果是这样,您调用.setup()以重置当前级别并显示TitleView. 如果您已经创建了游戏概览视图,这将是创建和展示它的地方。

最后要做的是使用与其他精灵列表相同的技术来绘制你的敌人。将以下内容添加到.on_draw()

def on_draw(self) -> None:
    arcade.start_render()

    # Draw all the sprites
    self.background.draw()
    self.walls.draw()
    self.coins.draw()
    self.goals.draw()
    self.ladders.draw()
    self.enemies.draw()
    self.player.draw()

您可以扩展此技术以创建任意数量的不同类型的敌人。

现在您已准备好启动一些平台!

移动平台

移动平台为您的游戏提供视觉和战略兴趣。它们使您能够建立需要思考和技能才能克服的世界和障碍:

您可以在arcade_platformer/15_moving_platforms.py和找到本节的代码assets/platform_level_02.tmx。如果您想自己构建移动平台,您可以在assets/platform_level_02_start.tmx.

由于平台在 中被视为墙壁arcade,因此在 Tiled 中以声明方式定义它们通常会更快。在 Tiled 中,打开您的地图并创建一个名为 的新对象层moving_platforms

为移动平台创建新层

在对象层上创建移动平台允许您定义arcade稍后用于移动平台的属性。在本教程中,您将创建一个移动平台。

选择此图层后,点击T添加新图块并选择将成为新平台的图块。将磁贴放在您希望它开始或结束的位置附近。单独显示完整的图块通常是最佳选择:

在moving_platforms 层上放置一个移动磁贴

一旦放置了移动的瓷砖,点击Esc停止放置瓷砖。

接下来,您定义自定义属性以设置移动平台运动的速度和限制。arcade使用以下定义的属性内置了对水平和垂直移动平台的支持:

  1. boundary_leftboundary_right限制平台的水平运动。
  2. boundary_topboundary_bottom限制平台的垂直运动。
  3. change_x 设置水平速度。
  4. change_y 设置垂直速度。

由于该平台承载罗兹在水平下方的敌人,只有boundary_leftboundary_rightchange_x属性定义float的值:

为移动平台定义自定义属性

您可以修改这些属性以适合您的关卡设计。如果您定义所有六个自定义属性,您的平台将以对角线模式移动!

设置好平台及其属性后,就可以处理新图层了。在 中PlatformerView.setup(),在处理地图图层之后和设置背景颜色之前添加以下代码:

self.coins = arcade.tilemap.process_layer(
    game_map, layer_name=coin_layer, scaling=MAP_SCALING
)

# Process moving platforms
moving_platforms_layer_name = "moving_platforms"
moving_platforms = arcade.tilemap.process_layer(
    game_map,
    layer_name=moving_platforms_layer_name,
    scaling=MAP_SCALING,
)
for sprite in moving_platforms:
    self.walls.append(sprite)

由于您的移动平台位于对象层中,因此它们必须与其他墙壁分开处理。但是,由于您的玩家需要能够站在它们上面,self.walls因此您将它们添加到物理引擎可以正确处理它们。

最后,您需要让您的平台移动。或者你呢?

记住你已经做过的事情:

  • 在 Tiled 中定义移动平台时,您可以设置自定义属性来定义其移动。
  • 处理moving_platforms图层时,将其中的所有内容添加到self.walls.
  • 创建 时self.physics_engine,您将self.walls列表作为参数传递。

这一切意味着,当您调用self.physics_engine.update().on_update(),您的所有平台都会自动移动!任何没有设置自定义属性的墙砖都不会移动。当 Roz 站在移动平台上时,物理引擎甚至足够智能:

您可以根据需要添加任意数量的移动平台,以创建任意复杂的世界。

结论

Pythonarcade是一个现代 Python 框架,非常适合制作具有引人入胜的图形和声音的游戏。面向对象并为 Python 3.6 及更高版本构建,arcade为程序员提供了一套现代工具来打造出色的游戏体验,包括平台游戏。arcade是开源的,总是欢迎贡献。

阅读本教程后,您现在可以:

  • 安装Pythonarcade
  • 创建基本的2D 游戏结构
  • 查找可用的游戏艺术品和其他资产
  • 使用平铺地图编辑器构建平台地图
  • 定义玩家动作、游戏奖励障碍
  • 使用键盘操纵杆输入控制您的播放器
  • 播放游戏动作的音效
  • 使用视口滚动游戏屏幕以保持您的玩家在视野中
  • 添加标题说明暂停屏幕
  • 在屏幕上移动非玩家游戏元素

这场比赛还有很多事情要做。以下是您可以实施的一些功能创意:

  • 添加游戏结束屏幕。
  • 在屏幕上动画硬币。
  • 当 Roz 与敌人碰撞时添加动画。
  • 检测 Roz 何时从地图上掉下来。
  • 给罗兹多重生命。
  • 添加高分表。
  • 使用arcade.PymunkPhysicsEngine提供更逼真的物理交互。

arcade图书馆还有很多值得探索的地方。有了这些技术,您现在就完全具备了出去制作一些很酷的游戏的能力!

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。