Input
Back to the reference

Input

Keyboard, pointer and gamepad, plus named actions and the remapping layer a player owns.

46 symbols

DEFAULT_ACTION_PRESSvariablenacatamalon

DEFAULT_ACTION_PRESS: 0.5

How far an analogue source travels before it counts as pressed, when the action does not say.

A different number from the dead zone on purpose. One number for both would mean 0.5 as the floor for a stick, which is very high: movement through actions would feel dead next to reading the stick, which here sits right beside it.

View source · input/actions/normalize_action_map.ts:25

describeBindingfunctionnacatamalon

describeBinding(binding: TActionBinding) => string

A binding the way a person reads it: 'Space', 'A', 'L STICK LEFT'.

It lives next to the labels of the pad rather than in each controls screen, because the names and the labels are one fact: a second copy would drift, and a wrong label still draws, so nobody would be told. A key reads as its character when it is one, and a named key is split into its words ('ArrowLeft' becomes 'Arrow Left').

Parameters

bindingTActionBinding

The binding.

View source · input/actions/describe_binding.ts:19

GAMEPAD_BUTTON_LABELSvariablenacatamalon

GAMEPAD_BUTTON_LABELS: Readonly<Record<TGamepadButtonName, string>>

What each button is called when it is shown to a player, for a controls screen that has to print what a key is bound to.

Here and not in the game, because the name and its label are one fact: a second hand-written list would drift the first time a name changed, and it would drift in silence, since a wrong label still looks fine.

View source · input/gamepad/gamepad_standard.ts:108

GAMEPAD_BUTTONSvariablenacatamalon

GAMEPAD_BUTTONS: typeOperator

Every standard button, in the order the specification gives them: the position in this list is the number the browser reports, so turning a name into a number is a lookup instead of a second table to keep in step.

'guide' is last because a pad is allowed not to have it, which makes it the only one a lookup can fall off the end of. Reads are checked against the pad's real button count anyway.

View source · input/gamepad/gamepad_standard.ts:50

gamepadAxisDirectionLabelfunctionnacatamalon

gamepadAxisDirectionLabel(axis: TGamepadAxisName, dir: -1 | 1) => string

How one direction of an axis reads on a controls screen: "L STICK LEFT", "L STICK DOWN".

A direction, and not an axis, is what a binding really names: an axis goes both ways and the thing bound to it does not. y grows downwards, so dir: 1 on a vertical axis is down, and writing that out is the whole point of this function: it is the one place that convention meets a person.

Parameters

axisTGamepadAxisName

The stick axis.

dir-1 | 1

Which way along it: -1 or 1.

View source · input/gamepad/gamepad_standard.ts:158

listenfunctionnacatamalon

listen(sprite: TSprite | TBox | TText | TNineSlice, events: TSpriteEvents) => () => void

Makes a sprite, a text, a nine-slice or a whole object react to the mouse and touch, after it was created.

The same events createSprite, createText and createNineSlice accept in their options (onClick, onPointerOver, onPointerOut, onPointerMove, onPointerDown, onPointerUp), for when they are not known at creation: a button that only becomes clickable when a level is cleared, or a sprite made in one place that another part of the game gives a behaviour.

  • It can be called at any moment: while the scene is built, every frame, or from another event.
  • It adds: whatever the sprite already reacted to keeps working, and two onClick both run, in the order they were connected.
  • It stops by itself when the sprite's part of the scene goes away. The function it returns stops it earlier, and removes only what this call connected.

On a sprite that has been destroyed it does nothing.

A whole object reacts as one thing. A button made of a border, a fill and a label is one button: a pointer over any of its pieces, or its children's, is over the object, and moving from the fill to the label is not leaving it. That is also how a behaviour reaches pieces it did not create, the ones a scene document or a pack made: listen(self, { onClick }). A piece that listens by itself inside it still answers for itself, being the more specific.

Parameters

spriteTSprite | TBox | TText | TNineSlice

The sprite, text, nine-slice or object that should react. A text reacts anywhere inside its block, a nine-slice anywhere inside its rectangle, and an object over any of its pieces.

eventsTSpriteEvents

Which events, and what to do on each.

Example

