Heads up display

The final piece our game needs is a User Interface (UI) to display things like score, a “game over” message, and a restart button.

Create a new scene, and add a CanvasLayer node named HUD. “HUD” stands for “heads-up display”, an informational display that appears as an overlay on top of the game view.

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

HUD 中需要显示以下信息:

  • 得分,由 ScoreTimer 更改。

  • 消息,例如 Game OverGet Ready!

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

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

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

  • 名为分数标签 ScoreLabelLabel

  • 名为消息 MessageLabel

  • 名为开始按钮 StartButtonButton

  • 名为信息计数器 MessageTimerTimer

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

  1. Under “Custom Fonts”, choose “New Font”

../../_images/custom_font1.png

  1. Click on the “Font” you added, and under “Font/Data/0”, choose “Load” and select the “Xolonium-Regular.ttf” file.

../../_images/custom_font2.png

Once you’ve done this on the ScoreLabel, you can click the down arrow next to the Font property and choose “Copy”, then “Paste” it in the same place on the other two Control nodes. Set “Custom Font Size” property of the ScoreLabel. A setting of 64 works well.

../../_images/custom_font3.png

注解

Anchors and Margins: Control nodes have a position and size, but they also have anchors and margins. Anchors define the origin - the reference point for the edges of the node. Margins update automatically when you move or resize a control node. They represent the distance from the control node’s edges to its anchor.

按如下图所示排列节点。点击“布局”按钮以设置 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

GDScript

C#

C++

  1. extends CanvasLayer
  2. signal start_game
  1. public class HUD : CanvasLayer
  2. {
  3. // Don't forget to rebuild the project so the editor knows about the new signal.
  4. [Signal]
  5. public delegate void StartGame();
  6. }
  1. // Copy `player.gdns` to `hud.gdns` and replace `Player` with `HUD`.
  2. // Attach the `hud.gdns` file to the HUD node.
  3. // Create two files `hud.cpp` and `hud.hpp` next to `entry.cpp` in `src`.
  4. // This code goes in `hud.hpp`. We also define the methods we'll be using here.
  5. #ifndef HUD_H
  6. #define HUD_H
  7. #include <Button.hpp>
  8. #include <CanvasLayer.hpp>
  9. #include <Godot.hpp>
  10. #include <Label.hpp>
  11. #include <Timer.hpp>
  12. class HUD : public godot::CanvasLayer {
  13. GODOT_CLASS(HUD, godot::CanvasLayer)
  14. godot::Label *_score_label;
  15. godot::Label *_message_label;
  16. godot::Timer *_start_message_timer;
  17. godot::Timer *_get_ready_message_timer;
  18. godot::Button *_start_button;
  19. godot::Timer *_start_button_timer;
  20. public:
  21. void _init() {}
  22. void _ready();
  23. void show_get_ready();
  24. void show_game_over();
  25. void update_score(const int score);
  26. void _on_StartButton_pressed();
  27. void _on_StartMessageTimer_timeout();
  28. void _on_GetReadyMessageTimer_timeout();
  29. static void _register_methods();
  30. };
  31. #endif // HUD_H

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

GDScript

C#

