Input
Volver a la referencia

Input

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

46 símbolos

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.

Ver el código · 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').

Parámetros

bindingTActionBinding

The binding.

Ver el código · 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.

Ver el código · 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.

Ver el código · 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.

Parámetros

axisTGamepadAxisName

The stick axis.

dir-1 | 1

Which way along it: -1 or 1.

Ver el código · 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.

Parámetros

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.

Ejemplo

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();
};
Ver el código · 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.

Parámetros

valueunknown

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

Ver el código · 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.

Parámetros

namestring

The action, as the input map names it.

optionsTUseActionsOptions

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

Ejemplo

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

const jump = useAction('jump');
useUpdate(() => { if (jump.justPressed()) velocity.y = -320; });
Ver el código · 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.

Parámetros

optionsTUseActionsOptions

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

Ejemplo

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();
};
Ver el código · 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.

Parámetros

targetTGamepadTarget

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

optionsTUseGamepadOptions

The stick's dead zone, 0.2 by default.

Ejemplo

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();
};
Ver el código · 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.

Ejemplo

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();
};
Ver el código · 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.

Ejemplo

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();
};
Ver el código · 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.

Ejemplo

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();
};
Ver el código · 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.

Parámetros

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.

Ejemplo

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;
});
Ver el código · 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.

Ver el código · input/actions/types/t_action.ts:35

TActionCaptureOptionstypenacatamalon

What "press anything" listens to while it waits.

Propiedades

sourcesopcional'key' | 'button' | 'axis'[]

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

deviceopcionalTActionDevice

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

cancelKeyopcionalstring | 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.

axisThresholdopcionalnumber

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.

Ver el código · 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.

Propiedades

bindingsTActionBinding[]
deadzoneopcionalnumber

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.

pressopcionalnumber

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.

Ver el código · 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.

Ver el código · 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.)

Ver el código · 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.

Propiedades

keyopcionalstring

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.

Ver el código · input/actions/types/t_action.ts:278

TActionsHandletypenacatamalon

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

Propiedades

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.

Ver el código · 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.

Propiedades

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
Ver el código · 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.

Ver el código · 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.

Ver el código · 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.

Propiedades

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
Ver el código · 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.

Ver el código · 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.

Ver el código · 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.

Propiedades

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.

Ver el código · 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.

Ver el código · 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.

Propiedades

durationopcionalnumber

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

strongopcionalnumber

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

weakopcionalnumber

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

delayopcionalnumber

Milliseconds to wait before starting, default 0.

Ver el código · 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.

Propiedades

onPointerOveropcionalTPointerListener

The pointer comes over the sprite.

onPointerOutopcionalTPointerListener

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

onPointerMoveopcionalTPointerListener

The pointer moves while over it.

onPointerDownopcionalTPointerListener

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

onPointerUpopcionalTPointerListener

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

onClickopcionalTPointerListener

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

Ver el código · input/types/t_sprite_events.ts:12

TUseActionsOptionstypenacatamalon

What the action hooks accept.

Propiedades

deviceopcionalTActionDevice

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.

Ver el código · hooks/input/use_actions.ts:11

TUseGamepadOptionstypenacatamalon

What useGamepad accepts.

Propiedades

deadzoneopcionalnumber

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.

Ver el código · input/gamepad/types/t_gamepad.ts:242