UI & HUD

Godot Fundamentals

Chapter 8 · UI & HUD

Chapter 7 built a health system that emits signals but has no visible interface at all. This chapter builds the missing half: a real, on-screen HUD — a health bar and a score display — that listens to those signals and updates itself, plus a simple pause menu that shows and hides on demand.

CanvasLayer — Drawing UI on Top of the Game

Every node used so far (Sprite2D, CharacterBody2D, Area2D) lives in the game world — it moves with the camera, and can be positioned anywhere in that world's own coordinate space. UI is different: a health bar should always sit in the same spot on screen, regardless of where the camera happens to be looking.

CanvasLayer solves this — anything placed under it draws in a separate layer, on top of the game world, using screen coordinates instead of world coordinates.

The standard shape — a scene's UI almost always lives under one CanvasLayer node, itself holding every HUD element as children. This keeps a clean separation: gameplay nodes below the CanvasLayer, interface nodes inside it.

Control Nodes & Anchors

UI elements — labels, buttons, bars — all inherit from Control, a node type built specifically for screen-space layout. A Control node uses anchors to describe where it sits relative to its parent's own edges, so it stays correctly placed even if the window is resized.

Anchor presets

The editor's Layout menu offers one-click presets — Top Left, Top Right, Full Rect, Center, and more — for the most common placements.

Containers

VBoxContainer, HBoxContainer, and MarginContainer automatically arrange their own children, instead of positioning each one by hand.

Prefer a preset over manual dragging — a health bar anchored to "Top Left" stays in the top left corner at any window size; one positioned only by dragging it to a pixel coordinate will drift out of place the moment the window is resized.

Label & ProgressBar

NodeKey propertyUse
LabeltextAny plain on-screen text — a score, a name, a message
ProgressBarvalue (with min_value/max_value)A health bar, a stamina bar, a loading indicator
# --- score_label.gd, attached to a Label --- extends Label func update_score(new_score: int) -> void: text = "Score: " + str(new_score)

Wiring the HUD to Chapter 7's Health Signal

This is where UI and signals meet directly — a ProgressBar-based health bar connects to the exact same health_changed signal from Chapter 7's example, with no changes needed to the health system itself.

# --- health_bar.gd, attached to a ProgressBar under a CanvasLayer --- extends ProgressBar func _ready() -> void: var health = get_node("/root/Main/Player/Health") health.health_changed.connect(_on_health_changed) # Set up the bar's own range to match the health system. min_value = 0 max_value = health.max_health value = health.current func _on_health_changed(current: int, max_health: int) -> void: value = current
Hardcoded paths like /root/Main/Player/Health are fragile — they break the moment a scene is reorganized. This is acceptable for a small, single-scene example like this course, but a larger project typically reaches for either an exported node reference (dragged into the Inspector) or an autoload/singleton — a globally-accessible script Godot keeps alive across every scene. Autoloads are a genuinely deep topic on their own and are outside this course's own Fundamentals scope, but worth knowing the name of for when a project outgrows a hardcoded path like this one.

Showing & Hiding Menus

Every Control (and every Node2D-derived node, in fact) has a visible property, along with show() and hide() convenience methods that set it for you.

# --- pause_menu.gd, attached to a Control that's hidden by default --- extends Control func _ready() -> void: hide() # start hidden, same as setting visible = false func _process(delta: float) -> void: if Input.is_action_just_pressed("pause"): visible = not visible
Coming from Python visible = not visible flips a boolean the same way it would in Python — nothing new syntactically. What's Godot-specific is what a hidden Control actually does: setting visible = false stops it from being drawn or processing mouse/keyboard input from that Control at all, without needing a separate "is this menu open" flag threaded through the rest of your code the way you might manage visibility state by hand in a plain Python UI toolkit.

Coding Challenges

Challenge 1
Build a CanvasLayer containing a Label anchored to the top-left corner. Write a script with a score variable starting at 0, a function that adds 10 to it and updates the Label's text to "Score: X", and call that function a few times from _ready() to confirm it updates correctly.
→ Solution
Challenge 2
Build the full Chapter 7 health.gd health system plus a ProgressBar HUD element that connects to its health_changed signal, matching this chapter's own health_bar.gd example. Call take_damage() a few times and confirm the bar's value visibly drops each time.
→ Solution
Challenge 3
Build a Control-based pause menu that starts hidden. Bind a "pause" action to the Escape key in the Input Map, and toggle the menu's visibility each time it's pressed using visible = not visible.
→ Solution

Quick Reference — UI & HUD

  • CanvasLayer — draws UI in screen space, on top of the game world
  • Control nodes use anchors (Layout menu presets) to stay correctly placed at any window size
  • Containers (VBoxContainer, HBoxContainer, MarginContainer) auto-arrange their children
  • Label.text — plain text display
  • ProgressBar.value/min_value/max_value — a health/stamina bar
  • A HUD element connects to a game system's own signal (Chapter 7) — no changes needed to that system
  • visible, show(), hide() — toggle any Control's own visibility