Event-driven communication between nodes using Godot’s signal system
Signals are Godot’s implementation of the observer pattern, allowing nodes to communicate without directly referencing each other. This keeps your code flexible, decoupled, and easier to maintain.
A Signal is a built-in type that represents an event emitted by an Object. When a signal is emitted, all connected Callables (functions) are triggered, allowing multiple objects to react to the same event without tight coupling.
Signals are Godot’s equivalent to events, delegates, or callbacks in other languages and frameworks.
You can declare custom signals in your scripts using the signal keyword:
extends Node# Simple signal with no parameterssignal health_depleted# Signal with parameterssignal health_changed(old_value, new_value)# Signal with typed parameters (GDScript 2.0)signal damage_taken(amount: int, damage_type: String)# Signal with multiple parameterssignal item_collected(item_name: String, quantity: int, rarity: String)
Emit a signal using the emit() method or the emit_signal() function:
extends CharacterBody2Dsignal health_changed(old_health, new_health)signal diedvar health = 100: set(value): var old_health = health health = clamp(value, 0, 100) health_changed.emit(old_health, health) if health <= 0: died.emit()func take_damage(amount: int): health -= amount
When you emit a signal, all connected functions are called immediately in the order they were connected.
func _ready(): # Connect to a built-in signal var button = Button.new() button.pressed.connect(_on_button_pressed) # Connect to a custom signal var player = get_node("Player") player.health_changed.connect(_on_player_health_changed) player.died.connect(_on_player_died)func _on_button_pressed(): print("Button was pressed!")func _on_player_health_changed(old_health, new_health): print("Health: ", old_health, " -> ", new_health)func _on_player_died(): print("Player died!") get_tree().reload_current_scene()
You can bind additional parameters to a signal connection using Callable.bind():
func _ready(): # Create multiple enemies with different types var enemy1 = Enemy.new() var enemy2 = Enemy.new() # Bind different parameters to identify which enemy died enemy1.died.connect(_on_enemy_died.bind("Goblin", 10)) enemy2.died.connect(_on_enemy_died.bind("Orc", 25))# Signal parameters come first, bound parameters come afterfunc _on_enemy_died(enemy_type: String, score: int): print(enemy_type, " defeated! +", score, " points")
When you emit a signal with parameters and also use bind(), the emitted parameters come first, followed by the bound parameters.
extends Nodesignal attack_landed(target: Node, damage: int)func _ready(): var player = get_node("Player") # Bind weapon type player.attack_landed.connect(_on_attack.bind("Sword"))func _on_attack(target: Node, damage: int, weapon: String): # target and damage come from emit() # weapon comes from bind() print("Hit ", target.name, " for ", damage, " with ", weapon)
You can disconnect signals when they’re no longer needed:
func _ready(): var button = Button.new() button.pressed.connect(_on_button_pressed) # Later, disconnect the signal button.pressed.disconnect(_on_button_pressed)func _on_button_pressed(): print("This won't be called after disconnect")
When a node is freed, all its signal connections are automatically cleaned up. You usually don’t need to manually disconnect signals.
You can modify signal behavior using connection flags:
func _ready(): var button = Button.new() # One-shot: Disconnect automatically after first emission button.pressed.connect(_on_button_pressed, CONNECT_ONE_SHOT) # Deferred: Call the function at the end of the frame button.pressed.connect(_on_button_pressed, CONNECT_DEFERRED) # Reference counted: Allow multiple connections to the same callable button.pressed.connect(_on_button_pressed, CONNECT_REFERENCE_COUNTED)
func _ready(): var button = Button.new() # Check if signal is connected if not button.pressed.is_connected(_on_button_pressed): button.pressed.connect(_on_button_pressed) # Check if signal has any connections if button.pressed.has_connections(): print("Button has listeners") # Get all connections var connections = button.pressed.get_connections() for connection in connections: print("Connected to: ", connection["callable"])
Godot nodes come with many useful built-in signals:
func _ready(): # Tree signals tree_entered.connect(_on_tree_entered) tree_exiting.connect(_on_tree_exiting) tree_exited.connect(_on_tree_exited) # Child signals child_entered_tree.connect(_on_child_added) child_exiting_tree.connect(_on_child_removed) # Renamed signal renamed.connect(_on_node_renamed)func _on_tree_entered(): print("Node entered the scene tree")func _on_tree_exiting(): print("Node is about to leave the tree")func _on_child_added(child: Node): print("Child added: ", child.name)
Here’s a complete example showing how to build a health system using signals:
# player.gdextends CharacterBody2Dsignal health_changed(current: int, maximum: int)signal damage_taken(amount: int)signal healed(amount: int)signal diedvar max_health = 100var health = 100: set(value): var old_health = health health = clamp(value, 0, max_health) # Emit appropriate signals health_changed.emit(health, max_health) if health < old_health: damage_taken.emit(old_health - health) elif health > old_health: healed.emit(health - old_health) if health <= 0 and old_health > 0: died.emit()func take_damage(amount: int): health -= amountfunc heal(amount: int): health += amount
# ui_health_bar.gdextends ProgressBarfunc _ready(): var player = get_node("/root/Main/Player") # Connect to player signals player.health_changed.connect(_on_player_health_changed) player.died.connect(_on_player_died) # Initialize the health bar max_value = player.max_health value = player.healthfunc _on_player_health_changed(current: int, maximum: int): max_value = maximum value = current # Change color based on health percentage var percent = float(current) / maximum if percent < 0.25: modulate = Color.RED elif percent < 0.5: modulate = Color.YELLOW else: modulate = Color.GREENfunc _on_player_died(): # Animate health bar on death var tween = create_tween() tween.tween_property(self, "modulate:a", 0.0, 0.5)
# game_manager.gdextends Nodefunc _ready(): var player = get_node("/root/Main/Player") player.damage_taken.connect(_on_player_damaged) player.healed.connect(_on_player_healed) player.died.connect(_on_player_died)func _on_player_damaged(amount: int): print("Player took ", amount, " damage") # Play damage sound, spawn particles, etc.func _on_player_healed(amount: int): print("Player healed for ", amount) # Play heal effectfunc _on_player_died(): print("Game Over") await get_tree().create_timer(2.0).timeout get_tree().reload_current_scene()
Why Use Signals Instead of Direct Function Calls?
Signals provide several advantages:
Decoupling: The player doesn’t need to know about the UI or game manager
Flexibility: Multiple systems can respond to the same event
Maintainability: Easy to add/remove listeners without modifying the player
Scene Independence: The player scene works standalone without dependencies
You can combine signals with node groups for powerful patterns:
# enemy.gdextends CharacterBody2Dsignal alertedfunc _ready(): add_to_group("enemies") alerted.connect(_on_alerted)func alert(): alerted.emit()func _on_alerted(): # This enemy is alerted print(name, " is alerted!") # Alert all other enemies in the group get_tree().call_group("enemies", "alert")
var custom_signal = Signal()func _ready(): # Create a signal from an object and name var obj = Node.new() var sig = Signal(obj, "some_signal") # Check if object has a signal if obj.has_signal("some_signal"): sig.connect(_on_signal_emitted)func _on_signal_emitted(): print("Custom signal emitted")
func _ready(): var area = $DetectionArea area.body_entered.connect(_on_body_entered) area.body_exited.connect(_on_body_exited)func _on_body_entered(body: Node2D): if body.is_in_group("player"): print("Player detected!")func _on_body_exited(body: Node2D): if body.is_in_group("player"): print("Player left detection range")