Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 15 minutes
  • Examples run with godot 4.7.2-stable and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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):

switch.gd: a switch that emits toggled GD · switch.gd
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")
door.gd: connected in the editor GD · door.gd
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")
lamp.gd: connected in code GD · lamp.gd
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")
room.gd: connects the lamp, then flips the switch twice GD · room.gd
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()
The connection room.tscn saved, and two flips of the switch, Godot 4.7.2 Python · room_session.py
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:

Interactive · message by message One flip of the switch, one call per connection Step through the first flip() of the run. Each arrow is a call that emit() makes, in the order of the connections.
One flip of the switch, one call per connection, step 5 of 5. Messages so far: Switch to Door: _on_switch_toggled(true); Switch to Lamp: turn(true).Switchemits toggledDooreditor connectionLampcode connection_on_switch_toggled(true)turn(true)

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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().
  2. 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.
  3. 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.
  4. Then the lamp. When the door's function returns, emit() calls the next connection, the lamp's turn(true), which prints "Lamp lights up".
  5. 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

  1. One question flip() prints "Switch: flipped on…", calls toggled.emit(true), then prints "Switch: emit() has returned". The door and the lamp are connected. Which line comes right after "Switch: flipped on…"?

    Choose one answer.

    Show the answer: One question

    Answer: Door opens

    A plain connection runs inside emit(). The run printed Door opens, Lamp lights up, and only then "emit() has returned".

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:

health.gd: a typed signal, emitted right and wrong GD · health.gd
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)
misspelt.gd: the misspelling through the signal itself GD · misspelt.gd
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)
Emitting with the wrong values, and two misspelt names, Godot 4.7.2 Python · health_session.py
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:

Timeline · 4 events in order Signals across Godot 4 Walk through the versions in order. Each one changed how signals are written or shown.
  1. 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.

  2. 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.

  3. 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. 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

  1. One question health_changed(old_value: int, new_value: int) is connected to a function that takes two ints. What happens on health_changed.emit(75)?

    Choose one answer.

    Show the answer: One question

    Answer: An error for that connection, "Method expected 2 argument(s), but called with 1", and the code after emit() goes on

    The declared parameters document the signal and shape the functions the editor generates, but each emit() must pass matching values; a mismatch fails at that call, connection by connection.

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:

options.gd: four connections, four options GD · options.gd
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)
Bound values, a one-shot connection, a deferred one and the source, Godot 4.7.2 Python · options_session.py
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

  1. Question 1 of 2 stepped_on(weight: int) is connected with _on_stepped.bind("north"). What does _on_stepped receive on stepped_on.emit(10)?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 10, then "north"

    bind() adds values after those of the signal, so one function can serve many senders that each pass their own name or id.

  2. Question 2 of 2 A connection made with CONNECT_DEFERRED is emitted in frame 1. When does its function run, and with which value?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Later in frame 1, after the processing is done, with the value given to that emit()

    CONNECT_DEFERRED calls the function at idle time, the end of the frame, with the values of the emit. It suits a handler that must not run while the emitter is in the middle of something.

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.

guard.gd: a new lambda on every entry, the mistake GD · guard.gd
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"))
hall.gd: the guard comes and goes, and the bell rings GD · hall.gd
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()
A listener that enters three times, then is freed, Godot 4.7.2 Python · hall_session.py
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

  1. One question guard.gd connects a new lambda to the bell in _enter_tree() and never disconnects. The guard is removed and added again twice. How many lines does the next ring print?

    Type a number.

    A hint

    Count how many times the guard entered the tree.

    Show the answer: One question

    Answer: 3 lines

    The guard entered three times: once with the scene and twice more. Each entry connected a new lambda, a new Callable, so nothing refused it; the run printed Guard heard the bell three times.

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:

Interactive · step by step Three plates signal up, the puzzle calls down Step through one round of the puzzle. Arrows from the plates are signals; the arrow to the door is a method call.
Three plates signal up, the puzzle calls down, step 5 of 5. Showing Puzzle (plates down: all three), Door (open), Plate1, Plate2, Plate3. In focus: Plate3, Door.pressedpressedpressedopen()Puzzleplates down: all threeDooropenPlate1Plate2Plate3open()

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

  1. One question In the plate puzzle, which node should call door.open()?

    Choose one answer.

    Show the answer: One question

    Answer: The puzzle, the parent of the plates and the door, when its record shows all three plates down

    The plates emit pressed and released, and the puzzle, which owns all four nodes, connects them and decides.

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.

  1. Put plate.gd, door.gd, the starter puzzle.gd and check_puzzle.gd together in a folder that also holds an empty project.godot.
  2. In _ready(), connect the pressed and released signals of $Plate1, $Plate2 and $Plate3, and write the two functions they call.
  3. From a terminal in that folder, run godot --headless --no-header --script check_puzzle.gd.
