Lesson 11
Numbers in the Output panel aren't a game. We add a proper on-screen HUD with score, level, and lines using Control nodes, plus a Game Over panel with a Restart button.
UI should ignore the game camera and always sit on top. That's what a CanvasLayer is for. Build the HUD as children of one.
main.tscn. Right-click Main → Add Child → CanvasLayer, rename HUD.HUD, add a Label. Rename it ScoreLabel. In the Inspector set its position to the right of the well (e.g. x=340, y=20) and type a placeholder like Score: 0.LevelLabel and LinesLabel; stack them below (y=60, y=100).Node paths for UI
From main.gd you reach these with $HUD/ScoreLabel. Because the HUD is a child of Main, the path is stable. If you nest deeper, update the path accordingly.
Cache the labels and add an update method to main.gd:
@onready var score_label: Label = $HUD/ScoreLabel
@onready var level_label: Label = $HUD/LevelLabel
@onready var lines_label: Label = $HUD/LinesLabel
func _refresh_hud() -> void:
score_label.text = "Score: %d" % score
level_label.text = "Level: %d" % level
lines_label.text = "Lines: %d" % lines_cleared_totalCall _refresh_hud() at the end of _ready() and inside _award_score() so it updates on every clear:
func _award_score(cleared: int) -> void:
cleared = clampi(cleared, 0, 4)
score += LINE_SCORES[cleared]
lines_cleared_total += cleared
_update_level()
_refresh_hud()HUD, add a PanelContainer, rename GameOverPanel. Center it over the well.Game Over) and a Button (text: Restart, name it RestartButton).GameOverPanel and turn its Visible property OFF in the Inspector — it should be hidden until you lose.Show the panel on game over and wire the button. In main.gd:
@onready var game_over_panel: PanelContainer = $HUD/GameOverPanel
@onready var restart_button: Button = $HUD/GameOverPanel/VBoxContainer/RestartButton
func _ready() -> void:
board = Board.new(COLS, ROWS)
_draw_border()
game_over_panel.visible = false
restart_button.pressed.connect(_on_restart_pressed)
_refresh_hud()
spawn_piece()
func _on_restart_pressed() -> void:
get_tree().reload_current_scene()Reveal the panel in _trigger_game_over():
func _trigger_game_over() -> void:
game_over = true
active_piece = null
game_over_panel.visible = trueSignals: pressed.connect
Buttons emit a pressed signal. restart_button.pressed.connect(_on_restart_pressed) runs our function when clicked. reload_current_scene() is the simplest possible restart — it rebuilds the whole scene fresh.
Match the node path exactly
The path $HUD/GameOverPanel/VBoxContainer/RestartButton must match your tree names exactly, including capitalization. A wrong path gives a null and a crash on the @onready line. Rename carefully or copy the path with right-click → Copy Node Path.
Checkpoint — expected state
pressed signal that reloads the scene.