export const Level: TSceneFn = () => {
    const scene = useScene();
    const door = createSprite({ key: 'door', transform: { x: 400, y: 160 } });
    let enemiesLeft = 3;
    let doorOpen = false;

    useUpdate(() => {
        // The door only becomes clickable once every enemy is gone
        if (!doorOpen && enemiesLeft === 0) {
            doorOpen = true;
            listen(door, { onClick: () => scene.change('NextLevel') });
        }
    });

    return createScene();
};
View source · events/listen.ts:75

normalizeActionMapfunctionnacatamalon

normalizeActionMap(value: unknown) => TActionMap

Takes whatever came out of a file and gives back a map the engine can trust: unknown kinds of binding are dropped, keys are written the one way the keyboard writes them, repeats inside one action go (the strongest source wins at runtime, so a repeat would only be clutter in the controls screen), and an action with no name is not an action.

Total and quiet about the details: a project file is not a promise, and the game still has to run.

Parameters

valueunknown

Whatever a file gave back, as JSON.parse left it.

View source · input/actions/normalize_action_map.ts:81

useActionfunctionhooknacatamalon

useAction(name: string, options: TUseActionsOptions) => TAction

One action, which is what most game code reads.

The same as useActions with one name filled in. Reach for useActions when something reads several, and for this when it reads one.

Parameters

namestring

The action, as the input map names it.

optionsTUseActionsOptions

Which player's devices to read, as for useActions.

Example

declare const velocity: { x: number; y: number };

const jump = useAction('jump');
useUpdate(() => { if (jump.justPressed()) velocity.y = -320; });
View source · hooks/input/use_actions.ts:97

useActionsfunctionhooknacatamalon

useActions(options: TUseActionsOptions) => TActionsHandle

The game's named actions.

An action is a name, 'jump', that the game binds to whatever it likes: a key, a gamepad button, a stick direction, several at once. The game asks about the name, so the same code works on a keyboard and on a pad with no branching, and the player can change what it is bound to without the game knowing.

The list of actions is the game's: given to createGame, or written in the project's project.json by the editor.

Parameters

optionsTUseActionsOptions

Which player's devices to read, in a game with more than one.

Example

declare const hero: TSprite;
declare const jump: () => void;
declare let charge: number;

export const Player: TSceneFn = () => {
    const input = useActions();

    useUpdate((delta) => {
        // Four actions, one direction: the keys, the d-pad and the stick all arrive here, and the
        // dead zone is round, so a gentle diagonal stays a diagonal.
        const { x, y } = input.vector('move_left', 'move_right', 'move_up', 'move_down');
        hero.transform.x += x * 120 * delta;
        hero.transform.y += y * 120 * delta;

        if (input.justPressed('jump')) jump();
        // A trigger bound to 'fire' reads how far it is pulled; a key bound to it reads 1.
        charge = input.value('fire');
    });

    return createScene();
};
View source · hooks/input/use_actions.ts:72

useGamepadfunctionhooknacatamalon

useGamepad(target: TGamepadTarget, options: TUseGamepadOptions) => TGamepad

A gamepad, to ask in every frame what the player is holding, has just pressed, or how far a stick is pushed.

With no number it follows the first pad connected, which is what a one player game means. With a number it is that port and only that one, which is what local multiplayer means: player two has to keep meaning one particular controller even when player one unplugs theirs. Do not pin a single player game to port 0: the ports belong to the browser, and a pad unplugged and plugged back in usually lands in another one.

What comes back is alive. It says there is no pad while the scene is being built even with a controller plugged in, because browsers hide a pad until a button is pressed on it, and it starts saying there is one by itself when the player wakes theirs up. So read it inside useUpdate, never once at the start.

Parameters

targetTGamepadTarget

The port, or nothing to follow the first pad connected.

optionsTUseGamepadOptions

The stick's dead zone, 0.2 by default.

Example

declare const fire: () => void;

export const Level: TSceneFn = () => {
    const pad = useGamepad();
    const keys = useKeyboard();
    const ship = createSprite({ key: 'ship' });

    useUpdate((delta) => {
        const { x, y } = pad.leftStick();
        ship.transform.x += x * 150 * delta;
        ship.transform.y += y * 150 * delta;
        if (pad.justPressed('a') || keys.justPressed('Space')) fire();
    });

    return createScene();
};
View source · hooks/input/use_gamepad.ts:48

