Lesson 08

The HUD: fragment counter and win screen

The player needs on-screen feedback, not Output logs. We add a 3D-game HUD — a fragment counter and an objective hint — plus a win screen with a restart, using the same Control/CanvasLayer approach as the 2D track.

▸ By the end of this lesson: A live fragment counter, an objective prompt, and a win screen with restart

8.1Add a HUD layer

  1. Open main.tscn. Add a child CanvasLayer to Main, rename HUD. 2D UI sits on a CanvasLayer even in a 3D game — it renders on top of the 3D viewport.
  2. Under HUD, add a Label named FragmentLabel, anchored top-left, text Fragments: 0 / 0.
  3. Add another Label named ObjectiveLabel below it, text Collect all fragments.

8.2Drive the HUD from Main

main.gdgdscript
@onready var fragment_label: Label = $HUD/FragmentLabel
@onready var objective_label: Label = $HUD/ObjectiveLabel

func _refresh_hud() -> void:
	fragment_label.text = "Fragments: %d / %d" % [collected_count, total_fragments]
	if collected_count >= total_fragments:
		objective_label.text = "Reach the exit!"
	else:
		objective_label.text = "Collect all fragments"

Call _refresh_hud() at the end of _ready() and inside _on_fragment_collected():

main.gd (replace)gdscript
func _on_fragment_collected(_frag: Area3D) -> void:
	collected_count += 1
	_refresh_hud()
	if collected_count >= total_fragments:
		exit.activate()

8.3A win screen

  1. Under HUD, add a PanelContainer named WinPanel, centered.
  2. Inside it add a VBoxContainer with a Label (text You escaped the glitch!) and a Button named RestartButton (text Play Again).
  3. Select WinPanel and set its Visible to OFF.
main.gdgdscript
@onready var win_panel: PanelContainer = $HUD/WinPanel
@onready var restart_button: Button = $HUD/WinPanel/VBoxContainer/RestartButton

# in _ready(), after other setup:
	win_panel.visible = false
	restart_button.pressed.connect(_on_restart_pressed)
	_refresh_hud()

func _on_exit_reached() -> void:
	win_panel.visible = true
	# Free the mouse so the player can click Play Again.
	Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
	get_tree().paused = true

func _on_restart_pressed() -> void:
	get_tree().paused = false
	get_tree().reload_current_scene()

Unpause before reloading

We pause the tree on win for a clean freeze. If you reload the scene while still paused, the new scene starts frozen. Always set paused = false before reload_current_scene().

Restart button needs to run while paused

Because we pause on win, the button's node must process while paused. Select HUD (or the button) and set Process → Mode to Always, or the click won't register.

  1. Run with F6. The counter updates as you collect; the objective text flips when the exit unlocks.
  2. Win the game — the win panel appears, the mouse frees, and Play Again resets everything.

Checkpoint — expected state