Introduction
Signals, as described by the official documentation, are a feature of Godot Engine that allows you to create game objects that can react to each other without having to directly reference one another. You can check the Official documentation or the Official tutorials for a better explanation on signals.
This reference guide will explain how to use a SignalBus. Some of the code here comes from comments from the official tutorials and salandered blog post, you can check them out if you want to know more.
Signals, in general, are used when you have a low level object that needs to communicate something to a higher level object, many examples in the official documentation are centered around buttons communication that they were pressed and now something needs to happen.
The SignalBus pattern is a pattern created by the community were, instead of using the UI, you create a script, register as an Autoload global and place all you signals there. It’s a single place where you can quickly check which signals were created, how many arguments they need and which type they accept as parameters.
we tend to name events in the past tense such as DoorOpened or PositionNodeEntered.
Quick Example
Warning
The SignalBus.gd script needs to be registered as an Autoload with “Global Variable” Checked
SignalBus.gd
extends Node
signal enemy_died()Emitting the signal:
Enemy.gd
func _foo():
SignalBus.enemy_died.emit()Consuming the signal:
func _ready():
SignalBus.enemy_died.connect(_on_enemy_died)
func _on_enemy_died():
# Play a sound or increase xp or smt elseCaveat 1: Using the Signal Bus for built in signals
Create a relay script.
SignalBus.gd
# with no parameters
signal button_pressed()
# with parameters
signal player_collision(body: Node) #signal doesn't need same name as builtin signal, only same parametersButtonPressRelay.gd
extends Node
@export var button: BaseButton #the button to relay signals from
func _on_pressed():
SignalBus.button_pressed.emit() #relay the signal to the signal bus
func _ready():
button.pressed.connect(_on_pressed) #connect to the button's signalsRigidBody3DBodyEnteredRelay.gd
extends Node
@export var rigid_body_3d: RigidBody3D
func _on_body_entered(body: Node):
SignalBus.player_collision.emit(body)
func _ready():
rigid_body_3d.body_entered.connect(_on_body_entered)Caveat 2: Signal Bus might throw warnings about unused signals
Because the Signal Bus does not use the signals in the same script, the game might throw warnings about unused signals. The best way to solve is to temporarily disable them.
@warning_ignore_start("unused_signal")
signal foo(x)
signal bar(y)
# ...
@warning_ignore_restore("unused_signal")In case you don’t care about these warnings you can toggle the warning globally in Project Settings, with advanced settings toggled on: Debug > GDScript > Warnings > Unused Signal — set this to “ignore”.
Caveat 3: Type mismatch and arg count
As documented here, “Godot 4 is strict about signal argument counts and types. If the emitter passes an int but the handler expects a float, or vice versa, the handler may silently not run while Godot logs a low-severity warning you probably never see. Declare signals with explicit types and match handler signatures exactly.”
Keep in mind that Godot only accepts passing arguments as positional arguments, if you come from a language that allows you to pass parameters as keyword arguments this can be a bit frustrating, I know.
There are a couple of ways of getting around that limitation. I’ll show you how to use them depending which Godot version you are.
Godot < 4.5
If you just want a single argument…
func _ready() -> void:
for sig in get_signal_list():
var signal_name: String = sig["name"]
connect(signal_name, _log.bind(sig).unbind(sig["args"].size())) # bind your new arg, unbind the old args
func _log(sig: Dictionary) -> void:
print("[SignalBus] %s" % sig["name"])And in case you want all of the previous arguments plus extra arguments, it’s a bit ugly but it works
func _ready() -> void:
for sig in get_signal_list():
var signal_name: String = sig["name"]
connect(signal_name, _log.bind(sig)) # simply add your new arguments here
func _log(..._args) -> void:
print("[SignalBus] %s" % _args[-1]["name"]) # your new argument will always be the last in the list, you can access the other arguments by positionGodot > 4.5
After 4.5 they introduced variadic arguments (as described here and here). Which means that now you can make the same thing above on the signal bus a bit less hacky.
Again, single argument script below:
func _ready() -> void:
for sig in get_signal_list():
var signal_name: String = sig["name"]
connect(signal_name, func (..._args): _log(sig)) # notice the ... it's beautiful isn't it?
func _log(sig: Dictionary) -> void:
print("[SignalBus] %s" % sig["name"]) # then you don't have to deal with unpredictable argument size hereAnd even if you want all of the arguments it’s still straight forward:
signal_bus.gd
func _ready() -> void:
for sig in get_signal_list():
var signal_name: String = sig["name"]
connect(signal_name, func (..._args): _log(sig, _args)) # you can control the order in which they are added
func _log(sig: Dictionary, _args) -> void:
var line := "[SignalBus] (%s)" % sig["name"]Conclusion
Here is the full code on how to implement a signal bus in Godot. I’ve included a structure for logging when a signal is fired.
SignalBus.gd
extends Node
@warning_ignore_start("unused_signal")
signal something_happened_with(data)
signal something_else_happened_with_no_data()
func _ready() -> void:
if not OS.is_debug_build():
return
for sig in get_signal_list():
var signal_name: String = sig["name"]
connect(signal_name, func (..._args): _log(sig, _args))
func _log(sig: Dictionary, _args) -> void:
var line := "[SignalBus] (%s)" % sig["name"]
var pairs := []
for i in range(_args.size()):
var arg_name = sig["args"][i]["name"]
var arg_value = _args[i]
pairs.append("%s=%s" % [arg_name, arg_value])
line += " [%s]" % ", ".join(pairs)
print(line)
@warning_ignore_restore("unused_signal")This will format your output in something more or less like this:
[SignalBus] (ready) []
[SignalBus] (something_happened_with) [arg_name=arg_value]
[SignalBus] (something_else_happened_with_no_data) []