Search code examples
godotgdscriptgodot4

How to write a static event emitter in GDScript?


Godot doesn't support static signals, so I tried two approaches:

Empty Signal:

static var my_signal: Signal = Signal()

Custom type:

static var my_signal: StaticSignal = StaticSignal.new()

class_name StaticSignal

var _callables: Dictionary

func connect(callable: Callable) -> void:
    self._callables[callable] = true

func disconnect(callable: Callable) -> void:
    self._callables.erase(callable)

func emit(data: Variant) -> void:
    for callable in self._callables:
        callable(data)

The problem is when it comes to .connecting to the signal. With StaticSignal I am getting a .connect member not found error. With Signal I am getting an ignored runtime error.

static func _static_init():
    ChatLog.new_message.connect(func(msg):
        if len(_log) >= LIMIT:
            _log.remove_at(0)
        _log.append(msg))

Solution

  • I will show two approaches: one on how to create static signals and the other on arguably the "expected" approach.

    Static Signals

    Godot doesn't support the keywords to define a static signal. However, we know that when we load() or preload() some gdscript we get a container resource object of type GDScript. A gdscript with a class_name is symbolically bound to a GDScript.

    The gdscript parser has a sort of special treatment of these symbols. We can bypass that. Since the the symbol is actually backed by a GDScript object we can cast it to GDScript or an ancestor. Then we can add our static signals to the GDScript resource that the class name is bound to.

    Additionally, Godot 4 handles circular dependencies with class_names so this pattern actually possible.

    The script with static signals:

    extends Node
    class_name StaticSignalsClass
    
    static var static_signal_1: Signal = (func():
        # We have to manually add a user signal.
        (StaticSignalsClass as Object).add_user_signal("static_signal_1")
        # Now return a reference to the signal we just defined.
        return Signal(StaticSignalsClass, "static_signal_1")
    ).call()
    
    # We can also define a helper static method:
    static func make_signal(p_obj, p_signal_name: StringName) -> Signal:
        # We use GDScript's duck typing to avoid having to cast p_obj.
        p_obj.add_user_signal(p_signal_name)
        return Signal(p_obj, p_signal_name)
    
    static var static_signal_2: Signal = make_signal(StaticSignalsClass, "static_signal_2")
    

    The client script:

    static func _static_init():
        StaticSignalsClass.static_signal_1.connect(func(): print("Hello, ss1."))
        StaticSignalsClass.static_signal_2.connect(func(): print("Hello, ss2."))
    
    # Somewhere else...
    func emit_stuff():
        StaticSignalsClass.static_signal_1.emit()
        StaticSignalsClass.static_signal_2.emit()
    

    "Expected" Signal Center

    We define an autoload that is the center for our signals. You can create namespaces by

    1. Adding a property that holds a script with more signals.
    2. Follow the RenderingServer pattern with the namespace in the name.

    The caveat is to be mindful of the order autoloads are loaded. If an earlier initialized autoload accesses SignalCenter it will fail. (However, on _ready will work.)

    extends Node
    # autoload name: SignalCenter
    
    signal chatlog_new_message(msg: String)
    signal chatlog_user_joined(guest_id: int)
    signal chatlog_user_left(guest_id: int)
    
    # Or
    
    var chatlog := ChatLogSignalCenter.new()
    
    ### ChatLogSignalCenter script
    
    extends Object
    
    signal new_message(msg: String)
    signal user_joined(guest_id: int)
    signal user_left(guest_id: int)
    

    The client script:

    extends Node
    class_name StaticClient
    
    static func _static_init():
        SignalCenter.chatlog_new_message.connect(func(): print(msg))
    
        # or
    
        SignalCenter.chatlog.new_message.connect(func(): print(msg))