Work in progress

The content of this page was not yet updated for Godot 4.2 and may be outdated. If you know how to improve this page or you can confirm that it's up to date, feel free to open a pull request.

分数与重玩

在这一部分中,我们会添加计分、播放音乐、重启游戏的能力。

我们要用一个变量来记录当前的分数,使用最简的界面在屏幕上显示。我们会用文本标签来实现。

在主场景中,添加一个新的 Control 节点作为 Main 的子项,命名为 UserInterface。你会被自动切换到 2D 屏幕,可以在这里编辑你的用户界面 User Interface(UI)。

名为分数标签 ScoreLabelLabel

image1

检查器中将该 LabelText 设为类似“Score: 0”的占位内容。

image2

并且,文本默认是白色的,和我们的游戏背景一样。我们需要修改它的颜色,才能在运行时看到。

向下滚动到 Theme Overrides(主题覆盖)然后展开 Colors(颜色)并点击 Font Color(字体颜色)旁边的黑框来为文字着色

image3

最后单击视口中的文本,将其拖离左上角。

image4

UserInterface 节点让我们可以将 UI 组合到场景树的一个分支上,并且也让主题资源能够传播到它的所有子节点上。我们将会用它来设置游戏的字体。

创建 UI 主题

再次选中 UserInterface 节点。在检查器中为 Theme -> Theme 创建一个新的主题资源。

image5

单击这个资源就会在底部面板中打开主题编辑器。会展示使用你的主题资源时内置 UI 控件的外观。

image6

默认情况下,主题只有一个属性,Default Font(默认字体)。

参见

你可以为主题资源添加更多属性,从而设计更复杂的用户界面,不过这就超出本系列的范畴了。要学习主题的创建和编辑,请参阅 GUI 皮肤简介

这里需要的是一个字体文件,就是你电脑上用的那种。常见的字体文件格式有两种:TrueType 字体(TTF)和 OpenType 字体(OTF)。

文件系统面板中,展开 fonts 目录,单击我们包含在项目里的 Montserrat-Medium.ttf 文件并将其拖放到Default Font(默认字体)上。文本就又会出现在主题预览中了。

文本有一点小。将Default Font Size(默认字体大小)设置为 22 像素即可增大文本的大小。

image7

跟踪得分

我们下一步是进行计分。为 ScoreLabel 附加一个新的脚本,并在其中定义 score(分数)变量。

extends Label

var score = 0

每踩扁一只怪物,这个分数就应该加 1。我们可以使用它们的 squashed 信号来得知发生的时间。不过,因为我们是用代码实例化的怪物,我们无法在编辑器中将怪物的信号连接到 ScoreLabel

不过,我们可以在每次生成一只怪物时通过代码来进行连接。

打开 main.gd 脚本。如果它还开着,你可以在脚本编辑器左栏中点击它的名字。

image8

另一种方法是在文件系统面板中双击 main.gd 文件。

_on_mob_timer_timeout() 函数的最后添加下面这行代码:

func _on_mob_timer_timeout():
    #...
    # We connect the mob to the score label to update the score upon squashing one.
    mob.squashed.connect($UserInterface/ScoreLabel._on_mob_squashed.bind())

这一行的意思是,当小怪发出 squashed 信号时,ScoreLabel 节点就会接收到并调用 _on_mob_squashed() 函数。

回到 ScoreLabel.gd 脚本,定义回调函数 _on_mob_squashed()

这里我们将进行加分并更新显示的文本。

func _on_mob_squashed():
    score += 1
    text = "Score: %s" % score

第二行用 score 变量的值替换占位符 %s。使用此功能时,Godot 会自动将值转换为字符串文本,这在向标签中输出文本或者使用 print() 函数时非常方便。

参见

可以在 GDScript 格式字符串 学习字符串格式化相关的更多信息。在 C# 中请考虑使用“$”进行字符串插值

你现在可以玩游戏,压死几个敌人,看看分数的增长。

image9

备注

在一个复杂的游戏中,你可能想把你的用户界面与游戏世界完全分开。在这种情况下,你就不会在标签上记录分数了。相反,你可能想把它存储在一个单独的、专门的对象中。但当原型设计或你的项目很简单时,保持你的代码简单就可以了。编程总是一种平衡的行为。

重玩游戏

我们现在就要添加死亡后重玩的能力。玩家死亡后,我们会在屏幕上现实一条消息并等待输入。

回到 main.tscn 场景,选中 UserInterface 节点,添加 ColorRect 节点作为其子项并命名为 Retry(重试)。该节点会使用单一色彩填充矩形,我们用它来覆盖画面,达到变暗的效果。

要使其覆盖整个视口,可以使用工具栏中 锚点预设 菜单。

image10

点击打开,并应用整个矩形命令。

image11

