Skip to content

G1.2 — Nodes & the SceneTree

What you'll learn

  • State what a node is, and that its type determines what it can do.
  • Read the base hierarchy Node → CanvasItem → {Node2D, Control} and choose the right base.
  • Understand the SceneTree: the live tree of nodes, rooted at one root, that drives the game.
  • See that parent/child means ownership, and that a node outside the tree gets no callbacks.

How it applies

  • The wrong base type silently lacks abilities. A UI element built on Node2D is positioned by raw coordinates instead of anchors and will not adapt to window size; a world object built on Control fights the layout system. Picking the base from the hierarchy is picking the abilities and the rules the node plays by.
  • Freeing a parent frees its children. Parent/child is ownership: remove the parent and the whole subtree goes with it. That is a feature (delete an enemy, its health bar and hitbox go too) and a hazard (free the wrong parent and lose nodes you needed). Knowing the rule prevents both surprises.
  • A node outside the tree is inert. If a node is created but never added as a child, it gets no _ready, no _process, no input, and draws nothing — the "my script isn't running" bug whose cause is that the node was never in the tree. Being in the tree is what makes a node live.
  • Order in the tree is order of execution and drawing. Nodes process and draw in tree order, so two nodes' relationship in the tree affects which draws on top and which updates first — a common source of "the UI is behind the world" or "this updated a frame late."

Concepts

What a node is

A node is the basic unit of a Godot game: a named object with a type, a set of properties, and optionally children. The type is the important part — it decides what the node can do. A Sprite2D can show a texture; a Button can be clicked; a Timer can count down; a plain Node does nothing visual but is a fine home for logic. You build a game by assembling nodes into a tree and attaching scripts (Part A) to give them behavior.

The base hierarchy, and choosing a base

Every node type descends from Node. The branch you care about in 2D:

Node                     (base of everything; logic/managers live here)
└── CanvasItem           (anything that draws in 2D)
    ├── Node2D           (positioned in the 2D world: sprites, bodies, areas)
    └── Control          (UI: laid out by anchors and containers)
  • Node — no position, no drawing. The right base for a manager, a state holder, an autoload, a pure-logic component.
  • Node2D — has a 2D transform (position, rotation, scale). The base for things that exist in the world: the player, enemies, projectiles, pickups.
  • Control — laid out by anchors and containers (G2.4), not by a raw position. The base for UI: labels, buttons, health bars, menus.

Node2D and Control both descend from CanvasItem, the common 2D-drawing base — which is why both can be visible and modulated, but they are positioned by different systems. Mixing them up (a Control where you needed world placement, or a Node2D where you needed responsive UI) is the node-level version of the "wrong base class" lesson from L1.1.

Example

A tiny scene and the base each node should have:

Main            (Node      — just organizes; no transform needed)
├── Player      (Node2D    — exists in the world, has a position)
│   └── Sprite2D
└── HUD         (CanvasLayer + Control children — UI, pinned to the screen)
    └── HealthBar (Control)

Player is a Node2D because it occupies world space; HealthBar is a Control because it is UI laid out on screen. Putting HealthBar under Player as a Node2D would make it move and scale with the world camera instead of staying pinned — the reason UI sits under a CanvasLayer (G2.3).

The SceneTree

When the game runs, all active nodes form the SceneTree — a single tree rooted at a root viewport (the game window). The Scene dock you edit in is the design-time view of a scene; at runtime, the running scene is placed into this one big tree. get_tree() from any node returns the SceneTree object, which drives the main loop, delivers the per-frame callbacks (G2.1), and manages pause and scene changes.

A node becomes part of the tree when it is added as a child of a node already in the tree (via add_child, or by being part of a scene that is loaded). Until then it is an orphan: it exists in memory but is not live.

Parent/child is ownership

The parent/child relationship is ownership, not mere grouping:

  • Freeing a node frees its entire subtree. queue_free() on a parent takes its children with it.
  • A Node2D's transform is relative to its parent: move the parent and the children move with it (G2.2). This is why you group a character and its sprite, weapon, and hitbox under one parent.
  • Processing, visibility, and pause cascade down the tree.

So the structure is not cosmetic. Where a node sits decides what owns it, what it moves with, and when it lives and dies. The guidance (which the game books lean on heavily) is to make parent/child reflect ownership: if removing the parent should logically remove the child, the child belongs under it.

