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

Finding nodes: paths, groups, unique names

Reach Godot nodes with get_node, $ and @onready, scene-unique %names and groups, and wire scenes with exported references instead of paths that break.

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

What you will learn

  • Find nodes with get_node, $ and @onready, and handle a missing node with get_node_or_null
  • Use scene-unique %Names, and set the owner of nodes made in code so that they work
  • Group nodes and reach them with get_nodes_in_group and call_group
  • Connect scenes with exported node references and signals instead of fixed paths

Before you start

What you’ll learn · Before you start

What you will learn

  • Find nodes with get_node, $ and @onready, and handle a missing node with get_node_or_null
  • Use scene-unique %Names, and set the owner of nodes made in code so that they work
  • Group nodes and reach them with get_nodes_in_group and call_group
  • Connect scenes with exported node references and signals instead of fixed paths

Before you start

On this page

Rename two nodes of a level, and every script that finds other nodes by their place in the tree can break. The checker at the end of this page renames World and Panel; a HUD that found the player with get_node("/root/Main/World/Player") failed all 4 of its checks, and the same HUD passed all 4 once it found the player through an exported reference and its label through a unique name.

This page goes through each way one node finds another: paths with get_node() and $, variables set with @onready, scene-unique names, groups, and references that a scene sets up. For each one you will see, on a recorded run, what it depends on and how it fails.

A node path is read like a folder path, from the node that asks

level.tscn has the same shape as the level of the previous lesson: Level, with Player and HUD under it, a Sword under Player and a Score label under HUD. paths.gd, on Level, reaches the other nodes in each way a script can write a path:

paths.gd: the same tree, reached by different paths GD · paths.gd
extends Node

# Attached to Level: reaches the other nodes of level.tscn by path, the ways a script can write one.


func _ready() -> void:
	print("$Player/Sword            -> ", $Player/Sword.name)
	print("get_node(\"HUD/Score\")    -> ", get_node("HUD/Score").name)
	var score := $HUD/Score
	print("Score: ../../Player      -> ", score.get_node("../../Player").name)
	print("/root/Level/HUD          -> ", get_node("/root/Level/HUD").name)
	print("Score's own path         -> ", score.get_path())
	print("from Score to Sword      -> ", score.get_path_to($Player/Sword))
	print("get_node_or_null(\"HUD/Lives\") -> ", get_node_or_null("HUD/Lives"))
	print("get_node(\"HUD/Lives\")    -> ", get_node("HUD/Lives"))
	print("The function goes on after the error")
	get_tree().quit()
Paths from Level and from Score, and one that leads nowhere, Godot 4.7.2 Python · paths_session.py
from session import files, run

files("project.godot", "level.tscn", "paths.gd")
# The last get_node() asks for a node that does not exist, so the run prints an engine error on purpose.
run("""
!godot --headless --no-header level.tscn
""")

Output

