Skip to content

The scene graph ​

willow keeps everything you see in one tree of nodes. Each node has a transform (position, scale, rotation, pivot, skew) and an alpha, and its children inherit them. Every frame willow walks the tree, works out each node's world transform, and draws it. You change node fields; willow does the drawing.

text
Scene
├── root  (engine.root2d)        the world: drawn through the camera
│   ├── background   Quad
│   ├── level        Node
│   │   ├── tiles    TileLayer
│   │   └── player   Sprite
│   │       └── nameTag  Text      moves with the player
│   └── lights       LightLayer
└── screenRoot  (engine.screen2d)  the screen: no camera
    └── hud          Node
        └── score    Text

Nodes and containers ​

Node is the base class of every display object. A plain new Node("name") draws nothing; it is a container you group other nodes under. Move the container and everything inside it moves too.

Node typeDraws
NodeNothing. A group.
SpriteAn image or part of one.
SpriteBatchThousands of simple sprites from one texture.
QuadA solid rectangle (or a line, with Quad.line).
TextA string in a font.
Polygon, MeshFilled shapes and raw triangles.
TileLayerA grid of tiles.
ParticleEmitterPooled particles.
LightLayerDarkness with lights cut out of it.
Rope, DistortionGridA ribbon along a path; a deformable textured grid.

Every one of them extends Node, so they all have x, y, add, onClick and the rest.

The scene ​

The window app has one scene. It owns two roots and a camera:

WhatScript nameUse it for
World rootengine.root2d, or Scene.main.rootThe game world. Drawn through the camera, so it scrolls and zooms.
Screen rootengine.screen2d, or Scene.main.screenRootMenus and HUDs. Drawn after the world, in screen pixels, without the camera.
Cameraengine.camera, or Scene.main.cameraWhich part of the world is on screen. See Camera and layers.

Pointer input reaches the screen root first. If no visible, interactive node there takes the event, it falls through to the world.

engine is the window app's global. Scene.main is the same scene, typed Scene | null because a console program has no scene. Library code that should load in any program reads Scene.main and checks it for null:

ts
import { Quad } from "willow";

class Hud {
    static main(): void {
        const scene: Scene | null = Scene.main;
        if (scene === null) {
            console.log("no scene: this program has no window");
            return;
        }
        const bar: Quad = new Quad(scene.width, 32, Color.hex("#00000080"));
        scene.screenRoot.add(bar);
        scene.backgroundColor = 0x0E1E3CFF;
    }
}

The scene also knows its size (width, height), the last frame time (frameTime), the time since it started (elapsed), and which node has keyboard focus (focus).

Draw order ​

Three things decide what draws on top, in this order:

  1. renderLayer, 0 to 255. A higher layer always draws over a lower one, wherever the node sits in the tree. A node with no layer of its own uses its nearest ancestor's, so set it once on a subtree's root. The scene has named values: LAYER_BACKGROUND (0), LAYER_DEFAULT (64), LAYER_UI (192) and LAYER_OVERLAY (255).
  2. zOrder within the same parent. Higher draws on top.
  3. Tree order. Among siblings with the same zOrder, a child added later draws on top. addAt(child, index) inserts at a position.

globalOrder breaks ties between nodes on the same render layer regardless of where they are in the tree.

ts
import { Node, Quad } from "willow";

class Layers {
    static main(): void {
        const world: Node = new Node("world");
        const ui: Node = new Node("ui");
        engine.root2d.add(ui);
        engine.root2d.add(world);

        // ui was added first, but its layer puts it on top of the world.
        ui.renderLayer = engine.scene.LAYER_UI;
        world.renderLayer = engine.scene.LAYER_DEFAULT;

        ui.add(new Quad(200, 40, Color.WHITE));
        world.add(new Quad(800, 600, Color.hex("#224466")));
    }
}

Adding, removing and disposing ​

CallEffect
parent.add(child)Appends child. If it had a parent, it moves.
parent.addAt(child, i)Inserts at index i.
parent.removeChild(child) / child.remove()Detaches it. It still exists and can be added again.
parent.removeAllChildren()Detaches every child.
node.dispose()Frees the node now. Any later use raises InvalidStateError.
parent.children()A snapshot array of direct children.
parent.contains(child)True for a direct child.

Removing a node is cheap and reversible. Dispose a node when you are done with it for good, for example an enemy that died; a disposed node leaves its parent's children() and its tweens stop.

Events up the tree ​

Besides the typed handlers (onClick and friends, see Input and hit testing), any node can emit a named event that bubbles up through its ancestors:

ts
import { Node, Event } from "willow";

class Bubbling {
    static main(): void {
        const level: Node = new Node("level");
        const chest: Node = new Node("chest");
        level.add(chest);

        level.on("opened", (e: Event): void => {
            console.log(e.currentTarget.name + " heard " + e.type + " from " + e.target.name + ": " + e.data);
        });

        chest.emit("opened", "50 gold");
    }
}

Output:

text
level heard opened from chest: 50 gold

A listener can call e.stopPropagation() to stop the event going further up. off(name, handler) and offAll() remove listeners.

Next ​