C++

  1. func show_message(text):
  2. $Message.text = text
  3. $Message.show()
  4. $MessageTimer.start()
  1. public void ShowMessage(string text)
  2. {
  3. var message = GetNode<Label>("Message");
  4. message.Text = text;
  5. message.Show();
  6. GetNode<Timer>("MessageTimer").Start();
  7. }
  1. // This code goes in `hud.cpp`.
  2. #include "hud.hpp"
  3. void HUD::_ready() {
  4. _score_label = get_node<godot::Label>("ScoreLabel");
  5. _message_label = get_node<godot::Label>("MessageLabel");
  6. _start_message_timer = get_node<godot::Timer>("StartMessageTimer");
  7. _get_ready_message_timer = get_node<godot::Timer>("GetReadyMessageTimer");
  8. _start_button = get_node<godot::Button>("StartButton");
  9. _start_button_timer = get_node<godot::Timer>("StartButtonTimer");
  10. }
  11. void HUD::_register_methods() {
  12. godot::register_method("_ready", &HUD::_ready);
  13. godot::register_method("show_get_ready", &HUD::show_get_ready);
  14. godot::register_method("show_game_over", &HUD::show_game_over);
  15. godot::register_method("update_score", &HUD::update_score);
  16. godot::register_method("_on_StartButton_pressed", &HUD::_on_StartButton_pressed);
  17. godot::register_method("_on_StartMessageTimer_timeout", &HUD::_on_StartMessageTimer_timeout);
  18. godot::register_method("_on_GetReadyMessageTimer_timeout", &HUD::_on_GetReadyMessageTimer_timeout);
  19. godot::register_signal<HUD>("start_game", godot::Dictionary());
  20. }

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

GDScript

C#

C++

  1. func show_game_over():
  2. show_message("Game Over")
  3. # Wait until the MessageTimer has counted down.
  4. yield($MessageTimer, "timeout")
  5. $Message.text = "Dodge the\nCreeps!"
  6. $Message.show()
  7. # Make a one-shot timer and wait for it to finish.
  8. yield(get_tree().create_timer(1), "timeout")
  9. $StartButton.show()
  1. async public void ShowGameOver()
  2. {
  3. ShowMessage("Game Over");
  4. var messageTimer = GetNode<Timer>("MessageTimer");
  5. await ToSignal(messageTimer, "timeout");
  6. var message = GetNode<Label>("Message");
  7. message.Text = "Dodge the\nCreeps!";
  8. message.Show();
  9. await ToSignal(GetTree().CreateTimer(1), "timeout");
  10. GetNode<Button>("StartButton").Show();
  11. }
  1. // This code goes in `hud.cpp`.
  2. // There is no `yield` in GDNative, so we need to have every
  3. // step be its own method that is called on timer timeout.
  4. void HUD::show_get_ready() {
  5. _message_label->set_text("Get Ready");
  6. _message_label->show();
  7. _get_ready_message_timer->start();
  8. }
  9. void HUD::show_game_over() {
  10. _message_label->set_text("Game Over");
  11. _message_label->show();
  12. _start_message_timer->start();
  13. }

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

注解

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

GDScript

C#

C++

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

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

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

GDScript

C#

C++

  1. func _on_StartButton_pressed():
  2. $StartButton.hide()
  3. emit_signal("start_game")
  4. func _on_MessageTimer_timeout():
  5. $Message.hide()
  1. public void OnStartButtonPressed()
  2. {
  3. GetNode<Button>("StartButton").Hide();
  4. EmitSignal("StartGame");
  5. }
  6. public void OnMessageTimerTimeout()
  7. {
  8. GetNode<Label>("Message").Hide();
  9. }
  1. // This code goes in `hud.cpp`.
  2. void HUD::_on_StartButton_pressed() {
  3. _start_button_timer->stop();
  4. _start_button->hide();
  5. emit_signal("start_game");
  6. }
  7. void HUD::_on_StartMessageTimer_timeout() {
  8. _message_label->set_text("Dodge the\nCreeps");
  9. _message_label->show();
  10. _start_button_timer->start();
  11. }
  12. void HUD::_on_GetReadyMessageTimer_timeout() {
  13. _message_label->hide();
  14. }

将 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”消息:

GDScript

C#

C++

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

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

GDScript

C#

C++

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

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

GDScript

C#

C++

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

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

删除旧的小怪

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

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

../../_images/group_tab.png

Now all mobs will be in the “mobs” group. We can then add the following line to the new_game() function in Main:

GDScript

C#

C++

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

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

The game’s mostly done at this point. In the next and last part, we’ll polish it a bit by adding a background, looping music, and some keyboard shortcuts.