本文为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 游戏之前对两者都感到满意。
您可以在此位置找到此项目的完整版本:
-
https://github.com/godotengine/godot-demo-projects
先决条件¶
本分步教程适用于遵循完整 入门指南的初学者。
如果您是一位经验丰富的程序员,您可以在此处找到完整演示的源代码:Godot 演示项目。
我们准备了一些您需要下载的游戏资源,以便我们可以直接跳转到代码。
您可以通过单击下面的链接下载它们。
dodge_the_creeps_2d_assets.zip。
设置项目¶
在这简短的第一部分中,我们将设置和组织项目。
启动 Godot 并创建一个新项目。
创建新项目时,您只需要选择一个有效的项目路径。您可以保留其他默认设置。
GDScript
下载dodge_the_creeps_2d_assets.zip。本压缩文件包含您将用于制作游戏的图像和声音。提取存档并将art/
和fonts/
目录移动到您的项目目录。
您的项目文件夹应如下所示。
这个游戏是为纵向模式设计的,所以我们需要调整游戏窗口的大小。单击“项目”->“项目设置”打开项目设置窗口,然后在左侧栏中打开“显示”->“窗口”选项卡。在那里,将“视口宽度”设置为480
,将“视口高度”设置为720
。
此外,在“拉伸”选项下,将“模式”设置为“canvas_items
长宽比” 。这确保了游戏在不同尺寸的屏幕上一致地缩放。keep
组织项目¶
在这个项目中,我们将制作 3 个独立的场景:Player
、Mob
和 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
操作相关联。
重复这些步骤以添加另外三个映射:
-
move_left
映射到左箭头键。 -
move_up
映射到向上箭头键。 -
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
中添加/减去以获得总方向。例如,如果您同时按住right
和down
,则生成的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 并添加以下节点:
-
RigidBody2D(已命名
Mob
)-
动画精灵2D
-
CollisionShape2D
-
VisibleOnScreenNotifier2D
-
不要忘记设置子节点,使他们无法被选中,就像您在 Player 场景中所做的那样。
在RigidBody2D属性中,设置Gravity Scale
为0,这样生物就不会向下掉落。此外,在 CollisionObject2D部分下,取消选中属性Mask中的1。这将确保生物不会相互碰撞。
像为播放器所做的那样设置AnimatedSprite2D 。这次,我们有 3 个动画:fly
、swim
和walk
。art 文件夹中的每个动画都有两个图像。
必须为每个单独的动画设置Animation Speed
属性。将所有 3 个动画的属性值均调整为。3
您可以使用输入字段Animation Speed
右侧的“播放动画”按钮来预览动画。
我们将随机选择其中一种动画,这样生物就会有一些变化。
与玩家图像一样,这些生物图像也需要按比例缩小。将 AnimatedSprite2D
的Scale
属性设置为(0.75, 0.75)
类似于Player
场景,MOD场景也需要为碰撞添加一个CapsuleShape2D。
要将形状与图像对齐,您需要将Rotation
属性设置为Degrees
90
(在检查器的“变换(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
此外,将StartTimer
的One Shot
属性设置为“On”并将StartPosition
节点的Position
设置为(240, 450)
生成生物¶
主节点将生成新的生物,我们希望它们出现在屏幕边缘的随机位置。添加一个Path2D节点,命名MobPath
为Main
的子节点。选择 时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.tscn
到Mob 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
、 ScoreTimer
和MobTimer
)的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.0
和250.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节点:Label和Button。
创建以下节点作为HUD
节点的子节点:
-
标签名为
ScoreLabel
。 -
标签名为
Message
。 -
名为
StartButton
的按钮。 -
定时器名为
MessageTimer
.
单击ScoreLabel
并在检查器的Text
字段中键入一个数字。Control
节点的默认字体很小并且不能很好地缩放。游戏资源中包含一个名为“Xolonium-Regular.ttf”的字体文件。要使用此字体,请执行以下操作:
-
在“主题覆盖 > 字体”下,选择“加载”并选择“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)
连接MessageTimer
的timeout()
信号和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
的子节点 。【分别命名为】Music
和DeathSound
。【分别】单击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 游戏。