本文为Google Translate英译中结果,DrGraph在此基础上加了一些校正。英文原版页面:

Your first 2D game — Godot Engine (stable) documentation in English

第一个 2D 游戏

在这个循序渐进的教程系列中,您将使用 Godot 创建您的第一个完整的 2D 游戏。到本系列结束时,您将拥有自己的一个简单而完整的游戏,如下图所示。

您将了解 Godot 编辑器的工作原理、构建项目以及构建 2D 游戏的方法。

注:这个项目是对 Godot 引擎的介绍。它假设您已经有一些编程经验。如果您完全不熟悉编程,您应该从这里开始:脚本语言

游戏名为“Dodge the Creeps!”。您的角色必须移动并尽可能长时间地避开敌人。

您将学习:

  • 使用 Godot 编辑器创建完整的 2D 游戏。

  • 构建一个简单的游戏项目。

  • 移动玩家角色并改变其精灵。

  • 生成随机敌人。

  • 计算分数。

和更多。

你会发现另一个系列,你将在其中创建一个类似的游戏,但在 3D 中。不过,我们建议您从这个开始。

为什么从二维开始?

如果您是游戏开发新手或不熟悉 Godot,我们建议您从 2D 游戏开始。这将使您在处理往往更复杂的 3D 游戏之前对两者都感到满意。

您可以在此位置找到此项目的完整版本:

先决条件

本分步教程适用于遵循完整 入门指南的初学者。

如果您是一位经验丰富的程序员,您可以在此处找到完整演示的源代码:Godot 演示项目

我们准备了一些您需要下载的游戏资源,以便我们可以直接跳转到代码。

您可以通过单击下面的链接下载它们。

dodge_the_creeps_2d_assets.zip

设置项目

在这简短的第一部分中,我们将设置和组织项目。

启动 Godot 并创建一个新项目。

创建新项目时,您只需要选择一个有效的项目路径。您可以保留其他默认设置。

GDScript

下载dodge_the_creeps_2d_assets.zip。本压缩文件包含您将用于制作游戏的图像和声音。提取存档并将art/ 和fonts/目录移动到您的项目目录。

您的项目文件夹应如下所示。

这个游戏是为纵向模式设计的,所以我们需要调整游戏窗口的大小。单击“项目”->“项目设置”打开项目设置窗口,然后在左侧栏中打开“显示”->“窗口”选项卡。在那里,将“视口宽度”设置为480,将“视口高度”设置为720

此外,在“拉伸”选项下,将“模式”设置为canvas_items长宽比” 。这确保了游戏在不同尺寸的屏幕上一致地缩放。keep

组织项目

在这个项目中,我们将制作 3 个独立的场景:PlayerMob和 HUD,【最终】我们会将它们组合到游戏Main场景中。

在较大的项目中,创建文件夹来保存各种场景及其脚本可能很有用,但对于这个相对较小的游戏,您可以将场景和脚本保存在项目的根文件夹中,由res://. 您可以在左下角的文件系统停靠栏中看到您的项目文件夹:

有了项目,我们就可以在下一课中设计玩家场景了。

创建玩家场景

项目设置就绪后,我们就可以开始处理玩家控制的角色了。

第一个场景将定义Player对象。创建单独的 Player 场景的好处之一是我们可以单独测试它,甚至在我们创建游戏的其他部分之前。

节点结构

首先,我们需要为玩家对象选择一个根节点。作为一般规则,场景的根节点应该反映对象所需的功能——对象什么。单击“其他节点”按钮并向场景添加一个Area2D节点。

Godot 将在场景树中的节点旁边显示一个警告图标。你现在可以忽略它。我们稍后会解决。

我们Area2D可以检测重叠或撞到玩家的物体。通过双击将节点名称更改为Player。现在我们已经设置了场景的根节点,我们可以添加额外的节点来赋予它更多的功能。

在我们向Player节点添加任何子节点之前,我们要确保我们不会通过单击不小心移动它们或调整它们的大小。选择节点并单击锁右侧的图标。它的工具提示【为】“使选定节点的子节点不可选”。

保存场景。单击场景 -> 保存,或在 Windows/Linux上按Ctrl + S 或macOS 上按Cmd + S

