分数与重玩

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

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

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

添加一个 Label 节点并重命名为 ScoreLabel

image0

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

image1

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

Scroll down to Theme Overrides, and expand Colors and click the black box next to Font Color to tint the text.

image2

选择一个暗色调,与 3D 场景形成对比。

image3

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

image4

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

创建 UI 主题

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

image5

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

image6

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

参见

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

单击 Default Font(默认字体)属性,创建一个新的 DynamicFont

image7

单击展开 DynamicFont,然后展开 Font(字体)部分。在这里你会看到一个空的 Font Data(字体数据)字段。

image8

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

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

文本有一点小。将 Settings -> Size 设置为 22 像素即可增大文本的大小。

image9

跟踪得分

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

extends Label

var score = 0

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

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

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

image10

或者,你也可以在文件系统面板中双击 Main.gd 文件。

_on_MobTimer_timeout() 函数的最后添加下面这一行代码。

func _on_MobTimer_timeout():
    #...
    # We connect the mob to the score label to update the score upon squashing one.
    mob.connect("squashed", $UserInterface/ScoreLabel, "_on_Mob_squashed")

这一行的意思是,当小怪发出 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 格式字符串

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

image11

备注

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

重玩游戏

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

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

你可以使用工具栏上的布局菜单来让它覆盖整个视口。

image12

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

image13

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

image14

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

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

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

image15

接下来,添加一个 Label 作为 Retry 的子项,将其 Text 设为“Press Enter to retry.”(按回车键重试。)

image16

要将其移动并锚定到屏幕中央,请对其使用局部 -> 居中

image17

编写重试选项

我们现在就可以去编写代码,在玩家死亡时显示 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 在游戏开始时自动加载节点或场景,不依赖于当前场景。你还可以用它来创建能够全局访问的对象。

场景菜单中单击新建场景来创建一个新场景。

image18

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

image19

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

image20

将这个场景保存为 MusicPlayer.tscn

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

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

image21

现在再运行游戏,音乐就会自动播放了。而且即便你死了然后重试,它还是会在持续播放。

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

image22

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

image23

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

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

这是一个仅供参考的 Main.gd 脚本。

extends Node

export (PackedScene) var mob_scene


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


func _unhandled_input(event):
    if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible:
        get_tree().reload_current_scene()


func _on_MobTimer_timeout():
    var mob = mob_scene.instance()

    var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
    mob_spawn_location.unit_offset = randf()

    var player_position = $Player.transform.origin
    mob.initialize(mob_spawn_location.translation, player_position)

    add_child(mob)
    mob.connect("squashed", $UserInterface/ScoreLabel, "_on_Mob_squashed")


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