godot-signal-architecture

Expert blueprint for signal-driven architecture using "Signal Up, Call Down" pattern for loose coupling. Covers typed signals, signal chains, one-shot connections, and AutoLoad event buses. Use when implementing event systems OR decoupling nodes. Keywords signal, emit, connect, CONNECT_ONE_SHOT, CONNECT_REFERENCE_COUNTED, event bus, AutoLoad, decoupling.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "godot-signal-architecture" with this command: npx skills add thedivergentai/gd-agentic-skills/thedivergentai-gd-agentic-skills-godot-signal-architecture

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 typingsignal 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 effectsdied.emit() calls queue_free() inside? Listeners can't respond before node freed. Emit FIRST, then cleanup.
  • NEVER use string-based signal namesconnect("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

Related

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

godot-master

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

godot-shaders-basics

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

godot-ui-theming

No summary provided by upstream source.

Repository SourceNeeds Review