Signals are a very important part of Godot and GDScript. Signals allow nodes in Godot to communicate with one another. Signals are very important for witting flexible, decoupled and maintainable code.
In this article we’ll deal with scripts using only code. Personally I think it’s best to keep the signal management inside your scripts. You can of course connect signals using the editor GUI if you wish.
If you don’t know how to use signals, don’t worry! I’ll teach you the most important things to know about proper signals usage! I’ll start with simple concepts and build slowly upon that!
You need to be at least familiar with GDScript before reading this article!
Simple GDScript Signal Emitting
Let’s dive straight in with a simple example.
# player.gd
signal zero_life
var myhealth=100
func _process(delta):
if myhealth<=0:
emit_signal("zero_life")
We have a script called player.gd
. We start by defining a signal called zero_life
. During process we keep on checking for the value of myhealth
(not the best way to do things but works as an example).
If myhealth
smaller than zero emit the signal “zero_life”
Some things to keep in mind:
- We need to define the signal somewhere in the script before emitting it. At the top of the script is a good place.
- We emit the signal using…
emit_signal
. Obvious, right? - There’s nothing fancy about a signal. It doesn’t have any intrinsic properties (yet) aside from it’s name.
One thing that kinda sucks is that we’ll be emitting that signal ad nauseam (until we’ll get sick) in the _process
function.
Not a good idea usually – this type of signal we want to emit only once (but other types might be emitted at higher rates)
# player.gd
signal zero_life
var signal_emitted=false
var myhealth=100
func _process(delta):
if not signal_emitted and myhealth<=0:
emit_signal("zero_life")
signal_emitted=true
This is like the first example with some minor differences. First we define a variable to check if we have emitted the signal.
In the _process()
we start by checking if our variable (signal_emitted
) is false. If it is false and myhealth
smaller than zero, proceed.
We emit the signal, same as before. But we make signal_emitted
true so that the next update this block of code won’t be called.
(we could’ve written if myhealth<=0 and not signal_emitted
too. But this way is just a bit more effficient since we’ll only do one check, not two. If not_signal_emmited
– which will probably be the case after myhealth
is less than zero, only then check for myhealth
. But if we write it the other way around we would keep checking for both myhealth
and signal_emitted
)
How Are GDScript Signals Useful
Alright, so we’re emitting a signal in the player.gd
. What’s the big deal? Who cares, huh?
Well, other nodes/scripts might care.
Let’s say we have a game_manager.gd
script that monitors the game’s state. If the player life is below zero trigger certain events (like show some UI elements, load another scene, etc). So we need to know when that particular event happens.
How to do that?
We connect game_manager.gd
to player.gd
life_zero
signal. When player.gd
emits life_zero
signal, call a specific function in game_manager.gd
.
Now you’re probably just shaking your head. “Why bother with signal and stuff when we can simply call game_manager.gd
function directly from player.gd
?
Heck, we save ourselves the trouble and just write:
# player.gd
var func_called=false
var myhealth=100
func _process(delta):
if not func_called and myhealth<=0:
get_node("game_manager").on_player_zero_life()
func_called=true
Or do we??
Code Without GDScript Signals Is Bad
At first glance maybe. But consider the following things:
Tight coupling – BAD!
Now player.gd
is tightly coupled with game_manager.gd
. Delete game_manager.gd
and player.gd
will throw an error complaining it can’t find the node. Move game_manager.gd
and you’ll have to update it’s location in the player.gd
code too. The life and fate of player.gd
is tightly coupled to game_manager.gd
And granted, it’s not a big deal if only game_manager.gd
is coupled with player.gd
. But what if you have 3 or 5 or 10 other scripts which need to be notified? The trouble just gets 3x, 5x, 10x worse.
Very low re-usability – BAD!
Let’s say that somehow you can live with the tight coupling between player.gd
and all the other dependent scripts.
You use player.gd
in a scene where all those dependent nodes exist (at the exact paths defined in player.gd
). Very well. But now you need to re-use player.gd
in another scene where some of those nodes don’t exist. BUMMER!
You could jump through hoops and add conditionals and error checking. It’ll probably work in a way – but your code will become complex, more unreadable and hard to maintain.
According to Godot official docs recommended best practices scenes should have no dependencies.
That is – ideally you can drop a scene into any other scene and it will work. Sometimes that will more challenging but you should always strive towards it.
Simple Connect To Emitted GDScript Signal
So what’s a better way? Well, signals, of course. Let’s see how to actually connect to signals
# game_manager.gd
func on_player_zero_life():
print("Starting game over sequence")
func _ready():
var player_node=get_node("player")
player_node.connect("zero_life", self, "on_player_zero_life")
Let’s dissect the code:
We define a function to be called when signal is emitted on_player_zero_life()
.
In _ready()
we do the actual connecting. First we get the player
node. This is important. The signal belongs to the player
node. To connect to that signal we need a reference to the player
node. Once we get a reference to the player.gd
we call the connect
function (belonging to player.gd
)
Take a look at the parameters of the function connect.
The first parameter is the signal we’re connecting too. Easy.
Third parameter (on_player_zero_life
) is the function name to be called when signal is emitted.
Second parameter is the object to whom the function (on_player_zero_life
) belongs. self
means on the callee script itself.
Use Middleman To Connect GDScript Signal
Let`s see an example where that second parameter might be different:
#game_manager.gd
func _ready():
var player_node=get_node("player")
var some_ui_node=get_node("../some_ui_node")
player_node.connect("zero_life", some_ui_node, "on_player_zero_life")
We start as usual and get the player_node
. Then we get another node called some_ui_node
. game_manager
is the middle man, connecting some_ui_node
to players node signal. Note how game_manager
doesn’t have anymore on_player_zero_life
function.
That’s because this function should exist in some_ui_node.gd
script
#some_ui_node.gd
func on_player_zero_life():
print("Updating an UI element")
When Using A Middleman To Connect Signals Is A Good Idea
This setup could work well in certain situations. What situations? Here’s an example:
- you have multiple (different)
player
scenes, each one with different functionality. The one thing uniting those scenes is that they all emit a signal callezero_life
. (for exampleplayer_red
,player_blue
,player_npc
– all inheriting code fromplayer
or implementing common functionality) - you have a single
some_ui_node
in your game. But you need to connect it to a certainplayer
script depending on the game scene. In one scene you need to connect it toplayer_red
. In another toplayer_npc
. What to do? Using conditionals in the function that does the connect is ugly. It would look something like this:
#some_ui_node_name.gd
func _ready():
var player_red=get_node("player_red")
var player_npc=get_node("player_npc")
if player_npc and not player_red:
player_npc.connect("zero_life",self,"on_player_zero_life")
elif player_red and not player_npc:
player_red.connect("zero_life",self,"on_player_zero_life")
Ugly, right? And as the numbers of nodes to check increases, so does complexity!
Instead if you use a middle man (game_manager
) you don’t need to worry about that in some_ui_node
. It’s the game_manager
job to connect some_ui_node
to the correct player
node.
#game_manager.gd
func _ready():
var some_ui_node=get_node("../some_ui_node")
if check_scene("player_npc"):
player_npc.connect("zero_life", some_ui_node, "on_player_zero_life")
elif check_scene("player_red"):
player_red.connect("zero_life", some_ui_node, "on_player_zero_life")
Using A Middleman Might Lead To Clean Code
At this point you might scoff. It seems that we just moved functionality from some_ui_node
to game_manager
. We’re just using check_scene()
function inside game_manager
and if player_npc and not player_red:
conditional inside some_ui_node
.
And what’s up with that? Why are the functionalities different?
They’re different because game_manager
will probably have access to more data and children nodes then some_ui_node
.
After all children nodes should know as little as possible about their parents/sibling nodes. It makes for decoupled, maintainable and flexible code. One manager object knowing about 100 children nodes is acceptable. 100 children nodes knowing about each other it’s nightmare on Code Street.
But that’s not all! There’s another golden rule in programming called DRY! Don’t Repeat Yourself!
Suppose you have another UI node that needs to be connected to the zero_life
signal. If you’re checking inside the UI nodes, you’ll have to do the checking twice! Once or each node! Probably the exact same piece of code.
If you need to connect multiple UI nodes the repeated codes multiplies across each UI node. I’m shivering just thinking at maintaining such a mess.
If you’re using a manager on the other hand you’re doing the checking once. Then you connect all the needed UI nodes.
#game_manager.gd
func _ready():
var some_ui_node=get_node("../some_ui_node")
var another_ui_node=get_node("../another_ui_node")
if check_scene("player_npc"): #checking once
player_npc.connect("zero_life", some_ui_node, "on_player_zero_life")
player_npc.connect("zero_life", another_ui_node, "on_player_zero_life")
elif check_scene("player_red"): # checking once
player_red.connect("zero_life", some_ui_node, "on_player_zero_life")
player_red.connect("zero_life", another_ui_node, "on_player_zero_life")
Now you know quite a lot about properly connecting signals to nodes! Bravo! But wait…there’s more. Good stuff too!
Pass Parameters To GDScript Signals – Emitting
What if you need to pass an argument alongside your emitted signal? Let’s say you want to pass the enemy_node
that killed the player? Might be useful to the connected nodes (that receive the zero_life
signal)
# player.gd
signal zero_life(killer_enemy)
var signal_emitted=false
var myhealth=100
func _process(delta):
if not signal_emitted and myhealth<=0:
var killer_enemy=get_killer_enemy_node()
emit_signal("zero_life",killer_enemy)
signal_emitted=true
We’re back at editing player.gd
. It’s same old same old with some differences. Start by noting the declaration of the signal. It looks like a function call and inside it we use a parameter called killer_enemy
.
This means that when we emit the signal we’ll also pass an argument. The argument is killer_enemy
node. The functions that connect to this signal need to be declared accordingly
Pass Parameters To GDScript Signals – Connecting
Let’s suppose some_ui_node
will connect directly to the player_node
(no need for a manager object to simplify the example)
#some_ui_node_name.gd
func _ready():
var player=get_node("player")
player.connect("zero_life",self,"on_zero_player_life")
func on_zero_player_life(killer_enemy):
print("I was killed by %s" % killer_enemy)
The function connected to the emitted signal needs to have a killer_enemy
parameter defined. Omitting the parameter from the function definition might result in an error. You can also get an error if you call emit_signal("zero_life")
but don’t use the argument killer_enemy
.
Remember when I told you that checking for myhealth
inside process it’s bad practice? Let me show you a better way.
A Better Way To Emit GDScript Signal When Variable Has Value X
Here’s player.gd
# player.gd
signal zero_life(killer_enemy)
var myhealth=100 setget set_myhealth
func set_myhealth(val):
myhealth=val
if myhealth<=0:
var killer_enemy=get_killer_enemy()
emit_signal("zero_life",killer_enemy)
Now we don’t need to bother to check within _process()
function. We’ll just use a setget
. What’s a setget
? It’s way of using setters and getters. Setters are functions that get called when a value is set. Getters are functions that get called when a value is queried (read from somewhere else in the code)
Here we have a simple setter function called set_myhealth
. When someone sets this variable (like player.myhealth=43
) this function will get called.
We start by actually doing the most basic of jobs – updating the myhealth
value to the passed value. I mean, we don’t have to do this. We can just ignore the passed value. Or we can modify it in a way (like myhealth=val*100
). Etc.
Next we check if myhealth
is less or equal than 0. If it is, get killer_enemy
node and emit zero_life
signal.
Easy peasy!
Of course, this could still be called multiple times if enemies aren’t aware the player health is zero. Such additional checking can be implemented on the enemies or the player itself. That is not relevant for the current subject.
Advanced GDScript Signal Usage – Bind Array
So far you saw how to add a parameter to an emitted signal. That parameter originates with the node doing the actual emitting.
You also saw the middle man connecting emitted signals with other nodes. Now suppose you want to pass some extra parameters. But this time these parameters will originate with the middleman.
Why would you want that? Well, maybe because the node emitting the signal doesn’t have all the information needed. Going back to our example we pass the value of the enemy that killed the player. Cool.
The some_ui_node
needs that information – maybe it will print the name of the killer enemy. But it may need another piece of information, such as the status of the game save. Was the game saved?
If the game was saved then display the name of the killer enemy. If the game wasn’t saved then show a message prompting the player to save.
Who’s going to send that info to some_ui_node
? The player
node could but it’s not a good idea. Each node should know as little as possible about its environment. The player should have access to data about enemy that killed it. But the status of the game save? No way Jose, it doesn’t need access to that data.
game_manager
on the other hand probably has access to that data. It would be cool to pass this extra info to some_ui_node
#game_manager.gd
var game_saved=true
func _ready():
var player=get_node("player")
var some_ui_node=get_node("some_ui_node")
player.connect("zero_life",some_ui_node,"on_zero_player_life",[game_saved,42])
In the code above we are doing just that. Note the fourth argument to connect()
function – an array. This is called a bind array. It’s a way of saying that we’re passing some extra parameters, nicely packed in an array.
We’re passing two extra parameters – game_saved
status and an constant number (just to show you that it is possible).
Meanwhile, this is how some_ui_node
looks like:
#some_ui_node.gd
func on_zero_player_life(killer_enemy,game_saved,secret_of_life):
print("I was killed by %s" % killer_enemy)
print("game saved status %s" % game_saved)
print("The secret of life is %s" % secret_of_life)
We declare on_zero_player_life
function as usual. The first parameter remains the killer_enemy
– it will passed from the node emitting the signal. The second and third parameters are those from the bind array, from the middleman node.
It’s just some extra functionality that might come in handy sometime!
Advanced GDScript Signal Usage – Fun with Flags!
This is the fifth and final argument you can pass to the connect()
function. Flags!
Flags are a way to signal some special things about how signal behave.
According to Godot docs you can pass these flags:
● CONNECT_DEFERRED = 1
Connects a signal in deferred mode. This way, signal emissions are stored in a queue, then set on idle time.
● CONNECT_PERSIST = 2
Persisting connections are saved when the object is serialized to file.
● CONNECT_ONESHOT = 4
One-shot connections disconnect themselves after emission.
● CONNECT_REFERENCE_COUNTED = 8
Connect a signal as reference-counted. This means that a given signal can be connected several times to the same target, and will only be fully disconnected once no references are left.
You can pass multiple flags using |
operator. Like so: connect("my_signal",self,"my_func",[],CONNECT_ONESHOT | CONNECT_PERSIST)
Two flags seem to have immediate utility: CONNECT_PERSIST and CONNECT_ONESHOT.
You should use CONNECT_PERSIST if you want your connections to be saved when serializing your objects.
The other interesting flag is CONNECT_ONESHOT. After the signal is emitted the connection is broken right away. Here’s an example:
You have an explosion particle you want to show when the player dies. It wouldn’t really make sense to show it multiple times.
#game_manager.gd
func _ready():
var player=get_node("player")
var explosion_effect=get_node("explosion_effect")
player.connect("zero_life",explosion_effect,"on_zero_player_life",[],CONNECT_ONESHOT)
Note how we don’t pass a bind array anymore. We do in a way, but it’s empty. The explosion_effect
node doesn’t need to know about those variables.
After the signal is emitted the connection is broken. Even if an enemy hits the player again while he’s dead (thus possibly triggering on_zero_player_life
signal again) the explosion won’t know about it.
Of course, you’ll need to reconnect explosion_effect
node to zero_life
signal on game restart. That is when the player is revived or a new game starts.
GDSCript Signals – Conclusion
Wow, what a journey! You now know quite a lot about signals and their usage. You saw both simple and advanced use cases for their usage.
You can connect to emitted signals directly from the node doing the connecting. Or you can use a middleman node to connect signals to other nodes.
You can pass parameters from an emitted signal. You can also pass extra parameters from the node doing the connecting. Those extra parameters are packaged into a bind array.
You wen’t even deeper into the rabbit hole with flags! You use flags to have special signal functionality, such as disconnecting the connected node after signal was emitted once.
By now you should be ready to use signals in all kinds of situations. As you see you can build some pretty advanced game functionality with them.
Signals will keep your code clean, flexible and easy to read and maintain. Use them while developing games! Take advantage of signal’s power!
This tutorial is FIRE. I did not understand much the official Godot tutorial about signals. but now everything is clear.