什么都没发生。好吧,是几乎什么都没有;只有四个绿色的大头针移动到了选择框的四个角落。

image12

这是因为 UI 节点(图标都是绿色)使用的是锚点和边距,它们都相对于它们父节点包围框。这里的 UserInterface 节点比较小,所以 Retry 会受限于它。

选中 UserInterface 然后也对其使用锚点预设 -> 整个矩形Retry 节点就应该覆盖整个视口了。

让我们修改它的颜色,把游戏区域变暗。选中 Retry,在检查器中将 Color(颜色)设置为透明的暗色。要实现整个效果,可以在取色器中将 A 滑动条拖到左边。它控制的是颜色的 Alpha 通道,也就是不透明度。

image13

接下来,添加一个 Label 的节点作为 Retry 的子节点并且设置他的 Text 为“Press Enter to retry”。将其移动至屏幕中央,并且选择 Anchor Preset -> Center(锚点预设 > 居中)。

image14

编写重试选项

我们现在就可以去编写代码,在玩家死亡时显示 Retry 节点,重玩时隐藏。

打开 main.gd 脚本。首先,我们想要在游戏开始时隐藏覆盖层。将这一行加到 _ready() 函数中。

func _ready():
    $UserInterface/Retry.hide()

然后在玩家受到攻击时,我们就显示这个覆盖层。

func _on_player_hit():
    #...
    $UserInterface/Retry.show()

最后,当 Retry 节点可见时,我们需要监听玩家的输入,按下回车键时让游戏重启。可以使用内置的 _unhandled_input() 回调来实现,任何输入都会触发这个回调。

如果玩家按下了预设的 ui_accept 输入动作并且 Retry 是可见状态,我们就重新加载当前场景。

func _unhandled_input(event):
    if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible:
        # This restarts the current scene.
        get_tree().reload_current_scene()

我们可以通过 get_tree() 函数访问全局 SceneTree 对象,然后用它来重新加载并重启当前场景。

添加音乐

要添加音乐,让音乐在后台连续播放,我们就要用到 Godot 的另一项特性:自动加载

要播放音频,只需往场景里添加一个 AudioStreamPlayer 节点,然后为它附加一个音频文件。启动场景时,就会自动播放。然而,如果重新加载了场景,比如我们在重玩的时候就这么干了,这些音频节点也会被重置,音乐也就会从头开始播放。

你可以使用自动加载功能来让 Godot 在游戏开始时自动加载节点或场景,不依赖于当前场景。你还可以用它来创建能够全局访问的对象。

场景菜单中单击新建场景,或者使用当前打开的场景旁边的 + 图标来创建一个新场景。

image15

单击其他节点按钮,创建一个 AudioStreamPlayer 然后将其重命名为 MusicPlayer(音乐播放器)。

image16

我们在 art/ 目录中包含了一条音乐音轨 House In a Forest Loop.ogg。单击并把它拖放到检查器中的 Stream(流)属性上。同时要打开 Autoplay,这样音乐就会在游戏开始时自动播放了。

image17

将这个场景保存为 MusicPlayer.tscn

我们需要将其注册为自动加载。前往菜单项目 -> 项目设置...,然后单击自动加载选项卡。

路径输入框中需要输入场景的路径。单击文件夹图标打开文件浏览器,然后双击 MusicPlayer.tscn。接下来,单击右侧的添加按钮,将该节点进行注册。

image18

MusicPlayer.tscn 现在会被加载到任何你打开或播放的场景中。 因此,如果你现在运行游戏,音乐将在任何场景中自动播放。

在这一节课结束之前,我们来看一下在底层发生了什么。运行游戏时,你的场景面板会多出来两个选项卡:远程本地

image19

你可以在远程选项卡中查看运行中的游戏的节点树。你会看到 Main 节点以及场景中所包含的所有东西,最底部是实例化的小怪。

image20

顶部的是自动加载的 MusicPlayer 以及一个 root 节点,这是你的游戏的视口。

这一节课就是这样。在下一部分,我们会添加动画,让游戏更美观。

这是完整的 main.gd 脚本,仅供参考。

extends Node

@export var mob_scene: PackedScene

func _ready():
    $UserInterface/Retry.hide()


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

    # Choose a random location on the SpawnPath.
    # We store the reference to the SpawnLocation node.
    var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
    # And give it a random offset.
    mob_spawn_location.progress_ratio = randf()

    var player_position = $Player.position
    mob.initialize(mob_spawn_location.position, player_position)

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

    # We connect the mob to the score label to update the score upon squashing one.
    mob.squashed.connect($UserInterface/ScoreLabel._on_mob_squashed.bind())

func _on_player_hit():
    $MobTimer.stop()
    $UserInterface/Retry.show()

func _unhandled_input(event):
    if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible:
        # This restarts the current scene.
        get_tree().reload_current_scene()