This guide covers a pattern to maintain signal connections in Godot projects of growing sizes.
Thanks to Ombarus for sharing this design pattern. He presented it in a devlog video on his YouTube channel.
Maintaining signal connections isn’t the easiest in Godot, especially when wiring them via code. Godot 3.1 doesn’t offer any visual cues for signals connected through code as opposed to signals connected with the editor. Connecting signals through the editor or via code have different advantages.
Since Godot 3.2, an icon in the script editor’s margin indicates signal connections to a given function.
connect()
method globally in the project.Node
only. Any Object
can define, emit, and connect signals. See the Object class’s docs.With these guidelines and in our work, we’re trying to decouple code to create independent, reusable, and scalable systems. This comes at a cost: we lose the ability to connect signals across separate systems easily, and it tends to lead to spaghetti code. The Event singleton is a pattern to reduce this effect at the expense of introducing a global dependency.
We couldn’t directly connect a signal in a deeply nested node from DialogSystem
to a nested node in the Board
tree branch while following the decoupling guidelines.
One solution is to declare the signal connection between those systems in a script attached to the Game
node. The problem is that we can lose track of connections since they’re not declared in the scripts attached to the nodes that need these connections themselves.
In a complex system, you might have hundreds of signals emitted and connected all over the place. To manage this we can use dedicated Events
singletons (autoloaded scripts):
Here’s an example Events.gd
:
signal party_walk_started(msg)
signal party_walk_finished(msg)
...
signal dialog_system_proceed_pressed(msg)
signal dialog_system_cancel_pressed(msg)
...
signal battle_started(msg)
signal battle_finished(msg)
This singleton’s only purpose is to lists signals that can be emitted and connected to. A “deeply” nested node like $Game/Party/Godette/Walk
could then emit the appropriate signal directly using the global Events
node: Events.emit_signal("party_walk_started", {destination = destination})
. Other nested nodes could connect to these signals: Events.connect("party_walk_started", self, "_on_Party_walk_started")
etc.
This way, we can encapsulate signal connections in the related nodes instead of managing them in the code of some parent script like Game.