iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…