Signals & Game Events

Godot Fundamentals

Chapter 7 · Signals & Game Events

Chapter 3 declared a first custom signal; Chapter 6 connected a built-in one, body_entered. Both times, the connection was made entirely from code. This chapter goes deeper: connecting signals visually in the editor, controlling exactly how a connection behaves, pausing a function until a signal fires, and — the real point of all of it — using signals to design game systems that don't depend tightly on each other.

Connecting Signals in the Editor

Every connection shown so far has used .connect() in a script. Godot's editor also offers a fully visual alternative:

  1. Select a node in the Scene panel.
  2. Open the Node dock (usually tabbed next to the Inspector) and click "Signals."
  3. Every signal that node offers — built-in and custom — is listed here.
  4. Double-click a signal, choose the node that should handle it, and Godot generates a matching handler function automatically, already connected.
Editor vs. code — which to use? Both create the exact same underlying connection. The editor is convenient for a connection that's fixed and known in advance (a specific button's own pressed signal, wired once at design time). Code-based connections are the better choice when the connection needs to happen dynamically — for instance, connecting to a signal on a node that was only just instanced at runtime, the way Chapter 3's spawned scenes were.

One-Shot Connections & Disconnecting

By default a connection stays active and fires every time the signal is emitted, for as long as both nodes exist. Two situations need something different: a connection that should only ever fire once, and a connection that needs to be torn down deliberately, before either node is freed.

# Fires exactly once, then Godot removes the connection automatically. enemy.died.connect(_on_first_kill, CONNECT_ONE_SHOT) # Manually tearing down a connection made earlier. enemy.died.disconnect(_on_first_kill)
A leftover connection can crash your game — if node A connects to a signal on node B, and B is later freed with queue_free() while A still expects to receive that signal, referencing a freed node from the handler function raises a real error. Disconnecting explicitly (or using CONNECT_ONE_SHOT for anything genuinely one-time) avoids this class of bug.

await — Pausing Until a Signal Fires

GDScript's await keyword pauses a function's own execution until a given signal actually fires, then continues from exactly that point — useful for sequencing things like a short delay, an animation finishing, or a cutscene waiting for input.

func show_damage_popup() -> void: print("Ouch!") # Pause here for exactly 1 second, without freezing the rest of the game. await get_tree().create_timer(1.0).timeout print("Popup faded") func wait_for_enemy_death() -> void: await enemy.died print("The enemy is gone - continuing")
await doesn't freeze the game — unlike a blocking sleep() call in plain Python, everything else in the game keeps running normally while one function is paused on await. Only that specific function's own execution is suspended, waiting for its signal.

Designing With Signals: A Decoupled Health System

The real payoff of signals shows up once several systems need to react to the same event without knowing about each other. A health system is the classic example: it shouldn't need to know a health bar, a screen shake effect, or a "low health" warning sound all exist — it should just announce that health changed, and let anything interested react on its own.

# --- health.gd, attached to the Player --- extends Node signal health_changed(current: int, max_health: int) signal died var current: int = 100 var max_health: int = 100 func take_damage(amount: int) -> void: current = max(current - amount, 0) health_changed.emit(current, max_health) if current == 0: died.emit()

A completely separate HUD scene, and a completely separate audio-warning script, can each independently connect to the exact same health_changed signal — neither needs to know the other exists, and neither needs any change if a third listener is added later. Chapter 8 builds a real HUD that does exactly this.

Coming from Python This "one emitter, many independent listeners" shape is the same reasoning behind Python's own observer patterns, a pub/sub library, or Django's own signal framework, if you've encountered it — the emitter never imports or references any of its listeners. What's distinctive in Godot is how lightweight declaring a new one is: a single signal line, no separate event-bus class to set up, no manual list of callback functions to maintain by hand.

Coding Challenges

Challenge 1
Add a Button node to a scene. Using the editor's Node dock (not code), connect its pressed signal to a new handler function that prints "Button clicked!". Confirm it works by running the scene.
→ Solution
Challenge 2
Write a function that prints "Get ready...", uses await with get_tree().create_timer() to pause for 2 seconds, then prints "Go!". Confirm the rest of the scene (e.g. a spinning sprite from Chapter 4) keeps moving during that 2-second pause rather than freezing.
→ Solution
Challenge 3
Build the health.gd script from this chapter's own example. From two separate, unrelated nodes, each independently connect to its health_changed signal — one printing "HUD: health is now X/Y", the other printing "Audio: low health warning!" only when health drops to 30 or below. Call take_damage() a few times and confirm both listeners react correctly on their own.
→ Solution

Quick Reference — Signals & Game Events

  • Node dock > Signals tab — connect visually in the editor; generates a handler automatically
  • signal_name.connect(handler, CONNECT_ONE_SHOT) — fires once, then auto-disconnects
  • signal_name.disconnect(handler) — tears down a connection manually
  • await signal_name / await get_tree().create_timer(secs).timeout — pauses one function without freezing the game
  • Design systems to emit signals about what happened; let listeners react independently, with no direct reference back to the emitter