GDScript Signals Cheat Sheet
Quick-reference for Godot 4 signal patterns. Each section includes copy-ready snippets with inline output comments using real game scenarios.
Declaring Custom Signals
Signals are declared at the top of a script. Godot 4 uses typed parameters for clarity.
signal died
func take_lethal_hit() -> void:
died.emit()signal health_changed(new_hp: int, max_hp: int)
signal item_collected(item_name: String, value: int)
func take_damage(amount: int) -> void:
hp -= amount
health_changed.emit(hp, max_hp)# Declare signals at the top of the script, before vars
signal enemy_died(enemy: Node2D, xp_reward: int)
signal wave_complete(wave_number: int)
var hp: int = 100Signals are always declared before variables and functions by GDScript convention.
Connecting Signals
Use .connect() with a Callable reference. Godot 4 replaced the old string-based connect() from Godot 3.
func _ready() -> void:
$Enemy.died.connect(_on_enemy_died)
func _on_enemy_died() -> void:
score += 100func _ready() -> void:
$StartButton.pressed.connect(func():
start_game()
)@onready var player: CharacterBody2D = $Player
func _ready() -> void:
player.health_changed.connect(_update_health_bar)
func _update_health_bar(new_hp: int, max_hp: int) -> void:
$HealthBar.value = new_hpEmitting Signals
Call .emit() on the signal object. Arguments must match the signal declaration.
signal level_complete
func reach_exit() -> void:
level_complete.emit() # all connected callbacks firesignal coin_collected(coin_value: int, position: Vector2)
func _on_body_entered(body: Node2D) -> void:
if body.is_in_group("player"):
coin_collected.emit(value, global_position)
queue_free()Built-in Signals
Godot nodes come with pre-defined signals. Connect them in code or via the editor.
# Timer
$Timer.timeout.connect(_on_timeout)
# Area2D collision
$Hitbox.body_entered.connect(_on_body_entered)
$Hitbox.area_entered.connect(_on_area_entered)
# Button
$PlayButton.pressed.connect(_on_play_pressed)
# AnimationPlayer
$Anim.animation_finished.connect(_on_anim_finished)# Fires when the node enters the scene tree
tree_entered.connect(_on_added_to_tree)
# Fires when the node is about to be removed
tree_exiting.connect(_on_leaving_tree)
# Fires when all children are ready
ready.connect(_on_ready_signal)Passing Extra Data with bind()
Use .bind() to attach extra arguments to a callback. Bound args are appended after signal args.
for i in range(3):
var btn: Button = $HBox.get_child(i)
btn.pressed.connect(_on_slot_pressed.bind(i))
func _on_slot_pressed(slot_index: int) -> void:
equip_item(slot_index) # 0, 1, or 2signal upgrade_selected
func show_upgrades(upgrades: Array[Resource]) -> void:
for upgrade in upgrades:
var btn := Button.new()
btn.text = upgrade.name
btn.pressed.connect(_pick_upgrade.bind(upgrade))
$Panel.add_child(btn)
func _pick_upgrade(upgrade: Resource) -> void:
apply_upgrade(upgrade)One-shot and Deferred Connections
CONNECT_ONE_SHOT auto-disconnects after the first emit. CONNECT_DEFERRED delays the call to the end of the frame.
# Callback fires exactly once, then auto-disconnects
$Door.opened.connect(_on_door_first_open, CONNECT_ONE_SHOT)
func _on_door_first_open() -> void:
unlock_achievement("explorer")# Callback runs at end of frame (safe for tree modifications)
$Enemy.died.connect(_on_enemy_died, CONNECT_DEFERRED)
func _on_enemy_died() -> void:
# Safe to remove nodes here — deferred until frame end
$Enemy.queue_free()# One-shot AND deferred
$Trigger.body_entered.connect(
_on_cutscene_trigger,
CONNECT_ONE_SHOT | CONNECT_DEFERRED
)Await Signals
Use await to pause execution until a signal fires. Great for sequential game logic.
func flash_damage() -> void:
modulate = Color.RED
await get_tree().create_timer(0.15).timeout
modulate = Color.WHITEfunc play_intro() -> void:
$AnimPlayer.play("intro_cutscene")
await $AnimPlayer.animation_finished
start_gameplay()func death_sequence() -> void:
$Anim.play("death")
await $Anim.animation_finished
await get_tree().create_timer(1.0).timeout
emit_signal("respawn_requested")Each await pauses the current function. Other nodes keep running normally.
Disconnecting Signals
Disconnect to prevent callbacks from firing. Always check is_connected() first to avoid errors.
func _exit_tree() -> void:
if $Timer.timeout.is_connected(_on_timeout):
$Timer.timeout.disconnect(_on_timeout)var _attack_cb: Callable
func _ready() -> void:
_attack_cb = _on_attack_pressed.bind(weapon_id)
$AttackBtn.pressed.connect(_attack_cb)
func _exit_tree() -> void:
if $AttackBtn.pressed.is_connected(_attack_cb):
$AttackBtn.pressed.disconnect(_attack_cb)When using bind(), store the Callable so you can disconnect the exact same reference later.
Signal Bus Pattern (Autoload)
A global autoload script that holds signals. Nodes emit and connect through the bus without direct references to each other.
# events.gd — add as Autoload named "Events"
extends Node
signal enemy_died(enemy: Node2D, xp: int)
signal coin_collected(value: int)
signal game_over
signal score_changed(new_score: int)# enemy.gd
func die() -> void:
Events.enemy_died.emit(self, xp_reward)
queue_free()# hud.gd
func _ready() -> void:
Events.score_changed.connect(_on_score_changed)
Events.game_over.connect(_on_game_over)
func _on_score_changed(new_score: int) -> void:
$ScoreLabel.text = str(new_score)The signal bus decouples systems. The enemy does not need a reference to the HUD.
Signals with Groups
Groups let you broadcast to multiple nodes at once. Combine with signals for event-driven group communication.
# All enemies take damage from a bomb
get_tree().call_group("enemies", "take_damage", 50)
# All coins play a collect animation
get_tree().call_group("collectibles", "play_collect")# In events.gd autoload:
signal freeze_all_enemies
# In game_manager.gd:
func activate_freeze_power() -> void:
Events.freeze_all_enemies.emit()
# In enemy.gd:
func _ready() -> void:
Events.freeze_all_enemies.connect(_on_freeze)
func _on_freeze() -> void:
$AnimPlayer.pause()
set_physics_process(false)Common Signal Pitfalls
Quick fixes for the most frequent signal bugs in Godot 4.
# BUG: reconnecting in _ready on re-enter
func _ready() -> void:
$Timer.timeout.connect(_on_timeout) # fires twice!
# FIX: guard with is_connected()
func _ready() -> void:
if not $Timer.timeout.is_connected(_on_timeout):
$Timer.timeout.connect(_on_timeout)# Emitter fires but receiver was queue_free'd
# Godot handles this silently — no crash, but no callback either
# FIX: disconnect before freeing, or use one-shot
func _exit_tree() -> void:
Events.wave_complete.disconnect(_on_wave_complete)signal hit(damage: int, attacker: Node2D)
# BUG: callback signature doesn't match
func _on_hit(damage: int) -> void: # missing attacker!
pass
# FIX: match the signal's parameter list
func _on_hit(damage: int, attacker: Node2D) -> void:
knockback(attacker.global_position)Godot 4 catches argument mismatches at parse time. If you see "too few arguments", check the signal declaration.
Can you write this from memory?
Connect the $SpawnTimer node's timeout signal to a method called _on_spawn_timer_timeout.