Common pitfalls
These are the things that trip people up most often in willow. Each one is short, says what happens, and says what to do instead.
Rotation is in radians
rotation, skewX, skewY and setRotation() take radians, not degrees. node.rotation = 90 turns the node about 14 full times, which looks like nothing much happened.
ts
node.rotation = Math.PI / 2; // a quarter turn, clockwise on screen
node.rotation = 45 * Math.PI / 180; // from degreesThe pivot is in pixels, the anchor is a fraction
pivotX, pivotY and setPivot(x, y) are in the node's local pixels. setAnchor(nx, ny) takes a fraction of the node's size and sets the pivot for you. Mixing them up is the usual reason a sprite spins around its corner.
ts
import { Quad } from "willow";
class Pivots {
static main(): void {
const q: Quad = new Quad(100, 50, Color.WHITE);
q.setAnchor(0.5, 0.5);
console.log("anchor 0.5 -> pivot " + q.pivotX + ", " + q.pivotY);
q.setPivot(0.5, 0.5);
console.log("setPivot 0.5 -> pivot " + q.pivotX + ", " + q.pivotY);
}
}Output:
text
anchor 0.5 -> pivot 50, 25
setPivot 0.5 -> pivot 0.5, 0.5To centre a node, use setAnchor(0.5, 0.5) or alignPivot(). See Nodes and transforms.
Alpha and tint multiply
A node's alpha multiplies with its parent's alpha, all the way up the tree. A sprite's tint is a separate multiplier on top, and its alpha counts too. So a sprite at alpha = 1 can still be invisible if its parent is at 0, or if its tint has an alpha of 0. Use alpha to fade, and tint to colour.
A raw number used as a colour is always 0xRRGGBBAA. A six-digit number such as 0xFF0000 has an alpha of 0. Write all eight digits, or use Color.hex("#FF0000"), which treats a six-digit string as opaque. See Shapes and meshes.
add() moves a node
A node has one parent. Adding it somewhere else takes it out of the old parent first; it is not copied.
ts
import { Node } from "willow";
class Moves {
static main(): void {
const a: Node = new Node("a");
const b: Node = new Node("b");
const coin: Node = new Node("coin");
a.add(coin);
b.add(coin);
console.log("a has coin: " + a.contains(coin) + ", b has coin: " + b.contains(coin));
}
}Output:
text
a has coin: false, b has coin: trueTo show the same thing twice, make two nodes.
remove() is reversible, dispose() is not
remove() detaches a node and keeps it, so you can add it back. dispose() frees it for good, and every node below it. Any later use of a disposed node, even reading x, raises InvalidStateError.
ts
import { Node } from "willow";
class Gone {
static main(): void {
const enemy: Node = new Node("enemy");
const healthBar: Node = new Node("healthBar");
enemy.add(healthBar);
enemy.dispose();
try {
console.log(healthBar.x);
} catch (e) {
console.log("" + e);
}
}
}Output:
text
InvalidStateError: Node.x: node has been disposedDrop your own references to a node when you dispose it, and dispose the parent rather than each child. Disposing twice is safe.
A node with no handler is not hit
A node takes pointer events only while it has a pointer handler assigned (onClick, onPointerDown, onDrag and the rest). A node without one is not hit tested at all, so the click goes to whatever is under it. interactable defaults to true; setting it to false takes a node out of hit testing while keeping its handlers, which is how you disable a button.
So if clicks on a panel's background reach the world behind it, give the panel a handler, even an empty one. See Input and hit testing.
Masks clip drawing, not input
setMask() changes what is drawn. It does not change hit testing, so the hidden part of a masked node can still be clicked. If that matters, give the node a hit shape that matches the visible area with setHitRect or setHitCircle. See Effects.
Hooks are single slots
onUpdate, onTick, onClick and the other handlers are fields. Assigning a second function replaces the first; it does not add to it. To run two things, call both from one function, or use named events with on(name, handler), which keep a list. Assign null to detach a handler.
A lifecycle hook (onUpdate, onTick and the rest) that throws is reported once and then turned off, so a node that suddenly stops moving may have thrown on an earlier frame. Check the log.
Names are labels, not keys
A node's name is for debugging and lookup. Nothing stops two nodes having the same name, and a NodeIndex keeps them all: findByName returns the first one registered, while findAllByName and each see every one. To find a group, tag the nodes and use findByTag. See Camera and layers.
The camera's position is the centre of the view
You do not need to set up a camera: with an untouched camera, world (0, 0) is the top-left of the window. When you do move it, camera.x and camera.y are the world point at the centre of the screen, not the top-left corner. follow(node) with no lerp snaps to the node; a small lerp such as 0.1 eases toward it. See Camera and layers.
HUDs belong on the screen root
Anything under engine.root2d is drawn through the camera, so it scrolls and zooms with the world. Put menus, scores and health bars under engine.screen2d instead. See The scene graph.