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 .connect
ing 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))
I will show two approaches: one on how to create static signals and the other on arguably the "expected" approach.
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_name
s 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()
We define an autoload that is the center for our signals. You can create namespaces by
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))