注:对于这个项目,我们将遵循 Godot 命名约定。

  • GDScript:类(节点)使用 PascalCase,变量和函数使用 snake_case,常量使用 ALL_CAPS(参见 GDScript 风格指南)。

  • C#:类、导出变量和方法使用 PascalCase,私有字段使用 _camelCase,局部变量和参数使用 camelCase(参见C# 风格指南)。连接信号时,请小心准确地键入方法名称。

精灵动画

单击Player节点并添加 (Ctrl + A ) 子节点AnimatedSprite2D。【AnimatedSprite2D】将为我们的播放器处理外观和动画。请注意,节点旁边有一个警告符号。【AnimatedSprite2D 】需要一个SpriteFrames资源,这是它可以显示的动画列表。若要创建一个,请在检查器的Animation选项卡下找到该属性【Sprite Frames】,然后单击“[empty]”->“New SpriteFrames”。再次点击打开“SpriteFrames”面板【在底部】:

左边是动画列表。单击“默认【default】”并将其重命名为“行走【walk】”。然后单击“添加动画”按钮创建第二个名为“向上”的动画。在“文件系统【FileSystem】”选项卡中找到播放器图像 - 它们位于您之前解压缩的art文件夹中。将每个动画的两个图像(名为 playerGrey_up[1/2]playerGrey_walk[1/2])拖到相应动画面板的“动画帧”一侧:

玩家图像对于游戏窗口来说有点太大,所以我们需要缩小它们。单击该AnimatedSprite2D节点并将Scale属性设置为(0.5, 0.5)。您可以在Node2D标题下的检查器中找到它【Scale属性在Node2D类中声明定义】。

最后,添加一个CollisionShape2D作为Player的子项 。这将决定Player的“碰撞箱”,或碰撞区域的边界。对于这个角色,最适合用一个CapsuleShape2D节点来确定碰撞区域形状,所以在 Inspector 中的“Shape”旁边,单击“[empty]”->“New CapsuleShape2D”。使用两个尺寸手柄,调整形状的大小以覆盖精灵:

完成后,您的Player场景应如下所示:

确保在这些更改后再次保存场景。

在下一部分中,我们将向播放器节点添加一个脚本来移动它并为其设置动画。然后,我们设置碰撞检测以了解玩家何时会被某物击中。

Player编码

在本课中,我们将添加player的移动、动画代码,并进行检测碰撞设置。

为此,我们需要添加一些无法从内置节点获得的功能,因此我们将添加一个脚本。单击该Player节点并单击“附加脚本”按钮:

在脚本设置窗口中,您可以保留默认设置。只需点击“创建”:

注:如果您正在创建 C# 脚本或其他语言,请在点击创建之前从语言下拉菜单中选择语言。

注:如果这是您第一次接触 GDScript,请在​​继续之前阅读 脚本语言

首先声明这个对象需要的成员变量:

GDScript

extends Area2D

@export var speed = 400 # How fast the player will move (pixels/sec).
var screen_size # Size of the game window.

在第一个speed变量上使用关键字export,其作用是允许我们后续在检查器中设置它的值。这对于您希望能够像节点的内置属性一样进行调整的值非常方便。单击该Player 节点,您将看到该属性现在出现在检查器的“脚本变量”部分中。请记住,如果您更改此处的值,它将覆盖脚本中写入的值。

注:如果您使用的是 C#,则无论何时您想要查看新的导出变量或信号,都需要(重新)构建项目程序集。可以通过单击编辑器右上角的“构建”按钮手动触发此构建。

也可以从 MSBuild 面板触发手动构建。单击编辑器窗口底部的“MSBuild”一词以显示 MSBuild 面板,然后单击“生成”按钮。

当一个节点进入场景树时调用该_ready()函数,这是找到【获取】游戏窗口大小的好时机:

GDScript

func _ready():
    screen_size = get_viewport_rect().size

现在我们可以使用该_process()函数来定义玩家将要做什么。 每帧都会被调用_process(),因此我们将使用它来更新游戏的元素,我们预计这些元素会经常更改。对于播放器,我们需要做以下事情:

  • 检查输入。

  • 向给定的方向移动。

  • 播放适当的动画。

首先,我们需要检查输入——玩家是否按下了一个键?对于这个游戏,我们有 4 个方向输入要检查。输入操作在“输入映射”下的项目设置中定义。在这里,您可以定义自定义事件并为其分配不同的键、鼠标事件或其他输入。对于这个游戏,我们将箭头键映射到四个方向。

