平视显示器

我们的游戏最后还需要用户界面(User Interface,UI),显示分数、“游戏结束”信息、重启按钮。

创建新场景,然后添加一个 CanvasLayer 节点并命名为 HUD。“HUD”是“heads-up display”(平视显示器)的缩写,是覆盖在游戏视图上用来显示信息的显示器。

CanvasLayer 节点可以让我们在游戏的其他部分的上一层绘制 UI 元素,这样它所显示的信息就不会被任何游戏元素(如玩家或敌人)所覆盖。

HUD 中需要显示以下信息:

  • 得分,由 ScoreTimer 更改。

  • 消息,例如“Game Over”或“Get Ready!”

  • “Start”按钮来开始游戏。

UI 元素的基本节点是 Control 。要创建 UI,我们需使用 Control 下的两种节点:LabelButton

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

  • 名为分数标签 ScoreLabelLabel

  • 名为消息 MessageLabel

  • 名为开始按钮 StartButtonButton

  • 名为信息计数器 MessageTimerTimer

点击 ScoreLabel 并在“检查器”的 Text 字段中键入一个数字。 Control 节点的默认字体很小,不能很好地缩放。游戏素材包中有一个叫作“Xolonium-Regular.ttf”的字体文件。 使用此字体需要执行以下操作:

  1. 在“Custom Fonts”中,选择“新建 Font”

../../_images/custom_font1.png
  1. 点击您添加的“Font”,然后在“Font/Data/0”的下拉选项中选择“加载”并选择“Xolonium-Regular.ttf”文件。

../../_images/custom_font2.png

ScoreLabel 上完成此操作后,可以单击 Font 属性旁边向下的箭头,然后选择“复制”,然后将其“粘贴”到其他两个 Control 节点的相同位置。设置 ScoreLabel 的“Custom Font Size”属性,把它设成 64 即可。

../../_images/custom_font3.png

注解

锚点和边距:Control 节点具有位置和大小,但它也有锚点(Anchor)和边距(Margin)。锚点定义的是原点——节点边缘的参考点。移动或调整控件节点大小时,边距会自动更新。边距表示的是控件节点的边缘与锚点的距离。

按如下图所示排列节点。点击“布局”按钮以设置 Control 节点的布局:

../../_images/ui_anchor.png

你可以拖动节点以手动放置它们,或者要进行更精确的放置,请使用以下设置:

ScoreLabel

  • 布局:“顶部全幅”

  • Text0

  • Align:“Center”

Message

  • 布局:“水平居中全幅”

  • TextDodge the Creeps!

  • Align:“Center”

  • Autowrap:“启用”

StartButton

  • TextStart

  • 布局:“底部居中”

  • Margin

    • Top:-200

    • Bottom:-100

MessageTimer 中,将 Wait Time 设置为 2 并将 One Shot 属性设置为“启用”。

现将这个脚本添加到 HUD

extends CanvasLayer

signal start_game
public class HUD : CanvasLayer
{
    // Don't forget to rebuild the project so the editor knows about the new signal.

    [Signal]
    public delegate void StartGame();
}
// Copy `player.gdns` to `hud.gdns` and replace `Player` with `HUD`.
// Attach the `hud.gdns` file to the HUD node.

// Create two files `hud.cpp` and `hud.hpp` next to `entry.cpp` in `src`.
// This code goes in `hud.hpp`. We also define the methods we'll be using here.
#ifndef HUD_H
#define HUD_H

#include <Button.hpp>
#include <CanvasLayer.hpp>
#include <Godot.hpp>
#include <Label.hpp>
#include <Timer.hpp>

class HUD : public godot::CanvasLayer {
    GODOT_CLASS(HUD, godot::CanvasLayer)

    godot::Label *_score_label;
    godot::Label *_message_label;
    godot::Timer *_start_message_timer;
    godot::Timer *_get_ready_message_timer;
    godot::Button *_start_button;
    godot::Timer *_start_button_timer;

public:
    void _init() {}
    void _ready();
    void show_get_ready();
    void show_game_over();
    void update_score(const int score);
    void _on_StartButton_pressed();
    void _on_StartMessageTimer_timeout();
    void _on_GetReadyMessageTimer_timeout();

    static void _register_methods();
};

#endif // HUD_H

start_game 信号通知 Main 节点,按钮已经被按下。

func show_message(text):
    $Message.text = text
    $Message.show()
    $MessageTimer.start()
public void ShowMessage(string text)
{
    var message = GetNode<Label>("Message");
    message.Text = text;
    message.Show();

    GetNode<Timer>("MessageTimer").Start();
}
// This code goes in `hud.cpp`.
#include "hud.hpp"

void HUD::_ready() {
    _score_label = get_node<godot::Label>("ScoreLabel");
    _message_label = get_node<godot::Label>("MessageLabel");
    _start_message_timer = get_node<godot::Timer>("StartMessageTimer");
    _get_ready_message_timer = get_node<godot::Timer>("GetReadyMessageTimer");
    _start_button = get_node<godot::Button>("StartButton");
    _start_button_timer = get_node<godot::Timer>("StartButtonTimer");
}

void HUD::_register_methods() {
    godot::register_method("_ready", &HUD::_ready);
    godot::register_method("show_get_ready", &HUD::show_get_ready);
    godot::register_method("show_game_over", &HUD::show_game_over);
    godot::register_method("update_score", &HUD::update_score);
    godot::register_method("_on_StartButton_pressed", &HUD::_on_StartButton_pressed);
    godot::register_method("_on_StartMessageTimer_timeout", &HUD::_on_StartMessageTimer_timeout);
    godot::register_method("_on_GetReadyMessageTimer_timeout", &HUD::_on_GetReadyMessageTimer_timeout);
    godot::register_signal<HUD>("start_game", godot::Dictionary());
}

