Skip to content

Input actions ​

Games should react to actions ("jump", "move_x"), not keys. In ThicketScript the core InputMap already binds keys and buttons to actions and handles rebinding and saving bindings. gamekit adds what a game needs on top:

  • InputPolicy reads InputMap and gives you an ActionFrame of named actions each tick.
  • InputRouter sends each action to the right InputContext, so a menu takes "confirm" while the game below keeps "move".
  • InputRecorder and InputReplay record frames and play them back, for replays and tests.

From InputMap to a frame ​

ts
enum Action { JUMP, LEFT, RIGHT, MOVE_X }

InputMap.bind(Action.JUMP as int, [Key.SPACE, GamepadButton.A]);
InputMap.axis(Action.MOVE_X as int, Action.LEFT as int, Action.RIGHT as int, GamepadAxis.LEFT_X);

const policy: InputPolicy = new InputPolicy()
    .watch("jump", Action.JUMP as int)
    .watch("move_x", Action.MOVE_X as int);

App.onTick((): void => {
    const frame: ActionFrame = policy.frame();
    if (frame.pressed("jump")) { /* jump */ }
    const move: float = frame.axis("move_x");   // -1 to 1
});

frame() builds the frame from InputMap.isDown, isPressed, isReleased and value, so its edges follow InputMap's rules: latched once per tick inside App.onTick, once per frame elsewhere. An axis action's pressed and released edges come from its two direction actions. A button's axis is 0 or 1.

InputPolicy.frame() reads real devices, so it needs a window project. A headless test builds ActionFrames directly, as below.

ActionFrame methodMeaning
held(action)Down this frame.
pressed(action)Went down this frame.
released(action)Went up this frame.
repeated(action)Key repeat fired.
axis(action)The value, -1..1 for an axis.

policy.focusLost() returns releases for every held action and clears the policy, for when the window loses focus. policy.close() cancels a pending InputMap.captureNext() when an options screen closes.

Sharing keys with willowui ​

willowui uses some keys itself: a modal takes Escape, a focused control takes Enter. When a game uses willowui, name the keys UI may claim with the third argument of watch, and call passThrough once:

ts
import { DEFAULT_INPUT_MANAGER } from "willowui/src/core/input";

policy.watch("back", Action.BACK as int, [Key.ESCAPE]).passThrough(DEFAULT_INPUT_MANAGER);

Then frame() reports no press or hold for an action whose key willowui used this frame, and drops that gesture's release too. Read the frame after willowui's per-frame update.

Contexts and routing ​

ts
import { ActionFrame, ActionState, InputContext, InputContextKind, InputRecorder, InputReplay, InputRouter, RoutedInput } from "gamekit";

// Contexts declare the actions they take. The newest context that declares an action gets it.
const play: InputContext = new InputContext("play", InputContextKind.SIMULATION, ["jump", "move_x"]);
const menu: InputContext = new InputContext("menu", InputContextKind.MODAL, ["confirm", "jump"]);
const router: InputRouter = new InputRouter();
router.push(play);
router.push(menu);

// A frame of action states: action, held, pressed, released, repeated, axis.
const frame: ActionFrame = new ActionFrame([
    new ActionState("jump", true, true, false, false, 1.0),
    new ActionState("move_x", true, false, false, false, -1.0),
]);
const routed: RoutedInput = router.route(frame);
console.log("menu jump: " + routed.forContext(menu).pressed("jump"));
console.log("play jump: " + routed.forContext(play).pressed("jump"));
console.log("play move_x: " + routed.forContext(play).axis("move_x"));

// Record frames, then replay them one per tick.
const recorder: InputRecorder = new InputRecorder();
recorder.record(frame);
recorder.record(new ActionFrame([]));
const replay: InputReplay = new InputReplay(recorder.recording());
while (!replay.finished) {
    const next: ActionFrame = replay.next();
    console.log("replayed frame with " + next.actions().length + " actions");
}
text
menu jump: true
play jump: false
play move_x: -1
replayed frame with 2 actions
replayed frame with 0 actions
  • new InputContext(name, kind, actions) declares which actions a context takes. InputContextKind is descriptive: SIMULATION, SCREEN, MODAL, TEXT_ENTRY, DEBUG.
  • router.push(context) and router.remove(context) manage the stack. The newest context that declares an action receives it. A modal takes only what it declares; everything else still reaches the game below.
  • A held action stays with the context that received its press until it is released. If that context is removed first, the next routed frame sends it a release, so nothing gets stuck down and no release leaks into the screen underneath.
  • With a SceneCoordinator, registerInputContext(scene, router, context) pushes a scene's context only while that scene is on top.

Record and replay ​

InputRecorder.record(frame) stores a copy of each frame; recording() returns an InputRecording. InputReplay.next() returns the next frame. It has no clock of its own: call it once per tick from whatever steps your simulation, and live and replayed runs stay comparable. See the replay recipe.

TickInput ​

Devices latch their edges once per drawn frame, so a game that ticks slower than it draws can miss a short tap. TickInput fixes that: feed it one policy frame per tick with next(frame), and it reports pressed on the first tick an action is held, never twice for one hold. axis(name, negative, positive) declares a two-button axis whose tap that was pressed and released between ticks still counts as one tick of -1 or 1.

ts
const ticks: TickInput = new TickInput().axis("move_x", "left", "right");
App.onTick((): void => { game.setInput(ticks.next(policy.frame())); });

Pointer input on willow ​

The willow family's WillowInputBridge decides which game target gets a pointer event when several overlap. Register WillowInputTargets with unique priorities; the winner is the only one that sees that pointer frame, and it keeps pointer capture even outside its area until release. A WillowViewport pairs a screen rectangle with a willow Camera2D, so a click lands on the world point that camera draws there, rotation included. unregister and focusLost cancel captures and report them through drainCancelled.

To let willowui have pointer input first, register the UI node as a target above the world target.

Reference: InputPolicy, ActionFrame, InputRouter, InputContext, InputReplay, TickInput, WillowInputBridge.