plate.gd: a plate that emits pressed and released GD · practice/plate.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()
door.gd: records each time it opens or closes GD · practice/door.gd
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")
puzzle.gd: the starter file GD · practice/puzzle.gd
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.
check_puzzle.gd: the checker of this task GD · practice/check_puzzle.gd
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:

The checker on a finished puzzle and on a counting one, Godot 4.7.2 Python · check_session.py
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.

  1. Question 1 of 15 What is a signal in Godot?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: A named event an object emits, which calls every function connected to it

    The emitter does not know its listeners, which keeps it reusable: a button emits pressed whatever the press does.

  2. Question 2 of 15 You connect a switch's toggled signal to the door in the editor's Signals dock. Where is that connection kept?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: In the scene file, as a [connection] line that names the signal, the two nodes and the method

    The editor names the function _on_<node>_<signal> by default, as _on_switch_toggled shows, and marks it in the script editor with a connection icon.

  3. Question 3 of 15 A switch emits toggled, and three handlers are connected. One of them takes a long time. What does the code after emit() in the switch wait for?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: All three handlers, because emit() returns only after each has run

    Slow work in a handler slows the emitter down. Keep handlers short, or defer the slow part.

  4. Question 4 of 15 How do you declare a signal that passes the old and the new health as ints, and emit it?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: signal health_changed(old_value: int, new_value: int), then health_changed.emit(old, new)

    Typed parameters say what the signal sends, and the editor uses them to write handlers with the right types.

  5. Question 5 of 15 Which misspelling is caught before the game runs: connect("health_chnaged", f) or health_chnaged.connect(f)?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: health_chnaged.connect(f), with a parse error; the string form fails only when the line runs

    This is why the upgrade guide recommends the Signal-based syntax over the string-based one.

  6. Question 6 of 15 In Godot 4.6 and later, what changes for a signal named _cooldown_finished, with a leading underscore?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: It is hidden from autocompletion and generated documentation, but works like any other signal

    A leading underscore marks a signal as internal: other code can still connect it, but autocompletion and the docs no longer offer it.

  7. Question 7 of 15 What does CONNECT_ONE_SHOT do?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: It disconnects the connection after its first emission

    A one-shot connection suits things that happen once, such as a tutorial hint on the first pickup.

  8. Question 8 of 15 A handler is connected with CONNECT_APPEND_SOURCE_OBJECT to stepped_on(weight: int). What must its parameters be?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: (weight, source), because the emitting object is added after the signal's values

    The editor's Append Source option, new in 4.5, does the same for connections made in the Signals dock, so one handler can tell its senders apart.

  9. Question 9 of 15 You connect hit to _on_hit.bind("head"), then the same signal to _on_hit.bind("legs"). What happens?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: The second connect() fails with "already connected", because a bound Callable equals the same method bound to other values

    To pass different values to the same method from one signal, use one connection and pass the values with the emit, or use different methods or lambdas.

  10. Question 10 of 15 A listener connected to a bell is freed. What happens at the next ring?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: Nothing for that listener; its connections went when it was freed, and there is no error

    The Signal class reference says a connection is lost when the callable's object is freed. Lambdas belong to the object that made them, so they go with it too.

  11. Question 11 of 15 A script connects func() -> void: print("hi") to a signal. How can it disconnect it later?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: Keep the lambda in a variable, connect that, and pass the same variable to disconnect()

    is_connected(note) printed false after disconnect(note). Without a variable, the only way out is freeing the object that made the lambda.

  12. Question 12 of 15 A HUD that is taken out of the tree and put back each time a menu opens must listen to the player. Where should it connect, so that it never listens twice?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: In _ready(), which runs once, or in _enter_tree() with a disconnect of the same Callable in _exit_tree()

    Pair connect() with disconnect() in matching callbacks, or connect once. A method callable is safer than a lambda, because a second connect() of it fails loudly instead of doubling the handler.

  13. Question 13 of 15 In "call down, signal up", what does "signal up" mean?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: A child reports what happened by emitting a signal, and does not call its parent

    The child stays reusable because it does not know who listens; whoever holds it decides what its events do.

  14. Question 14 of 15 Which version of plate.gd keeps the plate reusable in other puzzles?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: One that only emits pressed and released

    A node that only signals has no dependencies, the goal the Scene organization page sets for scenes.

  15. Question 15 of 15 The counting puzzle adds 1 on every pressed and opens the door at 3. Why did it fail the checker's "plate 1 bounces" case?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: Plate1 emitted pressed twice, so two plates down counted as three and the door opened

    Record which plates are down, as the solution's dictionary of names does: a second press of the same plate changes nothing.

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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

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 each emit() must pass matching values; the Signal-based name.connect() lets the parser catch a misspelt name, the string form does not.
  • bind() adds values after the emitted ones, CONNECT_ONE_SHOT runs once, CONNECT_DEFERRED runs 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.