A node outside the tree is inert

Because the SceneTree drives everything, a node that is not in the tree receives no lifecycle callbacks, no input, and does not draw. Creating a node with .new() and forgetting to add_child it is the classic "I wrote _ready but it never runs" bug_ready fires when the node enters the tree, and an orphan never does. Being in the tree is the difference between a node that exists and a node that participates.

Walkthrough

  1. In a new scene, add a plain Node as the root. Under it, add a Node2D, and under that, a Sprite2D (assign it any texture, or Godot's icon). Note the Scene dock shows this as a tree.
  2. Select the Node2D and move it in the 2D viewport. The Sprite2D child moves with it — the child's position is relative to the parent. Now move the Sprite2D alone; it moves within the parent.
  3. Add a Label directly under the root Node. Try to position it like a world object; notice it is a Control and behaves by anchors, not free placement (full treatment in G2.4). This is the Node2D-versus-Control distinction, felt.
  4. Run with F6. In the editor's Remote scene tree (you meet it properly in G2.8), observe that the running game's tree mirrors what you built — that live tree is the SceneTree.

Optional sanity check

Select the root Node, attach a script, and in _ready print "root ready". Run — it prints. Now, in that script, do var loose := Node.new() with its own _ready that prints "loose ready", but do not add_child(loose). Run again: "loose ready" never prints, because the node never entered the tree. Add add_child(loose) and it prints. That is the orphan-node rule, demonstrated.

Self-check quiz

Q1 — You are building a health bar that should stay pinned on screen. Which base type, and why?

A. Node2D, because it has a position you can set. B. Control (under a CanvasLayer), because UI is laid out by anchors and pinned to the screen, not placed in world space. C. Plain Node, because it is the simplest. D. Sprite2D, because it draws.

Reveal answer

B. A health bar is UI; Control nodes are laid out by anchors/containers and, under a CanvasLayer, stay fixed on screen regardless of the world camera. A would tie the bar to world coordinates so it scrolls with the camera. C cannot draw or lay out UI. D is a world sprite, not a layout-aware UI element.

Q2 — You call var n = Node.new() and give it a _ready that prints, but it never prints. Why?

A. _ready only runs in the main scene. B. The node was never added to the tree (add_child), so it is an orphan and never receives _ready. C. Node.new() is invalid. D. _ready requires @onready.

Reveal answer

B. _ready fires when a node enters the tree; a node created with .new() but never added as a child stays an orphan and gets no callbacks. A is false — _ready runs for any node that enters the tree, not only the main scene. C is valid code. D confuses a variable annotation with the lifecycle callback.

Q3 — What happens to a node's children when you queue_free() the node?

A. The children are reparented to the root automatically. B. The children are freed too — the whole subtree goes, because parent/child is ownership. C. Nothing; children are independent. D. Only direct children are freed, not grandchildren.

Reveal answer

B. Parent/child expresses ownership, so freeing a node frees its entire subtree, children and grandchildren alike. A invents automatic reparenting. C and D understate the rule — the whole subtree is freed, which is exactly why you group owned nodes (sprite, hitbox, bar) under the thing that owns them.

Integration question

Q4 — open

You build an enemy as Node2D with child nodes for its sprite, a hitbox, and a floating health bar, and you put the health bar's Control directly under the enemy Node2D. Two problems appear: the health bar scrolls and scales oddly with the camera instead of floating cleanly, and when the enemy dies you call queue_free() on the enemy and worry whether you must also free the hitbox and bar. Resolve both using this chapter's ideas about base types and parent/child ownership.

Reveal expected answer

The health-bar behavior is the Node2D-versus-Control distinction: a Control placed under a world Node2D is dragged through the world transform and camera, so it scales and scrolls instead of behaving like screen UI. The fix is to put UI under a CanvasLayer (or otherwise out of the world transform) so anchors lay it out against the screen — world objects are Node2D, UI is Control under a CanvasLayer (G2.3–G2.4). The cleanup worry is answered by parent/child being ownership: because the sprite, hitbox, and bar are children of the enemy, queue_free() on the enemy frees the entire subtree automatically — you do not free them individually. That is precisely why owned parts are grouped under the thing that owns them: one free call disposes the whole unit. The chapter's two threads — choose the base for the rules you want, and let the tree express ownership — each resolve one half of the problem.