useInputMapfunctionhooknacatamalon

useInputMap() => TInputMapHandle

Remapping: what a game's controls screen is built on.

It lists the game's actions, says what each one listens to, changes them, puts them back and warns that two actions are on the same button. If the game said where to keep them, every change is written and comes back next time; starting a new game does not wipe them, because the controls are a preference and not part of a saved game.

Example

declare const jumpRow: TText;

export const Controls: TSceneFn = () => {
    const map = useInputMap();

    // "Press a key or a button for JUMP..."
    listen(jumpRow, { onClick: () => {
        map.capture((binding) => {
            if (binding === null) return;              // they pressed Escape
            if (map.conflicts(binding, 'jump').length > 0) return;
            map.bind('jump', binding, 0);
        });
    } });

    return createScene();
};
View source · hooks/input/use_input_map.ts:38

useKeyboardfunctionhooknacatamalon

useKeyboard() => TKeyboard

The keyboard, to ask in every frame what the player is holding or has just pressed.

Ask it, do not wait for it. Games read the keyboard once per frame, from inside useUpdate, because that is where movement happens: isDown for what continues while a key is held, and justPressed for what happens once per press however long the key stays down.

Call it while the scene is being built, like every hook, and keep what it returns.

Example

declare const Bullet: (x: number) => void;

export const Level: TSceneFn = () => {
    const keys = useKeyboard();
    const ship = createSprite({ key: 'ship', transform: { x: 160, y: 200 } });
    const fire = useSpawn(Bullet);

    useUpdate((delta) => {
        if (keys.isDown('ArrowLeft')) ship.transform.x -= 150 * delta;
        if (keys.isDown('ArrowRight')) ship.transform.x += 150 * delta;
        if (keys.justPressed('Space')) fire(ship.transform.x);
    });

    return createScene();
};
View source · hooks/input/use_keyboard.ts:38

usePointerfunctionhooknacatamalon

usePointer() => TPointerHandle

Listens to the mouse and to touch: presses, releases, movement, and what is under the pointer.

Each listener receives where the pointer is and what it is over:

  • screenX/screenY: the position on the screen, in the game's pixels, ignoring any camera.
  • worldX/worldY: the same point as a place in this scene's world, through its camera. With no camera they are the same numbers.
  • target: the sprite or text on top under the pointer, or null over empty space. hits has all of them, top first. A text is touched anywhere inside its block, gaps and spaces included.
  • button: 0 the main button or a finger, 2 the right button.

Listeners run at the start of the next frame, not the instant the browser reports the click, so anything they do behaves as it would inside useUpdate. Several movements in one frame arrive as one, with the latest position.

They stop by themselves when the part of the scene that registered them goes away, and they are not called while their scene is paused. Each one returns a function that stops it earlier.

pick(x, y) asks the same question at any screen point, whenever you like.

onWheel hears the mouse wheel (or two fingers on a trackpad), with how far it turned in deltaY, and deltaX for sideways. While anything listens, the wheel over the game stops scrolling the page.

Example

export const Board: TSceneFn = () => {
    const pointer = usePointer();
    const red = getColor('#ff5566');

    pointer.onDown((info) => {
        if (info.target !== null) {
            info.target.tint = red;
        }
    });

    // The wheel zooms the camera: towards you is out, away is in.
    const camera = useCamera2d();
    pointer.onWheel(({ deltaY }) => {
        camera.zoom = Math.min(4, Math.max(0.5, camera.zoom * Math.exp(-deltaY * 0.001)));
    });

    return createScene();
};
View source · hooks/input/use_pointer.ts:58

useVectorfunctionhooknacatamalon

useVector(negativeX: string, positiveX: string, negativeY: string, positiveY: string, options: TUseActionsOptions & { deadzone }) => () => { x, y }

Four actions read as one direction: the way to move a character from the input map.

It gives back a function, called each frame, like the one useTween gives. The dead zone is round and applied once to the finished direction, which is what makes this identical to reading the stick when the four actions are bound to one, and what stops a gentle diagonal from collapsing onto the nearest straight line. A keyboard diagonal comes out the right length for free, which is the bug in every hand written x = right - left.

Parameters

negativeXstring

The action that points left.

positiveXstring

The action that points right.

negativeYstring

