Event Bus — the pattern to use last, not first
A coin is collected. The HUD counter should increase, an achievement should tick, a sound should play, and the tutorial popup should dismiss itself. The coin is fifteen nodes away from every one of them.
The tempting fix is a global object every script can reach — Events.coin_collected.emit() —
and everything that cares subscribes. It works immediately, it feels clean, and it is the
single easiest way to turn a comprehensible project into one where nothing can be traced.
This shelf teaches the pattern and the discipline that keeps it from eating your codebase, because those are genuinely the same lesson.
An honest disclaimer, up front#
Godot's documentation does not describe an event bus. It doesn't endorse one, doesn't provide one, and doesn't mention the idea anywhere. What it does have is a page called Autoloads versus regular nodes that lists three problems with exactly this kind of global object:
- Global state — one object holds data everything depends on, so a failure spreads everywhere.
- Global access — anything can call it from anywhere, so when something goes wrong the list of suspects is "the entire project".
- Global resource allocation — it's hard to judge how much the global thing should own.
Those are real, and this shelf agrees with them. The event bus is a community pattern that solves a real problem the engine leaves to you, and it is worth using narrowly. Anyone who presents it as best practice is telling you about a convention, not about Godot.
Try these four things first#
By the time a bus is the right answer, you should have rejected all of these for a stated reason:
| Reach for | When |
|---|---|
@onready var x := %Child | Talking to your own children. Direct, typed, obvious |
@export var target: Node2D | Talking to something outside your scene — assign it in the Inspector |
| A plain signal upward | Reporting to whoever owns you. This is the default |
| Groups | Broadcasting to many things of a kind: get_tree().call_group("enemies", "alert") |
Call down, signal up. A parent knows its children concretely and calls their methods; a
child never calls upward — it emits a past-tense signal (died, coin_collected) and stays
ignorant of who listens. Most communication that feels like it needs a bus is actually one
missing signal on an intermediate node.
The bus earns its place only when the emitter and the listener are in genuinely different branches of the tree, with no sensible shared ancestor to mediate — and when wiring a reference would mean threading it through five layers that don't care about it.
What this shelf teaches#
| Part | You build | The idea |
|---|---|---|
| 01 | An Events autoload that declares only signals, plus connection hygiene | The bus itself is twenty lines; the rules are the work |
| 02 | Naming conventions, a debug tracer, and the review test for bus abuse | How to still understand this in six months |
The one-sentence rule#
A bus carries announcements, never commands.
Events.player_died.emit() is an announcement — a fact about the world that already happened.
Whoever cares reacts. Events.show_game_over_screen.emit() is a command dressed as a signal:
the emitter has decided what should happen and is using the bus as a remote control. The second
one is how buses turn into invisible spaghetti, and part 02 shows how to spot it in your own
code.
Past tense in the name is not a style preference. It's the test.