单击项目 -> 项目设置以打开项目设置窗口,然后单击顶部的输入地图选项卡。在顶部栏中键入“move_right”,然后单击“添加”按钮添加​​​​​​​move_right操作。

我们需要为此操作分配一个键。单击右侧的“+”图标,打开事件管理器窗口。

应自动选择“侦听输入...”字段。按键盘上的“右”键,菜单现在应该如下所示。

选择“确定”按钮。“右”键现在与move_right操作相关联。

重复这些步骤以添加另外三个映射:

  1. move_left映射到左箭头键。

  2. move_up映射到向上箭头键。

  3. move_down映射到向下箭头键。

您的输入地图选项卡应如下所示:

单击“关闭”按钮关闭项目设置。

注:我们只将一个键映射到每个输入操作,但您可以将多个键、操纵杆按钮或鼠标按钮映射到相同的输入操作。

【DrGraph】:Godot这里有点过度设计,感觉直接用键值常量作为参数调用更为快捷,代码更易读。

您可以使用Input.is_action_pressed()检测某个键是否被按下,如果它被按下就返回true,它【未被按下】就会返回false

GDScript

func _process(delta):
    var velocity = Vector2.ZERO # The player's movement vector.
    if Input.is_action_pressed("move_right"):
        velocity.x += 1
    if Input.is_action_pressed("move_left"):
        velocity.x -= 1
    if Input.is_action_pressed("move_down"):
        velocity.y += 1
    if Input.is_action_pressed("move_up"):
        velocity.y -= 1

    if velocity.length() > 0:
        velocity = velocity.normalized() * speed
        $AnimatedSprite2D.play()
    else:
        $AnimatedSprite2D.stop()

我们首先设置velocity(0, 0),表示默认情况下,玩家不应移动。然后我们检查每个输入并从velocity中添加/减去以获得总方向。例如,如果您同时按住rightdown,则生成的velocity向量将为(1, 1). 在这种情况下,由于我们添加了水平和垂直移动,玩家沿对角线移动的速度会比仅水平移动更快。

