Skip to content

Core Concepts

Architecture Overview

GDSignalBus uses a global singleton pattern to provide a high-performance signal bus system for Godot 4. Its core architecture includes:

  • Global Signal Bus: Centralized management of all signals
  • Channel System: Signal grouping and isolation
  • Instance-level Signals: Object-level unique signals
  • Async Processing: Background thread support
  • Signal Filtering: Conditional subscriptions
  • Signal Bridging: Native signal connections

Global Signal Bus

Singleton Pattern

GDSignalBus uses a global singleton pattern, ensuring only one signal bus instance exists throughout the game:

gdscript
# Get singleton
var bus = SignalBus.get_singleton()

# Accessible from anywhere
bus.emit("global_event", ["data"])
bus.subscribe("global_event", handler)

Advantages

  • Decoupling: Eliminates direct dependencies between nodes
  • Centralized Management: All signals managed in one place
  • Performance Optimization: High-performance signal distribution with C++ implementation
  • Memory Safety: Automatic cleanup of invalid connections

Channel System

Channel Concept

Channels allow grouping signals to avoid naming conflicts:

gdscript
# Same signal name in different channels
bus.emit_on_channel("gameplay", "player_action", ["jump"])
bus.emit_on_channel("ui", "player_action", ["menu_open"])

# Listen to specific channels
bus.subscribe_on_channel("gameplay", "player_action", _on_gameplay_action)
bus.subscribe_on_channel("ui", "player_action", _on_ui_action)

Channel Isolation

  • Namespace: Avoid signal name conflicts
  • Logical Grouping: Related signals in same channel
  • Performance Optimization: Reduce unnecessary signal processing

Instance-level Signals

Unique Identification

Provide unique signals for different object instances using the same script:

gdscript
# Player.gd
extends Node

func _ready():
    # Create unique signal for each instance
    bus.subscribe_instance(self, "health_changed", _on_health_changed)

func take_damage(amount):
    health -= amount
    # Send instance-level signal
    bus.emit_instance(self, "take_damage", [amount])

func _on_health_changed(args):
    var amount = args[0]
    update_health_bar(health)

Application Scenarios

  • Multiplayer Games: Independent events for each player
  • Enemy AI: State changes of different enemies
  • UI Components: Different instances of same component

Signal Filtering

Conditional Subscriptions

Only receive signals that meet specific conditions:

gdscript
# Only receive specific type of item collection events
bus.subscribe_filtered("item_collected", 
    func(args): 
        var value = args[0]
        return value > 10,
    _on_valuable_item
)

# Only receive player health events below 30%
bus.subscribe_filtered("health_changed",
    func(args): 
        var health = args[0]
        return health < 30,
    _on_low_health
)

Filter Types

  • Type Filtering: Based on parameter types
  • Value Filtering: Based on parameter values
  • Custom Filtering: Complex conditional logic

Async Processing

Background Threads

Time-consuming operations are processed in background threads:

gdscript
# Async file loading
bus.subscribe_async_with_callbacks("load_file",
    func(args):
        var path = args[0]
        # Simulate time-consuming operation
        OS.delay_msec(1000)
        return "Loaded data from " + path,
    _on_load_complete,
    _on_load_error
)

# Emit async signal
bus.emit_async("load_file", ["save_data.dat"])

Async Advantages

  • Non-blocking: Doesn't block main thread
  • Performance Improvement: Maintains smooth game experience
  • Resource Management: Automatic thread lifecycle management

Signal Bridging

Native Signal Connections

Connect Godot node's native signals to SignalBus:

gdscript
# Bridge button signal
bus.bridge_signal($Button, "pressed")

# Forward signal to different name
bus.relay_node_signal($Button, "pressed", "ui_button_pressed")

# Subscribe to bridged signals
bus.subscribe("pressed", _on_button_pressed)
bus.subscribe("ui_button_pressed", _on_ui_button_pressed)

Bridging Advantages

  • UI Decoupling: Separate UI logic from game logic
  • Centralized Management: All UI events processed through SignalBus
  • Flexible Configuration: Can rename and reroute signals

Memory Management

Automatic Cleanup

GDSignalBus automatically manages memory:

gdscript
# Connections automatically cleaned up when object is freed
func _exit_tree():
    # Optional: Explicit cleanup (recommended)
    bus.unsubscribe_all(self)
    # Or rely on automatic cleanup

Best Practices

  • Timely Disconnection: Disconnect when no longer needed
  • Avoid Circular References: Be mindful of reference relationships between objects
  • Use Weak References: Use weak references when possible

Performance Considerations

Optimization Recommendations

  1. Reasonable Channel Usage: Avoid too many channels
  2. Signal Filtering: Reduce unnecessary callbacks
  3. Batch Processing: Combine related signals
  4. Async Processing: Use async for time-consuming operations

Performance Metrics

  • Signal Distribution: High-performance C++ implementation
  • Memory Usage: Minimized memory footprint
  • CPU Overhead: Zero-cost abstraction design

Debugging Tools

Built-in Debugging

gdscript
# Enable debug mode
bus.set_debug_enabled(true)

# View signal statistics
print("Signal list: ", bus.get_signal_list())
print("Channel list: ", bus.get_channel_list())
print("Subscriber count: ", bus.get_subscriber_count("player_died"))
print("Total signals emitted: ", bus.get_total_signals_emitted())

Debugging Features

  • Signal Statistics: View signal usage
  • Performance Analysis: Monitor signal distribution performance
  • Connection Tracking: Debug signal connection issues

Released under the MIT License