When we build a scene with a tree of nodes, it is no problem for a parent node to access methods of child nodes using the get_node, find_node or get_child methods for example. Here is some code to demonstrate this:
extends Node2D var _label1 var _label2 func _ready(): # Find child node based on it's name _label1 = get_node("Label") _label1.text = "Hello" # Find descendant node _label2 = find_node("Deep_Label")
It is good practice to design nodes such that they may be tested on their own i.e. by running the scene that the node is the root of. It should not throw up any errors of not being able to find something.
However, it is bad practice to try to directly access methods of the parent, ancestors, or other branches of the node tree of the game. This is because, the tightly-coupled path to the method is easily broken when we test sub nodes of the game. Here are some examples of this:
extends Label func _ready(): # Call a method of the parent node get_parent().reset_score() # Find a node in another branch var deep_label = get_tree().get_root().find_node("Deep_Label", true, false)
To get around this problem, we emit signals that any other upstream node may listen out for. The node that emits the signal does not need to know what is listening to it, so it may be tested in isolation without errors due to broken links. It is said to be loosely-coupled.
For nodes to listen to signals, they connect to the exposed signals of downstream nodes in the tree.
We don't connect to parent nodes, ancestors, or other branches since that would be bad practice as mentioned previously. We only connect downstream of the node tree and emit signals otherwise.
It's ok to chain signal propagation in this manner if you need to do so to maintain loose coupling between nodes.
In order to access a signal in another branch, it is possible to have a root node connect to the signal and then activate the handler method in it's downstream node that wants to observe it. For example, the game score may be affected by action in the main scene and the root node needs to update the score in the UI when it observes certain events.
In our game design, we should have various scenes that contain related chunks of functionality (objects such as a space craft). These may well have internal signals that are connected to the root node of the scene. Then the root node may emit signals to the outside world when it observes it's internal signals. Like a chain reaction to events.
For the above example, the space ship may observe signals that it's shields sustained damage. Then the damage handler script would emit a damage update signal to any observer in the universe that is interested in that.
To emit a signal, we first publish it with a signal statement with the name of the signal and an optional list of parameters that are emitted with it.
To do this, we use the emit_signal method with optional parameters.
Here are some examples:
extends Node2D # Signal with no arguments signal node_ready # Signal with arguments signal car_ready(driver, team, grid_pos) func _ready(): # Resume execution on the next frame. yield(get_tree(), "idle_frame") # Emit the signals emit_signal("node_ready") emit_signal("car_ready", "Mario", "Dodgers", 1)
Note that we may have one or many signals emitted from a node. In the above code we publish 2 signals. Also, the yield statement is making use of an internal signal to delay emitting our signals until the scene tree has been built.
Connecting to signals
To connect to signals, we use the connect method of the node that we want to connect to. And, we tell the node which one of it's signals we want to connect to. Also, we tell it what method to run (the signal handler) when the signal is observed. And finally, we may provide extra parameters if needed (such as an ID number).
Hopefully this will become clearer after a few examples.
extends Node2D func _ready(): # Connect to node_ready signal and add extra argument get_node("Emitter").connect("node_ready", self, "_node_ready", ["Emitter node ready"]) # Connect to car_ready signal get_node("Emitter").connect("car_ready", self, "_on_Emitter_car_ready") # Handler method for node_ready func _node_ready(txt): print(txt) # Handler method for car_ready func _on_Emitter_car_ready(driver, team, grid_pos): print("%s %s %d" % [driver, team, grid_pos]) # Auto-generated handler method # after connecting with extra argument in the editor func _on_Emitter_node_ready(extra_arg_0): pass # Replace with function body.
In many cases we will connect to built-in nodes such as timers and buttons that have pre-defined signals that they emit in response to events. The names of these signals are listed in the documentation and in the Node tab of the Editor.
Finally, it is quite code heavy and easy to make mistakes coding signal connections by hand, so I recommend making connections in the Editor. The Node tab lists the available signals for a selected node. And, we may select a signal to connect, which node to connect it to, and add any additional parameters that we want to pass.
The editor suggests a handler method name, and it will generate the method code if it is not already in the target script file. In this case, the code for doing the connecting is added to the scene file rather than the script file.
Global Event Bus
In GoDot we may have Autoload scripts that expose signals, constants, variables, and functions globally. This gives us the opportunity to implement a global event bus that brokers all of the game's signals between publisher nodes and subscriber nodes.
Then: any node may connect to, or emit any of the signals published in the Autoload script.
Example code for our event bus (event_bus.gd):
extends Node # Tell Godot to ignore warnings of unused signals #warning-ignore:unused_signal # List of published signals signal new_points(points) signal end_game
If we add this file to Project->Project Settings->Autoload and keep the default Name of event_bus, then any node may connect to or emit a signal as in the following example:
extends Node2D var score = 0; func _ready(): event_bus.connect("new_points", self, "_add_new_points") func _on_Button_button_down(): event_bus.emit_signal("new_points", 3) func _add_new_points(points): score += points
The above code would be attached to a scene containing a button. When the button is clicked it emits a signal to add 3 points to the score. And this signal has been connected to inside of the _ready function and linked to a handler function for the event.
It makes no sense to emit and connect to the same signal in the same script (because the action may be done directly), but we are just demonstrating the technique here.
And: even though the Event Bus script that publishes the signals is external to this script, since it is a global, it is loaded even when testing the related scene in isolation from the rest of the game.
Problems with the Global Event Bus
Godot emits warnings of unused signals for all of the signals published by the Event Bus because there is no code in the same script file for emitting the signals. These warnings may be suppressed by adding code as in the example given before each signal declaration or in the Project Settings->Gdscript->Warnings area and unchecking the Unused Script box.
Another way is to add signal emitter functions to the Event Bus script file and call these scripts from our scripts that would otherwise connect to the signals.
For medium/large projects it could become difficult to debug our signals because there may be large numbers of nodes connecting globally to the Event Bus and it is difficult to trace which node triggered a signal emission without passing reference data.
So use the Event Bus with caution and always consider the merits of encapsulating areas of common functionality and minimising the complexity of your signal wiring system.
The Observer Pattern
The Godot signalling system is actually an implementation of the Observer Pattern from Computer Science. Events that happen within the system emit signals that are subscribed to by interested observers which run handler code in response to the events. And there is loose coupling between the emitter and observer so that they are able to function independently.