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 TextNodes 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 type | Draws |
|---|---|
Node | Nothing. A group. |
Sprite | An image or part of one. |
SpriteBatch | Thousands of simple sprites from one texture. |
Quad | A solid rectangle (or a line, with Quad.line). |
Text | A string in a font. |
Polygon, Mesh | Filled shapes and raw triangles. |
TileLayer | A grid of tiles. |
ParticleEmitter | Pooled particles. |
LightLayer | Darkness with lights cut out of it. |
Rope, DistortionGrid | A 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:
| What | Script name | Use it for |
|---|---|---|
| World root | engine.root2d, or Scene.main.root | The game world. Drawn through the camera, so it scrolls and zooms. |
| Screen root | engine.screen2d, or Scene.main.screenRoot | Menus and HUDs. Drawn after the world, in screen pixels, without the camera. |
| Camera | engine.camera, or Scene.main.camera | Which 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:
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) andLAYER_OVERLAY(255).zOrderwithin the same parent. Higher draws on top.- 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
| Call | Effect |
|---|---|
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 goldA listener can call e.stopPropagation() to stop the event going further up. off(name, handler) and offAll() remove listeners.
Next
- Nodes and transforms
- Lifecycle and time covers
onReady,onUpdate,onTickand pausing a subtree.