Godot引擎 4.0 文档 - 第一个 2D 游戏

news2024/11/24 3:55:05

本文为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 个独立的场景: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 并添加以下节点:

  • RigidBody2D(已命名Mob

    • 动画精灵2D

    • CollisionShape2D

    • VisibleOnScreenNotifier2D

不要忘记设置子节点,使他们无法被选中,就像您在 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节点:Label和Button。

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

  • 标签名为ScoreLabel

  • 标签名为Message

  • 名为StartButton的按钮。

  • 定时器名为MessageTimer.

单击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 游戏。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/555598.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

上交清华搞事情!发起最全学科大模型中文知识及推理评测!GPT-4 竟然血洗所有国产模型

夕小瑶科技说 原创 作者 | 小戏&#xff0c;Python 从 OpenAI 的 ChatGPT、Meta 的 LLaMA、Anthropic 的 Claude 到复旦的 Moss、清华的 ChatGlm、MiniMax 的 Glow&#xff0c;国内的国外的大模型百花齐放层出不穷。那么&#xff0c;抛出一个相信大家都会关心的问题&#xff…

网狐大联盟服务端源码分析之服务核心-ServiceCore

工程属性分析: 1.工程属性->动态链接库 2.dll类型->MFC共享DLL 3.字符集->Unicode 4.库导出类型->使用模块定义文件def 5.生成的导出模块函数与对应的地址定义lib文件 源码分析: 头文件分析: 头文件与对应含义表示如下:

开关电源PCB排版基本规则

开关电源PCB排版是开发电源产品中的一个重要过程。许多情况下&#xff0c;一个在纸上设计得非常完美的电源可能在初次调试时无法正常工作&#xff0c;原因是该电源的PCB排版存在着许多问题。 为了适应电子产品飞快的更新换代节奏&#xff0c;产品设计工程师更倾向于选择在市场…

Linux安装部署Redis6.2.5图文教程

Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库&#xff0c;并提供多种语言的 API。最近学习需要用到Redis&#xff0c;所以就去Linux服务器上部署一个&#xff0c;做下记录&#xff0c;方便…

PostgreSQL中的行锁

行锁在PG中比较特殊&#xff0c;在9.4以前&#xff0c;只有两种类型的行锁&#xff0c; FOR UPDATE 和FOR SHARE&#xff0c;因为只有两种锁&#xff0c;粒度比较大&#xff0c;极大的影响了并发性。所以从9.4开始引入了FOR KEY SHARE和FOR NO KEY UPDATE这两种行锁。目前这四种…

防火墙——SNAT和DNAT策略的原理及应用、防火墙规则的备份、还原和抓包

防火墙—SNAT和DNAT策略的原理及应用、防火墙规则的备份、还原和抓包 一、SNAT策略概述1、SNAT应用环境2、SNAT原理3、SNAT转换的前提条件 二、SNAT策略的应用1、临时打开2、永久打开3、SNAT转换1&#xff1a;固定的公网IP地址4、SNAT转换2&#xff1a;非固定的公网IP地址&…

一文搞懂,PO设计模式详解

PO模式&#xff1a; 全称&#xff1a;page objece&#xff0c;分层机制&#xff0c;让不同层去做不同类型的事情&#xff0c;让代码结构清晰&#xff0c;增加复⽤性。 PO模式的优势&#xff1a; 1&#xff09;效率⾼ &#xff1a;同理&#xff0c;PO模式的逻辑层⽅法有具体定…

Flink第六章:多流操作

系列文章目录 Flink第一章:环境搭建 Flink第二章:基本操作. Flink第三章:基本操作(二) Flink第四章:水位线和窗口 Flink第五章:处理函数 Flink第六章:多流操作 文章目录 系列文章目录前言一、分流1.侧输出流(process function) 二、合流1. 联合&#xff08;Union&#xff09;2…

CVE-2018-2894WebLogic未授权任意文件上传

CVE-2018-2894WebLogic未授权任意文件上传 这个洞的限制就比较多了 限制版本 Oracle WebLogic Server版本 10.3.6.0 12.1.3.0 12.2.1.2 12.2.1.3 限制配置 该漏洞的影响模块为web服务测试页&#xff0c;在默认情况下不启用。 /ws_utc/config.do /ws_utc/begin.do 默认情况下不…

在职字节6年,一个29岁女软件测试工程师的心声

简单的先说一下&#xff0c;坐标杭州&#xff0c;14届本科毕业&#xff0c;算上年前在字节跳动的面试&#xff0c;一共有面试了有6家公司&#xff08;因为不想请假&#xff0c;因此只是每个晚上去其他公司面试&#xff0c;所以面试的公司比较少&#xff09; 其中成功的有4家&a…

Linux防火墙----firewalld

文章目录 一、firewalld概述二、firewalld 与 iptables 的区别三、firewalld 区域的概念四、firewalld数据处理流程五、firewalld防火墙的配置方法5.1 使用firewall-config 图形工具5.2 编写/etc/firewalld/中的配置文件5.3使用firewall-cmd 命令行工具 一、firewalld概述 fir…

AI:帮助你更好地发声!

正文共 978 字&#xff0c;阅读大约需要 3 分钟 公务员必备技巧&#xff0c;您将在3分钟后获得以下超能力&#xff1a; 快速生成倡议书 Beezy评级 &#xff1a;B级 *经过简单的寻找&#xff0c; 大部分人能立刻掌握。主要节省时间。 推荐人 | Kim 编辑者 | Linda ●图片由Lex…

当你知道前后端分离与不分离的6个特点,你就不该再当点工了

Web 应用的开发主要有两种模式&#xff1a; 前后端不分离 前后端分离 理解它们的区别有助于我们进行对应产品的测试工作。 前后端不分离 在早期&#xff0c;Web 应用开发主要采用前后端不分离的方式&#xff0c;它是以后端直接渲染模板完成响应为主的一种开发模式。以前后端不…

linux存储技术学习资料

参考 https://www.cnblogs.com/pengdonglin137/p/16525428.html Linux I/O栈 Linux内核的I/O栈大图知乎Linux I/O专栏1Linux 块设备之Block Layer层架构演变Linux VFS机制简析&#xff08;一&#xff09;Linux VFS机制简析&#xff08;二&#xff09;Linux Kernel文件系统写I…

keycloak入门

realm&#xff1a;领域&#xff0c;指的是在某一个软件业务领域中所涉及的用户认证授权管理相关的对象&#xff0c;在这个realm中有用户、角色、会话session等等用于认证授权管理的对象。 假设一个公司A使用一个erp系统&#xff0c;那么就可以给这个公司A设置一个realm&#xf…

微信小程序nodejs+vue高校食堂餐厅点餐订餐系统ja221

本文以实际运用为开发背景&#xff0c;运用软件工程原理和开发方法&#xff0c;它主要是采用 语言 node.js 框架&#xff1a;Express 前端:Vue.js 数据库&#xff1a;mysql 数据库工具&#xff1a;Navicat 开发软件&#xff1a;VScode 前端vueelementui, (1) vue引入elementu…

NFC入门介绍

缩写词 NFCNear Field Communication近场通信OEMOriginal Equipment Manufacturer原始设备制造商HWHardware硬件OMAPIOpen Mobile Application Programming Interface开发移动应用程序编程接口eSEEmbedded Secure Element嵌入式安全元件SEMSSecure Element Management Service…

5月22日比特币披萨日,今天你吃披萨了吗?

比特币披萨日 1. Laszlo Hanyecz2. 最贵披萨诞生记3. 梭哈买披萨4. 未完待续 2010年5月22日&#xff0c;美国佛罗里达州的程序员Laszlo Hanyecz&#xff08;拉兹洛哈涅克斯&#xff09;用10000个比特币购买了棒约翰&#xff08;Papa Johns&#xff09;比萨店一个价值25美元的奶…

Three.js--》实现3d水晶小熊模型搭建

目录 项目搭建 初始化three.js基础代码 加载背景纹理 加载小熊模型 今天简单实现一个three.js的小Demo&#xff0c;加强自己对three知识的掌握与学习&#xff0c;只有在项目中才能灵活将所学知识运用起来&#xff0c;话不多说直接开始。 项目搭建 本案例还是借助框架书写…

vTESTstudio概述

vTESTstudio支持的测试用例编写方式 项目层级结构 从用例编写到测试执行及生成报告的整个流程 vTESTsutido 开发&#xff0c;CANoe执行测试 界面简介 CANoe 创建的测试用例用Test Modules执行&#xff0c;vTESTstudio 创建的测试用例用Test Units执行 先在vTESTstudio里创建pr…