Skip to content

Lifecycle and time ​

willow runs two clocks. Frames happen once per screen refresh. Ticks happen at a fixed rate, 60 per second by default (App.tps, or tps in thicket.json). Put game logic on ticks so it runs the same on every machine, and put purely visual work on frames.

App-wide hooks ​

HookRuns
App.onTick(fn)Once per tick. After a slow frame, several ticks run to catch up (at most App.maxCatchUpTicks, default 8).
App.onFrame(fn)Once per drawn frame, with dt in seconds.
App.onResize(fn)When the window changes size.
App.onFocus(fn) / App.onBlur(fn)When the window gains or loses focus.
App.onQuit(fn)When the app is closing.

Each returns a function that unsubscribes it. Useful readings: App.tick (ticks so far), App.time (scaled seconds), App.realTime (wall seconds), App.actualFps, App.actualTps.

Node hooks ​

Any node can carry its own hooks. Assign a function; assign null to remove it.

HookRuns
onEnter()When the node is added to a parent.
onExit()When it is removed from its parent.
onReady()Once, on the first frame the node is in the tree.
onTick()Once per tick, parents before children.
onUpdate(dt)Every frame, after onReady.
onLateUpdate(dt)Every frame, after every node's onUpdate and the tweens. Good for cameras and things that follow other things.
ts
import { Sprite } from "willow";

class Enemy {
    static spawn(x: float, y: float): Sprite {
        const e: Sprite = new Sprite("assets/slime.png");
        e.setAnchor(0.5, 1.0);
        e.setPosition(x, y);
        e.interpolate = true;

        let dir: float = 1;
        e.onReady = (): void => {
            console.log("slime ready at " + e.x);
        };
        e.onTick = (): void => {
            e.x += 2 * dir;
            if (e.x > x + 100 || e.x < x - 100) {
                dir = -dir;
                e.scaleX = dir;
            }
        };
        return e;
    }

    static main(): void {
        engine.root2d.add(Enemy.spawn(200, 400));
        engine.root2d.add(Enemy.spawn(500, 400));
    }
}

A hook that throws is reported once and then turned off, so one bad enemy does not flood the log.

Interpolation ​

Move a node in onTick and it moves in steps of 1/60 second. On a 144 Hz screen that looks jerky. Set interpolate = true and willow draws x, y, rotation, scaleX and scaleY blended between the last two ticks. Your code still reads the real values.

After teleporting an interpolated node, call resetInterpolation(), or it visibly slides from the old spot for one tick.

Pausing ​

paused = true on a node stops its subtree's onTick, onUpdate, onLateUpdate, ticker and tweens. Drawing continues, so the game freezes in place. pauseMode lets parts of a paused tree keep going:

PauseModeBehaviour
INHERITFollows its parent (default).
ALWAYSKeeps running while an ancestor is paused: a pause menu inside the game tree.
WHEN_PAUSEDRuns only while an ancestor is paused.
DISABLEDNever runs.
ts
import { Node, Text, PauseMode } from "willow";

class PauseDemo {
    static main(): void {
        const game: Node = new Node("game");
        engine.root2d.add(game);

        const menu: Text = new Text("Paused");
        menu.visible = false;
        menu.pauseMode = PauseMode.WHEN_PAUSED;
        menu.onUpdate = (dt: float): void => {
            menu.alpha = 0.6 + 0.4 * Math.sin(App.realTime * 4);
        };
        game.add(menu);

        App.onFrame((dt: float): void => {
            if (Keyboard.isPressed(Key.ESCAPE)) {
                game.paused = !game.paused;
                menu.visible = game.paused;
            }
        });
    }
}

Time scale ​

node.timeScale speeds up or slows down a subtree: 0.5 is half speed (slow motion), 2 is double. It scales dt for onUpdate and onLateUpdate, the number of onTick calls, and the node's ticker. App.timeScale does the same for the whole app. A node with unscaled = true ignores both, which suits UI that should not slow down with the game.

Next ​