M1.4 — Autoloads & the Main Scene¶
What you'll learn
- Decide when state belongs in an autoload, and register
SignalBusandGameState. - Choose between a script autoload and a scene autoload.
- Build a
Mainscene that splits World (camera-affected) from UI (CanvasLayer), and set the run target soF5works. - Use a global signal bus to decouple systems as they multiply.
How it applies
- The world and the HUD must not share a transform. When the camera follows the player, the
world scrolls; the health bar must not. A
CanvasLayergives the UI its own coordinate space so it stays pinned while the world moves beneath it. Skip this split and your HUD slides off-screen the first time the player walks. - Systems that must talk without knowing each other. In an ARPG, "an enemy died" is interesting
to the loot system, the XP system, the quest counter, and a sound player — none of which the enemy
should hold references to. A global signal bus lets the enemy announce the event once and lets any
system subscribe, with no direct coupling. This is the architecture that keeps M3–M8 from
collapsing into a tangle of
get_nodepaths. - One source of truth for run state. Current gold/Ember, the player's level, the active save slot — global, single-instance state. An autoload is Godot's built-in answer, reachable by name from any script without threading references through the scene tree.
- Determinism and testability. A
GameStateautoload is also where a tester's seeded RNG and debug toggles live — a single, inspectable place to force a known state, rather than state smeared across nodes.
Concepts¶
Autoloads (singletons)¶
An autoload is a node Godot instantiates once at startup and makes reachable globally by a name you
choose. Register res://scripts/game_state.gd as GameState, and any script anywhere can write
GameState.gold += 10 — no lookup, no reference passing. It is Godot's built-in singleton mechanism.
The official best-practice test: make something an autoload only when it is globally accessible, tracks its own data internally, and exists in isolation (it does not need to be a child of any particular scene). State that fits — run-wide currency, the event bus — qualifies. A node that only one scene cares about does not; keep that local.
A common beginner mistake is one giant Game autoload that owns everything. Mature Godot projects
instead use a handful of topical autoloads, each a coherent concern. This book starts with two and
will note when a third is justified:
SignalBus— a global event bus: a script that only declares signals, which any system emits and any system connects to.GameState— run-wide data: gold/Ember, player level reference, active save slot, a seeded RNG for QA-reproducible drops.
Script autoload vs scene autoload¶
You can register either a bare .gd script or a .tscn scene as an autoload:
- Register the script when the autoload is pure logic and data (constants, signals, counters).
SignalBusandGameStateare script autoloads. - Register the scene when the autoload needs to own child nodes declaratively — a
Timer, anAudioStreamPlayer. A music/audio autoload that owns players is the usual scene-autoload case (you may add one around M3–M4).
Start with script autoloads; switch a specific autoload to a scene when it grows a node it needs to own.
The signal bus pattern¶
A signal bus is deliberately dumb: it declares signals and does nothing else.
# res://scripts/signal_bus.gd
extends Node
## Emitted by an enemy's Health when it reaches zero. Carries the dying enemy and its world position
## so the loot system can drop at the death site and the XP system can award.
signal enemy_died(enemy: Node, at_position: Vector2)
## Emitted when the player's run-wide currency changes.
signal gold_changed(new_total: int)
An enemy emits SignalBus.enemy_died.emit(self, global_position) when it dies; the loot system, XP
system, and a kill-counter each SignalBus.enemy_died.connect(...) independently. The enemy holds no
reference to any of them, and adding a fourth listener later changes nothing on the enemy's side. As the
project grows you group the bus's signals by tier with comment banners (input → decision → state) — but
the pattern is visible already in two signals.
Example
Without a bus, "drop loot when an enemy dies" tempts the enemy to call
get_node("/root/Main/LootSystem").spawn_drop(...) — a hard path that breaks the moment the scene
tree is rearranged, and that couples every enemy to the loot system's location. With the bus, the
enemy emits one signal and never names the loot system. The loot system can move anywhere in the
tree, be added or removed, or be replaced by a test double, and the enemy is untouched.
The main scene: world vs UI¶
The main scene is the scene Godot loads on launch. Its root organizes the two coordinate spaces a game needs:
Main (Node2D) # the root; the whole game hangs here
├── World (Node2D) # camera-affected: tilemap, player, enemies, loot
└── UI (CanvasLayer) # screen-pinned: HUD, inventory, menus
A CanvasLayer renders its children on an independent layer with its own transform, untouched by the
Camera2D that will live under World. So a health bar parented under UI stays fixed on screen while
the player and dungeon scroll under World. This split is small now and load-bearing for the rest of
the book: every world thing goes under World, every screen thing under UI.
Main is intentionally thin. It is a container and a wiring point, not a god object — game logic lives
in the actors, components, and systems you add beneath it, communicating through the signal bus.
Run target¶
F5 runs the main scene; F6 runs the currently edited scene. Until a main scene is set, F5
prompts you to pick one. You set it in Project → Project Settings → Application → Run → Main Scene, or
accept the prompt the first time you press F5.
Walkthrough¶
Create the two autoloads¶
- In the FileSystem dock, right-click
scripts/→ Create New → Script. Name itsignal_bus.gd, base typeNode. Replace its body with thesignal_bus.gdshown above (two signals, no logic). Save. - Create
scripts/game_state.gd, base typeNode. Give it a minimal body: - Register both:
Project → Project Settings → Globals → Autoload. For Path, pickres://scripts/signal_bus.gd; the Node Name auto-fills toSignalBus; click Add. Repeat forres://scripts/game_state.gd→GameState. Both rows appear in the list, enabled.
Example
Autoload order matters when one autoload references another in _ready. The engine instantiates
them top-to-bottom, finishing each one's _ready before the next begins. If GameState._ready ever
needed to connect to a SignalBus signal, SignalBus would have to sit above GameState in the
list so it exists first. Right now neither references the other, so order is free — but the rule is
worth holding for when it bites.
Build the main scene¶
Scene → New Scene → Other Node → Node2D. Rename the rootMain. Save it asres://scenes/world/main.tscn.- With
Mainselected, add a childNode2D, rename itWorld. - With
Mainselected again (notWorld), add a childCanvasLayer, rename itUI. The tree now readsMain → {World, UI}. - Set the run target:
Project → Project Settings → Application → Run → Main Scene→res://scenes/world/main.tscn. - Press
F5. The game window opens at1280×720, empty and dark — no error. An empty main scene that runs is the M1 finish line: the coordinate systems (space, files, input, globals, scene root) are all locked. M2 puts a player underWorld.
Optional sanity check
With the game running, confirm the window obeys M1.1: drag its edge and watch the empty viewport
letterbox instead of squashing (Aspect = keep). Then add a temporary print("bus up: ", SignalBus)
to GameState._ready, relaunch, and confirm the Output panel prints a valid SignalBus node — proof
both autoloads exist globally. Remove the print after.
Self-check quiz¶
Q1 — Why is the HUD placed under a CanvasLayer (UI) rather than directly under World?
A. CanvasLayer renders faster than Node2D.
B. A CanvasLayer has its own transform and ignores the world Camera2D, so the HUD stays pinned
while the world scrolls.
C. Control nodes can only be children of a CanvasLayer.
D. It is required for autoloads to find the HUD.
Reveal answer
B. The whole point is the independent transform: when Camera2D under World moves, anything
under World moves with it, but CanvasLayer children are drawn in their own screen space and
stay put. A is not a meaningful difference. C is false — Control nodes work anywhere, they just
scroll with the camera if placed under World. D is fabricated.
Q2 — Which of these is the best candidate to become an autoload?
A. The player's Sprite2D.
B. A global SignalBus that only declares signals every system emits and subscribes to.
C. The current arena's TileMap.
D. A single enemy's Hurtbox.
Reveal answer
B. It is globally accessed, tracks its own thing (the set of signals), and exists in isolation from any one scene — the three autoload conditions. A, C, and D are all local to a particular scene or actor; making any of them global would couple the whole project to one instance and break the moment there are two enemies, two arenas, or a fresh player.
Q3 — An enemy needs to trigger a loot drop when it dies. Which approach matches this chapter's architecture?
A. The enemy calls get_node("/root/Main/World/LootSystem").drop(...).
B. The enemy emits SignalBus.enemy_died.emit(self, global_position); the loot system connects to
that signal.
C. The loot system polls every enemy's health each frame.
D. The enemy instances the loot drop itself and never tells anyone.
Reveal answer
B. The bus lets the enemy announce the event without naming or locating any listener; the loot system (and XP, and a kill counter) subscribe independently. A hard-codes a tree path that breaks on any rearrangement and couples the enemy to the loot system's location. C wastes work and still couples the systems. D removes the seam entirely — XP, counters, and sound can never react, because the event was never announced.
Integration question¶
Q4 — open
M1 is four chapters of decisions that produce a blank window: resolution/stretch (M1.1), folder
layout (M1.2), input actions (M1.3), and now autoloads plus the Main → {World, UI} root. Name the
single property these four chapters share, and explain why the signal bus specifically is the piece
that lets M3–M8 add systems without rewriting the actors that came before.
Reveal expected answer
All four lock a coordinate system that every later chapter is reasoned against and that is painful
to change once code depends on it: screen space (resolution/stretch), file space (res://
layout), input space (actions), and now the scene-root contract plus global state. M1 is the
"lock the systems" module; M2 onward never revisits them. The signal bus is the piece that keeps
future additions cheap because it inverts the dependency: instead of each new system reaching
into the actors to observe them (which would force editing the actors every time a system is
added), the actors announce events once on the bus and any number of present-or-future systems
subscribe. The enemy written in M4 emits enemy_died; the loot system (M6), the XP system (M5),
and the save system (M8) all attach to it later without the enemy changing — the same asymmetry
that makes the architecture scale.