Think of a signal like a list of functions that should all be called when something happens. For example: buttons have a pressed
signal. You can connect that signal to any function, and that function will be called when the button is pressed. In that case, the button is in charge of “emitting” the pressed
signal. If you write your own signal, you’re in charge of emitting it. In other words, you control when it goes down its list of functions and calls them all one at a time. Which functions are on that list? You’re in charge of that too. You control it by “connecting” that signal to functions.
Let’s say your mob needs to tell your main script when the mob dies.
You might write this code in your mob script:
signal on_death()
# A bunch of code for your mob
func die():
on_death.emit()
When you call mob.die()
, it will “emit” the on_death
signal, going down its list of functions (in other words, its list of connections) and executing them all.
But it’s not connected to anything yet.
You can connect signals in the editor, or in script. To do it in script, you might write in main.gd:
func _ready():
mob.on_death.connect(add_one_to_score)
# a bunch of code for main.gd
func add_one_to_score():
# This is called automatically when mob emits the on_death signal
score += 1
The parentheses when creating a signal can be confusing at first, since it looks like a function, but its not, it’s something else.
You can, however pass information between signals. For example, if you want some mobs to add different amounts to your score, you might change mob.gd to look like this:
signal on_death(score_value)
# A bunch of code for your mob
func die():
var score_value = randi() % 3 + 1
on_death.emit(score_value)
Now, when the mob dies, it picks a random number between one and three to be added to the player’s score.this value is “emitted” with its on_death
signal, and is therefore passed as a parameter to any function this signal is connected to. Remember that mob.on_death is connected to the function add_one_to_score
in main.gd. This function does not take any parameters. When on_death
is emitted with a parameter, it will try to call add_one_to_score
with that parameter, and this will cause an error. We can fix this by changing main.gd to look like this:
func _ready():
mob.on_death.connect(add_to_score)
# a bunch of code for main.gd
func add_to_score(score_val):
# This is called automatically when mob emits the on_death signal
score += score_val
Now, the signal passes one parameter, and the function accepts one parameter, so they are compatible.
It is also possible to have main.gd pass additional parameters to a function, like so:
func _ready():
mob.on_death.connect(add_to_score.bind(mob))
# a bunch of code for main.gd
func add_to_score(score_val, mob):
# This is called automatically when mob emits the on_death signal
score += score_val
mob.queue_free()
In this example, when mob emits the on_death
signal, before the function add_to_score
is called, the mob itself is tacked on to the end as another parameter. This way, you can have main.gd control some extra information about each connection. In the code above, the mob decides it should die, so it calls the die
function. It chooses a random number, let’s say it picks 2. It emits the on_death
signal with the value 2. However, since the signal for this particular mob was connected (in main.gd) with an extra binding, the function add_to_score
is called like this: add_to_score(2, mob)
, which allows the add_to_score
function to remove the mob from the scene with the queue_free
function.
Here’s another example.
You might have a script which is in charge of creating and destroying all mobs in the scene. This script might be called MobManager.gd. in this script, you might have some code like this:
const mob_scene = preload("path/to/mob.tscn")
var score = 0
func _ready():
# create 5 mobs
for x in range(5):
# A new mob is created each loop, and so new_mob
# holds something different each loop
var new_mob = mob_scene.instantiate()
add_child(new_mob)
# Connect this loop's mob, and bind this loop's
# mob in that connection
new_mob.on_death.connect(kill_mob.bind(new_mob))
func kill_mob(score_value, mob):
score += score_value
mob.queue_free()
Here, the script creates 5 mobs, and the kill_mob
function will be called when any one of them emits the on_death
signal. However, mobmanager.gd still knows which mob to kill, because when its connected, each mob binds itself to the kill_mob
function individually. (Each connection is unique) Therefore when the signal is emitted, the kill_mob
function will be called with the same mob which emitted that signal. The same is true for all 5 mobs here.
This system can be really useful for structuring code, and getting things done in a clean way. I recommend the call-down, signal-up architecture, where parent nodes can communicate down the tree by calling functions on their children directly, like child.function()
, whereas children nodes communicate up the tree via signals.
Under this system, parent nodes have knowledge of their children, but children nodes don’t need any knowledge of their parents, meaning they can be used in more than one place and still work.
Hopefully this helps!