UI and signals
A button counter, subscription cleanup, tree reentry and keyboard focus.
A signal reports an event, such as a button press. In C#, subscribe a method to the corresponding event rather than checking the button every frame.
1. Create the interface
Create a separate document with a Control root, name it Counter and save as Counter.rscene. Control elements are designed for interfaces, with dimensions, anchors and layout rules.
Counter (Control)
├── CountLabel (Label)
└── AddButton (Button)Select the root and choose Full Rect in the Layout tools. Keep the children anchored at the top left. Set these Layout properties in the inspector:
| Object | Position | Size | Text |
|---|---|---|---|
CountLabel | (32, 32) | (200, 40) | 0 |
AddButton | (32, 96) | (200, 48) | Add +1 |
If the inspector shows Offsets, the button's equivalent bounds are Left 32, Top 96, Right 232, Bottom 144. This document needs no camera or light.
2. Add Counter.cs
Attach Scripts/Counter.cs to the Counter root, not the button. Use this complete code:
using Reckfell;
public partial class Counter : Control
{
private int _count;
private Label _label = null!;
private Button _button = null!;
public override void _EnterTree()
{
_label = GetEntity<Label>("CountLabel");
_button = GetEntity<Button>("AddButton");
_button.Pressed += AddOne;
_label.Text = _count.ToString();
}
public override void _ExitTree()
{
if (ReckfellObject.IsInstanceValid(_button))
_button.Pressed -= AddOne;
}
private void AddOne()
{
_count += 1;
_label.Text = _count.ToString();
}
}Save, build C# and run the current document using F6. Press the button several times: the label should show 1, 2, 3 and so on.
How the pieces connect
_count stores an integer and starts at zero. _label and _button hold child references. On tree entry, _EnterTree() finds the already constructed children and connects the handler. If you need data calculated by a child's _Ready(), use the parent's _Ready(): children may not be ready during _EnterTree().
Pressed += AddOne means “call AddOne when the button reports a press.” Write AddOne without parentheses here: you are passing an event handler rather than calling it immediately.
_count += 1 increments the counter. ToString() converts the number to text for Label.Text. private marks a field or method as internal to this class. null! allows later initialization; it does not create a Label or fix a misspelled CountLabel name.
Why the subscription has a matching removal
Pressed -= AddOne disconnects the same method connected with +=. _ExitTree() performs this cleanup: an interface removed from the level stops receiving events. IsInstanceValid() also accounts for the button being deleted separately.
Adding the same Counter back with AddChild() makes _EnterTree() reconnect the handler and preserves _count. This differs from deletion with QueueFree(): a new instance starts at zero. The _EnterTree() / _ExitTree() pair also works on reentry, when _Ready() does not repeat automatically.
Do not connect Pressed both through this code and through the editor: one press could add two. Do not subscribe every frame in _Process(). Use a named method when you need to unsubscribe; a new lambda passed to -= is not the previously connected lambda.
Check mouse and keyboard activation
- After launch, click the button three times: the label should read
3. - Move focus to the button with Tab and activate
ui_accept— Enter or Space by default. The counter should increase by one. - If the project changes built-in input actions, check
ui_acceptin Input Map. The button also needs an enabled focus mode.
Use Pressed for one action per activation. For a switch, use Toggle Mode and Toggled; do not reconstruct its state by counting clicks.
| Symptom | Check |
|---|---|
GetEntity fails on startup | The script is attached to Counter; its children are named exactly CountLabel and AddButton, with the correct types |
| One press adds two | The editor also connects the signal, or other code repeats the subscription |
| The button is visible but does not respond | Check Disabled, an overlapping Control, and its Mouse Filter |
| Tab does not focus the button | Check its focus mode, visibility and the ui_focus_next action |
| Access fails after button deletion | Do not retain an unchecked native reference for a later call; match subscriptions to lifetimes |
A separate screen or a HUD
This document is a separate screen. In a game with a moving world camera, a HUD usually goes in a CanvasLayer containing a Control tree. This keeps the camera from moving the counter off-screen.
Use containers such as VBoxContainer and HBoxContainer, plus anchors, for resizable layouts instead of scaling text with Scale.
The counter resets on each run: this example has no disk persistence. Reference: Control, Button, Label.