UI: score, message and button
Place a CanvasLayer above the level, size its controls and connect the button once.
UI prefab
Create a CanvasLayer root named Hud and save as Prefabs/Hud.rprefab:
Hud (CanvasLayer, DodgeHud.cs)
└── Root (Control)
├── ScoreLabel (Label)
├── MessageLabel (Label)
├── StartButton (Button)
└── InstructionsLabel (Label)Set Root to Full Rect with Mouse Filter = Ignore. For the 640 × 360 arena, set these rectangles with offsets from the top-left corner:
| Control | Left, Top, Right, Bottom | Additional settings |
|---|---|---|
ScoreLabel | 24, 16, 240, 52 | Font size 24 |
MessageLabel | 60, 120, 580, 172 | Centered text, font size 24 |
StartButton | 220, 200, 420, 248 | 200-wide, 48-high button |
InstructionsLabel | 60, 320, 580, 352 | Centered text |
Keep anchors at zero on all four child Controls so the table defines rectangles from the top-left corner. Set Mouse Filter = Ignore on the three Labels and keep Stop on the button. Leave Hud.Layer = 1: CanvasLayer puts UI on a separate layer above 2D objects. These positions target a fixed window; variable sizes need another layout with containers and anchors.
Code
Attach Scripts/DodgeHud.cs to the root. Text follows TranslationServer.GetLocale(): ru uses Russian, other locales use English. Both versions use the same paths and signals.
Complete DodgeHud.cs
using Reckfell;
public partial class DodgeHud : CanvasLayer
{
[Signal] public delegate void StartRequestedEventHandler();
private Label _score = null!;
private Label _message = null!;
private Label _instructions = null!;
private Button _button = null!;
public override void _EnterTree()
{
_score = GetEntity<Label>("Root/ScoreLabel");
_message = GetEntity<Label>("Root/MessageLabel");
_instructions = GetEntity<Label>("Root/InstructionsLabel");
_button = GetEntity<Button>("Root/StartButton");
_button.Pressed += OnPressed;
_instructions.Text = L("Движение: WASD или стрелки", "Move: WASD or arrow keys");
}
public override void _ExitTree()
{
if (ReckfellObject.IsInstanceValid(_button))
_button.Pressed -= OnPressed;
}
public void SetScore(int score) => _score.Text = L("Счёт: ", "Score: ") + score;
public void ShowReady() => ShowMessage(L("Избегайте белых кругов", "Avoid the white circles"), L("Начать", "Start"));
public void ShowGameOver() => ShowMessage(L("Игра окончена", "Game over"), L("Ещё раз", "Restart"));
public void ShowPlaying()
{
_message.Hide();
_button.Hide();
}
public void ShowSetupError()
{
ShowMessage(L("Проверьте Prefabs/Enemy.rprefab", "Check Prefabs/Enemy.rprefab"), "");
_button.Disabled = true;
}
private void ShowMessage(string message, string buttonText)
{
_message.Text = message;
_message.Show();
_button.Text = buttonText;
_button.Show();
}
private void OnPressed() => EmitSignal(SignalName.StartRequested);
private static string L(string ru, string en) => TranslationServer.GetLocale().StartsWith("ru") ? ru : en;
}SetScore() only updates its label. ShowPlaying() hides the message and button; ShowGameOver() restores them while preserving score. The button emits StartRequested instead of replacing the level itself; DodgeGame controls the round.
If the button is visible but cannot be clicked, check overlapping Controls and Mouse Filter: decorative elements should not consume the click. Root using Ignore does not require disabling its child button's input.
Connection
The script connects Button.Pressed to a named method, so no second editor connection is needed. Tree exit removes the handler, preventing a second start request after reinserting UI. Restore visible state from current game state explicitly; a signal does not store history.
Place an instance of Hud.rprefab in the level as Hud. Build all scripts and run Arena.rscene: the button appears before play, score remains during play, and a hit restores the message and restart button. If the button is missing, check Root/StartButton, dimensions, visibility and the launched document.