Game Development with Godot & GDScript Module 4 – The scene tree, nodes and signals
Signals and connections
Declare, emit and connect Godot 4.7 signals in code and in the editor, use bind, one-shot and deferred connections, disconnect safely and signal up.
What you will learn
- Connect built-in and custom signals in code and in the editor, and read the connection a scene saves
- Declare signals with typed parameters and emit them with matching values
- Use bind, one-shot and deferred connections, and disconnect a connection safely
- Design scenes that signal up and call down instead of reaching across the tree
Before you start
What you’ll learn · Before you start
What you will learn
- Connect built-in and custom signals in code and in the editor, and read the connection a scene saves
- Declare signals with typed parameters and emit them with matching values
- Use bind, one-shot and deferred connections, and disconnect a connection safely
- Design scenes that signal up and call down instead of reaching across the tree
Before you start
On this page
A wall switch with a door and a lamp connected to it made 2 calls every time it was flipped, and both ran before the
switch’s own next line. That is all a signal is: a list of functions that one emit() calls, at once and in order,
without the switch knowing who is on the list.
This page connects signals in the editor and in code, declares your own with typed parameters, and uses the options
that change when and how often a function runs: bind(), one-shot and deferred connections. You will also see how a
connection ends, why a lambda connected in the wrong place runs three times, and how a puzzle of three plates and a
door stays reusable by sending signals up and calls down.
Emitting a signal calls every connected function, in order
switch.gd declares a signal: A named event that an object emits, such as a button's pressed or a custom health_changed. Every function connected to it is called each time it is emitted, so the sender needs no reference to its listeners., toggled, and emits it when the switch is flipped. Two nodes
listen. The door was connected in the editor: select the Switch, open the Signals tab beside the Inspector,
double-click toggled and pick the Door; Godot names the new function _on_switch_toggled. The lamp is connected in
code by room.gd, the scene’s root, with $Switch.toggled.connect($Lamp.turn):
extends Node
# A wall switch. It says when it is flipped, and it knows nothing about who listens.
signal toggled(on: bool)
var on := false
func flip() -> void:
on = not on
print("Switch: flipped ", "on" if on else "off", ", emitting toggled(", on, ")")
toggled.emit(on)
print("Switch: emit() has returned") extends Node
# Connected to the switch in the editor: the Signals dock wrote the [connection] line at the end of room.tscn.
func _on_switch_toggled(on: bool) -> void:
print(" Door ", "opens" if on else "closes") extends Node
# Connected to the switch in code, by the room.
func turn(on: bool) -> void:
print(" Lamp ", "lights up" if on else "goes dark") extends Node
# The room connects its lamp to the switch in code, lists the connections, then flips the switch twice.
func _ready() -> void:
$Switch.toggled.connect($Lamp.turn)
for connection in $Switch.toggled.get_connections():
print("toggled is connected to ", connection.callable)
$Switch.flip()
$Switch.flip()
get_tree().quit() from session import files, run
files("project.godot", "room.tscn", "room.gd", "switch.gd", "door.gd", "lamp.gd")
run("""
grep connection room.tscn
godot --headless --no-header room.tscn
""") Output
$ grep connection room.tscn [connection signal="toggled" from="Switch" to="Door" method="_on_switch_toggled"] $ godot --headless --no-header room.tscn toggled is connected to Node(door.gd)::_on_switch_toggled toggled is connected to Node(lamp.gd)::turn Switch: flipped on, emitting toggled(true) Door opens Lamp lights up Switch: emit() has returned Switch: flipped off, emitting toggled(false) Door closes Lamp goes dark Switch: emit() has returned
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 room_session.py
The editor’s connection is saved in the scene: the last line of room.tscn names the signal, both nodes and the
method, and Godot makes the connection again every time the scene is instanced. The code connection exists only while
the game runs. Step through the first flip:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Two connections. Before anything is flipped, toggled has two connections: the Door's _on_switch_toggled, from the [connection] line of room.tscn, and the Lamp's turn, which room.gd connected in _ready().
- flip() emits. The room calls $Switch.flip(). The switch turns on and calls toggled.emit(true). It does not know who listens; it only announces what happened.
- First the door. emit() calls the first connection at once, with the value it was given. The door prints "Door opens" before anything else happens.
- Then the lamp. When the door's function returns, emit() calls the next connection, the lamp's turn(true), which prints "Lamp lights up".
- emit() returns. Only now does emit() return to flip(), which prints "emit() has returned". Every handler ran inside that one emit() call, in the order of the connections.
emit(true) called the door’s function, then the lamp’s, and returned only after both had finished, which is why
emit() has returned came last. A plain connection runs inside the emit() call, in the order the connections were
made: the scene made the door’s connection when it was instanced, before room.gd’s _ready() made the lamp’s.
get_connections() lists them in that order.
Checkpoint
Check: What emit() does with its connections
A custom signal declares what it sends
A signal is declared at the top of a script with the keyword signal, a name and, optionally, typed parameters:
signal health_changed(old_value: int, new_value: int). The style guide asks for names in the past tense, such as
door_opened or score_changed, because a signal reports something that has happened. health.gd emits its
signal once correctly and twice wrongly, then connects by a misspelt name; misspelt.gd makes the same misspelling a
different way:
extends Node
# A signal with typed parameters, emitted the right way and two wrong ways, then connected by a misspelt name.
signal health_changed(old_value: int, new_value: int)
func _ready() -> void:
health_changed.connect(_on_health_changed)
print("-- emit(100, 75)")
health_changed.emit(100, 75)
print("-- emit(75): one value")
health_changed.emit(75)
print("-- emit(75, \"fifty\"): a String")
health_changed.emit(75, "fifty")
print("-- connect(\"health_chnaged\", …): the name as a String, misspelt")
connect("health_chnaged", _on_health_changed)
print("-- the end of _ready()")
get_tree().quit()
func _on_health_changed(old_value: int, new_value: int) -> void:
print(" health ", old_value, " -> ", new_value) extends Node
# The same misspelling through the signal itself, which the parser can check.
signal health_changed(old_value: int, new_value: int)
func _ready() -> void:
health_chnaged.connect(_on_health_changed)
func _on_health_changed(old_value: int, new_value: int) -> void:
print(old_value, " -> ", new_value) from session import files, run
files("project.godot", "health.tscn", "health.gd", "misspelt.gd")
# Both commands report errors on purpose: the first while it runs, the second before anything runs.
run("""
!godot --headless --no-header health.tscn
!godot --headless --no-header --check-only --script misspelt.gd
""") Output
$ godot --headless --no-header health.tscn
-- emit(100, 75)
health 100 -> 75
-- emit(75): one value
ERROR: Error calling from signal 'health_changed' to callable: 'Node(health.gd)::_on_health_changed': Method expected 2 argument(s), but called with 1.
at: emit_signalp (core/object/object.cpp:1311)
GDScript backtrace (most recent call first):
[0] _ready (res://health.gd:12)
-- emit(75, "fifty"): a String
ERROR: Error calling from signal 'health_changed' to callable: 'Node(health.gd)::_on_health_changed': Cannot convert argument 2 from String to int.
at: emit_signalp (core/object/object.cpp:1311)
GDScript backtrace (most recent call first):
[0] _ready (res://health.gd:14)
-- connect("health_chnaged", …): the name as a String, misspelt
ERROR: In Object of type 'Node': Attempt to connect nonexistent signal 'health_chnaged' to callable 'Node(health.gd)::_on_health_changed'.
at: connect (core/object/object.cpp:1550)
GDScript backtrace (most recent call first):
[0] _ready (res://health.gd:16)
-- the end of _ready()
$ godot --headless --no-header --check-only --script misspelt.gd
SCRIPT ERROR: Parse Error: Identifier "health_chnaged" not declared in the current scope.
at: GDScript::reload (res://misspelt.gd:8)
ERROR: Failed to load script "res://misspelt.gd" with error "Parse error".
at: load (modules/gdscript/gdscript_resource_format.cpp:46)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 health_session.py
The typed parameters did not stop the wrong emits from running. emit(75) and emit(75, "fifty") each printed an
error for the connected function, Method expected 2 argument(s) and Cannot convert argument 2 from String to int,
and _ready() went on. The types document the signal, and the editor uses them to write handlers with the right
parameters, but every emit() must still pass matching values. The names are another matter.
connect("health_chnaged", …), with the name as a string, failed only when that line ran;
health_chnaged.connect(…) did not even parse. That is why Godot’s guide to upgrading from Godot 3 recommends the
Signal-based syntax. The way signals are written and shown changed several times in Godot 4:
- 4.0
Signals become values you can call methods on new syntax
The upgrade guide recommends the Signal-based syntax, toggled.connect(callable) and toggled.emit(…), over the string-based connect("toggled", …) that converted projects keep. A misspelt name then fails before the game runs.
- 4.5
The editor can pass the source along
The connect dialog gained an Append Source option that passes the emitting node right after the signal's own arguments, so one handler can serve many senders. In code the same option is the flag CONNECT_APPEND_SOURCE_OBJECT.
- 4.6
Signals that start with an underscore are hidden
A signal named like _internal_changed no longer appears in autocompletion or in generated documentation, so a leading underscore now marks a signal as internal to its class.
- 4.7
Tweens can wait for a signal
Tween.tween_await() pauses a tween until a signal is emitted, which helps with dialogue and cutscenes. The next lesson waits for signals in your own functions.
The timeline has 4 events, from 4.0 to 4.7.
Checkpoint
Check: Declaring signals and emitting values
Options decide when, how often and with what a handler runs
A connection can carry options. options.gd connects four functions to one signal, stepped_on(weight: int), each a
different way, and emits it in frames 1 and 2. In frame 3 it tries to connect a method that is already connected:
extends Node
# One signal and four connections, each made with a different option. Frame numbers show when the deferred one runs.
signal stepped_on(weight: int)
var frame := 0
func _ready() -> void:
stepped_on.connect(_on_stepped.bind("north"))
stepped_on.connect(_on_first_step, CONNECT_ONE_SHOT)
stepped_on.connect(_on_later, CONNECT_DEFERRED)
stepped_on.connect(_on_with_source, CONNECT_APPEND_SOURCE_OBJECT)
print("connections: ", stepped_on.get_connections().size())
func _process(_delta: float) -> void:
frame += 1
if frame <= 2:
print("frame ", frame, ": emit(", frame * 10, ")")
stepped_on.emit(frame * 10)
print("frame ", frame, ": emit() returned, connections: ", stepped_on.get_connections().size())
else:
print("frame 3: the same method again, bound to another value")
print(" connect() returned ", stepped_on.connect(_on_stepped.bind("south")))
print(" _on_stepped.bind(\"north\") == _on_stepped.bind(\"south\"): ",
_on_stepped.bind("north") == _on_stepped.bind("south"))
get_tree().quit()
func _on_stepped(weight: int, plate: String) -> void:
print(" bound: weight ", weight, ", plate ", plate)
func _on_first_step(weight: int) -> void:
print(" one-shot: weight ", weight)
func _on_later(weight: int) -> void:
print(" deferred: weight ", weight, ", still in frame ", frame)
func _on_with_source(weight: int, source: Node) -> void:
print(" source appended: weight ", weight, ", from ", source.name) from session import files, run
files("project.godot", "options.tscn", "options.gd")
# Frame 3 connects a method that is already connected, so the run reports an error on purpose.
run("""
!godot --headless --no-header --fixed-fps 60 options.tscn
""") Output
$ godot --headless --no-header --fixed-fps 60 options.tscn
connections: 4
frame 1: emit(10)
bound: weight 10, plate north
one-shot: weight 10
source appended: weight 10, from Plate
frame 1: emit() returned, connections: 3
deferred: weight 10, still in frame 1
frame 2: emit(20)
bound: weight 20, plate north
source appended: weight 20, from Plate
frame 2: emit() returned, connections: 3
deferred: weight 20, still in frame 2
frame 3: the same method again, bound to another value
ERROR: Signal 'stepped_on' is already connected to given callable 'Node(options.gd)::_on_stepped' in that object.
at: connect (core/object/object.cpp:1562)
GDScript backtrace (most recent call first):
[0] _process (res://options.gd:25)
connect() returned 31
_on_stepped.bind("north") == _on_stepped.bind("south"): true
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 options_session.py
| Written as | What changes | In the run |
|---|---|---|
connect(f.bind("north")) |
f gets the bound values after the emitted ones |
weight 10, plate north |
connect(f, CONNECT_ONE_SHOT) |
the connection goes after its first emission | ran in frame 1 only; 4 connections became 3 |
connect(f, CONNECT_DEFERRED) |
f runs at idle time, at the end of the frame |
ran after emit() returned, still in frame 1 |
connect(f, CONNECT_APPEND_SOURCE_OBJECT) |
the emitting object comes after the emitted values | from Plate |
connect(f, CONNECT_REFERENCE_COUNTED) |
the same f may be connected again; each connect needs its own disconnect |
not in this run |
Flags combine with |, as in CONNECT_DEFERRED | CONNECT_ONE_SHOT. A deferred call: A call made with call_deferred() or a CONNECT_DEFERRED connection. It runs at idle time, after the frame's _process() calls and before its timers and queued deletions, instead of at once. More in “await, coroutines and timers”
function got the value of its own emit, 10 in frame 1 and 20 in frame 2, so deferring changes when it runs, not what
it receives. Defer a handler that must not run while the emitter is in the middle of something, such as one that frees
the emitter.
Most connections need none of these options. Reach for bind() when one function listens to several senders and must
know which one spoke, for a one-shot connection when an event matters only the first time, such as a hint on the first
pickup, and for the source flag when you would otherwise bind every sender to itself.
One method, two bound values, one signal
stepped_on.connect(_on_stepped.bind("south")) failed with already connected, because a bound Callable counts as
the same as the method bound to any other values: _on_stepped.bind("north") == _on_stepped.bind("south") printed
true. To send different values through one signal to one method, pass them with emit(); to listen to several
senders with one method, bind on each sender’s signal, as the practice below does.
Checkpoint
Check: bind, one-shot, deferred and other options
A connection lasts until it is disconnected or a side is freed
A connection has no end of its own. It goes when code calls disconnect() with the same
Callable: A function as a value. A method's name without brackets, such as total, gives one; call() runs it with arguments and callv() with an array of them. More in “Functions, scope and variadic arguments”, when a one-shot connection has run, or when either object is freed. hall.tscn
shows the last case and a common bug next to it: its guard connects a A function written inside an expression, such as func() -> void: print('hi'). It can be kept in a variable or passed to connect(), and each time its func line runs it makes a new Callable.
to a bell in _enter_tree() and never disconnects it.
extends Node
# The mistake: a new lambda is connected each time the guard enters the tree, and nothing disconnects it.
@export var bell: Node
func _enter_tree() -> void:
bell.rang.connect(func() -> void: print(" ", name, " heard the bell")) extends Node
# Rings the bell around the guard's comings and goings, then shows a lambda that can be disconnected.
func _ready() -> void:
var bell: Node = $Bell
var guard: Node = $Guard
print("connections: ", bell.rang.get_connections().size())
print("-- ring")
bell.rang.emit()
for i in 2:
remove_child(guard)
add_child(guard)
print("-- the guard left and came back twice; connections: ", bell.rang.get_connections().size())
print("-- ring")
bell.rang.emit()
guard.free()
print("-- the guard was freed; connections: ", bell.rang.get_connections().size())
print("-- ring")
bell.rang.emit()
print("-- a lambda kept in a variable")
var note := func() -> void: print(" the hall heard the bell")
bell.rang.connect(note)
bell.rang.disconnect(note)
print("still connected after disconnect(note): ", bell.rang.is_connected(note))
get_tree().quit() from session import files, run
files("project.godot", "hall.tscn", "hall.gd", "bell.gd", "guard.gd")
run("""
godot --headless --no-header hall.tscn
""") Output
$ godot --headless --no-header hall.tscn connections: 1 -- ring Guard heard the bell -- the guard left and came back twice; connections: 3 -- ring Guard heard the bell Guard heard the bell Guard heard the bell -- the guard was freed; connections: 0 -- ring -- a lambda kept in a variable still connected after disconnect(note): false
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 hall_session.py
The guard entered the tree three times, with the scene and twice more, and each entry connected a new lambda. A lambda
is a new Callable every time its func line runs, so Godot saw three different connections and refused none: the next
ring printed Guard heard the bell three times. When the guard was freed, all three went with it, and the last ring
printed nothing at all, without an error. The last lines show the way to disconnect a lambda: keep it in a variable
and pass that same variable to disconnect().
| Connected in | Disconnect in | Or |
|---|---|---|
_ready(), once |
nothing: the connection goes when either side is freed | the usual choice |
_enter_tree(), on every entry |
_exit_tree(), with the same Callable |
for nodes that come and go |
| the editor’s Signals dock | nothing: the scene makes it once per instance | for connections inside one scene |
A lambda connected on every entry
bell.rang.connect(func() -> void: …) in _enter_tree() adds one more listener each time the node returns to the
tree. Connect in _ready(), which runs once, or connect a method instead: a second connect() of the same method
fails loudly with already connected rather than doubling the handler.
Checkpoint
Check: Disconnecting and freed listeners
Signals go up, calls go down
A pressure-plate puzzle has three plates and a door, all children of the puzzle node. A plate that knew about the door
would work only in this puzzle. A plate that only emits pressed and released works in any scene, and the puzzle,
which holds all four nodes, connects the plates and tells the door what to do. This is
call down, signal up: A rule of thumb for Godot scenes: a parent calls methods on its children, and a child reports events by emitting signals, so no child depends on the scene that uses it. More in “Finding nodes: paths, groups, unique names” at work: events travel up as signals, orders travel down as
method calls. Step through one round as the finished puzzle of the practice plays it:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The puzzle connects its plates. In its _ready(), the puzzle connects each plate's pressed and released signals to its own functions, binding the plate's name. The plates know nothing about the puzzle or the door.
- Plate1 goes down. Plate1 emits pressed. The puzzle's function receives "Plate1" from the bind and records it. One plate is not enough, so the door stays shut.
- Plate1 bounces. A plate that bounces emits pressed twice. The puzzle records names in a dictionary, so Plate1 is still one entry. A counter would now say two.
- Plate2 goes down. Plate2 emits pressed, and two different plates are down. The door is still closed, as the checker expects after a bounce and one more plate.
- Plate3, and the door opens. With Plate3 all three are down. The puzzle calls down to its child, door.open(); the door records "open".
The puzzle binds each plate’s name to its connections, so one function serves all three plates and still knows which
one was pressed. It records the names of the plates that are down, so a plate that bounces and emits pressed twice
counts once. The Scene organization page gives the same advice in its own terms: design scenes with no dependencies,
and let an ancestor connect its children when they must work together.
Key idea
A child that only emits signals depends on nothing around it; the node that holds the children connects their signals and calls their methods, so every part can be reused.
Checkpoint
Check: Signal up, call down
Practise: a door that opens on three plates
puzzle.gd must open the door while all three plates are down and close it as soon as one comes up, using the plates’
signals only, with no _process() that checks them every frame.
- Put
plate.gd,door.gd, the starterpuzzle.gdandcheck_puzzle.gdtogether in a folder that also holds an emptyproject.godot. - In
_ready(), connect thepressedandreleasedsignals of$Plate1,$Plate2and$Plate3, and write the two functions they call. - From a terminal in that folder, run
godot --headless --no-header --script check_puzzle.gd.
extends Node
# A pressure plate. It knows nothing about doors: it only says when it goes down and when it comes up.
signal pressed
signal released
func press() -> void:
pressed.emit()
func release() -> void:
released.emit() extends Node
# The door of the puzzle. It records what it was told to do, so the checker can read it.
var is_open := false
var events: Array[String] = []
func open() -> void:
if not is_open:
is_open = true
events.append("open")
func close() -> void:
if is_open:
is_open = false
events.append("close") extends Node
# Opens the door while all three plates are down, and closes it as soon as one comes up.
# Your task: connect the plates' signals in _ready() and keep track of which plates are down. Do not use _process().
@onready var door: Node = $Door
func _ready() -> void:
pass # Connect the pressed and released signals of $Plate1, $Plate2 and $Plate3 here. extends SceneTree
# Checks puzzle.gd. Run it in the project's folder: godot --headless --no-header --script check_puzzle.gd
# Each case builds a fresh puzzle, presses and releases plates in a given order, and compares the door's events.
const CASES := [
["all three, in order", ["press 1", "press 2", "press 3"], ["open"]],
["all three, another order", ["press 3", "press 1", "press 2"], ["open"]],
["one comes up, then goes down again", ["press 1", "press 2", "press 3", "release 2", "press 2"], ["open", "close", "open"]],
["only two plates", ["press 1", "press 2"], []],
["plate 1 bounces: pressed twice", ["press 1", "press 1", "press 2"], []],
["a bounce, then the third plate", ["press 1", "press 1", "press 2", "press 3"], ["open"]],
]
var passed := 0
var total := 0
func _initialize() -> void:
var source := FileAccess.get_file_as_string("res://puzzle.gd")
var polls := source.contains("func _process") or source.contains("func _physics_process")
check(not polls, "puzzle.gd does not poll the plates every frame")
for case in CASES:
root.add_child.call_deferred(build_puzzle(case))
func _process(_delta: float) -> bool:
if root.get_child_count() < CASES.size():
return false
for i in CASES.size():
var puzzle := root.get_child(i)
for step: String in CASES[i][1]:
var parts := step.split(" ")
var plate := puzzle.get_node("Plate" + parts[1])
if parts[0] == "press":
plate.press()
else:
plate.release()
var events: Array[String] = puzzle.get_node("Door").events
check(events == Array(CASES[i][2], TYPE_STRING, "", null), "%s: door %s" % [CASES[i][0], events])
print("%d of %d checks passed" % [passed, total])
quit(0 if passed == total else 1)
return true
func build_puzzle(case: Array) -> Node:
var puzzle: Node = load("res://puzzle.gd").new()
puzzle.name = "Puzzle"
for i in 3:
var plate: Node = load("res://plate.gd").new()
plate.name = "Plate%d" % (i + 1)
puzzle.add_child(plate)
var door: Node = load("res://door.gd").new()
door.name = "Door"
puzzle.add_child(door)
return puzzle
func check(ok: bool, what: String) -> void:
total += 1
passed += int(ok)
print("%s %s" % ["PASS" if ok else "FAIL", what]) The checker builds six fresh puzzles, presses and releases the plates of each in a different order, and compares what the door recorded. Its run on a finished puzzle and on one that counts presses:
from session import files, run
# The checker on a puzzle.gd that solves the task, then on one that counts presses.
files("practice/project.godot=right/project.godot", "practice/check_puzzle.gd=right/check_puzzle.gd",
"practice/plate.gd=right/plate.gd", "practice/door.gd=right/door.gd", "practice/solution/puzzle.gd=right/puzzle.gd",
"practice/project.godot=counting/project.godot", "practice/check_puzzle.gd=counting/check_puzzle.gd",
"practice/plate.gd=counting/plate.gd", "practice/door.gd=counting/door.gd",
"practice/tempting/puzzle.gd=counting/puzzle.gd")
run("""
cd right
godot --headless --no-header --script check_puzzle.gd
cd ../counting
!godot --headless --no-header --script check_puzzle.gd
""") Output
$ cd right $ godot --headless --no-header --script check_puzzle.gd PASS puzzle.gd does not poll the plates every frame PASS all three, in order: door ["open"] PASS all three, another order: door ["open"] PASS one comes up, then goes down again: door ["open", "close", "open"] PASS only two plates: door [] PASS plate 1 bounces: pressed twice: door [] PASS a bounce, then the third plate: door ["open"] 7 of 7 checks passed $ cd ../counting $ godot --headless --no-header --script check_puzzle.gd PASS puzzle.gd does not poll the plates every frame PASS all three, in order: door ["open"] PASS all three, another order: door ["open"] PASS one comes up, then goes down again: door ["open", "close", "open"] PASS only two plates: door [] FAIL plate 1 bounces: pressed twice: door ["open"] PASS a bounce, then the third plate: door ["open"] 6 of 7 checks passed [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_session.py
The counting puzzle passed every case but one: when Plate1 bounced, two plates down added up to three presses, and the
door opened. The finished one keeps a dictionary of plate names, so a repeated press changes nothing. Once you see
7 of 7 checks passed, the questions below revisit the five ideas of this page, weakest first, and every pick is
explained the moment you make it:
Practice that adapts to you
Practise this lesson
15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up: what is a signal in Godot, and why use one instead of calling a method directly?
A signal is a named event that an object emits, such as a button’s pressed or a custom
health_changed(old_value, new_value). Other objects connect functions to it, in the editor’s Signals dock or with signal.connect(callable), and
every emit() calls each connected function at once, in the order of the connections. The point is the direction of
knowledge: a method call needs a reference to the object that is called, while a signal lets the emitter announce what
happened without knowing who listens. A pressure plate that emits pressed works in any puzzle; one that calls
door.open() works only next to that door. The Godot docs call signals their version of the observer pattern. A common
rule of thumb follows from it: call methods on your children, and let children report to you with signals.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A ScoreBoard node connects to the player's coin_collected signal with a lambda in _enter_tree(). The pause menu takes the ScoreBoard out of the tree and puts it back. After two pauses, every coin adds 3 points instead of 1. Explain why, prove it, and fix it.
-
Fresher to mid
A level connects each coin's collected(value) signal to on_coin_collected(value, coin), which adds the value and calls coin.free(). The score goes up, but every pickup prints "Attempted to free a locked object (calling or emitting)" and the coins stay in the level. Explain and fix it.
Key takeaways
emit()calls every connected function at once, in the order of the connections, and returns when the last has finished; the editor saves its connections in the scene as[connection]lines.- A signal declares its typed parameters with
signal name(…), but eachemit()must pass matching values; the Signal-basedname.connect()lets the parser catch a misspelt name, the string form does not. bind()adds values after the emitted ones,CONNECT_ONE_SHOTruns once,CONNECT_DEFERREDruns at the end of the frame with the emitted values, and one method bound twice to one signal is refused.- A connection lasts until it is disconnected with the same Callable, runs as a one-shot, or either side is freed; a lambda connected on every entry runs once per entry.
- Children signal up and parents call down, so a plate that only emits works in any puzzle.
Terms in this lesson
- signal
- A named event that an object emits, such as a button's pressed or a custom health_changed. Every function connected to it is called each time it is emitted, so the sender needs no reference to its listeners.
References
- Using signals (Godot Engine)
- Signal class reference (Godot Engine)
- Object class reference: connect, disconnect and ConnectFlags (Godot Engine)
- GDScript reference: signals, bind and lambdas (Godot Engine)
- GDScript style guide: naming signals (Godot Engine)
- Upgrading from Godot 3 to Godot 4: signal connection syntax (Godot Engine)
- Scene organization (Godot Engine)
- Godot 4.5 release page (append signal source) (Godot Engine)
- Godot 4.6 release page (hidden underscored signals) (Godot Engine)
- Godot 4.7 release page (tweens that wait for signals) (Godot Engine)
Report a problem with this lesson
Kept only in this browser. Your Learn progress