If you’ve worked in Godot long enough, you’ll have encountered the signal
pattern. This pattern has one part of the code providing signals that it emits, which other parts of the code can listen for and react to. It’s very useful for keeping the parts of your codebase separated by concern, thereby preventing explicit dependencies, while still allowing for communication between different systems. This pattern is commonly used with UI elements, which have no bearing on how the game systems work, but still need to know when certain events happen. With signals, the UI nodes can listen for when specific game events occur, then take the data from those signals and use it to update their visuals.
In Godot, this pattern is implemented through the use of a Node’s signal
keyword, emit_signal
and connect
methods, and a callback function. Example follows:
# Some example node script
extends Node
signal an_awesome_signal
func an_awesome_function():
emit_signal('an_awesome_signal', 'data that the listeners receive')
func another_awesome_function():
connect('an_awesome_signal', self, '_on_An_awesome_signal')
func _on_An_awesome_signal(data):
print(data) # 'data that the listeners receive'
It is considered good Godot practice to name your listener callbacks after the signal they are responding to, prefixed with
_on_
and with the first letter of the signal name capitalized.
Of course, you don’t have to just connect to signals within your node. Any node that is in the same scene as another node can connect to that node’s signals and listen for them. As explained, connecting nodes to one another allows for coding systems that need to respond to certain game events, but without having to call externalNode.external_node_method()
each time external_node_method
needs to be run in response to something happening.
Godot’s signal implementation is great, but there is a caveat: it doesn’t provide a clean way to listen for nodes which exist outside of the current scene. Let’s go back to the UI example. UI nodes and their code are usually kept separate from game systems code (after all, game systems shouldn’t need to manage the UI), often by creating entire scenes which house a portion of some UI widget, like a health bar. But how does said health bar know when it needs to be updated? Given this health bar (let’s call it HealthBarUI
) is separate from the systems which actually calculate an entity’s health, we can’t directly connect it to the health system.
One way to solve this problem is to use relative paths when connecting the signals, e.g. ../../../HealthBarUI
. Obviously, this solution is very brittle. If you decide that HealthBarUI
needs to be moved anywhere in the node tree, you’ll have to update the signal connection path accordingly. It’s not hard to imagine this becoming untenable when adding many nodes which are connected to other nodes outside of their scene tree; it’s a maintenance nightmare.
A better solution would be to create a global singleton which your nodes can connect to, instead, adding it to the global AutoLoads. This alleviates the burden of relative paths by providing a global singleton variable that is guaranteed to be accessible from every scene.
Many programmers will advise against using the Singleton pattern, as creating so-called “god objects” is an easy way to create messy, disorganized code that makes code reuse more difficult. I share this concern, but advocate that there are certain times where you want to have a global singleton, and I consider this one of them. As with all practices and patterns, use your best judgment when it comes to determining how to apply them to solve your systems design problems.
GDQuest gives a good example of this pattern in this article. Basically, for every signal which needs to be globally connected, you add that signal definition to the global singleton, connect your local nodes to the singleton, and call Singleton.emit_signal()
whenever you need to emit the global version of that signal. While this pattern works, it obviously gets more complex with each signal that you need to add. It also creates a hard dependency on the singleton, which makes it harder to reuse your nodes in other places without the global singleton.
I would like to propose a different take on the global singleton solution. Instead of explicitly defining global signals inside of a globally-accessible singleton file, we can dynamically add signals and connectors to a GlobalSignal
node through add_emitter
and add_listener
methods. Once a signal is registered, then whenever it is emitted by its node, any registered listeners of that signal will receive it and be able to respond to it, exactly the same as how signals normally work. We avoid a hard dependency on the GlobalSignal singleton because we’re just emitting signals the normal way. It’s a clean solution that takes advantage of how Godot’s signals work as much as possible.
Intrigued? Let me show you how it works.
If you want to skip to the final result, you can access the sample project here: https://github.com/Jantho1990/Godot-Global-Signal.
Also, this post was originally written for Godot 3.x. With Godot 4.0 being (finally) released, I did a quick conversion of the sample project to 4.0 and pushed it up in a separate branch. I won’t update the article with 4.0 versions of code at this time, but there aren’t too many changes, so it shouldn’t be too hard to follow and translate the differences.
Building the Basics
Let’s start by creating the file (I’m assuming you’ll have created a Godot project to work with, first). I’ve called it global_signal.gd
. It should extend the basic Node
. Once the file is created, we should add it to the global AutoLoads by going into Godot’s project settings, clicking the AutoLoad tab, then adding our script (with the variable name GlobalSignal
).
This is how we will make GlobalSignal
accessible from any scene’s code. Godot automatically loads any scripts in the AutoLoad section first and places them at the top of the game’s node hierarchy, where they can be accessed by their name.
With that out of the way, let’s start adding code to global_signal.gd
. First, we need a way for GlobalSignal
to know when a node has a signal that can be emitted globally. Let’s call these nodes emitters
. This code should take care of adding emitters for GlobalSignal
to keep track of:
# Keeps track of what signal emitters have been registered.
var _emitters = {}
# Register a signal with GlobalSignal, making it accessible to global listeners.
func add_emitter(signal_name: String, emitter: Object) -> void:
var emitter_data = { 'object': emitter, 'object_id': emitter.get_instance_id() }
if not _emitters.has(signal_name):
_emitters[signal_name] = {}
_emitters[signal_name][emitter.get_instance_id()] = emitter_data
Nothing too complex about this. We create a dictionary to store data for the emitter being added, check to see if we have an existing place to store signals with this name (and create a new dictionary to house them if not), then add it to the _emitters
dictionary, storing it by signal name and instance id (the latter being a guaranteed unique key that is already part of the node, something we’ll be taking advantage of later).
We can now register emitters, but we also need a way to register listener nodes. After all, what’s the point of having a global signal if nothing can respond to it? The code for adding listeners is nearly identical to the code for adding emitters; we’re just storing things in a _listeners
variable instead of _emitters
.
# Keeps track of what listeners have been registered.
var _listeners = {}
# Adds a new global listener.
func add_listener(signal_name: String, listener: Object, method: String) -> void:
var listener_data = { 'object': listener, 'object_id': listener.get_instance_id(), 'method': method }
if not _listeners.has(signal_name):
_listeners[signal_name] = {}
_listeners[signal_name][listener.get_instance_id()] = listener_data
With that, we now have the ability to add emitters and listeners. What we don’t yet possess is a way to connect these emitters and listeners together. Normally, when using signals, we’d have the listener node connect()
to the emitter node, specifying whatever signal it wants to connect to and the callback function which should be invoked (as well as the node where this callback function resides). We need to replicate this functionality here, but how do we ensure that a new emitter gets connected to all current and future listeners, and vice versa?
Simply put, every time we add a new emitter, we need to loop through GlobalSignal
‘s listeners, find the ones which want to connect with that emitter’s signal, and perform the connection. The same is true for when we add a new listener: when a new listener is added, we need to loop through the registered emitters, find the ones whose signal matches the one the listener wants to listen to, and perform the connection. To abstract this process, let’s create a couple of functions to take care of this for us.
# Connect an emitter to existing listeners of its signal.
func _connect_emitter_to_listeners(signal_name: String, emitter: Object) -> void:
var listeners = _listeners[signal_name]
for listener in listeners.values():
emitter.connect(signal_name, listener.object, listener.method)
# Connect a listener to emitters who emit the signal it's listening for.
func _connect_listener_to_emitters(signal_name: String, listener: Object, method: String) -> void:
var emitters = _emitters[signal_name]
for emitter in emitters.values():
emitter.object.connect(signal_name, listener, method)
Now we need to modify our existing add functions to run these connector functions.
func add_emitter(signal_name: String, emitter: Object) -> void:
# ...existing code
if _listeners.has(signal_name):
_connect_emitter_to_listeners(signal_name, emitter)
func add_listener(signal_name: String, listener: Object, method: String) -> void:
# ...existing code
if _emitters.has(signal_name):
_connect_listener_to_emitters(signal_name, listener, method)
We first check to make sure an emitter/listener has already been defined before we try to connect to it. Godot doesn’t like it when you try to run code on objects that don’t exist. š
With that, the last thing we need to finish the basic implementation is to add a way for removing emitters and listeners when they no longer need to be connected. We can implement such functionality thusly:
# Remove registered emitter and disconnect any listeners connected to it.
func remove_emitter(signal_name: String, emitter: Object) -> void:
if not _emitters.has(signal_name): return
if not _emitters[signal_name].has(emitter.get_instance_id()): return
_emitters[signal_name].erase(emitter.get_instance_id())
if _listeners.has(signal_name):
for listener in _listeners[signal_name].values():
if emitter.is_connected(signal_name, listener.object, listener.method):
emitter.disconnect(signal_name, listener.object, listener.method)
# Remove registered listener and disconnect it from any emitters it was listening to.
func remove_listener(signal_name: String, listener: Object, method: String) -> void:
if not _listeners.has(signal_name): return
if not _listeners[signal_name].has(listener.get_instance_id()): return
_listeners[signal_name].erase(listener.get_instance_id())
if _emitters.has(signal_name):
for emitter in _emitters[signal_name].values():
if emitter.object.is_connected(signal_name, listener, method):
emitter.object.disconnect(signal_name, listener, method)
As with the add functions, the remove functions are both almost identical. We take an emitter (or listener), verify that it exists in our stored collection, and erase it from the collection. After that, we check to see if anything was connected to the thing being removed, and if so we go through all such connections and remove them.
That’s it for the basic implementation! We now have a functional GlobalSignal
singleton that we can use to connect emitters and listeners dynamically, whenever we need to.
A Simple Test
Let’s create a simple test to verify that all this is working as intended.
This simple test is included in the sample project.
First, create a Node
-based scene in your project. Then, add a LineEdit
node and a Label
node (along with whatever other Control nodes you want to add to make it appear the way you want), and create the following scripts to attach to them:
# TestLabel
extends Label
func _ready():
GlobalSignal.add_listener('text_updated', self, '_on_Text_updated')
func _on_Text_updated(text_value: String):
text = text_value
# TestLineEdit
extends LineEdit
signal text_updated(text_value)
func _ready():
GlobalSignal.add_emitter('text_updated', self)
connect('text_changed', self, '_on_Text_changed')
func _on_Text_changed(_value):
emit_signal('text_updated', text)
You could also use the
value
argument for_on_Text_changed
, instead of taking the direct value oftext
. It’s a matter of preference.
Assuming you’ve implemented the code from this tutorial correctly, when you run the scene, you should be able to type in the LineEdit
node and see the values of the Label
node update automatically. If it’s working, congratulations! If not, go back and look through the code samples to see what you might’ve missed, or download the sample project to compare it with yours.
Now, obviously, this is a contrived example. GlobalSignal
would be overkill for solving such a simple scenario as the one presented in the test case. Hopefully, though, it illustrates how this approach would be useful for more complex scenarios, such as the HealthBarUI
example described earlier. By making our global signal definition dynamic, we avoid having to make updates to GlobalSignal
every time we need to add a new globally-accessible signal. We emit signals from the nodes, as you do normally; we just added a way for that signal to be listened to by nodes outside of the node’s scene tree. It’s powerful, flexible, and clean.
Resolving Edge Cases and Bugs
There are some hidden issues that we need to address, however. Let’s take a look at them and see how we can fix them.
Dealing with Destroyed Nodes
Let’s ask ourselves a hypothetical question: what would happen if a registered emitter or listener is destroyed? Say the node is freed by the parent (or the parent itself is freed). Would GlobalSignal
know this node no longer exists? The answer is no, it wouldn’t. Subsequently, what would happen if we’re looping through our registered emitters/listeners and we try to access the destroyed node? Godot gets unhappy with us, and crashes.
How do we fix this? There are two approaches we could take:
- We could poll our dictionaries of registered emitters and listeners every so often (say, once a second) to check and see if there’s any dead nodes, and remove any we find.
- Alternatively, we could run that same check and destroy whenever we make a call to a function which needs to loop through the lists of emitters and listeners.
Of those two options, I prefer the latter. By only running the check when we explicitly need to loop through our emitters and listeners, we avoid needlessly running the check and thereby introducing additional processing time when we don’t know that it’s necessary (which is what would happen if we went with polling). Thus, we’re going to implement this only-when-necessary check in the four places that need it: namely, whenever we add or remove an emitter or listener.
There is an argument to be made that running the check as part of adding/removing emitters/listeners adds additional processing time when performing these functions. That’s true, but in practice I’ve found that the added time isn’t noticeable. That said, if your game is constantly creating and destroying nodes that need to be globally listened to, and it’s measurably impacting game performance, it may prove better to implement a poll-based solution. I’m just not going to do it as part of this tutorial.
First, let’s create a function that will both perform the check and remove the emitter/listener if it is determined it no longer exists.
# Checks stored listener or emitter data to see if it should be removed from its group, and purges if so.
# Returns true if the listener or emitter was purged, and false if it wasn't.
func _process_purge(data: Dictionary, group: Dictionary) -> bool:
var object_exists = !!weakref(data.object).get_ref() and is_instance_valid(data.object)
if !object_exists or data.object.get_instance_id() != data.object_id:
group.erase(data.object_id)
return true
return false
First, we check all the possible ways that indicate that a node (or object, which is what a node is based on) no longer exists. weakref()
checks to see if the object only exists by reference (aka has been destroyed and is pending removal from memory), and is_instance_valid
is a built in Godot method that returns whether Godot thinks the instance no longer exists. I’ve found that I’ve needed both checks to verify whether or not the object truly exists.
You may want to abstract this object existence check into some kind of helper function that is made globally accessible. This is what I’ve done in my own implementation of
GlobalSignal
, but I chose to include it directly in this tutorial to avoid having to create another file exclusively to house that helper.
Even if we prove the object exists, we still need to check to make sure the stored instance id for the emitter/listener matches the current instance id of said object. If they don’t match, then it means the stored object is no longer the same as the one we registered (aka the reference to it changed).
If the object doesn’t exist, or if it’s not the same object as the one we registered, then we need to remove it from our dictionary. group
is the collection we passed in for validation (this will be explained in more detail momentarily), and group.erase(data.object_id)
deletes whatever value is stored at the key with the same name as data.object_id
. If we’ve reached this point, we then return true
. If we didn’t erase the object, we return false
.
With our purge function defined, let’s go ahead and modify our add and remove functions to implement it:
func _connect_emitter_to_listeners(signal_name: String, emitter: Object) -> void:
var listeners = _listeners[signal_name]
for listener in listeners.values():
if _process_purge(listener, listeners):
continue
emitter.connect(signal_name, listener.object, listener.method)
func _connect_listener_to_emitters(signal_name: String, listener: Object, method: String) -> void:
var emitters = _emitters[signal_name]
for emitter in emitters.values():
if _process_purge(emitter, emitters):
continue
emitter.object.connect(signal_name, listener, method)
func remove_emitter(signal_name: String, emitter: Object) -> void:
# ...existing code
if _listeners.has(signal_name):
for listener in _listeners[signal_name].values():
if _process_purge(listener, _listeners[signal_name]):
continue
if emitter.is_connected(signal_name, listener.object, listener.method):
emitter.disconnect(signal_name, listener.object, listener.method)
func remove_listener(signal_name: String, listener: Object, method: String) -> void:
# ...existing code
if _emitters.has(signal_name):
for emitter in _emitters[signal_name].values():
if _process_purge(emitter, _emitters[signal_name]):
continue
if emitter.object.is_connected(signal_name, listener, method):
emitter.object.disconnect(signal_name, listener, method)
For each function, the only thing we’ve changed is adding the _process_purge()
check before doing anything else with the emitters/listeners. Let’s examine what’s happening in _connect_emitter_to_listeners()
, to detail the logic.
As we start looping through our dictionary of listeners (grouped by signal_name
), we first call _process_purge(listener, listeners)
in an if
statement. From examining the code, listener
is the current listener node (aka the object we want to verify exists) and listeners
is the group of listeners for a particular signal_name
. If _process_purge()
returns true
, that means the listener did not exist, so we continue
to move on to the next stored listener. If _process_purge()
returns false, then the listener does exist, and we can proceed with connecting the emitter to the listener.
The same thing happens for the other three functions, just with different values passed into _process_purge()
, so I shan’t dissect them further. Hopefully, the examination of what happens in _connect_emitter_to_listeners()
should make it clear how things work.
That’s one issue down. Let’s move on to the last issue that needs to be addressed before we can declare GlobalSignal
complete.
Accessing an Emitter/Listener Before It’s Ready
Here’s another scenario to consider: what happens if we want to emit a globally-accessible signal during the _ready()
call? You can try this out yourself by adding this line of code to TestLineEdit.gd
, right after defining the global signal:
GlobalSignal.add_emitter('text_updated', self)
emit_signal('text_updated', 'text in _ready()')
We’d expect that, on starting our scene, our Label
node should have the text set to “text in _ready()”. In practice, however, nothing happens. Why, though? We’ve established that we can use GlobalSignal
to listen for nodes, so why doesn’t the connection in Label
seem to be working?
To answer this question, let’s talk a little about Godot’s initialization process. When a scene is added to a scene tree (whether that be the root scene tree or a parent’s scene tree), the _ready()
function is called on the lowermost child nodes, followed by the _ready()
functions of the parents of those children, and so on and so forth. For sibling children (aka child nodes sharing the same parent), Godot calls them in tree order; in other words, Child 1 runs before Child 2. In our scene tree composition for the sample project, the LineEdit
node comes before the Label
node, which means the _ready()
function in LineEdit
runs first. Since Label
is registering the global listener in its _ready()
function, and that function is running after LineEdit
‘s _ready()
function, our text_updated
signal gets emitted before the listener in Label
is registered. In other words, the signal is being emitted too early.
How do we fix this? In our contrived example, we could move the Label
to appear before the LineEdit
, but then that changes where the two nodes are being rendered. Besides, basing things on _ready()
order isn’t ideal. In the case where we want nodes in different scenes to listen for their signals, we can hardly keep track of when those nodes run their _ready()
function, at least not without some complex mapping of your scene hierarchy that is painful to maintain.
The best to solve this problem is to provide some way to guarantee that, when emit_signal
is called, that both the emitter and any listeners of it are ready to go. We’ll do this by adding a function called emit_signal_when_ready()
which we call whenever we need to emit a signal and guarantee that any listeners for it that have been defined in _ready()
functions are registered.
Unfortunately, we can’t override the existing
emit_signal
function itself to do this, becauseemit_signal
uses variadic arguments (aka the ability to define any number of arguments to the function), which is something Godot does not allow for user-created functions. Therefore, we need to create a separate function for this.
We’ll need to add more than just the emit_signal_when_ready()
function itself to make this functionality work, so I’ll go ahead and show all of the code which needs to be added, and then cover what’s going on in detail.
# Queue used for signals emitted with emit_signal_when_ready.
var _emit_queue = []
# Is false until after _ready() has been run.
var _gs_ready = false
# We only run this once, to process the _emit_queue. We disable processing afterwards.
func _process(_delta):
if not _gs_ready:
_make_ready()
set_process(false)
set_physics_process(false)
# Execute the ready process and initiate processing the emit queue.
func _make_ready() -> void:
_gs_ready = true
_process_emit_queue()
# Emits any queued signal emissions, then clears the emit queue.
func _process_emit_queue() -> void:
for emitted_signal in _emit_queue:
emitted_signal.args.push_front(emitted_signal.signal_name)
emitted_signal.emitter.callv('emit_signal', emitted_signal.args)
_emit_queue = []
# A variant of emit_signal that defers emitting the signal until the first physics process step.
# Useful when you want to emit a global signal during a _ready function and guarantee the emitter and listener are ready.
func emit_signal_when_ready(signal_name: String, args: Array, emitter: Object) -> void:
if not _emitters.has(signal_name):
push_error('GlobalSignal.emit_signal_when_ready: Signal is not registered with GlobalSignal (' + signal_name + ').')
return
if not _gs_ready:
_emit_queue.push_back({ 'signal_name': signal_name, 'args': args, 'emitter': emitter })
else:
# GlobalSignal is ready, so just call emit_signal with the provided args.
args.push_front(signal_name)
emitter.callv('emit_signal', args)
That’s quite a lot to take in, so let’s break it down, starting with the two class members being added, _emit_queue
and _gs_ready
.
_emit_queue
is a simple array that we’re going to use to keep track of any signals that have been marked as needing to be emitted when GlobalSignal
decides everything is ready to go. _gs_ready
is a variable that will be used to communicate when GlobalSignal
considers everything ready.
I use
_gs_ready
instead of_ready
to avoid giving a variable the same name as a class function. While I’ve found that Godot does allow you to do that, I consider it bad practice to have variables with the same name as functions; it’s confusing, and confusing code is hard to understand.
Next, let’s examine our call to _process()
(a built-in Godot process that runs on every frame update):
# We only run this once, to process the _emit_queue. We disable processing afterwards.
func _process(_delta):
if not _gs_ready:
_make_ready()
set_process(false)
set_physics_process(false)
If _gs_ready
is false (which is what we’ve defaulted it to), then we call _make_ready()
and subsequently disable the process and physics process update steps. Since GlobalSignal
doesn’t need to be run on updates, we can save processing time by disabling them once we’ve run _process()
the first time. Additionally, since GlobalSignal
is an AutoLoad, this _process()
will be run shortly after the entire scene tree is loaded and ready to go.
Let’s check out what _make_ready()
does:
# Execute the ready process and initiate processing the emit queue.
func _make_ready() -> void:
_gs_ready = true
_process_emit_queue()
The function sets _gs_ready
to true
, then calls _process_emit_queue()
. By marking _gs_ready
as true, it signals that GlobalSignal
now considers things to be ready to go.
Moving on to _process_emit_queue()
:
# Emits any queued signal emissions, then clears the emit queue.
func _process_emit_queue() -> void:
for emitted_signal in _emit_queue:
emitted_signal.args.push_front(emitted_signal.signal_name)
emitted_signal.emitter.callv('emit_signal', emitted_signal.args)
_emit_queue = []
Here, we loop through the _emit_queue
array, push the signal name to the front of the arguments array, and use callv
to manually call the emit_signal()
function on the emitter node, passing in the array of arguments (emit_signal()
takes the signal’s name as the first argument, which is why we needed to make the signal name the first member of the arguments array) . When we’ve gone through all of the members of _emit_queue
, we reset it to an empty array.
Finally, we come to the emit_signal_when_ready()
function, itself:
# A variant of emit_signal that defers emitting the signal until the first process step.
# Useful when you want to emit a global signal during a _ready function and guarantee the emitter and listener are ready.
func emit_signal_when_ready(signal_name: String, args: Array, emitter: Object) -> void:
if not _emitters.has(signal_name):
push_error('GlobalSignal.emit_signal_when_ready: Signal is not registered with GlobalSignal (' + signal_name + ').')
return
if not _gs_ready:
_emit_queue.push_back({ 'signal_name': signal_name, 'args': args, 'emitter': emitter })
else:
# GlobalSignal is ready, so just call emit_signal with the provided args.
args.push_front(signal_name)
emitter.callv('emit_signal', args)
First, we check to see if the signal we want to emit has been registered with GlobalSignal
, and return early if it is not (with an error pushed to Godot’s console to tell us this scenario happened). Our next action depends on the value of _gs_ready
. If it’s false
(aka we aren’t ready), then we add a new entry to _emit_queue
and pass in the signal name, arguments, and emitter node, all of which will be utilized during _process_emit_queue()
. If it’s true
, then we called this function after everything has been marked as ready; in that case, there’s no point in adding this to the emit queue, so we’ll just invoke emit_signal()
and call it a day.
With that, GlobalSignal
should now be able to handle dispatching signals and guaranteeing that the listeners defined during _ready()
functions are registered. Let’s test this by changing our modification to TestLineEdit
so it uses emit_signal_when_ready()
:
GlobalSignal.add_emitter('text_updated', self)
GlobalSignal.emit_signal_when_ready('text_updated', ['text in _ready()'], self)
Note that we need to convert our ‘text in _ready()’ argument to be wrapped in an array, since we need to pass an array of arguments to the function.
Also note that we have to pass in the emitter node, since we have to store that in order to call
emit_signal()
on it later.
If, when you run the scene, the Label
node shows our text string, that means our changes worked! Now we can declare GlobalSignal
done!
Using Global Signals
Congratulations! You now have a dynamic way to define globally-accessible signals that closely follows Godot’s natural signals implementation. Adding new global signals is easy, and doesn’t involve changing the GlobalSignal
singleton at all.
At this point, you might wonder, “Why not convert all of my signals to be global signals?” That’s not necessarily a great idea. Most of the time, we want to keep our signals local, as when we start connecting code from disparate parts of our code base it can make it confusing to recall which things are connected to what. By keeping signals local whenever possible, we make dependencies clearer and make it harder to design bad code.
That’s one of the things I actually like about this approach to implementing global signals. We’re still emitting signals locally; we just choose which signals need to also be exposed globally. You can connect signals locally and globally, with the same signal definitions.
What are some good use cases? UI nodes, as mentioned before, are a great example of a good use case for this pattern. An achievements system needing to know when certain events occur is another possible use case. Generally, this pattern is best suited for when you have multiple “major” systems that need to talk to one another in an agnostic manner, while local signal connections are better for communication between the individual parts of a major system.
As with any pattern or best practice, GlobalSignal
should be carefully considered as one of many solutions to your problem, and chosen if it proves to be the best fit.
One last time, here is the link to the sample project, if you didn’t build along with the tutorial, or just want something to compare your implementation against. (And if you are using Godot 4.0, here is the branch with that version of it!)
Hopefully, this approach to global signals helps you in your projects. I’ve certainly made great use of it in mine!
n0manarmy
2022-01-11 at 7:05 pmThis is slick! Good idea and thanks for the write up!
Josh Anthony
2022-01-17 at 7:42 pmThank you! Glad you liked it.
Patrick Mullen
2022-02-24 at 11:53 amVery cool and flexible design. I think there may still be some value in the method where signals are defined in the singleton, in that you have one place to look for to see which events are available in the global scope.
For emit_signal_when_ready, could you not use come construction with call_deferred to have the signals emitted in between scene tree initialization and the physics step?
Josh Anthony
2022-03-02 at 1:20 pmFor sure, having the global signals in one location is a valid reason to go with that approach. I think it makes it harder to easily set up global signals, but that may be something you desire as a principle. I personally prefer having the flexibility of my approach.
I could probably do something to have it fire after the scene tree initialization, yeah. That might be a good target for improvement. š
Ivailo Burov
2022-06-07 at 8:28 pmVery useful solution, thank you for your work. It really helps.
Josh Anthony
2022-06-09 at 5:29 pmYou’re welcome! Glad you found it useful. š
Maksym P
2023-03-07 at 10:52 pmThis is really great. I was confused about how godot expects to connect dynamic UI components with external game scenes, and you approach is great for that.
There’s a small change that needs to be updated for Godot 4: object+method is now combined into Callable. Which also means listener_data should be updated to store the callable, rather than the method
There’s also a bit of a bug where _process_purge only runs on emitters when adding a listener, and listeners when adding an emitter (and also only for the current signal). Say your project has 1 emitter for a signal; this would mean stale listeners would never get cleaned up. It might make sense to just check the whole map (every signal) any time anything gets added, as otherwise you are almost certain to have stale keys for a long time.
All in all though, thanks a lot!
Josh Anthony
2023-03-09 at 7:24 pmThank you for reading, and for the Godot 4 points! I myself haven’t tried out Godot 4 yet because the project I’m working on is still using 3.x. I was able to do a quick conversion of the project to 4.0 that seems to work fine, so I’ll push a branch with those changes and make note of it in the article.
As for when `_process_purge` runs, I intentionally did not make more effort to remove stale references because, most of the time, it’s not needed. The only time it should matter when a reference is stale is when we try to access it, so the check has to be made then. Otherwise, when there aren’t any stale references, the check does nothing, so why bother spending processing time checking it?
The one caveat is that this does mean memory is being wasted, so if that is a concern then, yes, you’d want to be more aggressive about clearing stale references. For my projects, so far, I’ve not needed to worry about that, so I just go with an approach that is only run when necessary. It’s essentially a performance design decision.
(If I’m missing a case where this is a problem, though, I’d love to know!)
Maksym P
2023-03-10 at 1:32 amAgreed it’s mostly a just a tradeoff
Ian William
2024-02-18 at 4:16 pm# This is working for me in 4.2.1
extends Node
# Keeps track of what signal emitters have been registered.
var _emitters = {}
# Queue used for signals emitted with emit_signal_when_ready.
var _emit_queue = []
# Keeps track of what listeners have been registered.
var _listeners = {}
# Is false until after _ready() has been run.
var _gs_ready = false
# We only run this once, to process the _emit_queue. We disable processing afterwards.
func _process(_delta):
if not _gs_ready:
_make_ready()
set_process(false)
set_physics_process(false)
# Connect an emitter to existing listeners of its signal.
func _connect_emitter_to_listeners(signal_name: String, emitter: Object) -> void:
var listeners = _listeners[signal_name]
for listener in listeners.values():
if _process_purge(listener, listeners):
continue
emitter.connect(signal_name, listener.method)
# Connect a listener to emitters who emit the signal it’s listening for.
func _connect_listener_to_emitters(signal_name: String, listener: Object, method: Callable) -> void:
var emitters = _emitters[signal_name]
for emitter in emitters.values():
if _process_purge(emitter, emitters):
continue
emitter.object.connect(signal_name, listener, method)
# Execute the ready process and initiate processing the emit queue.
func _make_ready() -> void:
_gs_ready = true
_process_emit_queue()
# Emits any queued signal emissions, then clears the emit queue.
func _process_emit_queue() -> void:
for emitted_signal in _emit_queue:
emitted_signal.args.push_front(emitted_signal.signal_name)
emitted_signal.emitter.callv(’emit_signal’, emitted_signal.args)
_emit_queue = []
# Register a signal with GlobalSignal, making it accessible to global listeners.
func add_emitter(signal_name: String, emitter: Object) -> void:
var emitter_data = { ‘object’: emitter, ‘object_id’: emitter.get_instance_id() }
if not _emitters.has(signal_name):
_emitters[signal_name] = {}
_emitters[signal_name][emitter.get_instance_id()] = emitter_data
if _listeners.has(signal_name):
_connect_emitter_to_listeners(signal_name, emitter)
# Adds a new global listener, making it accessible to global emitters.
func add_listener(signal_name: String, listener: Object, method: Callable) -> void:
var listener_data = { ‘object’: listener, ‘object_id’: listener.get_instance_id(), ‘method’: method }
if not _listeners.has(signal_name):
_listeners[signal_name] = {}
_listeners[signal_name][listener.get_instance_id()] = listener_data
if _emitters.has(signal_name):
_connect_listener_to_emitters(signal_name, listener, method)
# A variant of emit_signal that defers emitting the signal until the first process step.
# Useful when you want to emit a global signal during a _gs_ready function and guarantee the emitter and listener are ready.
func emit_signal_when_ready(signal_name: String, args: Array, emitter: Object) -> void:
if not _emitters.has(signal_name):
push_error(‘GlobalSignal.emit_signal_when_ready: Signal is not registered with GlobalSignal (‘ + signal_name + ‘).’)
return
if not _gs_ready:
_emit_queue.push_back({ ‘signal_name’: signal_name, ‘args’: args, ’emitter’: emitter })
else:
# GlobalSignal is ready, so just call emit_signal with the provided args.
args.push_front(signal_name)
emitter.callv(’emit_signal’, args)
# Checks stored listener or emitter data to see if it should be removed from its group, and purges if so.
# Returns true if the listener or emitter was purged, and false if it wasn’t.
func _process_purge(data: Dictionary, group: Dictionary) -> bool:
var object_exists = !!weakref(data.object).get_ref() and is_instance_valid(data.object)
if !object_exists or data.object.get_instance_id() != data.object_id:
group.erase(data.object_id)
return true
return false
# Remove emitter and disconnect any listeners connected to it.
func remove_emitter(signal_name: String, emitter: Object) -> void:
if not _emitters.has(signal_name): return
if not _emitters[signal_name].has(emitter.get_instance_id()): return
_emitters[signal_name].erase(emitter.get_instance_id())
if _listeners.has(signal_name):
for listener in _listeners[signal_name].values():
if _process_purge(listener, _listeners[signal_name]):
continue
if emitter.is_connected(signal_name, listener.object):
emitter.disconnect(signal_name, listener.object)
# Remove registered listener and disconnect it from any emitters it was listening to.
func remove_listener(signal_name: String, listener: Object, method: Callable) -> void:
if not _listeners.has(signal_name): return
if not _listeners[signal_name].has(listener.get_instance_id()): return
_listeners[signal_name].erase(listener.get_instance_id())
if _emitters.has(signal_name):
for emitter in _emitters[signal_name].values():
if _process_purge(emitter, _emitters[signal_name]):
continue
if emitter.object.is_connected(signal_name, listener, method):
emitter.object.disconnect(signal_name, listener, method)
Josh Anthony
2024-02-22 at 8:52 pmThank you! I’m curious, was something not working with the 4.0 version of the project I linked to at the beginning?
Andreas Decker
2024-04-23 at 4:22 amThis looks super useful! I’m refactoring my LDJam55 game, and will probably give it a try in Godot 4.2. If there are any issues with the Godot 4.0 branch, I’ll let you know, or just directly make a PR.
Is there any chance you might license the code on GitHub? It would be helpful to have explicit permission to use the code, particularly in Jams with rules explicitly around that and for commercial titles, obviously.
Thanks for the write-up š
Josh Anthony
2024-04-25 at 6:59 pmThanks for pointing that out! I’d always intended this to be used freely. I’ve gone ahead and committed an Unlicense to reflect my intent.
Good luck with the refactor!
WillYuen
2024-08-05 at 8:00 amIt is useful and clean. Just wanna thank you for writing it š
Josh Anthony
2024-08-06 at 9:13 pmYou’re welcome! Thank you for the kind words. š