The action that points up.

positiveYstring

The action that points down.

optionsTUseActionsOptions & { deadzone }

Which player's devices to read, and the deadzone below which the direction is zero.

Example

declare const hero: TSprite;
const SPEED = 120;

const move = useVector('move_left', 'move_right', 'move_up', 'move_down');
useUpdate((delta) => {
    const { x, y } = move();
    hero.transform.x += x * SPEED * delta;
    hero.transform.y += y * SPEED * delta;
});
View source · hooks/input/use_actions.ts:142

TActionBindingtypenacatamalon

TActionBinding: { type, key } | { type, button } | { type, axis, dir }

One physical thing an action listens to.

Three kinds, and the reason there is no fourth: a gamepad button covers the digital and the analogue ones alike, because a trigger already reports how far it is pressed. A separate kind for triggers would be a second name for one fact.

dir on an axis is not a threshold and is not optional: it is part of which input this is. An axis goes both ways and an action does not, so "the horizontal axis of the left stick" does not name an input at all until it says which way.

There is deliberately no kind for a whole stick. A stick is four axis bindings across four actions: if one binding could be two-dimensional, every action would be either a number or a direction, and that split spreads into the runtime, the saved file, the editor's table and the controls screen. One direction per binding is what lets "WASD or stick or d-pad" work through the same four names with no branching in the game.

View source · input/actions/types/t_action.ts:35

TActionCaptureOptionstypenacatamalon

What "press anything" listens to while it waits.

Properties

sourcesoptional'key' | 'button' | 'axis'[]

Which kinds of input can be bound. All three by default.

deviceoptionalTActionDevice

Which pad to listen to. 'any' by default.

cancelKeyoptionalstring | null

The key that reports nothing instead of binding itself. 'Escape' by default; null turns it off, which makes a rebind the player cannot back out of, so think first.

axisThresholdoptionalnumber

How far a stick has to be pushed to count, 0 to 1. 0.7 by default: well past any dead zone, so a resting stick with a bit of drift can never bind itself.

View source · input/actions/types/t_action.ts:181

TActionDeftypenacatamalon

One named action and everything it listens to.

deadzone and press are how the action feels, and they belong to the action rather than to one of its bindings: they have to be shared by every source, or the same action would mean one thing on a stick and another on a key.

Properties

bindingsTActionBinding[]
deadzoneoptionalnumber

Below this, the action reads as exactly 0. Default 0.2, the same as the stick's, so four actions on one stick give the same numbers as reading the stick directly.

pressoptionalnumber

How far an analogue source has to travel before it counts as pressed. Default 0.5.

A different number from the dead zone on purpose: a trigger at 30 % has a value and is not "pressed", which is what lets the same trigger be an accelerator and a trigger for firing.

View source · input/actions/types/t_action.ts:51

TActionDevicetypenacatamalon

TActionDevice: number | 'any' | 'keyboard'

Which devices a reader listens to. Not a property of a binding: a property of whoever is reading, which is what makes local multiplayer the same action names read twice.

'any' (the default) is the keyboard and every pad. A number is that pad and nothing else: a keyboard is not a port, so naming a port means pad only, which is exactly what player two wants. 'keyboard' is the other half of that split.

View source · input/actions/types/t_action.ts:96

TActionMaptypenacatamalon

TActionMap: TActionDef[]

A game's whole input map.

A list and not an object keyed by name, for two reasons a controls table makes obvious: renaming inside an object is a remove and an insert, which jumps the row out from under the cursor, and a repeated name can be seen and reported instead of quietly swallowed.

