Game Development with Godot & GDScript Module 4 – The scene tree, nodes and signals
The scene tree and node lifecycle
See the order of _init, _enter_tree, _ready, _process and _exit_tree in Godot 4.7, add and remove nodes at run time, and delete them safely with queue_free.
What you will learn
- Predict the order of _init, _enter_tree, _ready, _process and _exit_tree in a tree of nodes
- Add, remove and re-add nodes at run time with add_child, remove_child and request_ready
- Delete nodes with queue_free or free, and guard old references with is_instance_valid
- Clean up with _notification and NOTIFICATION_PREDELETE, and find orphan nodes that leak
Before you start
What you’ll learn · Before you start
What you will learn
- Predict the order of _init, _enter_tree, _ready, _process and _exit_tree in a tree of nodes
- Add, remove and re-add nodes at run time with add_child, remove_child and request_ready
- Delete nodes with queue_free or free, and guard old references with is_instance_valid
- Clean up with _notification and NOTIFICATION_PREDELETE, and find orphan nodes that leak
Before you start
On this page
In one frame of a five-node scene, Godot called its scripts 25 times: five _init(), five _enter_tree(), five
_ready(), five _process() and five _exit_tree(). Their order decides what each script can count on, and it is not
one order: _enter_tree() went down from the root, while _ready() came back up from the deepest node.
This page follows nodes through their whole life, from being made to being deleted. You will add and remove nodes while
the game runs, see why queue_free() waits for the end of the frame, catch the error that a deleted node leaves behind,
hear the notification a node gets as it is deleted, and find nodes that leak because nothing deleted them.
Nodes enter the tree from the top and get ready from the bottom
lifecycle.tscn is a small level: Level, with the children Player and HUD, a Sword under Player and a Score under HUD.
The same script, lifecycle_log.gd, is attached to all five nodes and prints each callback with the node’s name:
extends Node
# Attached to every node of lifecycle.tscn: prints each callback Godot makes, with the node's name.
func _init() -> void:
print("_init name '", name, "', parent ", get_parent())
func _enter_tree() -> void:
print("_enter_tree ", name)
func _ready() -> void:
print("_ready ", name)
func _process(_delta: float) -> void:
print("_process ", name)
func _exit_tree() -> void:
print("_exit_tree ", name) from session import files, run
files("project.godot", "lifecycle.tscn", "lifecycle_log.gd")
# One frame of the five-node scene; then the run ends, and the scene leaves the tree.
run("""
godot --headless --no-header --fixed-fps 60 --quit-after 1 lifecycle.tscn
""") Output
$ godot --headless --no-header --fixed-fps 60 --quit-after 1 lifecycle.tscn _init name '', parent <Object#null> _init name '', parent <Object#null> _init name '', parent <Object#null> _init name '', parent <Object#null> _init name '', parent <Object#null> _enter_tree Level _enter_tree Player _enter_tree Sword _enter_tree HUD _enter_tree Score _ready Sword _ready Player _ready Score _ready HUD _ready Level _process Level _process Player _process Sword _process HUD _process Score _exit_tree Score _exit_tree HUD _exit_tree Sword _exit_tree Player _exit_tree Level
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lifecycle_session.py
Read the output in blocks of five. _init() runs while the scene builds each node, before the node has a name or a
parent, so it printed an empty name five times. Then the nodes enter the scene tree: The one tree of nodes of a running game, with a Window named root at the top. Every node in it has a path from the top, such as /root/Player/Camera. More in “Nodes, scenes and instancing”, and
the two callbacks that follow go in opposite directions. Step through the run and watch the sub-labels collect each
node’s place:
The drawing shows the whole scene; its 10 steps are listed below.
Every step as text
- Five nodes are made. Godot reads lifecycle.tscn and makes its five nodes. Each one's _init() runs while it is being made, before it has a name or a parent: the run printed name '' and parent <Object#null> five times.
- Level enters first. The scene's root enters the tree first, so _enter_tree() runs on Level before any node below it. Its children exist, but they are not in the tree yet.
- Down one branch. Next comes Player, then Player's child Sword. Godot goes all the way down one branch before it starts the next one, the order called pre-order.
- Then the next branch. HUD enters fourth and its child Score fifth. All five _enter_tree() calls are done, and no node is ready yet.
- The deepest node is ready first. _ready() runs on a node only when all its children are ready. Sword has none, so its _ready() runs first of all.
- Then its parent. Player's only child is ready, so Player's _ready() runs second. Inside it, Player can use Sword, which is set up.
- The other branch. The same happens on the other side. Score is ready third and HUD fourth.
- The root is ready last. Level's _ready() runs last, when every node below it is ready. That makes the root's _ready() the place for code that needs the whole scene.
- Every frame, in tree order. From now on _process() runs once a frame on every node, in the same order as _enter_tree(), top to bottom as the Scene dock shows them. The run printed Level, Player, Sword, HUD and Score.
- Leaving in reverse. When the run ends, the nodes leave the tree in the exact reverse order. _exit_tree() ran on Score, HUD, Sword, Player and finally Level, so every child leaves before its parent.
_enter_tree() runs on a parent before its children, going down one branch before the next: Level, Player, Sword, HUD,
Score. This is tree order: The order in which Godot goes through the nodes of the scene tree: a parent before its children, and one branch to its end before the next, top to bottom as the Scene dock lists them. _enter_tree() and _process() follow it., the order of the Scene dock from top to bottom, and _process()
follows it too in every frame. _ready() waits instead: a node is ready only when all its children are, so the deepest
node of the first branch, Sword, was ready first and the root, Level, last. _exit_tree() is the mirror of
_enter_tree(): when the run ended, Score left first and Level last.
| Callback | When it runs | Order across the tree |
|---|---|---|
_init() |
once, while the node is made, before it has a name or a parent | as the nodes are made |
_enter_tree() |
every time the node enters the tree | parents first: Level, Player, Sword … |
_ready() |
once, when the node and all its children are in the tree | children first: Sword, Player, Score … |
_process(delta) |
every frame while the node is in the tree | tree order |
_exit_tree() |
every time the node leaves the tree | children first, the reverse of tree order |
The order has a practical meaning. In its _ready(), a parent can use its children, because their own _ready() has
already run. A child’s _ready() runs before its parent’s, so a child that reads something its parent sets in
_ready() gets the value from before that setup.
Key idea
_enter_tree() goes down the tree, parents first; _ready() comes back up, children first; _process() runs in
tree order every frame; _exit_tree() runs in reverse, so every child leaves before its parent.
Checkpoint
Check: The order of the lifecycle callbacks
Removing a node keeps it alive, and it is ready only once
A game adds nodes while it runs: a pause menu when the player presses Escape, a bullet for every shot. Called on a
parent that is in the tree, add_child() puts the node in and runs its _enter_tree() and _ready() before the call
returns. remove_child() is not its opposite in every way: it takes the node out and runs _exit_tree(), but it
deletes nothing. pause_menu.gd opens and closes a menu in the three ways a game does:
extends Node
# Opens and closes a pause menu the way a game might: add it, take it out, put it back.
const Probe := preload("res://lifecycle_log.gd")
func _ready() -> void:
var menu: Node = Probe.new()
menu.name = "PauseMenu"
print("-- add_child(menu)")
add_child(menu)
print("-- remove_child(menu)")
remove_child(menu)
print("menu is valid: ", is_instance_valid(menu), ", in the tree: ", menu.is_inside_tree())
print("-- add_child(menu) again")
add_child(menu)
print("-- menu.request_ready(), then remove_child() and add_child()")
menu.request_ready()
remove_child(menu)
add_child(menu)
get_tree().quit() from session import files, run
files("project.godot", "pause_menu.tscn", "pause_menu.gd", "lifecycle_log.gd")
run("""
godot --headless --no-header --fixed-fps 60 pause_menu.tscn
""") Output
$ godot --headless --no-header --fixed-fps 60 pause_menu.tscn _init name '', parent <Object#null> -- add_child(menu) _enter_tree PauseMenu _ready PauseMenu -- remove_child(menu) _exit_tree PauseMenu menu is valid: true, in the tree: false -- add_child(menu) again _enter_tree PauseMenu -- menu.request_ready(), then remove_child() and add_child() _exit_tree PauseMenu _enter_tree PauseMenu _ready PauseMenu _process PauseMenu _exit_tree PauseMenu
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 menu_session.py
Follow the same run as the life of one node. The branch at the side is the way back into the tree:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Made, not in the tree. Probe.new() makes the PauseMenu node and runs its _init(). The node exists, but it is in no tree, so Godot calls nothing else on it yet.
- Added and made ready. add_child(menu) puts it in the tree before the call returns. _enter_tree() runs, then _ready(), because the menu has no children to wait for.
- Taken out, not deleted. remove_child(menu) takes it out of the tree, and _exit_tree() runs. The node is not deleted. The run printed menu is valid true, in the tree false.
- Added again. add_child(menu) again runs _enter_tree() a second time but not _ready(). A node becomes ready once in its life.
- Ready on request. menu.request_ready() asks for one more _ready(). It runs at the next entry, so after one more remove_child() and add_child() the run printed _ready PauseMenu again.
- Processed every frame. While the menu is in the tree, _process() runs on it once a frame. One frame passed before this run ended, and the run printed _process PauseMenu once.
- Deleted at the end. Only freeing a node deletes it. Here the run ended, and Godot freed the whole tree, so the menu left it one last time with _exit_tree().
Three facts come out of the run. After remove_child() the menu was still valid and simply out of the tree; a node
outside the tree is an orphan node: A node that exists but is not in the scene tree, such as one made with new() and never added, or one taken out with remove_child(). It gets no callbacks, and it stays in memory until something frees it., and it stays in memory until something adds it back or
deletes it. Added again, it ran _enter_tree() but not _ready(): the Node class reference says _ready() may be
called only once for each node. And request_ready() asks for one more _ready(), which runs the next time the node
enters the tree, as the last add_child() showed. A menu that builds its buttons in _ready() and is reopened by
adding it back would therefore show its old buttons, unless it asks with request_ready().
Setting up again in _ready()
A pickup that resets its value in _ready() and is reused by taking it out and adding it back keeps its old value,
because _ready() does not run again. Put the reset in _enter_tree(), which runs on every entry, or call
request_ready() before the pickup goes back in.
Checkpoint
Check: Adding, removing and re-adding nodes
A queued node lives until the end of the frame
Bullets show the other half of a node’s life. gun.gd fires one bullet a frame for four frames, adding each as a child
of the gun, and every bullet deletes itself with queue_free() in its second frame:
extends Node
# Fires one bullet a frame for four frames. Each bullet is added as a child of the gun.
const Bullet := preload("res://bullet.gd")
var frame := 0
func _process(_delta: float) -> void:
frame += 1
if frame <= 4:
var bullet := Bullet.new()
bullet.name = "Bullet%d" % frame
add_child(bullet)
print("frame ", frame, ": fired ", bullet.name, "; children of the gun: ", get_child_count())
else:
print("frame ", frame, ": children of the gun: ", get_child_count()) extends Node
# A bullet that deletes itself in its second frame.
var age := 0
func _process(_delta: float) -> void:
age += 1
print(" ", name, " _process, age ", age)
if age == 2:
queue_free()
print(" ", name, " called queue_free(), and is still in the tree: ", is_inside_tree())
func _exit_tree() -> void:
print(" ", name, " leaves the tree") freeing.gd then deletes two nodes the two ways Godot offers, and makes the mistake that follows from the second one:
extends Node
# Deletes one node with queue_free() and one with free(), then looks at what is left of them.
var coin: Node2D
var gem: Node2D
var frame := 0
func _ready() -> void:
coin = Node2D.new()
coin.name = "Coin"
add_child(coin)
coin.queue_free()
print("Coin after queue_free(): valid ", is_instance_valid(coin), ", in the tree ",
coin.is_inside_tree(), ", queued ", coin.is_queued_for_deletion())
gem = Node2D.new()
gem.name = "Gem"
add_child(gem)
gem.free()
print("Gem after free(): valid ", is_instance_valid(gem), ", equal to null ", gem == null,
", prints as ", gem)
func _process(_delta: float) -> void:
frame += 1
print("frame ", frame, ": Coin valid ", is_instance_valid(coin), ", children ", get_child_count())
if frame == 2:
get_tree().quit()
# The mistake: using a variable that still refers to a node that was freed.
print("Gem's name: ", gem.name) from session import files, run
files("project.godot", "gun.tscn", "gun.gd", "bullet.gd", "freeing.tscn", "freeing.gd")
# Six frames of the gun, then the two ways of deleting a node; the second run ends with a script error on purpose.
run("""
godot --headless --no-header --fixed-fps 60 --quit-after 6 gun.tscn
!godot --headless --no-header --fixed-fps 60 freeing.tscn
""") Output
$ godot --headless --no-header --fixed-fps 60 --quit-after 6 gun.tscn
frame 1: fired Bullet1; children of the gun: 1
frame 2: fired Bullet2; children of the gun: 2
Bullet1 _process, age 1
frame 3: fired Bullet3; children of the gun: 3
Bullet1 _process, age 2
Bullet1 called queue_free(), and is still in the tree: true
Bullet2 _process, age 1
Bullet1 leaves the tree
frame 4: fired Bullet4; children of the gun: 3
Bullet2 _process, age 2
Bullet2 called queue_free(), and is still in the tree: true
Bullet3 _process, age 1
Bullet2 leaves the tree
frame 5: children of the gun: 2
Bullet3 _process, age 2
Bullet3 called queue_free(), and is still in the tree: true
Bullet4 _process, age 1
Bullet3 leaves the tree
frame 6: children of the gun: 1
Bullet4 _process, age 2
Bullet4 called queue_free(), and is still in the tree: true
Bullet4 leaves the tree
$ godot --headless --no-header --fixed-fps 60 freeing.tscn
Coin after queue_free(): valid true, in the tree true, queued true
Gem after free(): valid false, equal to null true, prints as <Freed Object>
frame 1: Coin valid false, children 0
frame 2: Coin valid false, children 0
SCRIPT ERROR: Invalid access to property or key 'name' on a base object of type 'previously freed'.
at: _process (res://freeing.gd:30)
GDScript backtrace (most recent call first):
[0] _process (res://freeing.gd:30)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 freeing_session.py
In the gun’s run, Bullet1 called queue_free() in frame 3 and was still in the tree on the next line; it left the
tree only after every other node had processed that frame. A bullet added during frame 1 also got its first _process()
in frame 2, not straight away. queue_free() only marks the node, and Godot deletes it at the end of the current frame.
Until then it is valid, it is still a child, and is_queued_for_deletion() says it is going. That makes it the safe
choice: any code that still holds the node this frame keeps working.
free() deletes at once. Gem was invalid on the very next line, yet the variable gem was not cleared: it still
held the deleted object, which prints as <Freed Object>, and reading gem.name two frames later stopped
_process() with a “previously freed” error. Godot 4.7.2 counts such a variable as equal to null, as the run
printed, but the Object class reference says not to test for a deleted object by comparing it with null: use
is_instance_valid(), which asks whether the object still exists.
| Call | What happens to the node | Use it for |
|---|---|---|
queue_free() |
deleted at the end of the frame, after the deferred calls; valid until then | deleting a node in a game, the usual choice |
free() |
deleted at once; every reference to it is “previously freed” | a node made and dropped in the same function |
remove_child() |
taken out of the tree, not deleted: an orphan, still valid | keeping a node to add again later |
Checkpoint
Check: queue_free(), free() and old references
Notifications tell a node everything, up to its deletion
The callbacks are the common cases of something more general. Godot sends every object
notification: A numbered message Godot sends to an object, such as NOTIFICATION_READY or NOTIFICATION_PREDELETE. A script hears every one in _notification(what); callbacks such as _ready() are notifications with a function of their own., numbered messages such as NOTIFICATION_READY, and a script hears all of
them in one function, _notification(what). chest.gd prints the ones it knows by name while notify.gd takes it
through its life. Then a RefCounted object, save_handle.gd, loses its last reference:
extends Node
# Prints each notification in NAMES that it receives. Godot sends a few more, which this script skips.
const NAMES := {
NOTIFICATION_PATH_RENAMED: "PATH_RENAMED",
NOTIFICATION_PARENTED: "PARENTED",
NOTIFICATION_ENTER_TREE: "ENTER_TREE",
NOTIFICATION_POST_ENTER_TREE: "POST_ENTER_TREE",
NOTIFICATION_READY: "READY",
NOTIFICATION_EXIT_TREE: "EXIT_TREE",
NOTIFICATION_UNPARENTED: "UNPARENTED",
NOTIFICATION_PREDELETE: "PREDELETE",
}
func _notification(what: int) -> void:
if NAMES.has(what):
print(" ", name, " got NOTIFICATION_", NAMES[what]) extends RefCounted
# Not a node: Godot deletes it by itself as soon as nothing refers to it any more.
func _notification(what: int) -> void:
if what == NOTIFICATION_PREDELETE:
print(" SaveHandle got NOTIFICATION_PREDELETE: the place to close its file") extends Node
# Takes a chest through its whole life, then drops the last reference to a RefCounted object.
const Chest := preload("res://chest.gd")
const SaveHandle := preload("res://save_handle.gd")
func _ready() -> void:
print("-- new(), then name = \"Chest\"")
var chest: Node = Chest.new()
chest.name = "Chest"
print("-- add_child(chest)")
add_child(chest)
print("-- remove_child(chest)")
remove_child(chest)
print("-- chest.free()")
chest.free()
print("-- handle = null")
var handle: RefCounted = SaveHandle.new()
handle = null
print("-- the end of _ready()")
get_tree().quit() end_of_frame.gd shows the end of a frame in which a node with children is queued for deletion. Player, Sword,
Shield and HUD carry goodbye_log.gd, which prints only _exit_tree() and NOTIFICATION_PREDELETE:
extends Node
# In frame 1, queues Player for deletion and asks for a deferred call; in frame 2, lists what is left.
var frame := 0
func _process(_delta: float) -> void:
frame += 1
if frame == 1:
print("frame 1, _process: queue_free() on Player, then call_deferred()")
$Player.queue_free()
report.call_deferred()
print("frame 1, _process ends")
elif frame == 2:
var names: Array[String] = []
for child in get_children():
names.append(child.name)
print("frame 2: children of Level: ", names)
get_tree().quit()
func report() -> void:
print("deferred call: Player is still valid: ", is_instance_valid($Player)) from session import files, run
files("project.godot", "notify.tscn", "notify.gd", "chest.gd", "save_handle.gd",
"end_of_frame.tscn", "end_of_frame.gd", "goodbye_log.gd")
run("""
godot --headless --no-header --fixed-fps 60 notify.tscn
godot --headless --no-header --fixed-fps 60 end_of_frame.tscn
""") Output
$ godot --headless --no-header --fixed-fps 60 notify.tscn -- new(), then name = "Chest" Chest got NOTIFICATION_PATH_RENAMED -- add_child(chest) Chest got NOTIFICATION_PARENTED Chest got NOTIFICATION_ENTER_TREE Chest got NOTIFICATION_POST_ENTER_TREE Chest got NOTIFICATION_READY -- remove_child(chest) Chest got NOTIFICATION_EXIT_TREE Chest got NOTIFICATION_UNPARENTED -- chest.free() Chest got NOTIFICATION_PREDELETE -- handle = null SaveHandle got NOTIFICATION_PREDELETE: the place to close its file -- the end of _ready() $ godot --headless --no-header --fixed-fps 60 end_of_frame.tscn frame 1, _process: queue_free() on Player, then call_deferred() frame 1, _process ends deferred call: Player is still valid: true PREDELETE Player _exit_tree Shield _exit_tree Sword _exit_tree Player PREDELETE Shield PREDELETE Sword frame 2: children of Level: ["HUD"] _exit_tree HUD PREDELETE HUD
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 notify_session.py
The chest heard more than the callbacks: PARENTED before it entered and UNPARENTED after it left, and
PREDELETE when free() deleted it. NOTIFICATION_PREDELETE comes when an object is about to be deleted, and it has
no callback of its own, so _notification() is the only way to act on it. The SaveHandle heard it the moment handle = null
dropped the only reference: a An object that counts the variables and other objects that refer to it, and deletes itself as soon as the count reaches zero. Resources work this way; nodes do not, so a node must be freed.
object deletes itself, while a node waits until something frees it.
The second run is a frame with a deletion in it. Step through what happened to Player:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Player is marked. In frame 1, Level's _process() calls $Player.queue_free() and report.call_deferred(). Neither happens yet; the rest of _process() runs, and the run printed "frame 1, _process ends".
- Deferred calls come first. When processing is over, Godot runs the calls that were deferred. report() finds Player still valid, because a queued node is deleted only after every deferred call.
- PREDELETE comes first. Now Player is deleted. It receives NOTIFICATION_PREDELETE first, while it is still in the tree with its children.
- The subtree leaves. Deleting a node takes it out of the tree, with everything under it: _exit_tree() runs on Shield, Sword and Player, children first.
- The children are deleted. Deleting a parent deletes its children too. Shield and Sword receive their PREDELETE last, after they have left the tree.
- The next frame. In frame 2, Level has one child left, HUD: the run printed children of Level: ["HUD"]. Then the game quits, and HUD goes the other way round: it leaves the tree first and gets its PREDELETE after.
report(), 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”, still found Player valid, because a queued node goes after every deferred call. Then
Player received PREDELETE while it was still in the tree, its subtree left the tree children first, and only then were
Sword and Shield deleted. When the game quits, the order is the other way round, as the run’s last two lines show: HUD
left the tree with the rest of the game, and only then was it deleted.
Undo in _exit_tree(), close in PREDELETE
Work that a node does on entering, such as joining a list of targets, is undone in _exit_tree(), which runs every
time the node leaves, deleted or not. Keep NOTIFICATION_PREDELETE for what must happen once, when the object is
deleted, such as closing a file it holds open.
Checkpoint
Check: Notifications and PREDELETE
A node nobody deletes is an orphan, and it leaks
despawn.gd spawns an enemy every frame and keeps two: each frame, the oldest one goes. One scene takes it out with
remove_child(), the other with queue_free(). Every frame prints the enemies in the tree and the orphan count from
Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT); in frame 5, the first scene also calls
Node.print_orphan_nodes():
extends Node
# Spawns an enemy every frame and keeps two: the oldest one goes, with remove_child() or with queue_free().
@export var use_queue_free := false
var frame := 0
func _process(_delta: float) -> void:
frame += 1
var enemy := Node2D.new()
enemy.name = "Enemy%d" % frame
add_child(enemy)
if get_child_count() > 2:
var oldest := get_child(0)
if use_queue_free:
oldest.queue_free()
else:
remove_child(oldest)
print("frame ", frame, ": enemies in the tree ", get_child_count(), ", orphan nodes ",
Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT))
if frame == 5 and not use_queue_free:
Node.print_orphan_nodes() from session import files, run
files("project.godot", "despawn.gd", "despawn.tscn", "despawn_fixed.tscn")
# Five frames with remove_child(), then five with queue_free(). print_orphan_nodes() starts each line with an object
# id that is new on every run, shown here as <id>.
run("""
godot --headless --no-header --fixed-fps 60 --quit-after 5 despawn.tscn
godot --headless --no-header --fixed-fps 60 --quit-after 5 despawn_fixed.tscn
""", mask=[(r"(?m)^\d+ - Stray Node", "<id> - Stray Node")]) Output
$ godot --headless --no-header --fixed-fps 60 --quit-after 5 despawn.tscn frame 1: enemies in the tree 1, orphan nodes 0.0 frame 2: enemies in the tree 2, orphan nodes 0.0 frame 3: enemies in the tree 2, orphan nodes 1.0 frame 4: enemies in the tree 2, orphan nodes 2.0 frame 5: enemies in the tree 2, orphan nodes 3.0 <id> - Stray Node: Enemy2 (Type: Node2D) (Source:) <id> - Stray Node: Enemy3 (Type: Node2D) (Source:) <id> - Stray Node: Enemy1 (Type: Node2D) (Source:) WARNING: 3 RIDs of type "CanvasItem" were leaked. at: _free_rids (servers/rendering/renderer_canvas_cull.cpp:2735) WARNING: 3 ObjectDB instances were leaked at exit (run with `--verbose` for details). at: cleanup (core/object/object.cpp:2536) $ godot --headless --no-header --fixed-fps 60 --quit-after 5 despawn_fixed.tscn frame 1: enemies in the tree 1, orphan nodes 0.0 frame 2: enemies in the tree 2, orphan nodes 0.0 frame 3: enemies in the tree 3, orphan nodes 0.0 frame 4: enemies in the tree 3, orphan nodes 0.0 frame 5: enemies in the tree 3, orphan nodes 0.0
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 orphans_session.py
With remove_child(), the level never held more than two enemies, so the scene tree looked tidy, but the orphan count
rose by one every frame from frame 3. print_orphan_nodes() named the three nodes, and when the run ended Godot
reported them twice: 3 ObjectDB instances were leaked at exit, and three leaked CanvasItem resources, the drawing
resources that each Node2D enemy had reserved on the rendering server. With queue_free(), the count stayed at 0. Notice also that the second scene printed 3 enemies in
the tree from frame 3: a queued enemy is still a child until the frame ends.
Both tools work only in debug builds, the kind the editor and a test run use; in a release export the monitor returns 0.
So watch the orphan count while you test. A count that only goes up means nodes are being taken out, or made with
new(), and never deleted.
Key idea
Only freeing deletes a node: a node taken out with remove_child() or never added is an orphan that keeps its memory
until something frees it, and the orphan monitor is how you see it.
Checkpoint
Check: Orphan nodes and leaks
Practise: a spawner that cleans up after itself
spawner.gd adds enemies to the level it is in, its parent, so that they do not move when the spawner moves. Your task:
when the spawner leaves the tree, it must free every enemy it spawned that is still alive. Some will already have been
deleted by the game.
- Create an empty folder for the task and an empty
project.godotfile inside it; save the two scripts below next to that file. - Write your code in place of the
passline of_exit_tree()inspawner.gd. - Start the checker from a terminal in the same folder:
godot --headless --no-header --script check_spawner.gd.
extends Node
# A spawner that adds enemies to the level it is in (its parent), so they do not move with it.
# Your task: when the spawner leaves the tree, free every enemy it spawned that is still alive.
var spawned: Array[Node] = []
func spawn(count: int) -> void:
for i in count:
var enemy := Node2D.new()
enemy.name = "Enemy%d" % (spawned.size() + 1)
get_parent().add_child(enemy)
spawned.append(enemy)
func _exit_tree() -> void:
pass # Replace this line with your code. extends SceneTree
# Checks spawner.gd. Run it in the project's folder: godot --headless --no-header --script check_spawner.gd
# The checker plays four frames: it builds a level, lets two enemies die, then deletes the spawner.
var level: Node
var player: Node
var spawner: Node
var enemies: Array[Node] = []
var orphans_at_start := 0
var frame := 0
var passed := 0
var total := 0
func _process(_delta: float) -> bool:
frame += 1
if frame == 1:
orphans_at_start = orphan_count()
level = Node.new()
level.name = "Level"
root.add_child(level)
player = Node2D.new()
player.name = "Player"
level.add_child(player)
spawner = load("res://spawner.gd").new()
spawner.name = "Spawner"
level.add_child(spawner)
spawner.spawn(5)
for child in level.get_children():
if child != player and child != spawner:
enemies.append(child)
check(enemies.size() == 5, "spawn(5) adds 5 enemies to the level (it added %d)" % enemies.size())
print("Two enemies die in the game: %s and %s" % [enemies[1].name, enemies[3].name])
enemies[1].queue_free()
enemies[3].queue_free()
elif frame == 2:
print("The spawner is deleted with queue_free()")
spawner.queue_free()
elif frame == 4:
var alive := 0
for enemy in enemies:
if is_instance_valid(enemy):
alive += 1
check(alive == 0, "no enemy of the spawner is left (%d left)" % alive)
check(is_instance_valid(player) and player.is_inside_tree(), "the player is still in the level")
check(orphan_count() == orphans_at_start, "no orphan nodes are left (%d at the start, %d now)" % [orphans_at_start, orphan_count()])
level.free()
print("%d of %d checks passed" % [passed, total])
quit(0 if passed == total else 1)
return false
func orphan_count() -> int:
return int(Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT))
func check(ok: bool, what: String) -> void:
total += 1
passed += int(ok)
print("%s %s" % ["PASS" if ok else "FAIL", what]) The checker builds a level with a player and the spawner, spawns five enemies, lets the game delete two of them, then deletes the spawner. This is what it printed for a correct spawner, and for a tempting one that frees every enemy in its list without looking first:
from session import files, run
# The checker on a spawner.gd that solves the task, then on one that frees every enemy without checking it first.
files("practice/project.godot=right/project.godot", "practice/check_spawner.gd=right/check_spawner.gd",
"practice/solution/spawner.gd=right/spawner.gd",
"practice/project.godot=tempting/project.godot", "practice/check_spawner.gd=tempting/check_spawner.gd",
"practice/tempting/spawner.gd=tempting/spawner.gd")
run("""
cd right
godot --headless --no-header --script check_spawner.gd
cd ../tempting
!godot --headless --no-header --script check_spawner.gd
""") Output
$ cd right
$ godot --headless --no-header --script check_spawner.gd
PASS spawn(5) adds 5 enemies to the level (it added 5)
Two enemies die in the game: Enemy2 and Enemy4
The spawner is deleted with queue_free()
PASS no enemy of the spawner is left (0 left)
PASS the player is still in the level
PASS no orphan nodes are left (0 at the start, 0 now)
4 of 4 checks passed
$ cd ../tempting
$ godot --headless --no-header --script check_spawner.gd
PASS spawn(5) adds 5 enemies to the level (it added 5)
Two enemies die in the game: Enemy2 and Enemy4
The spawner is deleted with queue_free()
SCRIPT ERROR: Cannot call method 'queue_free' on a previously freed instance.
at: _exit_tree (res://spawner.gd:19)
GDScript backtrace (most recent call first):
[0] _exit_tree (res://spawner.gd:19)
FAIL no enemy of the spawner is left (2 left)
PASS the player is still in the level
PASS no orphan nodes are left (0 at the start, 0 now)
3 of 4 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 tempting answer called queue_free() on Enemy2, which the game had already deleted. The error stopped
_exit_tree() at that line, so Enemy3 and Enemy5, later in the list, were never freed. A correct spawner ends with
4 of 4 checks passed. After that, the questions below go over the five ideas of this page, starting with the one you
know least, and each choice says at once why it is right or wrong:
Practice that adapts to you
Practise this lesson
17 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 the difference between _enter_tree() and _ready() in Godot?
_enter_tree() runs every time a node enters the scene tree, and it runs on a parent before its children, so when it
runs the children are not in the tree yet. _ready() runs once in a node’s life, on its first entry, and only after all
its children have entered and run their own _ready(). In a five-node scene on Godot 4.7.2 the enter calls came as
Level, Player, Sword, HUD, Score and the ready calls as Sword, Player, Score, HUD, Level. So _ready() is the place
for setup that needs the children, such as reading $Sword. A node taken out with remove_child() and added again gets
_enter_tree() again but not _ready(), unless request_ready() was called. _exit_tree() is the counterpart of
_enter_tree(), and it runs children first.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
During long boss fights a shooter's memory use climbs and never comes back, yet the scene tree never holds more than 40 bullets. Bullets that fly off the screen run get_parent().remove_child(self). What is going on, how do you prove it, and what do you change?
-
Fresher to mid
A Level script counts its enemies in _ready() and stores the number in enemy_count. The HUD, a child of Level, shows level.enemy_count in its own _ready() and always shows 0, though the level has 12 enemies. Explain the order of events and give two ways to fix it.
Key takeaways
_init()runs while a node is made, before it has a name or a parent;_enter_tree()then runs parents first,_ready()children first,_process()in tree order every frame, and_exit_tree()in reverse tree order.add_child()runs_enter_tree()and_ready()before it returns,remove_child()takes a node out without deleting it, and_ready()runs once in a node’s life unlessrequest_ready()asks for another.queue_free()deletes a node at the end of the frame, after the deferred calls, so it stays valid until then;free()deletes at once, andis_instance_valid()is the test for a reference that may be stale._notification(what)hears every notification, andNOTIFICATION_PREDELETEcomes just before an object is deleted; aRefCountedobject deletes itself when its last reference goes, while a node must be freed.- A node outside the tree that nothing frees is an orphan and leaks; the orphan monitor and
print_orphan_nodes()find it in debug builds.
Terms in this lesson
- notification
- A numbered message Godot sends to an object, such as NOTIFICATION_READY or NOTIFICATION_PREDELETE. A script hears every one in _notification(what); callbacks such as _ready() are notifications with a function of their own.
- orphan node
- A node that exists but is not in the scene tree, such as one made with new() and never added, or one taken out with remove_child(). It gets no callbacks, and it stays in memory until something frees it.
- tree order
- The order in which Godot goes through the nodes of the scene tree: a parent before its children, and one branch to its end before the next, top to bottom as the Scene dock lists them. _enter_tree() and _process() follow it.
References
- Using SceneTree (Godot Engine)
- Overridable functions (Godot Engine)
- Godot notifications (Godot Engine)
- Nodes and scene instances (Godot Engine)
- Node class reference: callbacks, add_child, remove_child, queue_free and request_ready (Godot Engine)
- Object class reference: free, _notification and NOTIFICATION_PREDELETE (Godot Engine)
- @GlobalScope class reference: is_instance_valid (Godot Engine)
- RefCounted class reference (Godot Engine)
- Performance class reference: OBJECT_ORPHAN_NODE_COUNT (Godot Engine)
Report a problem with this lesson
Kept only in this browser. Your Learn progress