Signals are Godot's observer pattern. One node emits, zero or more react. The emitter has no idea who is listening, and that is the whole point.
Godot 4 made signals first-class: they are a Variant type, connections use Callable instead of strings, and yield became await. If you learned signals in Godot 3, the mental model is the same but the syntax changed completely.
For timer-specific patterns (cooldowns, debounce, create_timer), see timers and signals. For wiring signals to spawned nodes, see scene instancing.
A signal is a first-class Variant type. Every Object maintains an internal list of connections. When you call emit(), Godot iterates that list and invokes each connected Callable synchronously, one after another, before returning. The emitter does not know or care who is listening.
This is the observer pattern baked into the engine. One signal can connect to multiple callbacks across different nodes. All of them fire on emission, but execution order across connections is not guaranteed.
The architecture rule that keeps Godot scenes reusable: call down, signal up. Parents call methods on their children directly. Children emit signals that parents connect to. Neither side knows more than it needs to.
If you use CONNECT_DEFERRED, the callback is queued to the next idle frame instead of running inline. This is safer when the callback modifies the scene tree.
Godot 3 used a string-based API: connect("signal_name", self, "_method"). Godot 4 replaced all of that with first-class Signal objects and Callables. If you are migrating, the mental model is identical but every line of signal code must be rewritten.
Declare signals at the top of your script, below extends and class_name, above variables:
signal health_changed(new_health: int)
signal died
signal damage_taken(amount: int, source: Node2D)
Type annotations on parameters are documentation and editor hints. They are not enforced at runtime. You can emit mismatched types without a parse error, though the callback may crash if it expects a specific type.
Naming conventions: past tense for events that happened (health_changed, died, door_opened). Use _started/_finished suffixes for boundaries (attack_started, attack_finished).
Emit with:
health_changed.emit(current_health)
died.emit()
damage_taken.emit(25, attacker)
Code connection (for dynamic nodes or conditional wiring):
button.pressed.connect(_on_button_pressed)
Lambda connection (for short inline reactions):
timer.timeout.connect(func(): can_attack = true)
Editor connection (for static scenes): select the node, go to Node > Signals tab, double-click the signal, pick target node and method. Editor connections are saved in the .tscn file and show as green icons.
Use code connections for dynamically spawned nodes and conditional wiring. Use editor connections for things that never change at runtime.
Callable.bind() appends extra arguments after the signal's own parameters:
for enemy in enemies:
enemy.died.connect(_on_enemy_died.bind(enemy))
func _on_enemy_died(enemy: Node) -> void:
score += enemy.xp_value
Callable.unbind(n) discards the last n signal arguments when your handler does not need them. For example, area.body_entered.connect(_on_entered.unbind(1)) lets you react to the event without accepting the body reference.
Connection flags control how the callback behaves. CONNECT_ONE_SHOT auto-disconnects after the first emission. CONNECT_DEFERRED queues the callback to idle time instead of running it inline. Combine flags with bitwise OR: signal.connect(handler, CONNECT_ONE_SHOT | CONNECT_DEFERRED).
Follow the "call down, signal up" rule:
Child to parent (most common direction): the child declares and emits a signal. The parent connects in _ready():
# In parent
func _ready() -> void:
$Player.health_changed.connect($HUD.update_health)
$Player.died.connect(_on_player_died)
Parent to child: call methods directly. No signal needed.
Sibling to sibling: the common parent wires them. $Player.health_changed.connect($HUD.update_health) in the parent's _ready().
Dynamically spawned nodes: connect when you instantiate:
func spawn_enemy() -> void:
var enemy := EnemyScene.instantiate() as CharacterBody2D
enemy.died.connect(_on_enemy_died.bind(enemy))
add_child(enemy)
Signal bus for distant, unrelated nodes: create an autoload singleton (often called GameEvents) that holds signal declarations. Any node in the project can emit or connect without knowing about the other:
# game_events.gd (Autoload)
extends Node
signal player_died
signal score_changed(new_score: int)
A HUD in one scene connects with GameEvents.score_changed.connect(_on_score_changed). An enemy in another scene emits with GameEvents.score_changed.emit(xp_value). Neither node references the other.
Use the signal bus sparingly for truly global events (pause, score changes, achievements, audio cues). If two nodes are in the same scene, wire them directly. Overuse turns the bus into a god object where everything routes through one place.
await pauses a function until a signal fires, turning the function into a coroutine:
func play_attack() -> void:
$AnimationPlayer.play("attack")
await $AnimationPlayer.animation_finished
apply_damage()
You can chain awaits for cutscene-style sequences:
func cutscene() -> void:
$Camera.pan_to(target)
await $Camera.pan_finished
$DialogBox.show_text("Hello!")
await $DialogBox.text_finished
get_tree().change_scene_to_file("res://next_level.tscn")
The function returns a Signal to its caller, which the caller can also await. This makes it easy to compose multi-step async flows.
Two dangers. First, if the node emitting the awaited signal is freed before emission, the coroutine hangs silently forever. Guard with is_instance_valid() or use timeouts. Second, never use await in _process() or _physics_process(). These run every frame, and each invocation spawns a new coroutine that piles up without finishing properly.
Autoload/singleton signals: always disconnect in _exit_tree(), because the singleton outlives your node and the connection becomes a dangling reference.
func _ready() -> void:
GameEvents.player_died.connect(_on_player_died)
func _exit_tree() -> void:
GameEvents.player_died.disconnect(_on_player_died)
Parent-child within the same scene: usually not needed. Godot cleans up connections when both nodes are freed together.
Lambda connections are hard to disconnect because you have no reference to the Callable. Store it in a variable if you need to disconnect later:
var _on_pressed_cb: Callable
func _ready() -> void:
_on_pressed_cb = func(): handle_press()
button.pressed.connect(_on_pressed_cb)
func _exit_tree() -> void:
button.pressed.disconnect(_on_pressed_cb)
CONNECT_ONE_SHOT handles cleanup automatically for one-time events.
What You'll Practice: GDScript Signals
Common GDScript Signals Pitfalls
- Connecting the same signal twice causes duplicate calls. Guard with is_connected() before reconnecting.
- Awaiting a signal from a freed node hangs the coroutine silently forever. Check is_instance_valid() first.
- Lambda connections stack on scene reload because each _ready() creates a new Callable. Use named methods instead.
- Modifying the scene tree during signal emission can crash. Use CONNECT_DEFERRED for callbacks that add or free nodes.
- Signal parameter types are documentation only. Emission is not validated at parse time, so mismatches cause runtime errors.
GDScript Signals FAQ
What is a signal in Godot?
A signal is an event that a node emits. Other nodes connect to it and react when it fires. This is the observer pattern built into the engine. The emitter does not know who is listening.
How do you pass data with signals in Godot 4?
Declare parameters on the signal: signal hit(damage: int, source: Node2D). Emit with values: hit.emit(25, attacker). The connected callback receives those values as function arguments.
How do signals work between scenes in Godot 4?
Follow "call down, signal up." Children emit signals, parents connect in _ready(). Siblings are wired by their common parent. For distant, unrelated nodes, use a signal bus autoload.
What is a signal bus in Godot?
An autoload singleton script that declares global signals. Any node can emit or connect: GameEvents.player_died.emit(). Use it for truly global events like pause, score changes, and achievements. Avoid overusing it for local communication.
Can you await a signal in GDScript?
Yes. await node.signal_name pauses the function until the signal fires, turning it into a coroutine. Common for animation waits, timer delays, and cutscene sequences.
What is CONNECT_DEFERRED in Godot?
A connection flag that queues the callback to the next idle frame instead of running it inline during emission. Use it when the callback modifies the scene tree (adding or freeing nodes) to avoid crashes.
How do signals differ from Godot 3 to Godot 4?
The string-based API (connect("signal_name", self, "_method")) was replaced with first-class signal objects (signal_name.connect(_method)). yield became await. Callables replaced string method names. The concepts are identical but the syntax is completely different.
Are signals slow in Godot?
No. A signal emission with one connection is roughly 3x a direct function call. Around 2,300 emissions per frame costs about 1ms. For most games, signal overhead is irrelevant.
Should I use signals or direct method calls?
Signals for upward/lateral communication and one-to-many events where the sender should not know the receiver. Direct calls for downward communication (parent to child) and when you need a return value.
How do you disconnect a signal in Godot 4?
Call signal.disconnect(callable). Check is_connected(callable) first to avoid errors. For autoload signals, always disconnect in _exit_tree() because the singleton outlives your node.
GDScript Signals Syntax Quick Reference
signal health_changed(new_health: int)
signal died
var health: int = 100
func take_damage(amount: int) -> void:
health -= amount
health_changed.emit(health)
if health <= 0:
died.emit()# Signal: enemy.died (no params)
# We want to know WHICH enemy died
for enemy in enemies:
enemy.died.connect(_on_enemy_died.bind(enemy))
func _on_enemy_died(enemy: CharacterBody2D) -> void:
score += enemy.xp_value
enemy.queue_free()# game_events.gd (registered as Autoload "GameEvents")
extends Node
signal player_died
signal score_changed(new_score: int)
signal game_paused
signal game_resumed
# Any script can emit:
# GameEvents.player_died.emit()
# Any script can connect:
# GameEvents.score_changed.connect(_on_score_changed)func play_attack() -> void:
$AnimationPlayer.play("attack")
await $AnimationPlayer.animation_finished
apply_damage()
func flash_red() -> void:
modulate = Color.RED
await get_tree().create_timer(0.15).timeout
modulate = Color.WHITE# Auto-disconnects after first emission
level_complete.connect(_on_level_complete, CONNECT_ONE_SHOT)
# Combine flags with bitwise OR
timer.timeout.connect(_on_timeout, CONNECT_ONE_SHOT | CONNECT_DEFERRED)