- Signal Architecture
- Signal Up/Call Down pattern, typed signals, and event buses define decoupled, maintainable architectures.
- Available Scripts
- global_event_bus.gd
- Expert AutoLoad event bus with typed signals and connection management.
- signal_debugger.gd
- Runtime signal connection analyzer. Shows all connections in scene hierarchy.
- signal_spy.gd
- Testing utility for observing signal emissions with count tracking and history.
- MANDATORY - For Event Bus
- Read global_event_bus.gd before implementing cross-scene communication. NEVER Do in Signal Architecture NEVER create circular signal dependencies — A signals to B, B signals back to A? Infinite loops + stack overflow. Use mediator (parent OR AutoLoad) to break cycle. NEVER skip signal typing — signal moved without types? No autocomplete OR type safety. Use signal moved(direction: Vector2) for editor support. NEVER forget to disconnect signals — Node freed but signal still connected? "Attempt to call on null instance" error. Disconnect in _exit_tree() OR use CONNECT_REFERENCE_COUNTED . NEVER connect signals in _ready() for dynamic nodes — Enemy spawned after level load? Signals not connected. Connect when instantiating OR use groups + await pattern. NEVER use signals for parent→child — Parent signaling to child breaks encapsulation. CALL DOWN directly: child.method() . Reserve signals for child→parent communication. NEVER emit signals with side effects — died.emit() calls queue_free() inside? Listeners can't respond before node freed. Emit FIRST, then cleanup. NEVER use string-based signal names — connect("heath_chnaged", ...) typo = silent failure. Use direct reference: player.health_changed.connect(...) . Use Signals For: UI button presses → game logic Player death → game over screen Item collected → inventory update Enemy killed → score update Cross-scene communication via AutoLoad Use Direct Calls For: Parent controlling child behavior Accessing child properties Simple, local interactions Implementation Patterns Pattern 1: Define Typed Signals extends CharacterBody2D
✅ Good - typed signals (Godot 4.x)
signal health_changed ( new_health : int , max_health : int ) signal died ( ) signal item_collected ( item_name : String , item_type : int )
❌ Bad - untyped signals
signal health_changed signal died Pattern 2: Emit Signals on State Changes
player.gd
extends CharacterBody2D signal health_changed ( current : int , maximum : int ) signal died ( ) var health : int = 100 : set ( value ) : health = clamp ( value , 0 , max_health ) health_changed . emit ( health , max_health ) if health <= 0 : died . emit ( ) var max_health : int = 100 func take_damage ( amount : int ) -> void : health -= amount
Triggers setter, which emits signal
Pattern 3: Connect Signals in Parent
game.gd (parent)
extends Node2D @ onready var player : CharacterBody2D = $Player @ onready var ui : Control = $UI func _ready ( ) -> void :
Connect child signals
player . health_changed . connect ( _on_player_health_changed ) player . died . connect ( _on_player_died ) func _on_player_health_changed ( current : int , maximum : int ) -> void :
Call down to UI
ui . update_health_bar ( current , maximum ) func _on_player_died ( ) -> void :
Orchestrate game over
ui . show_game_over ( ) get_tree ( ) . paused = true Pattern 4: Global Signals via AutoLoad For cross-scene communication:
events.gd (AutoLoad)
extends Node signal level_completed ( level_number : int ) signal player_spawned ( player : Node2D ) signal boss_defeated ( boss_name : String )
Any script can emit:
Events . level_completed . emit ( 3 )
Any script can listen:
Events . level_completed . connect ( _on_level_completed ) Advanced Patterns Pattern 5: Signal Chains
enemy.gd
signal died ( score_value : int ) func _on_health_depleted ( ) -> void : died . emit ( 100 ) queue_free ( )
combat_manager.gd
func _ready ( ) -> void : for enemy in get_tree ( ) . get_nodes_in_group ( "enemies" ) : enemy . died . connect ( _on_enemy_died ) func _on_enemy_died ( score_value : int ) -> void : GameManager . add_score ( score_value ) Events . enemy_killed . emit ( ) Pattern 6: One-Shot Connections For single-use signal connections:
Connect with CONNECT_ONE_SHOT flag
timer . timeout . connect ( _on_timer_timeout , CONNECT_ONE_SHOT ) func _on_timer_timeout ( ) -> void : print ( "This only fires once" )
Connection automatically removed
Pattern 7: Custom Signal Arguments
item.gd
signal picked_up ( item_data : Dictionary ) func _on_player_enter ( ) -> void : picked_up . emit ( { "name" : item_name , "type" : item_type , "value" : item_value , "icon" : item_icon } )
inventory.gd
func _on_item_picked_up ( item_data : Dictionary ) -> void : add_item ( item_data . name , item_data . type , item_data . value ) Best Practices 1. Descriptive Signal Names
✅ Good
signal button_pressed ( ) signal enemy_defeated ( enemy_type : String ) signal animation_finished ( animation_name : String )
❌ Bad
signal pressed ( ) signal done ( ) signal finished ( ) 2. Avoid Circular Dependencies
❌ BAD: A signals to B, B signals back to A
A.gd
signal data_requested func _ready ( ) : B . data_ready . connect ( _on_data_ready ) data_requested . emit ( )
B.gd
signal data_ready func _ready ( ) : A . data_requested . connect ( _on_data_requested )
✅ GOOD: Use a mediator (parent or AutoLoad)
Parent.gd
func _ready ( ) : A . data_requested . connect ( _on_A_data_requested ) B . data_ready . connect ( _on_B_data_ready ) 3. Disconnect Signals When Nodes Are Freed func _ready ( ) -> void : player . died . connect ( _on_player_died ) func _exit_tree ( ) -> void : if player and player . died . is_connected ( _on_player_died ) : player . died . disconnect ( _on_player_died ) Or use automatic cleanup:
Signal auto-disconnects when this node is freed
player . died . connect ( _on_player_died , CONNECT_REFERENCE_COUNTED ) 4. Group Related Signals
✅ Good organization
Combat signals
signal health_changed ( current : int , max : int ) signal died ( ) signal respawned ( )
Movement signals
signal jumped ( ) signal landed ( ) signal direction_changed ( direction : Vector2 )
Inventory signals
- signal
- item_added
- (
- item
- :
- Dictionary
- )
- signal
- item_removed
- (
- item
- :
- Dictionary
- )
- signal
- inventory_full
- (
- )
- Testing Signals
- func
- test_health_signal
- (
- )
- ->
- void
- :
- var
- signal_emitted
- :=
- false
- var
- received_health
- :=
- 0
- player
- .
- health_changed
- .
- connect
- (
- func
- (
- current
- :
- int
- ,
- _max
- :
- int
- )
- :
- signal_emitted
- =
- true
- received_health
- =
- current
- )
- player
- .
- health
- =
- 50
- assert
- (
- signal_emitted
- ,
- "Signal was not emitted"
- )
- assert
- (
- received_health
- ==
- 50
- ,
- "Health value incorrect"
- )
- Common Gotchas
- Issue
-
- Signal not firing
- Check
-
- Is the signal spelled correctly when connecting?
- Check
-
- Is the emitting code path actually being executed?
- Check
-
- Use
- print()
- before
- emit()
- to verify
- Issue
-
- Signal firing multiple times
- Cause
-
- Multiple connections to the same signal
- Solution
-
- Check connections or use
- CONNECT_ONE_SHOT
- Issue
-
- "Attempt to call function on a null instance"
- Cause
-
- Node was freed but signal still connected
- Solution
- Disconnect in _exit_tree() or use CONNECT_REFERENCE_COUNTED Reference Godot Docs: Signals Best Practices: Signals Up, Calls Down Related Master Skill: godot-master