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));
Seven letters.
Discussion
Loading…