Input and hit testing
There are two ways to read input:
- Global state for game controls:
Keyboard,Mouse,Gamepad,TouchInputandInputMap. They are built in, with nothing to import. - Node handlers for things you point at: buttons, cards, units. willow finds the node under the pointer and calls its handler.
Global input
ts
import { Sprite } from "willow";
class Controls {
static main(): void {
const ship: Sprite = new Sprite("assets/ship.png");
ship.setAnchor(0.5, 0.5);
ship.setPosition(400, 300);
engine.root2d.add(ship);
App.onFrame((dt: float): void => {
let dx: float = Gamepad.axis(0, GamepadAxis.LEFT_X);
if (Keyboard.isDown(Key.LEFT)) {
dx = -1;
}
if (Keyboard.isDown(Key.RIGHT)) {
dx = 1;
}
ship.x += dx * 300 * dt;
if (Keyboard.isPressed(Key.SPACE) || Gamepad.isPressed(0, GamepadButton.A)) {
console.log("fire");
}
if (Mouse.isPressed(Mouse.LEFT)) {
console.log("clicked at " + Mouse.x + ", " + Mouse.y);
}
});
}
}isDown is true while held; isPressed and isReleased are true on the frame (or tick) the state changes. Mouse.x and Mouse.y are in screen pixels; convert them to the world with engine.camera.screenToWorld.
Node handlers
Assign a handler and the node takes part in hit testing. A node with no pointer handler is never hit.
| Handler | Payload | Runs when |
|---|---|---|
onPointerDown | PointerEvent | A button goes down over the node. |
onPointerUp | PointerEvent | A button goes up over the node. |
onClick | PointerEvent | Down and up both land on the node. |
onPointerMove | PointerEvent | The pointer moves over the node. |
onPointerEnter / onPointerLeave | PointerEvent | The pointer enters or leaves the node. These do not bubble. |
onDragStart, onDrag, onDragEnd | DragEvent | A press moves past the drag dead zone, keeps moving, then lets go. |
onWheel | WheelEvent | The wheel or trackpad scrolls over the node. |
onPinch | PinchContext | A two-finger pinch or rotate. |
ts
import { Quad, Text, PointerEvent, KeyModifiers } from "willow";
class ButtonDemo {
static main(): void {
const button: Quad = new Quad(160, 48, Color.hex("#3366CC"));
button.setPosition(320, 276);
engine.root2d.add(button);
const label: Text = new Text("Start");
label.setPosition(56, 12);
button.add(label);
button.onPointerEnter = (e: PointerEvent): void => {
button.color = Color.hex("#4A7FE6");
};
button.onPointerLeave = (e: PointerEvent): void => {
button.color = Color.hex("#3366CC");
};
button.onClick = (e: PointerEvent): void => {
const shift: boolean = (e.modifiers & KeyModifiers.SHIFT) !== 0;
console.log("start, shift held: " + shift + ", at " + e.x + ", " + e.y);
};
}
}A PointerEvent carries the point in the node's local space (x, y), in the world (worldX, worldY) and on screen (screenX, screenY), plus button (0 left, 1 right, 2 middle), modifiers and target.
interactable = false takes a node out of hit testing while keeping its handlers, which is how you disable a button.
Dragging
ts
import { Sprite, DragEvent, Pointer } from "willow";
class DragCard {
static main(): void {
const card: Sprite = new Sprite("assets/card.png");
card.setPosition(200, 200);
engine.root2d.add(card);
let startX: float = 0;
let startY: float = 0;
card.onDragStart = (e: DragEvent): void => {
startX = card.x;
startY = card.y;
card.alpha = 0.8;
};
card.onDrag = (e: DragEvent): void => {
card.x = startX + e.deltaX;
card.y = startY + e.deltaY;
};
card.onDragEnd = (e: DragEvent): void => {
card.alpha = 1;
};
Pointer.setDragDeadZone(6);
}
}deltaX and deltaY are the world distance since the drag began. The drag dead zone (Pointer.setDragDeadZone(px)) is how far a press must move before it counts as a drag rather than a click.
Pointer.capture(node) sends every pointer event to one node, wherever the pointer goes, until Pointer.release(). Use it for sliders and scroll bars.
Hit shapes
By default the hit area is the node's bounding box. Give a node a tighter shape in its local space:
| Call | Hit area |
|---|---|
setHitRect(x, y, w, h) | A rectangle. |
setHitCircle(cx, cy, r) | A circle. |
setHitPolygon([x0, y0, ...]) | A convex polygon, at least three points. |
clearHitShape() | Back to the bounding box. |
ts
import { Quad } from "willow";
class HitShapes {
static main(): void {
const coin: Quad = new Quad(64, 64, Color.WHITE);
coin.setHitCircle(32, 32, 30);
console.log("center: " + coin.hitShapeContains(32, 32));
console.log("corner: " + coin.hitShapeContains(2, 2));
coin.clearHitShape();
console.log("custom shape after clear: " + coin.hasHitShape());
}
}Output:
text
center: true
corner: false
custom shape after clear: falseMasks do not change hit testing. A node clipped by a mask is still hit over its full shape.
Clicks on empty space and keys as events
engine.scene.onBackgroundClick(fn) runs when a click misses every node, which is handy for deselecting. engine.scene.onKeyDown(fn) and onKeyUp(fn) deliver each key change as a KeyEvent with keyCode (a Key constant), label, charCode, repeat and the modifier flags, in the order they arrived.
Next
- Camera and layers
- Reference: Pointer input, Node