(The player's own changes are an object, because that one is sparse and nobody renames an action from a controls screen.)

View source · input/actions/types/t_action.ts:82

TActionPersisttypenacatamalon

Where the player's own bindings are kept.

The adapter is the store's (localStorageAdapter, indexedDbAdapter or one of your own). What is deliberately not reused is the store itself: a remap is a preference, not part of a saved game, and starting a new game must not reset the controls.

Properties

keyoptionalstring

What to save it under. Has a prefix by default, because browser storage belongs to the whole site: a page that hosts several games would have them overwriting each other's controls.

View source · input/actions/types/t_action.ts:278

TActionsHandletypenacatamalon

The actions of a game, as it reads them. Returned by useActions.

Properties

deviceTActionDevice

Which devices this handle is listening to.

isDown
justPressed
justReleased
value
rawValue
strength
vector
has
namestypeOperator

Every action name, in the order the game declared them.

View source · input/actions/types/t_action.ts:114

TGamepadtypenacatamalon

One pad, as a game reads it. Returned by useGamepad.

Everything here is live: a handle taken while the scene is being built says there is no pad, and starts saying there is one by itself when the player's shows up. That matters more than it sounds, because browsers hide a pad until a button is pressed on it (so that a page cannot tell who you are by your hardware): "no pad" is the normal state at the start of a scene even with a controller plugged in.

Held and just pressed split exactly like the keyboard's, and for the same reason: hold left to walk, justPressed('a') to jump once however long the button is held.

One thing it cannot do, and it is the browser's limit rather than a choice: a press and a release that both happen between two frames leave no trace. A pad sends no events, it can only be asked how it is now, so "what happened since the last frame" is a comparison of two answers. At 60 frames a second that is a tap under 16 ms, which no thumb produces on a real button. The keyboard does catch that case, because it gets real events.

Properties

indexnumber

The port being read right now. Fixed for a handle pinned to a number; for one that follows the first pad, it changes when pads are plugged in or out.

connectedboolean

Whether there is a pad in that port right now.

idstring

What the browser calls it, or empty with nothing connected.

mappingstring

'standard', or empty for a pad the browser did not recognise.

buttonCountnumber

How many buttons the pad says it has, 0 with nothing connected.

axisCountnumber

How many axes the pad says it has, 0 with nothing connected.

isDown
justPressed
justReleased
value
axis
leftStick
rightStick
deadzonenumber

The dead zone this handle uses, 0 to 1.

setDeadzone
rawButton
rawButtonValue
rawJustPressed
rawJustReleased
rawAxis
canRumbleboolean

Whether the browser offers vibration for this pad, so an options screen can grey out its switch instead of offering one that does nothing.

rumble
stopRumble
onConnect
onDisconnect
View source · input/gamepad/types/t_gamepad.ts:92

TGamepadAxisNametypenacatamalon

TGamepadAxisName: 'leftX' | 'leftY' | 'rightX' | 'rightY'

A stick axis of the standard mapping. X grows to the right and Y grows downwards, which is what the browser reports and also how this engine measures the screen: pushing the stick up gives a negative leftY, the same sign a sprite needs to move up.

View source · input/gamepad/gamepad_standard.ts:36

TGamepadButtonNametypenacatamalon

TGamepadButtonName: 'a' | 'b' | 'x' | 'y' | 'lb' | 'rb' | 'lt' | 'rt' | 'back' | 'start' | 'ls' | 'rs' | 'up' | 'down' | 'left' | 'right' | 'guide'

A button of the W3C standard mapping, by the name this engine gives it.

The names are the Xbox labels because that is how the specification itself defines the mapping: "the bottom button of the right cluster" is index 0, and every pad that reports mapping: 'standard' has agreed to put its bottom face button there.

Worth knowing before the first bug report: on a Nintendo or an 8BitDo pad the printed letters sit in the other order, so the button under the player's thumb when they press 'a' is the one labelled B on the plastic. That is the pad's labelling, not a mistake here: the position is what the standard mapping promises, and the position is what a game cares about.

A closed list of seventeen, so a typo is caught when the game is compiled.

View source · input/gamepad/gamepad_standard.ts:19

TGamepadInfotypenacatamalon

What a pad says about itself when it arrives or leaves.

mapping is the field worth reading: 'standard' means the button names line up with the plastic, and anything else (usually an empty string) is a pad the browser did not recognise, one of those cheap USB copies of a SNES or Mega Drive controller, whose buttons land wherever the manufacturer put them. Those are still perfectly playable through rawButton and rawAxis; what does not work on them are the names, and this is how a game finds that out instead of quietly reading the wrong button.

Properties

indexnumber

The port it is plugged into, the same number useGamepad(port) takes.

idstring

What the browser calls it, something like 'Xbox Wireless Controller (…)'.

mappingstring

'standard' when the names can be trusted, usually empty otherwise.

buttonCountnumber
axisCountnumber
View source · input/gamepad/types/t_gamepad.ts:17

TGamepadTargettypenacatamalon

TGamepadTarget: number | 'first'

Which pad a handle reads: a port number, or the first one connected.

The difference is not fussiness. The browser owns the port, not the player: a pad unplugged and plugged back in usually lands in a different one, so a single player game nailed to port 0 goes silent with nothing in the console to say why. Following the first pad is what such a game actually means; a number is what local multiplayer means, where "player two" has to keep meaning one particular controller.

View source · input/gamepad/types/t_gamepad.ts:233

TKeyNametypenacatamalon

TKeyName: string

A key, spelled the way the browser reports it in KeyboardEvent.key: space is ' ', letters are lowercase ('f'), and named keys keep their names ('ArrowLeft', 'Escape', 'Enter'). 'Space' is accepted for the space bar too.

An alias of string, so any literal still fits without a cast. What it buys is the name showing up in a signature, which says what kind of string is expected.

View source · input/types/t_key_name.ts:13

TPointerInfotypenacatamalon

What a pointer listener is told: where the mouse or finger is, which button, and what is under it.

Properties

screenXnumber

Where on the screen, in the game's pixels from the top-left corner. Ignores any camera.

screenYnumber
worldXnumber

The same point as a place in the world, through the camera of the scene that asked for the pointer. With no camera it equals screenX/screenY.

worldYnumber
buttonnumber

Which button: 0 the main one (left, or a finger), 1 the wheel, 2 the secondary one.

targetTSprite | TText | TNineSlice | null

The sprite, text or nine-slice on top under the pointer, or null over empty space.

hitsReadonlyArray<TSprite | TText | TNineSlice>

Every sprite, text and nine-slice under the pointer, the one on top first.

View source · input/types/t_pointer.ts:15

TPointerWheelInfotypenacatamalon

TPointerWheelInfo: TPointerInfo & { deltaY, deltaX }

What a wheel listener is told: everything a pointer listener is, and how far the wheel turned.

In pixels whichever way the browser counted them (some mice count lines, some pages), so one notch of an ordinary mouse wheel is around a hundred. Every turn since the last frame is added together: a quick spin is one call with a large number, not several small ones a frame apart.

View source · input/types/t_pointer.ts:61

TRumbleOptionstypenacatamalon

How hard and how long to shake the pad. Everything is optional: rumble() on its own is a short generic thump, which is what most calls want.

Properties

durationoptionalnumber

Milliseconds, default 200, never more than 5000: past that the browser cuts it without saying so.

strongoptionalnumber

The heavy motor, 0 to 1, default 1. The one felt as a thud.

weakoptionalnumber

The light motor, 0 to 1, default 1. The one felt as a buzz.

delayoptionalnumber

Milliseconds to wait before starting, default 0.

View source · input/gamepad/types/t_gamepad.ts:51

TSpriteEventstypenacatamalon

What a sprite or a text can react to by itself: in createSprite's or createText's options, or connected later with listen. Every one receives where the pointer is and what is under it. A text reacts anywhere inside its block, the gaps between letters included.

Properties

onPointerOveroptionalTPointerListener

The pointer comes over the sprite.

onPointerOutoptionalTPointerListener

The pointer stops being over it, or leaves the game.

onPointerMoveoptionalTPointerListener

The pointer moves while over it.

onPointerDownoptionalTPointerListener

A button is pressed, or a finger touches, over it.

onPointerUpoptionalTPointerListener

A button is released, or a finger lifts, over it.

onClickoptionalTPointerListener

Pressed and released without leaving it. The cursor turns into a hand over it.

View source · input/types/t_sprite_events.ts:12

TUseActionsOptionstypenacatamalon

What the action hooks accept.

Properties

deviceoptionalTActionDevice

Which devices to listen to. 'any' (the default) is the keyboard and every pad, which is one player. A port number is that pad and nothing else, which is how two players share one set of action names; 'keyboard' is the other half of that split.

View source · hooks/input/use_actions.ts:11

TUseGamepadOptionstypenacatamalon

What useGamepad accepts.

Properties

deadzoneoptionalnumber

Stick dead zone, 0 to 1. Default 0.2: inside the value Xbox pads themselves suggest, and above the drift of the cheap pads this engine's players will really own.

View source · input/gamepad/types/t_gamepad.ts:242