当想显示一条临时消息时,比如“Get Ready”,就会调用这个函数。

func show_game_over():
    show_message("Game Over")
    # Wait until the MessageTimer has counted down.
    yield($MessageTimer, "timeout")

    $Message.text = "Dodge the\nCreeps!"
    $Message.show()
    # Make a one-shot timer and wait for it to finish.
    yield(get_tree().create_timer(1), "timeout")
    $StartButton.show()
async public void ShowGameOver()
{
    ShowMessage("Game Over");

    var messageTimer = GetNode<Timer>("MessageTimer");
    await ToSignal(messageTimer, "timeout");

    var message = GetNode<Label>("Message");
    message.Text = "Dodge the\nCreeps!";
    message.Show();

    await ToSignal(GetTree().CreateTimer(1), "timeout");
    GetNode<Button>("StartButton").Show();
}
// This code goes in `hud.cpp`.
// There is no `yield` in GDNative, so we need to have every
// step be its own method that is called on timer timeout.
void HUD::show_get_ready() {
    _message_label->set_text("Get Ready");
    _message_label->show();
    _get_ready_message_timer->start();
}

void HUD::show_game_over() {
    _message_label->set_text("Game Over");
    _message_label->show();
    _start_message_timer->start();
}

当玩家死亡时调用这个函数。将显示“Game Over”2 秒,然后返回标题屏幕并显示“Start”按钮。

注解

当您需要暂停片刻时,可以使用场景树的 get_tree().create_timer(2) 函数替代使用 Timer 节点。这对于延迟非常有用,例如在上述代码中,在这里我们需要在显示“开始”按钮前等待片刻。

func update_score(score):
    $ScoreLabel.text = str(score)
public void UpdateScore(int score)
{
    GetNode<Label>("ScoreLabel").Text = score.ToString();
}
// This code goes in `hud.cpp`.
void HUD::update_score(const int p_score) {
    _score_label->set_text(godot::Variant(p_score));
}

每当分数改变,这个函数会被 Main 调用。

连接 MessageTimertimeout() 信号和 StartButtonpressed() 信号并添加以下代码到新函数中:

func _on_StartButton_pressed():
    $StartButton.hide()
    emit_signal("start_game")

func _on_MessageTimer_timeout():
    $Message.hide()
public void OnStartButtonPressed()
{
    GetNode<Button>("StartButton").Hide();
    EmitSignal("StartGame");
}

public void OnMessageTimerTimeout()
{
    GetNode<Label>("Message").Hide();
}
// This code goes in `hud.cpp`.
void HUD::_on_StartButton_pressed() {
    _start_button_timer->stop();
    _start_button->hide();
    emit_signal("start_game");
}

void HUD::_on_StartMessageTimer_timeout() {
    _message_label->set_text("Dodge the\nCreeps");
    _message_label->show();
    _start_button_timer->start();
}

void HUD::_on_GetReadyMessageTimer_timeout() {
    _message_label->hide();
}

将 HUD 场景连接到 Main 场景

现在我们完成了 HUD 场景,保存并返回 Main 场景。和 Player 场景的做法一样,在 Main 场景中实例化 HUD 场景。如果您没有错过任何东西,完整的场景树应该像这样:

../../_images/completed_main_scene.png

现在我们需要将 HUD 功能与我们的 Main 脚本连接起来。这需要在 Main 场景中添加一些内容:

在节点选项卡中,通过在“连接信号”窗口的“接收方法”中键入 new_game,将 HUD 的 start_game 信号连接到主节点的 new_game() 函数。观察绿色的连接图标现在是否在脚本中的 func new_game() 左边出现。

new_game() 函数中,更新分数显示并显示“Get Ready”消息:

$HUD.update_score(score)
$HUD.show_message("Get Ready")
var hud = GetNode<HUD>("HUD");
hud.UpdateScore(Score);
hud.ShowMessage("Get Ready!");
_hud->update_score(score);
_hud->show_get_ready();

game_over() 中我们需要调用相应的 HUD 函数:

$HUD.show_game_over()
GetNode<HUD>("HUD").ShowGameOver();
_hud->show_game_over();

最后,将下面的代码添加到 _on_ScoreTimer_timeout() 以保持不断变化的分数的同步显示:

$HUD.update_score(score)
GetNode<HUD>("HUD").UpdateScore(Score);
_hud->update_score(score);

现在你可以开始游戏了!点击“运行项目”按钮。将要求你选择一个主场景,因此选择 Main.tscn

删除旧的小怪

如果你一直玩到“游戏结束”,然后重新开始新游戏,上局游戏的小怪仍然显示在屏幕上。更好的做法是在新游戏开始时清除它们。我们需要一个同时让所有小怪删除它自己的方法,为此可以使用“分组”功能。

Mob 场景中,选择根节点,然后单击检查器旁边的“节点”选项卡(在该位置可以找到节点的信号)。 点击“信号”旁边的“分组”,然后可以输入新的组名称,点击“添加”。

../../_images/group_tab.png

现在,所有小怪都将属于“mobs”(小怪)分组。我们可以将以下行添加到 Main 中的 new_game() 函数中:

get_tree().call_group("mobs", "queue_free")
// Note that for calling Godot-provided methods with strings,
// we have to use the original Godot snake_case name.
GetTree().CallGroup("mobs", "queue_free");
get_tree()->call_group("mobs", "queue_free");

call_group() 函数调用组中每个节点上的删除函数——让每个怪物删除其自身。

游戏在这一点上大部分已经完成。在下一部分和最后一部分中,我们将通过添加背景,循环音乐和一些键盘快捷键来对其进行一些润色。