分数与重玩

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

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

在主场景中,添加一个新的 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 Base Scale(默认基础缩放)、Default Font(默认字体)、Default Font Size(默认字体大小)。

参见

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

这里的 Default Font 需要一个字体文件,就是你电脑上用的那种。常见的字体文件格式有两种: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() 函数。

回到 score_label.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

将这个场景保存为 music_player.tscn

We have to register it as an autoload. Head to the Project -> Project Settings… menu and click on the Globals -> Autoload tab.

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

image18

music_player.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()