如果我们对速度(velocity进行归一化,这意味着我们将其长度设置为1,然后乘以所需的速度,我们就可以防止这种情况发生。这意味着不再有快速的对角线移动。

注:如果您以前从未使用过矢量数学,或者需要复习,您可以在Vector math上查看 Godot 中矢量用法的解释。【不过不用担心,矢量数学知识】对于本教程的其余部分来说不是必需的。

我们还检查玩家是否正在移动,以便我们可以在 AnimatedSprite2D 上调用play()或 stop()

注:$get_node()的简写方式。所以在上面的代码中, $AnimatedSprite2D.play()与 get_node("AnimatedSprite2D").play()【是等价的】.

在 GDScript 中,$返回当前节点的相对路径处的节点,null如果未找到该节点则返回。由于 AnimatedSprite2D 是当前节点的子节点,我们可以使用 $AnimatedSprite2D.

现在我们有了移动方向,我们可以更新玩家的位置。我们也可以用来clamp()防止它离开屏幕。钳位值意味着将其限制在给定范围内。将以下内容添加到函数的底部_process(确保它没有在else下缩进):

GDScript:

position += velocity * delta
position.x = clamp(position.x, 0, screen_size.x)
position.y = clamp(position.y, 0, screen_size.y)

注:_process()函数中的delta参数指的是帧长度——前一帧完成所花费的时间。使用此值可确保即使帧速率发生变化,您的动作也能保持一致。

单击“播放场景”(F6,在 macOS 上Cmd + R)并确认您可以在屏幕上向各个方向移动播放器。

警告:如果您在“调试器”面板中收到错误消息[Attempt to call function 'play' in base 'null instance' on a null instance]这可能意味着您拼错了 AnimatedSprite2D 节点的名称。节点名称区分大小写,并且$NodeName必须与您在场景树中看到的名称相匹配。

选择动画

现在玩家可以移动了,我们需要根据动画的方向更改 AnimatedSprite2D 正在播放的动画。我们有“行走”动画,显示玩家向右行走。flip_h应使用左移属性水平翻转此动画。我们还有“向上”动画,它应该垂直翻转flip_v以向下移动。让我们把这段代码放在函数的末尾_process()

GDScript

if velocity.x != 0:
    $AnimatedSprite2D.animation = "walk"
    $AnimatedSprite2D.flip_v = false
    # See the note below about boolean assignment.
    $AnimatedSprite2D.flip_h = velocity.x < 0
elif velocity.y != 0:
    $AnimatedSprite2D.animation = "up"
    $AnimatedSprite2D.flip_v = velocity.y > 0

注:上面代码中的布尔赋值是程序员常用的简写形式。由于我们正在进行比较测试(布尔值)并且还分配了一个布尔值,因此我们可以同时进行这两项操作。考虑这段代码与上面的单行布尔赋值:

GDScript

if velocity.x < 0:
    $AnimatedSprite2D.flip_h = true
else:
    $AnimatedSprite2D.flip_h = false

再次播放场景并检查每个方向的动画是否正确。

注:这里的一个常见错误是输入错误的动画名称。SpriteFrames 面板中的动画名称必须与您在代码中键入的名称相匹配。如果您为动画命名"Walk",则还必须在代码中使用大写的“W”。

当您确定移动工作正常时,将此行添加到 _ready(),这样玩家将在游戏开始时隐藏:

GDScript

hide()

为碰撞做准备

我们想Player检测它何时被敌人击中,但我们还没有制造任何敌人!没关系,因为我们将使用 Godot 的信号 功能来使其工作。

在脚本顶部添加以下内容。如果您使用的是 GDScript,请在extends Area2D之后​ . 如果您使用的是 C#,请将其添加到​​​​​​​public partial class Player : Area2D之后

GDScript

signal hit

这定义了一个名为“hit”的自定义信号,我们将让我们的玩家在与敌人碰撞时发出(发送)。我们将使用它Area2D来检测碰撞。选择Player节点并单击检查器选项卡旁边的“节点”选项卡以查看播放器可以发出的信号列表:

请注意我们的自定义“命中”信号也在那里!由于我们的敌人将成为RigidBody2D节点【程序设计时就已经系统考虑各角色的特征】,因此我们需要【处理Area2D与RigidBody2D碰撞时发出的】body_entered(body: Node2D)信号。当身体接触玩家时会发出此信号。单击“Connect..”,出现“Connect a Signal”窗口。我们不需要更改任何这些设置,因此再次单击“连接”。Godot 会自动在您的播放器脚本中创建一个函数。

请注意绿色图标表示信号已连接到此功能。将此代码添加到函数中:

GD脚本C#C++

func _on_body_entered(body):
    hide() # Player disappears after being hit.
    hit.emit()
    # Must be deferred as we can't change physics properties on a physics callback.
    $CollisionShape2D.set_deferred("disabled", true)

每次敌人击中玩家时,都会发出信号。我们需要禁用玩家的碰撞,这样我们就不会hit多次触发信号。

注:如果禁用区域的碰撞形状发生在引擎的碰撞处理过程中,可能会导致错误。使用set_deferred()告诉 Godot 等待disable形状,直到这样做是安全的。

【DrGraph】:因为Godot Engine的物理引擎使用了多线程来提高效率,因此发生碰撞时(调用_on_body_entered函数),CollisionShape2D的disabled属性会被线程锁定,无法通过set_disabled来修改,这时需要使用set_deferred()来延迟调用【相当于加到队列中,可以修改的时候再处理】

最后一部分是添加一个函数,我们可以在开始新游戏时调用该函数来重置玩家。

GDScript

func start(pos):
    position = pos
    show()
    $CollisionShape2D.disabled = false

随着玩家的工作,我们将在下一课中处理敌人。

制造敌人

现在是时候让我们的玩家必须躲避敌人了。它们的行为不会很复杂:生物会在屏幕边缘随机生成,选择随机方向,并沿直线移动。

我们将创建一个Mob场景,然后我们可以实例化以在游戏中创建任意数量的独立生物。

节点设置

单击顶部菜单中的 Scene -> New Scene 并添加以下节点:

不要忘记设置子节点,使他们无法被选中,就像您在 Player 场景中所做的那样。

RigidBody2D属性中,设置Gravity Scale为0,这样生物就不会向下掉落。此外,在 CollisionObject2D部分下,取消选中属性Mask中的1。这将确保生物不会相互碰撞。

像为播放器所做的那样设置AnimatedSprite2D 。这次,我们有 3 个动画:flyswimwalk。art 文件夹中的每个动画都有两个图像。

必须为每个单独的动画设置Animation Speed属性。将所有 3 个动画的属性值均调整为。​​​​3

您可以使用输入字段Animation Speed右侧的“播放动画”按钮来预览动画。

我们将随机选择其中一种动画,这样生物就会有一些变化。

与玩家图像一样,这些生物图像也需要按比例缩小。将 AnimatedSprite2DScale属性设置为(0.75, 0.75)

类似于Player场景,MOD场景也需要为碰撞添加一个CapsuleShape2D。要将形状与图像对齐,您需要将Rotation属性设置为Degrees90(在检查器的“变换(Transform)”下)。​​​​​​​

保存场景。

敌人脚本

像这样添加一个脚本Mob

GDScript

extends RigidBody2D

现在让我们看看脚本的其余部分。在_ready()我们播放动画并随机选择三种动画类型之一:

GD脚本C#C++

func _ready():
    var mob_types = $AnimatedSprite2D.sprite_frames.get_animation_names()
    $AnimatedSprite2D.play(mob_types[randi() % mob_types.size()])

首先,我们从 AnimatedSprite2D 的属性中获取动画名称列表frames 。这将返回一个包含所有三个动画名称的数组:["walk", "swim", "fly"]

然后我们需要【在0~2之间】选择一个随机数,并从列表中选择这些名称之一(数组索引从0开始)。randi() % n选择一个介于0和n-1之间的随机整数。​​​​​​​

最后一块是让小怪在离开屏幕时删除自己。将VisibleOnScreenNotifier2D节点的screen_exited()信号连接到Mob并添加以下代码:

GDScript

func _on_visible_on_screen_notifier_2d_screen_exited():
    queue_free()

这样就完成了Mob场景。

准备好玩家和敌人后,在下一部分中,我们将把他们放在一个新场景中。我们会让敌人在游戏板周围随机生成并向前移动,将我们的项目变成一个可玩的游戏。

主要游戏场景

现在是时候将我们所做的一切整合到一个可玩的游戏场景中了。

创建一个新场景并添加一个名为Main节点【Node】。(我们使用 Node 而不是 Node2D 的原因是因为这个节点将成为处理游戏逻辑的容器。它本身不需要 2D 功能。)

单击实例按钮(由链接图标表示)并选择您保存的 player.tscn.

现在,将以下节点添加为 的子节点Main,并如图所示命名它们(值以秒为单位):

  • 计时器(​​​​​​​名称MobTimer)- 控制暴徒产生的频率

  • 计时器(名称ScoreTimer) - 每秒递增分数

  • 计时器(名称StartTimer)- 在开始前延迟

  • Marker2D (名称StartPosition) - 指示玩家的起始位置

设置每个Timer节点的Wait Time属性如下:

  • MobTimer:0.5

  • ScoreTimer:1

  • StartTimer:2

此外,将StartTimerOne Shot属性设置为“On”并将StartPosition节点的Position设置为​​​​​​​​​​​​​​​​​​​(240, 450)

生成生物

主节点将生成新的生物,我们希望它们出现在屏幕边缘的随机位置。添加一个Path2D节点,命名MobPathMain的子节点。选择 时Path2D,您会在编辑器顶部看到一些新按钮:

选择中间的一个(“添加点”)并通过单击以在显示的角处添加点来绘制路径。要使点捕捉到网格,请确保同时选择了“使用网格捕捉”和“使用智能捕捉”。这些选项可以在“锁定”按钮的左侧找到,分别显示为一些点和相交线旁边的磁铁。

重要提示:按顺时针顺序画出路径,否则你的生物生成时会指向而不是指向内

在图像中放置4点后,单击“关闭曲线”按钮,您的曲线将完成。

现在路径已定义,添加一个PathFollow2D 节点作为MobPath的子节点并将其命名为MobSpawnLocation。该节点在移动时会自动旋转并跟随路径,因此我们可以使用它来选择沿路径的随机位置和方向。

你的场景应该是这样的:

主脚本

添加一个脚本到Main. 在脚本的顶部,我们使用@export var mob_scene: PackedScene允许我们选择我们想要实例化的 Mob 场景。

GDScript

extends Node

@export var mob_scene: PackedScene
var score

单击Main节点,您将在“脚本变量”下的检查器中Main看到该Mob Scene属性。

您可以通过两种方式分配此属性的值:

  • 从“文件系统”停靠栏拖放mob.tscnMob Scene 属性中。

  • 单击“[空]”旁边的向下箭头并选择“加载”。选择mob.tscn

接下来,在场景面板中选择Main节点下的Player场景实例,并访问侧边栏上的 Node面板。确保在 Node面板中选择了 Signals 选项卡。

您应该看到Player节点的信号列表。在列表中找到并双击hit信号(或右键单击它并选择“连接...”)。这将打开信号连接对话框。我们想创建一个名为game_over的新函数,它将处理游戏结束时需要发生的事情。在信号连接对话框底部的“Receiver Method”框中键入“game_over”,然后单击“Connect”。您的目标是让Player中发出hit信号并在Main脚本中进行处理。将以下代码添加到新函数,以及一个new_game函数,用于为新游戏设置所有内容:

GDScript

func game_over():
    $ScoreTimer.stop()
    $MobTimer.stop()

func new_game():
    score = 0
    $Player.start($StartPosition.position)
    $StartTimer.start()

现在将每个定时器节点(StartTimer、 ScoreTimerMobTimer)的timeout()信号连接到主脚本。StartTimer将启动其他两个计时器。ScoreTimer将使分数增加 1。

GDScript

func _on_score_timer_timeout():
    score += 1

func _on_start_timer_timeout():
    $MobTimer.start()
    $ScoreTimer.start()

在 中_on_mob_timer_timeout(),我们将创建一个生物实例,沿Path2D随机选择一个起始位置,然后让生物开始运动。节点 PathFollow2D会随着路径自动旋转,因此我们将使用它来选择生物的方向及其位置。当我们生成一个生物时,我们将在150.0250.0之间选择一个随机值作为每个生物移动的速度(如果它们都以相同的速度移动会很无聊)。

请注意,必须使用add_child()将新实例添加到场景中。

GDScript

func _on_mob_timer_timeout():
    # Create a new instance of the Mob scene.
    var mob = mob_scene.instantiate()

    # Choose a random location on Path2D.
    var mob_spawn_location = get_node("MobPath/MobSpawnLocation")
    mob_spawn_location.progress_ratio = randf()

    # Set the mob's direction perpendicular to the path direction.
    var direction = mob_spawn_location.rotation + PI / 2

    # Set the mob's position to a random location.
    mob.position = mob_spawn_location.position

    # Add some randomness to the direction.
    direction += randf_range(-PI / 4, PI / 4)
    mob.rotation = direction

    # Choose the velocity for the mob.
    var velocity = Vector2(randf_range(150.0, 250.0), 0.0)
    mob.linear_velocity = velocity.rotated(direction)

    # Spawn the mob by adding it to the Main scene.
    add_child(mob)

重要提示:为什么用PI?在需要角度的函数中,Godot 使用弧度而不是度数。Pi 以弧度表示半圈,约 3.1415(也有TAU等于2 * PI)。如果您更习惯使用度数,则需要使用deg_to_rad()和​​​​​​​​​​​​​​rad_to_deg()函数在两者之间进行转换。

测试场景

让我们测试场景以确保一切正常。将此new_game 调用添加到_ready()

GDScript

func _ready():
    new_game()

让我们也指定Main为我们的“主场景”——游戏启动时自动运行的场景。按“播放”按钮并main.tscn在出现提示时选择。

注:如果您已经将另一个场景设置为“主场景”,则可以右键单击main.tscn文件系统停靠栏并选择“设置为主场景”。

您应该能够移动玩家,看到怪物生成,并看到玩家在被怪物击中时消失。

当您确定一切正常时,在_ready()中删除对new_game()的调用。

我们的游戏缺少什么?一些用户界面。在下一课中,我们将添加一个标题屏幕并显示玩家的得分。

顶层屏幕

我们的游戏需要的最后一块是用户界面 (UI),用于显示得分、“游戏结束”消息和重启按钮等内容。

创建一个新场景,并添加一个名为HUD的CanvasLayer节点 。“HUD”代表“顶层屏幕”,一种信息显示叠加层,显示在游戏视图顶部。

CanvasLayer节点让我们可以游戏其余部分之上的层上绘制 UI 元素,这样它显示的信息就不会被任何游戏元素(例如玩家或生物)覆盖。

HUD需要显示以下信息:

  • 得分,由ScoreTimer.

  • 消息,例如“游戏结束”或“准备好!”

  • 一个“开始”按钮开始游戏。

UI 元素的基本节点是Control。要创建我们的 UI,我们将使用两种类型的Control节点:LabelButton

创建以下节点作为HUD节点的子节点:

单击ScoreLabel并在检查器的Text字段中键入一个数字。Control节点的默认字体很小并且不能很好地缩放。游戏资源中包含一个名为“Xolonium-Regular.ttf”的字体文件。要使用此字体,请执行以下操作:

  1. 在“主题覆盖 > 字体”下,选择“加载”并选择“Xolonium-Regular.ttf”文件。

ScoreLabel上完成此操作后,您可以单击 Font 属性旁边的向下箭头并选择“复制”,然后将其“粘贴”到其他两个 Control 节点上的相同位置。设置ScoreLabel“主题覆盖 > 字体大小”下的“字体大小”属性。设置为64的效果就很好。

注:锚点: Control节点有位置和大小,但它们也有锚点。锚点定义原点——节点边缘的参考点。

如下图所示排列节点。您可以拖动节点以手动放置它们,或者为了更精确地放置它们,请使用具有以下设置的“锚点预设”:

Score标签

布局 :

  • 锚预设:Center Top

标签设置:

  • 文本 :0

  • 水平对齐 :Center

  • 垂直对齐 :Center

信息

布局 :

  • 锚预设:Center

标签设置:

  • 文本 :Dodge the Creeps!

  • 水平对齐 :Center

  • 垂直对齐 :Center

  • 自动换行模式:Word

开始按钮

布局 :

  • 锚预设:Center Bottom

按钮设置:

  • 文本 :Start

  • Y 位置:580(控制 - 布局/转换)

MessageTimer上,将Wait Time设置为2并将One Shot属性设置为“开”。

现在将此脚本添加到HUD

GDScript

extends CanvasLayer

# Notifies `Main` node that the button has been pressed
signal start_game

我们现在想临时显示一条消息,比如“Get Ready”,所以我们添加如下代码

GDScript

func show_message(text):
    $Message.text = text
    $Message.show()
    $MessageTimer.start()

我们还需要处理当玩家输了时发生的事情。下面的代码将显示“游戏结束”2 秒,然后返回到标题屏幕,并在短暂的暂停后显示“开始”按钮。

GDScript

func show_game_over():
    show_message("Game Over")
    # Wait until the MessageTimer has counted down.
    await $MessageTimer.timeout

    $Message.text = "Dodge the\nCreeps!"
    $Message.show()
    # Make a one-shot timer and wait for it to finish.
    await get_tree().create_timer(1.0).timeout
    $StartButton.show()

当玩家输了时调用此函数。它将显示“Game Over”2 秒,然后返回到标题屏幕,并在短暂暂停后显示“Start”按钮。

注:当您需要短暂暂停时,使用 SceneTree 的create_timer()函数是使用 Timer 节点的替代方法。这对于添加延迟非常有用,例如在上面的代码中,我们希望在显示“开始”按钮之前等待一段时间。

添加以下代码以HUD更新分数

GDScript

func update_score(score):
    $ScoreLabel.text = str(score)

连接MessageTimertimeout()信号和StartButton 的pressed()信号,在新函数中添加如下代码:

GDScript

func _on_start_button_pressed():
    $StartButton.hide()
    start_game.emit()

func _on_message_timer_timeout():
    $Message.hide()

将 HUD 连接到 Main

现在我们已经完成了HUD场景的创建,回到Main. 像你做Player场景一样,在Main中实例化HUD场景。场景树应该是这样的,所以【你要检查一下】确保没有遗漏任何东西:

现在我们需要将HUD功能连接到我们的Main脚本。这需要在Main场景中添加一些内容:

在“节点”选项卡中,通过单击“连接信号”窗口中的“选择”按钮并选择new_game()方法或在窗口中的“接收器方法”下方键入“new_game”,将 HUD 的start_game信号连接到主节点的【new_game()函数】。确认【在脚本界面中】,绿色连接图标现在出现func new_game()的旁边。

请记得从_ready()中删除对new_game()的调用。

new_game()中,更新分数显示并显示“准备好”消息:

GDScript

$HUD.update_score(score)
$HUD.show_message("Get Ready")

game_over()我们需要调用相应的HUD函数:

GDScript

$HUD.show_game_over()

提醒一下:我们不想自动开始新游戏,所以如果您还没有,请删除_ready()中new_game()的调用。

最后,将此添加到_on_score_timer_timeout()以更新显示与不断变化的分数同步:

GDScript

$HUD.update_score(score)

现在你可以玩了!单击“播放项目”按钮。你会被要求选择一个主场景,可选择main.tscn

移除旧的 creeps

如果您一直玩到“Game Over”,然后立即开始新游戏,则前一游戏的小兵可能仍会出现在屏幕上。如果他们在新游戏开始时全部消失会更好。我们只需要一种方法来告诉所有的暴徒自己离开。我们可以使用“组”功能来做到这一点。

Mob场景中,选择根节点并单击检查器旁边的“节点”选项卡(与您找到节点信号的位置相同)。在“信号”旁边,单击“组”,您可以输入新的组名称【mobs】并单击“添加”。

现在所有的生物都将在“mobs组中。然后我们可以将以下行添加到Main的函数new_game()中:

GDScript

get_tree().call_group("mobs", "queue_free")

call_group()函数在组中的每个节点上调用命名函数 - 在这种情况下,我们告诉每个mob【mobs组中的所有对象】删除自己。

至此游戏基本完成。在下一部分和最后一部分中,我们将通过添加背景、循环音乐和一些键盘快捷键来对其进行一些润色。

整理

我们现在已经完成了游戏的所有功能。以下是添加更多“果汁【小细节】”以改善游戏体验的一些剩余步骤。

随意用自己的想法扩展游戏玩法。

背景

默认的灰色背景不是很吸引人,所以让我们改变它的颜色。一种方法是使用ColorRect节点。使它成为下面的第一个节点,Main这样它就会被绘制在其他节点的后面。 ColorRect只有一个属性:Color. 选择您喜欢的颜色,然后在视口顶部的工具栏或检查器中选择“布局”->“锚点预设”->“全矩形”,使其覆盖屏幕。

如果有的话,您也可以通过使用节点来添加背景图像 TextureRect

声音特效

声音和音乐可能是增加游戏体验吸引力的最有效方式。在您的游戏资产文件夹中,您有两个声音文件:背景音乐“House In a Forest Loop.ogg”和玩家失败时的“gameover.wav”。

添加两个AudioStreamPlayer节点作为Main的子节点 。【分别命名为】MusicDeathSound。【分别】单击Stream属性,选择“加载”,然后选择相应的音频文件。

所有音频的Loop属性缺省为禁用(disabled)。如果您希望音乐无缝循环,请单击“流文件”箭头,选择Make Unique,然后单击“流文件”并选中Loop复选框。

要播放音乐,在new_game() 函数中添加$Music.play(),game_over()函数中添加$Music.stop()

最后,在game_over()函数中添加$DeathSound.play()

键盘快捷键

由于游戏是用键盘控制的,如果我们也可以通过键盘上的一个键来启动游戏就方便了。我们可以使用Button节点的“快捷方式”属性来做到这一点。

在上一课中,我们创建了四个输入动作来移动角色。我们将创建一个类似的输入操作来映射到开始按钮。

选择“项目”->“项目设置”,然后单击“输入【映射Input Map】”选项卡。以与创建移动输入操作相同的方式,创建一个名为start_game的新输入操作并为该Enter 键添加一个键映射。

如果您有可用的控制器支持,现在是添加控制器支持的好时机。连接或配对您的控制器,然后在您希望为其添加控制器支持的每个输入操作下,单击“+”按钮并按下相应的按钮、方向键或您想要映射到相应输入操作的摇杆方向.

HUD场景中,选择StartButton并在 Inspector 中找到其Shortcut属性。通过在框内单击创建一个新的快捷方式资源,打开事件数组并通过单击Array[InputEvent] (size 0)添加一个新的数组元素。

创建一个新的InputEventAction并将其命名为start_game

现在,当开始按钮出现时,您可以单击它或按下Enter 以开始游戏。

这样,您就完成了您在 Godot 中的第一个 2D 游戏。

你必须制作一个玩家控制的角色、在游戏板周围随机生成的敌人、计算分数、实现游戏结束和重播、用户界面、声音等等。恭喜!

还有很多东西要学,但你可以花点时间欣赏你所取得的成就。

准备就绪后,您可以继续学习您的第一个 3D 游戏,学习在 Godot 中从头开始创建一个完整的 3D 游戏。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