Scene Graph
A scene graph is a hierarchical tree of game objects. Each node has a local transform; parents propagate to children. Lets you move a car and its wheels together, or build modular prefabs.
Parent/child transforms, traversal, prefabs
EXAMPLE
// 1) Node structure
class Node {
constructor(name) {
this.name = name;
this.parent = null;
this.children = [];
// Local transform
this.position = { x: 0, y: 0, z: 0 };
this.rotation = { x: 0, y: 0, z: 0 };
this.scale = { x: 1, y: 1, z: 1 };
// Cached world transform (recomputed on update)
this.worldMatrix = mat4.identity();
}
add(child) {
if (child.parent) child.parent.remove(child);
child.parent = this;
this.children.push(child);
}
remove(child) {
const i = this.children.indexOf(child);
if (i >= 0) {
this.children.splice(i, 1);
child.parent = null;
}
}
updateTransforms() {
const local = mat4.compose(this.position, this.rotation, this.scale);
this.worldMatrix = this.parent
? mat4.multiply(this.parent.worldMatrix, local)
: local;
for (const c of this.children) c.updateTransforms();
}
}
// 2) Build a hierarchy
const scene = new Node('scene');
const car = new Node('car');
car.position = { x: 5, y: 0, z: 0 };
scene.add(car);
for (let i = 0; i < 4; i++) {
const wheel = new Node(`wheel_${i}`);
wheel.position = wheelPositions[i];
car.add(wheel);
}
// Move the car → wheels move with it
car.position.x += 1;
scene.updateTransforms();
// 3) Detach + reattach (useful for picking up items)
const gun = new Node('gun');
world.add(gun);
// player picks up gun:
player.hand.add(gun); // gun now follows the player's hand
// player drops gun in the world at its current position:
const worldPos = gun.worldPosition(); // compute before reparenting
world.add(gun);
gun.setWorldPosition(worldPos);
// 4) Traverse — visitors / filters
function traverse(node, visit) {
visit(node);
for (const c of node.children) traverse(c, visit);
}
function findByName(root, name) {
let found = null;
traverse(root, n => { if (n.name === name) found = n; });
return found;
}
function renderTree(root) {
traverse(root, n => {
if (n.mesh) renderer.draw(n.mesh, n.worldMatrix);
});
}
// 5) Bounding-box culling — only render what's visible
function render(root, camera) {
traverse(root, n => {
if (!n.mesh) return;
if (camera.frustum.intersects(n.boundingBox)) {
renderer.draw(n.mesh, n.worldMatrix);
}
});
}
// 6) Static vs dynamic — optimisation
// Mark nodes as static (don't recompute transforms each frame)
class Node {
isStatic = false;
transformDirty = true;
setDirty() {
this.transformDirty = true;
for (const c of this.children) c.setDirty();
}
updateTransforms() {
if (!this.transformDirty && this.isStatic) return;
// recompute...
this.transformDirty = false;
for (const c of this.children) c.updateTransforms();
}
}
// 7) Components — composition over inheritance
class Entity extends Node {
components = [];
add(c) { this.components.push(c); c.entity = this; return c; }
get(Cls) { return this.components.find(c => c instanceof Cls); }
}
class Mesh { /* … */ }
class Health { hp = 100; }
class AIBehavior { /* … */ }
class Physics { /* … */ }
const enemy = new Entity('zombie');
enemy.add(new Mesh('zombie_model'));
enemy.add(new Health());
enemy.add(new AIBehavior());
enemy.add(new Physics());
// Update systems iterate by component type
class HealthSystem {
update(scene, dt) {
traverse(scene, n => {
const h = n.get?.(Health);
if (h && h.hp <= 0) scene.remove(n);
});
}
}
// 8) Prefabs — reusable templates
function makeTree() {
const tree = new Entity('tree');
tree.add(new Mesh('tree_trunk'));
const leaves = new Entity('leaves');
leaves.position = { x: 0, y: 5, z: 0 };
leaves.add(new Mesh('leaves_model'));
tree.add(leaves);
return tree;
}
// Spawn many
for (let i = 0; i < 100; i++) {
const t = makeTree();
t.position = randomPos();
forest.add(t);
}
// 9) Engines that ship with scene graphs
// Unity: GameObject hierarchy with Transform components
// Godot: Node tree (Node3D / Node2D)
// Unreal: Actor + USceneComponent attachments
// Three.js: Object3D with .parent + .children
// Babylon.js: Node + TransformNode
// PlayCanvas: Entity with parent/child
// 10) Performance tips
// • Reuse transform matrices; don't allocate per frame
// • Frustum cull at the scene-graph level (skip subtrees outside the camera)
// • Spatial partitioning (octree, BVH) for very large scenes — scene graph isn't enough
// • Cache hierarchies; avoid recomputing world transforms when nothing moved
// • Detached nodes still need cleanup (dispose meshes, textures)
// 11) Common bugs
// • Reparenting without preserving world position → object teleports
// • Forgetting to mark dirty on transform change → stale rendering
// • Deep hierarchies — every frame recomputes matrices; flatten when possible
// • Circular references (A.add(B), B.add(A)) → infinite loop in traversal
// • Holding strong references to removed nodes → GC pressure, leaks
// 12) Scene graph vs ECS (Entity-Component-System)
// Scene graph : hierarchical, transform-centric, good for visible / spatial objects
// ECS : data-oriented, components in tight arrays, great for many similar entities
// Modern engines often combine both: scene graph for the world hierarchy, ECS for game logic
// 13) When to prefer scene graphs
// - Object groups move together (vehicle + passengers, character + weapon)
// - You author scenes in an editor with parent / child relations
// - Modular prefab system with composition
// 14) When ECS wins
// - Thousands of independent entities (bullets, particles, NPCs)
// - Performance critical (cache-friendly)
// - Complex behavior decoupled from visuals
// 15) Editors
// - Unity Hierarchy + Inspector — drag-and-drop scene graph
// - Godot Scene panel
// - Three.js / Babylon — no built-in editor; pair with their REPL or build tooling
Why it matters
Scene graphs let you compose worlds: parent a sword to a hand, a hand to an arm, an arm to a body. The trick is keeping the hierarchy shallow enough that per-frame transform updates stay cheap.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// A tree of nodes whose transforms compose down to children. // Common in 3D engines. Move a parent, all children move.Try it Yourself »
Discussion
Loading…