Create a new scene with CanvasLayer as the root node
Save the scene as hud.tscn
Think of a CanvasLayer as a clear sheet of glass laid right over the top of your game screen.
Normally, every 2D node in your scene lives in the same shared world space—if a giant asteroid drifts across the middle of the screen, or if a camera pans away, standard nodes move with the world or can get covered up by gameplay objects.
Placing your UI nodes (like your Play button, score labels, and health bars) inside a CanvasLayer solves two major problems automatically:
Always on Top: By default, a CanvasLayer renders on layer 1 (above your main game world on layer 0). No matter how many lasers, explosions, or space rocks fill the screen, your menu and HUD will always draw cleanly on top of the action.
Locked to the Screen: A CanvasLayer ignores your game world's position and camera movement. Even if you add a scrolling camera or screen-shake later, anything inside the CanvasLayer stays pinned to the exact same spot on the player's monitor.
Add a Timer and MarginContainer as children
Select the MarginContainer
Set the Anchor to Top Wide
In Theme Overrides set margins to 20
Add an HBoxContainer as a child of MarginContainer
This will position the score on the left
And the lives on the right
As children of HBoxContainer add
A Label renamed to ScoreLabel
another HBoxContainer renamed to LivesCounter
Set Horizontal size to Expand
Add a TextureRect as a child of LivesCounter
Set player_small.png as the Texture
Set Stretch Mode to Keep Aspect Centered
Select the L1 node and duplicate it twice
We will hide and show these nodes
To display the number of player lives
Add a VBoxContainer as a child of HUD
Inside the VBox add a Label named Message
Add a TextureButton named StartButton
Set the Layout of VBoxContainer to Center Wide
In Theme Overrides set Separation to 100
Use play_button.png for the Normal Texture
Use play_button_h.png for Hover Texture
Set Horizontal size to Shrink Center
Set Horizontal Alignment to Center
Add a script to the HUD node
Add a custom signal to broadcast game start
As your Scene Tree hierarchy grows deeper, referring to nodes using long path strings with the $ shortcut can become tedious and fragile. Furthermore, because a script runs before its child nodes are fully loaded into memory, trying to assign node references at the very top of a script will throw a null instance error.
To solve both problems, we use the @onready annotation. This tells Godot: "Wait until this node and all its children have safely entered the scene tree before caching this reference into a variable."
A shortcut for creating onready variables is to drag the node from the scene tree into the script and then hold down Ctrl as you release it.
Create a function to show a message
Create a function to update the lives
Create a game over function
Create the pressed function
Create the timeout function
Connect the pressed and timout signals in code or the Inspector
Add an instance of the HUD to the scene
Add variables to the main script to track
Add a function to setup a new game
Note the $Player.reset() function
We will create this soon though...
Create a function for new levels
Announce level number
Spawn number of rocks to match
Dynamic Text Formatting with %s
In your new_level() function, you used this line: $HUD.show_message("WAVE %s" % level)
The %s acts as a placeholder (or "blank slot") inside a text string.
When Godot runs this line, it looks at the % level instruction at the end and injects the current numerical value of your level variable right into that placeholder. For example:
If level is 1, it evaluates to "WAVE 1"
If level is 2, it evaluates to "WAVE 2"
This is a clean, Python-inspired shortcut in GDScript. It is much easier to read and maintain than manual string conversion and concatenation (like "WAVE " + str(level)), especially when you need to format multiple variables into a single sentence.
Remove the code that is spawning rocks in the ready function
This was for testing and we don't need it anymore
Add a check to count how many rocks are left
Connect the start_game signal of HUD to
the new_game function in Main
RETURN TO THE PLAYER SCENE
Add a signal for lives changed
And a signal for dead to the
Add a variable to flag resetting the position of the player
Add a setter variable for lives
Automated Property Changes with Setter Functions
In Godot 4, a setter function is a special function that runs automatically behind the scenes whenever you change the value of a variable.
Normally, if a player loses a life, you would have to remember to manually write code to update the UI, emit signals, and check if the game is over every single time you change lives. If you forget in even one place, your game bugs out.
By attaching a setter to your variable, written like this: var lives = 3: set = _set_lives
Whenever you write lives -= 1 anywhere in your script, Godot automatically interrupts and runs your setter function first. This guarantees that your health bars, UI labels, and state transitions always update instantly and cleanly, no matter where in your code the variable was modified.
Called when a new game starts
Reset the players position
Back to the center of the screen
In the integrate forces function