iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Input

Game input flow: poll device state each frame or react to events. Map physical inputs (key codes, button IDs) to abstract actions (jump, fire) for rebinding + multi-device support.

Polling, events, action mapping, gamepad

EXAMPLE
// 1) JavaScript / browser — keyboard via events
const keys = new Set();
addEventListener('keydown', (e) => keys.add(e.code));
addEventListener('keyup',   (e) => keys.delete(e.code));

function update(dt) {
    if (keys.has('KeyW')) player.y -= player.speed * dt;
    if (keys.has('KeyS')) player.y += player.speed * dt;
    if (keys.has('KeyA')) player.x -= player.speed * dt;
    if (keys.has('KeyD')) player.x += player.speed * dt;
    if (keys.has('Space') && player.canJump) player.jump();
}

// 2) Avoid keypress / key — they're deprecated / unreliable. Use keydown / keyup with e.code.
//    e.code   = physical key (KeyW, ArrowUp, Space, Enter)
//    e.key    = mapped character (w, W, ' ', Enter)
//    Prefer e.code for game controls so layout-agnostic.

// 3) Edge-triggered actions — just-pressed / just-released
const pressed = new Set();          // keys actively held
const justPressed = new Set();      // newly pressed THIS frame
const justReleased = new Set();

addEventListener('keydown', (e) => {
    if (!pressed.has(e.code)) justPressed.add(e.code);
    pressed.add(e.code);
});
addEventListener('keyup', (e) => {
    pressed.delete(e.code);
    justReleased.add(e.code);
});

function lateUpdate() {
    justPressed.clear();
    justReleased.clear();
}

// 4) Mouse
let mouseX = 0, mouseY = 0;
const mouseButtons = new Set();
addEventListener('mousemove',  (e) => { mouseX = e.clientX; mouseY = e.clientY; });
addEventListener('mousedown',  (e) => mouseButtons.add(e.button));
addEventListener('mouseup',    (e) => mouseButtons.delete(e.button));

// Mouse lock (FPS)
canvas.addEventListener('click', () => canvas.requestPointerLock());
addEventListener('mousemove', (e) => {
    if (document.pointerLockElement === canvas) {
        camera.yaw   -= e.movementX * 0.002;
        camera.pitch -= e.movementY * 0.002;
    }
});

// 5) Touch (mobile)
canvas.addEventListener('touchstart', (e) => {
    for (const t of e.changedTouches) touches.set(t.identifier, { x: t.clientX, y: t.clientY });
});
canvas.addEventListener('touchend', (e) => {
    for (const t of e.changedTouches) touches.delete(t.identifier);
});

// Virtual analog stick from touch — compute delta from origin

// 6) Gamepad — Gamepad API
function pollGamepad() {
    const gp = navigator.getGamepads()[0];
    if (!gp) return;
    const lx = applyDeadzone(gp.axes[0], 0.15);
    const ly = applyDeadzone(gp.axes[1], 0.15);
    const aPressed = gp.buttons[0].pressed;
    if (aPressed && !lastA) player.jump();
    lastA = aPressed;
    player.x += lx * speed;
    player.y += ly * speed;
}
function applyDeadzone(v, dz) { return Math.abs(v) < dz ? 0 : v; }
addEventListener('gamepadconnected',    (e) => console.log('gamepad', e.gamepad.id));
addEventListener('gamepaddisconnected', (e) => console.log('gone'));

// 7) Action mapping — abstract input from physical keys
const BINDINGS = {
    moveUp:    ['KeyW', 'ArrowUp',    'GamepadAxisYNeg'],
    moveDown:  ['KeyS', 'ArrowDown',  'GamepadAxisYPos'],
    moveLeft:  ['KeyA', 'ArrowLeft',  'GamepadAxisXNeg'],
    moveRight: ['KeyD', 'ArrowRight', 'GamepadAxisXPos'],
    jump:      ['Space',              'GamepadButton0'],
    fire:      ['Mouse0',             'GamepadButton7'],
};

function isDown(action) {
    for (const binding of BINDINGS[action]) {
        if (binding.startsWith('Key') && pressed.has(binding)) return true;
        if (binding === 'Mouse0' && mouseButtons.has(0))        return true;
        // … similar for gamepad bindings
    }
    return false;
}

// Use abstract actions in game code:
if (isDown('jump') && player.canJump) player.jump();
if (isDown('moveLeft'))                player.vx -= acc * dt;

// 8) Rebinding UI
function startRebind(action) {
    addEventListener('keydown', function rebind(e) {
        BINDINGS[action] = [e.code];
        save('controls', BINDINGS);
        removeEventListener('keydown', rebind);
    }, { once: true });
}

// 9) Input buffer — accept input that landed slightly before the game was ready
const INPUT_BUFFER_MS = 100;
let jumpQueuedAt = 0;
if (justPressed.has('Space')) jumpQueuedAt = performance.now();
if (jumpQueuedAt && performance.now() - jumpQueuedAt < INPUT_BUFFER_MS && player.canJump) {
    player.jump();
    jumpQueuedAt = 0;
}

// 10) Coyote time — let players jump for N ms after walking off a ledge
let groundedAt = 0;
if (player.onGround) groundedAt = performance.now();
const COYOTE = 100;
if (justPressed.has('Space') && performance.now() - groundedAt < COYOTE) player.jump();

// 11) Unity New Input System pattern (concepts apply broadly)
// - Define InputActions in the editor (Move, Jump, Fire)
// - Bind to multiple devices (keyboard, gamepad, touch)
// - Subscribe to action.performed in C#
// - Get free rebinding UI + control schemes (KBM / Gamepad / Touch)

// 12) Godot — Input Map (Project Settings → Input Map)
// - Define actions: 'move_left', 'jump'
// - Bind multiple events per action
// - Code: Input.is_action_pressed('jump') / is_action_just_pressed('jump')

// 13) Tips
// - ALWAYS check input via abstract actions, not key codes — opens the door to rebinding + accessibility
// - Process input ONCE per frame; cache 'just pressed' / 'just released' lists
// - Throttle expensive on-input work (firing weapons, etc.) by a cooldown timer
// - Test with: keyboard, mouse, touch, gamepad — each surfaces different bugs
// - Add 'coyote time' + 'input buffer' for snappier platformer feel

Why it matters

Build inputs around actions, not key codes. isDown(\"jump\") beats isKey(\"Space\") — you get rebinding, multi-device, and accessibility for free, and the gameplay code reads like a design doc.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
const keys = new Set();
addEventListener('keydown', e => keys.add(e.code));
addEventListener('keyup',   e => keys.delete(e.code));
if (keys.has('ArrowRight')) player.x += speed * dt;
Try it Yourself »

Exercise

Where keyboard input is gathered.

addEventListener(' ', e => keys.add(e.code));

Discussion

Loading…