$ godot --headless --no-header level.tscn
$Player/Sword            -> Sword
get_node("HUD/Score")    -> Score
Score: ../../Player      -> Player
/root/Level/HUD          -> HUD
Score's own path         -> /root/Level/HUD/Score
from Score to Sword      -> ../../Player/Sword
get_node_or_null("HUD/Lives") -> <Object#null>
ERROR: Node not found: "HUD/Lives" (relative to "/root/Level").
   at: get_node (scene/main/node.cpp:1976)
   GDScript backtrace (most recent call first):
       [0] _ready (res://paths.gd:15)
get_node("HUD/Lives")    -> <Object#null>
The function goes on after the error

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 paths_session.py

$Player/Sword is short for get_node("Player/Sword"): a path that starts at the node running the script and goes down through its children, like a folder path. .. climbs to the parent, so from Score ../../Player goes up to HUD, then to Level, then down to Player. A path that starts with / is absolute: it is read from root, the Window that Godot puts at the top of every running game. Step through the paths of the run:

Interactive · step by step Reading node paths in level.tscn Step through the paths of paths.gd. The nodes in focus are the ones each path walks through, from where it starts.
Reading node paths in level.tscn, step 6 of 6. Showing root, Level, Player, HUD, Sword, Score (/root/Level/HUD/Score). In focus: Score, Sword.rootthe Window Godot makesLevelruns paths.gdPlayerHUDSwordScore/root/Level/HUD/Score

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

Every step as text
  1. Every node has a path. While the game runs, level.tscn hangs under root, a Window that Godot makes. Read from the top like a folder path, Score's own path is /root/Level/HUD/Score, as get_path() printed.
  2. $Player/Sword from Level. A path without a leading / starts at the node that asks, here Level. Player is a child of Level and Sword a child of Player, so the path leads to Sword.
  3. ../../Player from Score. .. means the parent. From Score, the first .. is HUD and the second is Level; from there Player is a child. The path leaves the HUD's branch and enters the player's.
  4. /root/Level/HUD from anywhere. A path that starts with / is absolute. It is read from root whichever node asks, so it breaks as soon as any node on the way is renamed or moved.
  5. HUD/Lives leads nowhere. There is no Lives under HUD. get_node() printed Node not found and returned null, and the function went on; get_node_or_null() returned null without the error.
  6. The path Godot works out. get_path_to() gives the relative path from one node to another. From Score to Sword it is ../../Player/Sword, up two levels and down two.

The last two lines of the run are the case to handle. HUD has no child named Lives, so get_node() printed Node not found with the line that asked, returned null, and the function went on. get_node_or_null() returned null without the error, which is what you want for a node that may be missing on purpose. Either way, using the null is the error that stops the function.

Written as Reaches
$Player/Sword, get_node("Player/Sword") a node below this one, through each child on the way
get_node("../Player") a node beside this one: up to the parent, then down
get_node("/root/Level/HUD") a node named from the top, whichever node asks
get_node_or_null("HUD/Lives") the same as get_node(), but null without an error when nothing is there

The Godot documentation recommends avoiding .., because it makes a node depend on what is around it. Absolute paths depend on even more: every node from root down.

Checkpoint

Check: Node paths, $ and get_node()

  1. Question 1 of 2 Score is at /root/Level/HUD/Score and Sword at /root/Level/Player/Sword. Which path, used from Score, reaches Sword?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: ../../Player/Sword

    A relative path starts at the node that asks, and each .. climbs to the parent. From Score: HUD, then Level, then down to Player and Sword.

  2. Question 2 of 2 paths.gd calls get_node("HUD/Lives"), and HUD has no child named Lives. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: An error saying Node not found is printed, get_node() returns null, and the function goes on

    get_node() reports a missing node and returns null. Use get_node_or_null() when the node may be missing on purpose, and test the result before you use it.

@onready waits until the children exist

A HUD reads its labels often, so it keeps them in variables. The question is when those variables get their values. The same HUD, with a Score and a Lives label, runs here with three scripts. The first uses @onready: An annotation on a member variable that delays its default value until just before the node's _ready(), when the node's children exist, as in @onready var label: Label = $Score.; the second sets score_label = $Score as a plain default value; the third uses get_node_or_null("Lives") as one:

hud_onready.gd: the labels kept with @onready GD · hud_onready.gd
extends Node

# The HUD keeps its two labels in variables. @onready sets them just before _ready(), once the children exist.
@onready var score_label: Label = $Score
@onready var lives_label: Label = $Lives


func _init() -> void:
	print("  _init: score_label ", score_label)


func _enter_tree() -> void:
	print("  _enter_tree: score_label ", score_label)


func _ready() -> void:
	print("  _ready: score_label ", score_label.name, ", lives_label ", lives_label.name)
hud_dollar.gd: $Score as a plain default value GD · hud_dollar.gd
extends Node

# The same labels without @onready: the default value is worked out while the HUD is made, before its children exist.
var score_label: Label = $Score


func _ready() -> void:
	print("  _ready: score_label ", score_label)
hud_or_null.gd: get_node_or_null() as a plain default value GD · hud_or_null.gd
extends Node

# get_node_or_null() in a default value: Godot accepts it, and it runs while the HUD is made.
var lives_label: Label = get_node_or_null("Lives")


func _ready() -> void:
	print("  _ready: lives_label ", lives_label, ", though the HUD has a child named ", get_child(1).name)
One HUD with three scripts, made and added to the tree, Godot 4.7.2 Python · onready_session.py
from session import files, run

files("project.godot", "show_hud.tscn", "show_hud.gd", "hud_onready.tscn", "hud_onready.gd", "hud_dollar.tscn",
      "hud_dollar.gd", "hud_or_null.tscn", "hud_or_null.gd")
# The same HUD with three scripts; the second one does not even load, so its run prints a parse error on purpose.
run("""
godot --headless --no-header show_hud.tscn -- hud_onready.tscn
!godot --headless --no-header show_hud.tscn -- hud_dollar.tscn
godot --headless --no-header show_hud.tscn -- hud_or_null.tscn
""")

Output

$ godot --headless --no-header show_hud.tscn -- hud_onready.tscn
instantiate() res://hud_onready.tscn
  _init: score_label <null>
add_child()
  _enter_tree: score_label <null>
  _ready: score_label Score, lives_label Lives
$ godot --headless --no-header show_hud.tscn -- hud_dollar.tscn
instantiate() res://hud_dollar.tscn
SCRIPT ERROR: Parse Error: The default value uses "$" which won't return nodes in the scene tree before "_ready()" is called. Use the "@onready" annotation to solve this. (Warning treated as error.)
          at: GDScript::reload (res://hud_dollar.gd:4)
          GDScript backtrace (most recent call first):
              [0] _ready (res://show_hud.gd:9)
ERROR: Failed to load script "res://hud_dollar.gd" with error "Parse error".
   at: load (modules/gdscript/gdscript_resource_format.cpp:46)
   GDScript backtrace (most recent call first):
       [0] _ready (res://show_hud.gd:9)
add_child()
$ godot --headless --no-header show_hud.tscn -- hud_or_null.tscn
instantiate() res://hud_or_null.tscn
add_child()
  _ready: lives_label <Object#null>, though the HUD has a child named Lives

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 onready_session.py

A plain default value is worked out while the node is made, before the scene has added its children, so a lookup there finds nothing. @onready delays the line until just before _ready(), when the children are in place. Follow the three runs through the HUD’s first moments:

Interactive · stage by stage When each kind of variable gets its value Step through the three runs of the HUD. The sub-label of each stage says what the HUD's variables held there.
When each kind of variable gets its value, step 7 of 7. Showing instantiate() (lives_label: null for good), The children are added, add_child() (score_label: still null), @onready values (score_label: Score label), _ready(). In focus: instantiate(), _ready().instantiate()lives_label: null for goodThe children are addedScore and Lives existadd_child()score_label: still null@onready valuesscore_label: Score label_ready()the labels can be used

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

Every step as text
  1. The HUD is made. instantiate() makes the HUD node and its script. Every member's default value is worked out now, as the node is made, before the scene has added a single child. score_label is still null.
  2. Then its children. The scene adds Score and Lives under the HUD. The HUD is not in the tree yet, and its @onready variables are still empty.
  3. In the tree, not ready yet. add_child(hud) puts it in the tree, and _enter_tree() still printed score_label <null>. @onready values are not set on entry.
  4. The @onready lines run. Just before _ready(), Godot runs every @onready line in a hidden function, @implicit_ready in backtraces. $Score now finds the label.
  5. _ready() can use them. _ready() printed score_label Score and lives_label Lives. Code that runs in _ready() or later can use both.
  6. $ without @onready. hud_dollar.gd sets score_label = $Score as a plain default. It would run when the HUD is made, before Score exists, so Godot refuses the script with a parse error that suggests @onready.
  7. get_node_or_null() slips through. hud_or_null.gd uses get_node_or_null("Lives") as a default. The parser accepts it, the line runs before Lives exists, and lives_label stays null for good, with no error.

With @onready, score_label was still null in _init() and in _enter_tree(), and held the Score label in _ready(). The $Score default never ran at all: Godot refused the script with a parse error that names the cure, Use the "@onready" annotation. That message is a warning that Godot 4.7 treats as an error by default, so this mistake is caught before the game starts. The third script is the trap.

get_node_or_null() as a plain default

var lives_label: Label = get_node_or_null("Lives") passes the parser, runs while the HUD is made, and leaves lives_label null for good: the run printed <Object#null> though the HUD had a child named Lives. Write @onready var lives_label: Label = $Lives instead, so the line runs once Lives exists.

Checkpoint

Check: When @onready variables are set

  1. One question The HUD declares @onready var score_label: Label = $Score. What does score_label hold in the HUD's _enter_tree()?

    Choose one answer.

    Show the answer: One question

    Answer: null, because @onready values are set just before _ready()

    @onready variables get their values in a hidden step just before _ready(), which backtraces call @implicit_ready. Use them in _ready() or later.

A unique name survives moves, inside its own scene

A path from a HUD to its label names every node in between, so moving the label into a new panel breaks it. A scene-unique node: A node marked Access as Unique Name, which any node of the same scene reaches as %Name, whatever lies between. It is found through the node's owner, so other scenes reach it only through the instance, as in Player/%Eyes. avoids that. In the editor, right-click the node in the Scene dock and choose Access as Unique Name; a % sign appears beside it, and the scene file stores unique_name_in_owner = true. Code then reaches it as %Score or get_node("%Score"), whatever lies between.

main.tscn marks its Score label unique. It also instances player.tscn as Player, and in that scene a node named Eyes is unique too:

unique_names.gd: unique names before and after a rename GD · unique_names.gd
extends Node

# Attached to Main. Score is marked unique in main.tscn, Eyes in player.tscn, which Main instances as Player.


func _ready() -> void:
	print("%Score                 -> ", found("%Score"))
	print("$HUD/Panel/Score       -> ", found("HUD/Panel/Score"))
	print("-- rename Panel to TopBar")
	$HUD/Panel.name = "TopBar"
	print("%Score                 -> ", found("%Score"))
	print("$HUD/Panel/Score       -> ", found("HUD/Panel/Score"))
	print("%Eyes                  -> ", found("%Eyes"))
	print("Player/%Eyes           -> ", found("Player/%Eyes"))
	print("owner of Score: ", %Score.owner.name, ", owner of Eyes: ", $Player/Body/Eyes.owner.name)
	print("-- a Lives label made in code, marked unique")
	var lives := Label.new()
	lives.name = "Lives"
	$HUD/TopBar.add_child(lives)
	lives.unique_name_in_owner = true
	print("%Lives, no owner yet   -> ", found("%Lives"))
	lives.owner = self
	print("%Lives, owner = Main   -> ", found("%Lives"))
	get_tree().quit()


func found(path: NodePath) -> String:
	var node := get_node_or_null(path)
	return node.name if node else "not found"
Unique names in main.tscn and in an instanced player.tscn, Godot 4.7.2 Python · unique_session.py
from session import files, run

files("project.godot", "main.tscn", "unique_names.gd", "player.tscn")
run("""
godot --headless --no-header main.tscn
""")

Output

$ godot --headless --no-header main.tscn
%Score                 -> Score
$HUD/Panel/Score       -> Score
-- rename Panel to TopBar
%Score                 -> Score
$HUD/Panel/Score       -> not found
%Eyes                  -> not found
Player/%Eyes           -> Eyes
owner of Score: Main, owner of Eyes: Player
-- a Lives label made in code, marked unique
%Lives, no owner yet   -> not found
%Lives, owner = Main   -> Lives

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 unique_session.py

Step through the run, with main.tscn on the left and the instanced player.tscn on the right:

Interactive · step by step Two scenes, two sets of unique names Step through the run. The left column is main.tscn; the right column is player.tscn, instanced in it as Player.
Two scenes, two sets of unique names, step 5 of 5. Showing Main, HUD, Panel (TopBar, now with Lives), %Score, Player, Body, %Eyes. In focus: Main, Panel.Mainowner of main.tscnHUDPanelTopBar, now with Lives%Scoreowner MainPlayerowner of player.tscnBody%Eyesowner Player

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

Every step as text
  1. %Score from Main. Score is marked Access as Unique Name in main.tscn, and Main owns it. get_node("%Score") from Main finds it without naming HUD or Panel.
  2. Panel is renamed. The run renames Panel to TopBar. %Score still finds the label, while the path HUD/Panel/Score now leads nowhere.
  3. %Eyes from Main. Eyes is unique too, but in player.tscn, whose root Player owns it. From Main, %Eyes found nothing, because a unique name belongs to the scene that owns the node.
  4. Through the instance. Player/%Eyes found it. The path first reaches Player, the instance, and then looks up Eyes among the unique names that Player owns.
  5. A node made in code. A Lives label added by code and marked unique was not found at first, because a node made in code has no owner. After lives.owner = self, %Lives found it.

What decides all of this is the node’s owner: The node that a node belongs to for saving and for unique names: for every node a scene file makes, the root of that scene. A node made in code has no owner until its owner property is set.. Each node a scene file creates is owned by that scene’s root, and a unique name is registered with the owner. Score’s owner is Main, so %Score from Main found it, and still found it after Panel became TopBar. Eyes’ owner is Player, the root of player.tscn, so %Eyes from Main found nothing, while Player/%Eyes went to the instance first and found it there. The Scene unique nodes page states the rule plainly: only nodes of the same scene can look up its unique nodes by name.

Nodes made in code have no owner. The Lives label that the run added and marked unique was not found until the line lives.owner = self made Main its owner. The same owner rule decides which nodes are saved when a scene is packed into a file, which a later lesson builds on.

Checkpoint

Check: Scene-unique names and owners

  1. One question Eyes is a unique node inside player.tscn, which main.tscn instances as Player. How can Main's script reach Eyes?

    Choose one answer.

    Show the answer: One question

    Answer: With get_node("Player/%Eyes"), through the instance

    %Name looks in the unique names of the scene that owns the node that asks. To reach a unique node of an instanced scene, go to the instance first: Player/%Eyes.

Groups find every node of a kind, wherever it is

When an alarm goes off, every guard must react, and the guards sit in different parts of the level, some placed in the editor and some spawned during the game. A group: A name that tags nodes, such as guards. A node can be in many groups, and the SceneTree lists or calls the members that are inside the tree, in scene hierarchy order. tags nodes with a name, wherever they are. In the editor, the Groups dock adds the selected node to one; in a scene file it is groups=["guards"] on the node’s line; in code it is add_to_group("guards"). castle.tscn has two guards in the group, one under Tower and one under Gate, and alarm.gd adds a third while the game runs:

alarm.gd: a third guard, then the alarm GD · alarm.gd
extends Node

# Attached to Level. Guard1 and Guard2 are in the "guards" group in castle.tscn; Guard3 is made while the game runs.
const Guard := preload("res://guard.gd")


func _ready() -> void:
	var guard3: Node = Guard.new()
	guard3.name = "Guard3"
	guard3.add_to_group("guards")
	print("Guard3 is in the group: ", guard3.is_in_group("guards"))
	print("guards before add_child: ", names(get_tree().get_nodes_in_group("guards")))
	$Gate.add_child(guard3)
	print("guards after add_child:  ", names(get_tree().get_nodes_in_group("guards")))
	print("first guard: ", get_tree().get_first_node_in_group("guards").name,
			", guards: ", get_tree().get_node_count_in_group("guards"))
	print("call_group(\"guards\", \"alert\", \"the gate\"):")
	get_tree().call_group("guards", "alert", "the gate")
	print("call_group(\"guards\", \"alret\", \"the gate\"), with a typo:")
	get_tree().call_group("guards", "alret", "the gate")
	print("(no guard moved, and no error)")
	get_tree().quit()


func names(nodes: Array[Node]) -> Array[String]:
	var out: Array[String] = []
	for node in nodes:
		out.append(node.name)
	return out
guard.gd: what a guard does when called GD · guard.gd
extends Node

# A guard of the "guards" group.


func alert(where: String) -> void:
	print("  ", name, " runs to ", where)
Guards found by their group, and a call with a typo, Godot 4.7.2 Python · groups_session.py
from session import files, run

files("project.godot", "castle.tscn", "alarm.gd", "guard.gd")
run("""
godot --headless --no-header castle.tscn
""")

Output

$ godot --headless --no-header castle.tscn
Guard3 is in the group: true
guards before add_child: ["Guard1", "Guard2"]
guards after add_child:  ["Guard1", "Guard2", "Guard3"]
first guard: Guard1, guards: 3
call_group("guards", "alert", "the gate"):
  Guard1 runs to the gate
  Guard2 runs to the gate
  Guard3 runs to the gate
call_group("guards", "alret", "the gate"), with a typo:
(no guard moved, and no error)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 groups_session.py

Guard3 was in the group as soon as add_to_group() ran, but the tree’s list held only Guard1 and Guard2 until add_child() put Guard3 in the tree: the SceneTree’s group methods see only nodes inside the tree. After that, all three answered, in scene hierarchy order.

Call What it gives
get_tree().get_nodes_in_group("guards") every member in the tree, in hierarchy order
get_tree().get_first_node_in_group("guards") the first member, or null when the group is empty
get_tree().call_group("guards", "alert", "the gate") calls alert("the gate") on every member
is_in_group("guards") whether this node is in the group, in the tree or not

A typo in call_group() is silent

call_group("guards", "alret", "the gate") moved no guard and printed no error, because call_group() skips members that lack the method. Keep the method name in one place, such as a constant both scripts use, or check the effect in a test.

Checkpoint

Check: Groups and call_group()

  1. One question Guard3 calls add_to_group("guards") before it is added to the level. Is it in get_tree().get_nodes_in_group("guards") at that point?

    Choose one answer.

    Show the answer: One question

    Answer: No; is_in_group() is true, but the tree lists it only once it is in the tree

    A node keeps its groups while it is outside the tree, but SceneTree's group methods only find nodes in the tree.

Exported references and signals keep scenes apart

Back to the HUD of the opening. It must show the player’s health, and the player lives in another branch of the main scene. Writing the player’s path into the HUD’s script ties the HUD to that one layout. The HUD of the practice below declares the player instead:

@export var player: Node2D

The slot then appears in the HUD’s Inspector, and in main.tscn, which holds both nodes, you pick the player for it. The scene file stores the choice as a NodePath: A built-in type for a path to a node, written ^"Player/Sprite", already split into its parts. After a colon it can name a property, as in ^"Player/Sprite:modulate"; a leading / makes it absolute. More in “Strings, StringName and text formatting” from the HUD, player = NodePath("../World/Player"), and instantiate() turns it into the node itself before the scene enters the tree. The checker renamed World after that point, and the HUD still had its player.

The other direction matters as much. When the player has news, such as lost health, it should not look for a HUD at all: it emits 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. More in “Signals and connections”, and the scene that holds both connects it to the HUD. This rule of thumb is often put as 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.: a parent calls methods on its children, and a child reports events with signals, so no child depends on where it is used. The Scene organization page says the same in other words: design scenes to have no dependencies, and let an ancestor connect its children.

Choose a way for your own case. Answer the questions below the table, and it rules out the ways that would break:

Interactive · answer and compare How should the HUD find what it needs? Answer the questions about the two nodes; each answer rules out the ways that would break, with the reason.
Are the two nodes in the same scene file?
Can the nodes between them be renamed or moved later?
Do you need one particular node, or every node of a kind?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Six ways for one node to reach another, against what each depends on
Way A node in between renamed Across two scenes When it fails What it ties you to
Absolute path Written as: get_node("/root/Main/World/Player") Poor breaks: the checker's rename of World gave Node not found Mixed reaches anything, from anywhere, if the layout never changes Poor at run time, with an error and a null Poor the whole tree from root down
Relative path Written as: $"../World/Player" Poor breaks: HUD/Panel/Score was not found after the rename Mixed works, but .. reaches out of the scene Poor at run time, with an error and a null Mixed every node between the two
Unique name Written as: %HealthLabel Good %Score still found the label after Panel became TopBar Poor only inside its own scene; %Eyes was not found from Main Mixed at run time, if the mark or the owner is missing Good one name in the same scene
Group Written as: get_tree().get_first_node_in_group("player") Good the tree is searched, whatever the names Good finds members anywhere in the tree Mixed quietly: an empty list, or a call that does nothing Mixed a group name, a string the code must spell right
Exported reference Written as: @export var player: Node2D Good set before the rename, so the checker's HUD kept its player Good set where both are in view, in the parent scene Mixed visible: an empty slot in the Inspector Good only the type it expects
The other node signals Written as: the player emits health_changed Good the connection is to the node, not a path Good a parent connects it; neither scene knows the other Mixed quietly, if nobody connected Good the signal's name and arguments
How the answers decide
  • “Are the two nodes in the same scene file?” No, they come from different scenes: rules out Unique name. A unique name is found only inside the scene that owns the node: %Eyes from Main found nothing.
  • “Are the two nodes in the same scene file?” No, they come from different scenes: adds a caveat to Relative path. A path that climbs out of its scene with .. depends on how the other scene is put together.
  • “Can the nodes between them be renamed or moved later?” Yes, the layout will change: rules out Absolute path and Relative path. Both name every node on the way, so a rename or a move breaks them, as the checker's renames did.
  • “Do you need one particular node, or every node of a kind?” Every node of a kind, such as all guards: rules out Unique name and Exported reference and adds a caveat to Absolute path and Relative path. For every node of a kind, a group finds them all, including nodes added later; one reference or path finds one.
  • “Do you need one particular node, or every node of a kind?” One particular node: adds a caveat to Group. A group for one node works, but the code depends on a string, and an empty group fails quietly.

In short: inside one scene, a unique name or @onready with a short path; between scenes, an exported reference set by the scene that holds both, or a signal; for every node of a kind, a group. Keep absolute paths out of game code.

Key idea

A node should depend on as little as possible around it: reach your own children by path or unique name, let the scene that holds two nodes connect them, and let children report events with signals.

Checkpoint

Check: Exported references and signals between scenes

  1. One question The checker renamed World to Level1 after instantiate() but before add_child(). Why did the HUD with @export var player still have its player?

    Choose one answer.

    Show the answer: One question

    Answer: Godot sets an exported node reference to the node itself when the scene is instantiated, so later renames do not matter

    The scene file stores the reference as a path from the HUD, player = NodePath("../World/Player"), and instantiate() turns it into the node itself. From then on the variable holds the node, wherever it moves.

Practise: a HUD that survives a new layout

hud.gd shows the player’s health, and it works, as long as nobody changes the main scene. It finds the player with an absolute path and its label with $Panel/HealthLabel. Make it independent of the layout:

  1. Save player.gd, hud.gd and check_hud.gd below in a folder with an empty project.godot, then build main.tscn in the editor: Main (Node), with World (Node2D) and its child Player (Node2D, player.gd), and HUD (Node, hud.gd) with Panel (Control) and its child HealthLabel (Label).
  2. Change hud.gd so that the player is an exported Node2D and the label is %HealthLabel; in main.tscn, mark HealthLabel with Access as Unique Name and pick Player for the HUD’s new Player slot in the Inspector.
  3. Run godot --headless --no-header --script check_hud.gd in a terminal in that folder.
player.gd: the player's health GD · practice/player.gd
extends Node2D

# The player's health, which the HUD shows.
var health := 100
hud.gd: the HUD as it starts GD · practice/hud.gd
extends Node

# The HUD shows the player's health. As written, it finds both nodes by where they are in the tree.
# Your task: reach the player through an exported node reference and the label through its unique name.
@onready var player: Node2D = get_node("/root/Main/World/Player")
@onready var health_label: Label = $Panel/HealthLabel


func _process(_delta: float) -> void:
	health_label.text = "HP %d" % player.health
check_hud.gd: the checker of this task GD · practice/check_hud.gd
extends SceneTree

# Checks hud.gd and main.tscn. Run it in the project's folder: godot --headless --no-header --script check_hud.gd
# It renames World to Level1 and Panel to TopBar before the scene enters the tree, as a later change of the
# layout would, then takes 25 health from the player and reads the label.
var main: Node
var frame := 0
var passed := 0
var total := 0


func _process(_delta: float) -> bool:
	frame += 1
	if frame == 1:
		var source := FileAccess.get_file_as_string("res://hud.gd")
		check(not source.contains("/root"), "hud.gd uses no absolute path")
		main = load("res://main.tscn").instantiate()
		main.get_node("World").name = "Level1"
		main.get_node("HUD/Panel").name = "TopBar"
		print("World is now Level1 and Panel is TopBar")
		root.add_child(main)
	elif frame == 2:
		var hud := main.get_node("HUD")
		check(is_instance_valid(hud.player) and hud.player.name == "Player", "the HUD has the player")
		check(hud.health_label != null, "the HUD has its label")
		main.get_node("Level1/Player").health = 75
	elif frame == 3:
		var text: String = main.get_node("HUD/TopBar/HealthLabel").text
		check(text == "HP 75", "the label shows HP 75 (it shows %s)" % text)
		main.free()
		print("%d of %d checks passed" % [passed, total])
		quit(0 if passed == total else 1)
	return false


func check(ok: bool, what: String) -> void:
	total += 1
	passed += int(ok)
	print("%s %s" % ["PASS" if ok else "FAIL", what])

The checker renames World to Level1 and Panel to TopBar before the scene enters the tree, takes 25 health from the player and reads the label. On the HUD as it starts, and on a finished one:

The checker on the HUD as it starts, and on a finished HUD, Godot 4.7.2 Python · check_session.py
from session import files, run

# The checker on the HUD as it starts, which finds nodes by their place in the tree, then on an answer to the task.
files("practice/project.godot=start/project.godot", "practice/check_hud.gd=start/check_hud.gd",
      "practice/player.gd=start/player.gd", "practice/hud.gd=start/hud.gd", "practice/main.tscn=start/main.tscn",
      "practice/project.godot=right/project.godot", "practice/check_hud.gd=right/check_hud.gd",
      "practice/player.gd=right/player.gd", "practice/solution/hud.gd=right/hud.gd",
      "practice/solution/main.tscn=right/main.tscn")
run("""
cd start
!godot --headless --no-header --script check_hud.gd
cd ../right
godot --headless --no-header --script check_hud.gd
""")

Output

$ cd start
$ godot --headless --no-header --script check_hud.gd
FAIL hud.gd uses no absolute path
World is now Level1 and Panel is TopBar
ERROR: Node not found: "/root/Main/World/Player" (absolute path attempted from "/root/Main/HUD").
   at: get_node (scene/main/node.cpp:1973)
   GDScript backtrace (most recent call first):
       [0] @implicit_ready (res://hud.gd:5)
       [1] _process (res://check_hud.gd:21)
ERROR: Node not found: "Panel/HealthLabel" (relative to "/root/Main/HUD").
   at: get_node (scene/main/node.cpp:1976)
   GDScript backtrace (most recent call first):
       [0] @implicit_ready (res://hud.gd:6)
       [1] _process (res://check_hud.gd:21)
SCRIPT ERROR: Invalid access to property or key 'health' on a base object of type 'null instance'.
          at: _process (res://hud.gd:10)
          GDScript backtrace (most recent call first):
              [0] _process (res://hud.gd:10)
FAIL the HUD has the player
FAIL the HUD has its label
SCRIPT ERROR: Invalid access to property or key 'health' on a base object of type 'null instance'.
          at: _process (res://hud.gd:10)
          GDScript backtrace (most recent call first):
              [0] _process (res://hud.gd:10)
FAIL the label shows HP 75 (it shows HP)
0 of 4 checks passed
[exit status 1]
$ cd ../right
$ godot --headless --no-header --script check_hud.gd
PASS hud.gd uses no absolute path
World is now Level1 and Panel is TopBar
PASS the HUD has the player
PASS the HUD has its label
PASS the label shows HP 75 (it shows HP 75)
4 of 4 checks passed

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 starting HUD failed every check: both lookups printed Node not found, from @implicit_ready, the hidden step that sets @onready variables, and each frame then stopped at player.health. The finished HUD showed HP 75. When your own run reports 4 of 4 checks passed, go through the questions on the five ideas of the page; they start where your answers so far are weakest, and each answer is explained as soon as you pick 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 does $Player/Sword mean in a script?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: get_node("Player/Sword"), the node Sword under this node's child Player

    $ and get_node() take the same paths. Quote paths with spaces or unusual characters, as in $"../World/Player".

  2. Question 2 of 15 A script on HUD runs get_node("../Player"). Which node does it reach in level.tscn?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: Player, the HUD's sibling, a child of Level

    .. is the parent. It works, but it ties the HUD to where it sits; the docs recommend avoiding it.

  3. Question 3 of 15 A pickup finds the player with get_node("/root/Main/World/Player"). A designer moves Player under a new node, World/Characters. What happens?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: The lookup fails with Node not found and returns null, and the next use of it stops with an error

    Paths written in code break when the layout changes. An exported reference, a unique name or a group survives the same change.

  4. Question 4 of 15 When does Godot set a variable declared with @onready?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: Right before the node's _ready() runs

    @onready exists so that a default value can name children: they are in place just before _ready().

  5. Question 5 of 15 A script declares var score_label: Label = $Score, without @onready. What does Godot 4.7.2 do?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: It refuses to load the script, with a parse error that suggests @onready

    The warning GET_NODE_DEFAULT_WITHOUT_ONREADY is treated as an error by default, so this mistake is caught before the game runs.

  6. Question 6 of 15 A script declares var lives_label: Label = get_node_or_null("Lives") as a plain default, and the HUD has a Lives child. What does lives_label hold in _ready()?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: null, for good, with no error

    Add @onready to every default that looks up nodes. get_node_or_null() in a plain default is a silent null.

  7. Question 7 of 15 How do you make a node reachable as %HealthLabel in the editor?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: Right-click it in the Scene dock and choose Access as Unique Name

    The scene file then stores unique_name_in_owner = true on that node.

  8. Question 8 of 15 Code adds a Label under the HUD and sets unique_name_in_owner = true, but get_node("%Lives") from Main finds nothing. What is missing?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: An owner; set lives.owner to the scene's root, Main

    Nodes that a scene file creates are owned by the scene's root. Nodes made in code need their owner set before a unique name or saving the scene can work.

  9. Question 9 of 15 Panel, the parent of the unique Score label, is renamed to TopBar. Which lookups from Main still find Score?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: %Score only; HUD/Panel/Score no longer does

    A unique name skips the nodes in between, so renaming or moving them inside the scene does not matter.

  10. Question 10 of 15 What is a group in Godot?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: A name that tags nodes, so the tree can list them or call them all at once

    get_nodes_in_group(), get_first_node_in_group() and call_group() reach every member in the tree, in scene hierarchy order.

  11. Question 11 of 15 call_group("guards", "alret", "the gate") has a typo in the method name. What happens?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: Nothing at all; no guard moves and no error is shown

    Because a typo is silent, keep group method names in one place, such as a constant, or check the result in a test.

  12. Question 12 of 15 Guard1 is under Tower, Guard2 under Gate, and Guard3 was added under Gate after Guard2. Tower comes before Gate in the level. In which order does call_group() reach them?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: Guard1, Guard2, Guard3, the scene hierarchy order

    get_nodes_in_group() and call_group() follow the scene hierarchy. Do not let game logic depend on that order unless you control where nodes are added.

  13. Question 13 of 15 A Level needs its HUD to show a new score. Which way keeps the HUD independent of the Level?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: Level calls a method of its child, such as hud.show_score(score)

    A parent calls methods on its children; a child that has news emits a signal, and whoever owns both connects it.

  14. Question 14 of 15 Where is an exported node reference such as @export var player: Node2D set?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: In the Inspector of the scene that holds both nodes; the .tscn stores it as a path, which instantiate() turns into the node

    The scene that contains both nodes knows both, so it is the right place to connect them.

  15. Question 15 of 15 A coin scene must add to the score when it is picked up, and the coin is reused in many levels. What should the coin do?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: Emit a signal such as collected(value), which the level connects to the score

    Children signal up and parents call down: the coin emits, the level that instances it connects the signal. The next lesson covers signals in full.

Interview questions

Warm-up: what does $Score mean in GDScript, and why is it usually declared with @onready?

$Score is short for get_node("Score"): it finds the node named Score among the children of the node whose script runs it, and $HUD/Score follows a longer path the same way. Scripts keep such nodes in member variables, and a member’s default value is worked out while the node is made, before the scene has added its children, so a plain var score_label = $Score would find nothing. Godot 4.7 refuses that line with a parse error that suggests @onready. @onready var score_label: Label = $Score runs the lookup just before _ready(), when the children are in place. A plain default with get_node_or_null() is not caught and stays null for good, so give every node lookup in a default value the @onready annotation.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    After a teammate moves the Player under a new Characters node to tidy the level, the HUD stops updating and the output fills with Node not found errors from its @onready line, get_node("/root/Main/World/Player"). Fix it so that the next reorganisation cannot break the HUD, and explain your choice.

    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 wave spawner makes 10 guards with instantiate(), adds each to the guards group, calls get_tree().call_group("guards", "set_route", route), and only then adds them to the level. None of the new guards patrols, while the guards placed in the editor do. Explain and fix it.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A node path is read from the node that asks: $Player/Sword goes down, .. goes up, and a leading / starts at root; get_node() reports a missing node and returns null, get_node_or_null() returns null quietly.
  • A plain default value runs before a node’s children exist; @onready runs the lookup just before _ready(), and Godot 4.7 refuses $ without it.
  • A scene-unique name, %Score, skips the nodes in between and survives their renames, but only inside the scene that owns the node; a node made in code needs an owner first.
  • A group tags nodes anywhere in the tree; the SceneTree’s group calls see only nodes in the tree, in hierarchy order, and call_group() with a misspelt method does nothing at all.
  • Between scenes, use an exported reference that the scene holding both nodes sets, or a signal: call down, signal up.

Terms in this lesson

@onready
An annotation on a member variable that delays its default value until just before the node's _ready(), when the node's children exist, as in @onready var label: Label = $Score.
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.
group
A name that tags nodes, such as guards. A node can be in many groups, and the SceneTree lists or calls the members that are inside the tree, in scene hierarchy order.
owner
The node that a node belongs to for saving and for unique names: for every node a scene file makes, the root of that scene. A node made in code has no owner until its owner property is set.
scene-unique node
A node marked Access as Unique Name, which any node of the same scene reaches as %Name, whatever lies between. It is found through the node's owner, so other scenes reach it only through the instance, as in Player/%Eyes.

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.