Nodes & Scenes In Depth

Godot Fundamentals

Chapter 3 · Nodes & Scenes In Depth

Chapter 1 introduced the scene tree conceptually; Chapter 2 covered GDScript itself without touching nodes at all. This chapter puts both together: how to actually navigate a scene tree from code, how to create new nodes at runtime by instancing a whole scene, and a first look at signals — Godot's own way for one node to tell another that something happened, without the two needing to know much about each other.

Navigating the Scene Tree

Once a script is attached to a node, it can reach other nodes in the same tree in a few ways.

MethodGets
get_parent()This node's own parent
get_children()An Array of every direct child
get_node("Path")A specific descendant, by relative path
$PathShorthand for get_node("Path") — used constantly in real code
extends Node2D func _ready() -> void: # $ is shorthand for get_node() - both reach the same child. var sprite = $Sprite2D var same_sprite = get_node("Sprite2D") # A deeper path works the same way a file path does. var nested = $UI/HealthBar print(get_parent().name) print(get_children())
Prefer $Path for anything set up in the editor — it's shorter, and Godot's own editor autocompletes real node names as you type it, catching a typo immediately rather than failing silently at runtime.

Instancing Scenes at Runtime

Every scene saved to a .tscn file can be loaded and created fresh from a script — this is how a game spawns things that don't exist yet when the game starts: a bullet, an enemy, a pickup.

extends Node2D # preload() loads the scene once, at compile time - the fastest option # when the path is known in advance and never changes. const BulletScene: PackedScene = preload("res://bullet.tscn") func shoot() -> void: var bullet = BulletScene.instantiate() add_child(bullet) bullet.position = position
CallWhen it runsUse it when...
preload("res://x.tscn")At compile time, onceThe path is a fixed, known string
load("res://x.tscn")At runtime, each callThe path is built dynamically (e.g. from a variable)
.instantiate()Whenever calledCreates one real, independent copy of that scene as a node
Godot 3 vs. Godot 4 naming — a lot of tutorials still online use .instance(). That was the Godot 3 method name; Godot 4 renamed it to .instantiate(). If a tutorial's code throws an "Invalid call" error on that exact line, this rename is very often why.

An instanced node exists in memory but isn't part of the visible scene tree until add_child() actually attaches it — exactly like Chapter 2's typed Array: creating the object and placing it somewhere are two separate steps.

Removing a node

The reverse operation is queue_free() — it schedules the node for deletion at the end of the current frame, rather than deleting it immediately mid-execution (which can cause errors if other code is still using it that same frame).

bullet.queue_free()

A First Look at Signals

A signal is Godot's own way for a node to announce "something happened" without needing to know who, if anyone, is listening. Any node can declare a custom signal, emit it when the relevant thing occurs, and any other node can connect a function to run whenever that signal fires.

# --- enemy.gd (attached to the Enemy scene) --- extends Node2D signal died(enemy_name: String) func take_damage(amount: int) -> void: health -= amount if health <= 0: died.emit(name) queue_free()
# --- game.gd (a parent node that spawned the enemy) --- extends Node2D func _ready() -> void: $Enemy.died.connect(_on_enemy_died) func _on_enemy_died(enemy_name: String) -> void: print(enemy_name + " was defeated!")

Godot's built-in nodes already come with their own signals too — a Button has a pressed signal, an Area2D has a body_entered signal, and so on. Custom signals declared with signal work exactly the same way, connected and emitted with the identical syntax.

Coming from Python Signals are Godot's own built-in version of the observer pattern — if you've written a callback registered against an event (a button's on_click handler, a custom pub/sub system), the shape is the same idea: something emits an event, and zero or more listeners react to it, with neither side needing a direct reference to the other's internals. What's different is that this pattern is a first-class part of the language itself here, declared with signal and connected with .connect(), rather than something you'd build yourself with a list of callback functions the way you might in plain Python.
Why bother with signals instead of just calling a function directly? A signal keeps the Enemy scene fully independent of whatever happens to be listening — it doesn't need to know the parent scene exists at all. That decoupling is what lets the exact same Enemy scene be reused in a dozen different levels without editing its own script every time. Chapter 7 goes much deeper on this.

Coding Challenges

Challenge 1
Build a scene with a Node2D root and three Node2D children named "Head," "Body," and "Feet." Write a script on the root that uses get_children() to print the name of every child, and separately uses $Body to print that one child's name directly.
→ Solution
Challenge 2
Save a simple scene (e.g. a single Node2D) as its own .tscn file. From a different script, preload it, instantiate three copies with .instantiate(), and add_child() each one at a different position so all three appear in the running scene.
→ Solution
Challenge 3
Declare a custom signal called coin_collected(amount: int) on one node. Emit it with a sample amount, and connect a function on a different node that prints a message using the amount it received.
→ Solution

Quick Reference — Nodes & Scenes In Depth

  • get_parent(), get_children(), get_node("Path") / $Path
  • preload("res://x.tscn") (fixed path) vs. load(...) (dynamic path)
  • .instantiate() creates a node from a PackedScene — .instance() was the old Godot 3 name
  • add_child(node) attaches it to the tree; queue_free() removes it safely at end of frame
  • signal name(params) declares a signal; signal_name.emit(values) fires it; node.signal_name.connect(func) listens for it