Signals are Godot’s decoupled event system — a node announces something happened and others react without being tightly linked. Use them to build a HUD, and learn autoloads for game-wide state.
A signal is an event a node emits that other nodes can connect to — the observer pattern built into the engine. The emitter does not know or care who is listening, which keeps nodes loosely coupled: an enemy emits died and the score system, spawner, and sound each react independently. Signals are the idiomatic Godot way to communicate between nodes and are central to clean architecture.
extends CharacterBody2D # an enemy
signal died(points) # declare a custom signal
func take_damage(amount: int) -> void:
health -= amount
if health <= 0:
died.emit(100) # announce it — don't call the score system directly
queue_free()
# elsewhere, anyone interested connects and reacts (they stay decoupled):
# enemy.died.connect(_on_enemy_died)
# func _on_enemy_died(points): score += points
# the enemy doesn't KNOW about the score/UI/sound — they just listen. Loose
# coupling via signals = the observer pattern, built into Godot.Godot’s UI is built from Control nodes — Label, Button, ProgressBar, and containers that handle layout and anchoring so the interface adapts to any screen size. A HUD is a CanvasLayer of Control nodes drawn on top of the game. You update it by connecting gameplay signals to it, so the score label changes when a coin signal fires — UI and gameplay stay decoupled.
extends CanvasLayer # a HUD, drawn on top of the game world
@onready var score_label := $ScoreLabel # a Label Control node
@onready var health_bar := $HealthBar # a ProgressBar
func set_score(value: int) -> void:
score_label.text = "Score: %d" % value # %d format, like Python
func set_health(value: int) -> void:
health_bar.value = value
# wire gameplay -> UI via signals (keeps them decoupled):
# player.health_changed.connect(hud.set_health)
# Control nodes (Label, Button, ProgressBar, containers) auto-layout + anchor
# so the UI adapts to any screen size.Some data must persist across scenes and be reachable everywhere — the score, player progress, settings, an audio manager. An autoload (singleton) is a script registered to load once and stay alive for the whole game, accessible by name from any script. It is the right home for global state and manager systems, avoiding fragile chains of get_node across the tree.
# GameState.gd — registered as an Autoload (Project Settings > Autoload)
# it loads once and persists across ALL scenes, accessible by name.
extends Node
var score := 0
var current_level := 1
func add_score(points: int) -> void:
score += points
# from ANY script, anywhere, just use its name:
# GameState.add_score(100)
# GameState.score
# autoloads (singletons) hold game-wide state + managers (audio, save, settings)
# -> no fragile get_node chains for global data.Moving between the main menu, levels, and a game-over screen is scene switching. The SceneTree’s change_scene_to_file swaps the whole current scene for another, and you keep anything that must survive (the score) in an autoload. Understanding scene changes — and what persists across them — is how you structure a game’s overall flow, tying back to the state machine idea from the Game AI course.
# switch the whole active scene (menu -> level -> game over):
func start_game() -> void:
get_tree().change_scene_to_file("res://Level1.tscn")
func game_over() -> void:
get_tree().change_scene_to_file("res://GameOver.tscn")
# the OLD scene is freed; the NEW one loads. Anything that must survive the
# switch (score, settings) lives in an AUTOLOAD, not the scene.
# game flow (Menu -> Playing -> GameOver) is itself a state machine
# (see the Game AI course). Scene changes are the transitions.