Guide

Game input handling explained

Players judge a game in milliseconds. A jump that registers one frame late feels broken even when graphics are flawless. Input handling is the layer between physical devices — keyboard, mouse, touchscreen, gamepad — and your game logic. Get it wrong and platformers miss ledges, shooters aim behind targets, and mobile players abandon titles with invisible dead zones. This guide covers how professional engines structure input: polling vs events, action mapping, browser APIs, buffering tricks like coyote time, touch ergonomics, multiplayer authority, and accessibility remapping that works across devices.

Where input sits in the game loop

Input is always the first phase of the game loop: read devices, translate raw signals into game actions, then pass those actions to simulation and rendering. The order matters. If you process physics before reading input, a button press on frame N might not affect movement until frame N+1 — one frame of lag at 60 FPS is roughly 17 milliseconds, enough for players to notice in a tight platformer.

Most engines follow a two-step pattern each frame:

  1. Update input state — poll devices or drain event queues into a normalized structure (e.g. { jump: true, moveX: -1 }).
  2. Consume input in systems — player controller, UI focus, camera — using the snapshot from step 1, not live hardware reads mid-simulation.

Snapshotting prevents subtle bugs: half your physics step might see a key down and half might not if you read hardware twice. It also makes replays and networked games tractable — you record the action snapshot, not the raw USB report.

Polling vs event-driven input

There are two fundamental models:

  • Polling — each frame, ask "is key W pressed right now?" via KeyboardEvent state tracking or navigator.getGamepads(). Simple, predictable, works with fixed timesteps.
  • Events — the OS delivers keydown, pointerdown, or gamepad connect callbacks asynchronously. Efficient for UI, but events can arrive between frames or burst during a hitch.

Production games almost always hybridize: events update an internal state table; the game loop polls that table once per frame. For keyboard, track keysDown in keydown/keyup handlers, then read keysDown.has('Space') during update(). Never call preventDefault() on keys the browser needs (Tab, F5) unless you have a visible "press any key to start" gate — otherwise you break accessibility and frustrate players who cannot escape your canvas.

Pointer input adds wrinkle: mousemove can fire hundreds of times per second while requestAnimationFrame runs at 60–120 Hz. Buffer the latest mouse position in the event handler; read the buffered value once per frame. Same for touch touchmove on mobile.

Action mapping: decouple devices from gameplay

Hard-coding "Space jumps" scatters device knowledge through your codebase. Instead, define actions — semantic intents like Jump, Fire, Pause — and map physical inputs to them in one configuration layer.

// actions.json (conceptual)
{
  "Jump":  ["Keyboard.Space", "Gamepad.A", "Touch.ButtonA"],
  "Move":  { "axis": "Gamepad.LeftStickX", "keys": ["A","D","ArrowLeft","ArrowRight"] }
}

At runtime, the input manager resolves bindings into a flat action state each frame. Benefits multiply quickly:

  • Rebinding — players remap controls without recompiling.
  • Multi-device — keyboard and gamepad work simultaneously without duplicate logic.
  • AI and automation — bots inject actions through the same interface as humans.
  • Networking — send compact action enums over the wire instead of raw key codes.

For analog movement, normalize stick and WASD into a single moveVector with length clamped to 1.0 so diagonal keyboard input is not 41% faster than cardinal directions — a classic beginner bug.

Keyboard, mouse, and pointer lock

Browser keyboard input uses keydown and keyup. Use event.code (physical key position, e.g. KeyW) rather than event.key (character, affected by layout) for movement keys — except when you genuinely care about typed characters in a text field.

First-person and top-down games often request pointer lock via canvas.requestPointerLock(). While locked, mousemove delivers movementX/movementY deltas without cursor position — essential for infinite mouse look. Always provide an obvious exit (Escape releases lock per spec) and never trap users without instruction.

Click handling should distinguish primary action (left click / first touch) from context menus (right click). On touch devices, long-press is a poor substitute for right-click — add explicit UI buttons instead.

Gamepad API: sticks, triggers, and dead zones

The W3C Gamepad API exposes controllers through navigator.getGamepads(). Poll it every frame — button and axis values do not update via events alone. Listen for gamepadconnected to show "controller detected" UI and hot-swap bindings.

Analog sticks report -1.0 to 1.0 per axis but hardware rarely centers at exactly zero. Apply a dead zone: treat magnitudes below ~0.15 as zero, then rescale the remainder to 0–1 so full tilt still reaches max speed. Without this, characters drift slowly or cameras creep.

Triggers are often axes, not buttons — read axes[2] for RT with a threshold (e.g. > 0.5 = pressed). Shoulder bumpers map cleanly to digital actions. Vibration (gamepad.vibrationActuator) is optional delight — short pulses on hit confirm feedback without visual clutter.

Touch and mobile ergonomics

Touch lacks hover and offers no physical feedback. Design rules that differ from desktop:

  • Hit targets — minimum 44×44 CSS pixels per Apple HIG; cramped buttons cause mis-taps.
  • Virtual sticks — anchor joysticks where thumbs rest; clamp movement to a radius; optional floating stick that appears on touch.
  • Gesture conflicts — browser swipe-to-navigate and pull-to-refresh compete with game drags. Use touch-action: none on the game canvas after informing the player.
  • Multi-touch — track each touch.identifier separately so jump and move work concurrently.

Latency on mobile is higher than desktop — glass digitizer, compositor, and thermal throttling add milliseconds. Combine responsive input with generous buffering windows (next section) so jumps and dashes still feel fair on a phone.

Input buffering, coyote time, and jump grace

Strict "only jump while grounded" logic feels harsh because human reaction time exceeds one frame. Three standard grace mechanisms:

  • Input buffer — if jump is pressed in the air, remember it for 100–150 ms and execute on the next landing.
  • Coyote time — after walking off a ledge, allow jump for ~100 ms as if still grounded (named after the cartoon coyote who hangs mid-air before falling).
  • Jump grace / coyote + buffer combo — used together in polished platformers; tune windows per game feel, not realism.

These are implemented in the player controller, often backed by a finite state machine with timers on transitions from Grounded to Airborne. They cost almost nothing computationally but dramatically improve perceived quality.

The same pattern applies beyond platformers: fighting games buffer attack inputs during hit-stun; rhythm games align note hits to windows; card games queue "play" during opponent animations.

Multiplayer: authority and prediction

In networked games, who owns input determines fairness. Client-side prediction applies local input immediately while awaiting server confirmation — see our multiplayer netcode guide for reconciliation details. The input layer's job is to timestamp actions and send compact payloads: { tick: 1842, actions: Jump | MoveLeft }, not raw key events.

Never trust remote clients for authoritative hits in competitive modes — the server validates actions against physics state. Input handling on the server replays received actions against the same simulation code local players use, which is why shared action enums and deterministic simulation matter.

Accessibility and remapping

Accessible input is good design, not a niche feature:

  • Full remapping — every action bindable to keyboard, mouse, gamepad, or touch.
  • Toggle vs hold — option for aim, sprint, and crouch; some players cannot hold triggers.
  • Adjustable sensitivity — separate sliders for mouse, stick, and touch.
  • Visual alternatives — on-screen prompts that mirror physical bindings; do not rely on color alone.
  • Pause always reachable — Esc or a persistent UI button; required for motor and cognitive accessibility.

Store bindings in localStorage or cloud saves so players keep settings across sessions. Test with only keyboard, only gamepad, and only touch — not everything at once on a developer desk setup.

Common pitfalls

  • Reading input mid-physics — causes frame-split inconsistencies; snapshot once at loop start.
  • Diagonal speed boost — normalize movement vectors to max length 1.
  • Missing blur/focus handlers — keys stay "down" when tab loses focus; clear state on window.blur.
  • Ignoring repeat rate — holding a key fires repeated keydown; gate UI typing separately from gameplay.
  • Gamepad not polled — connecting a controller does nothing until you call getGamepads() each frame.
  • Touch scrolling the page — forgotten preventDefault on game canvas causes accidental navigation.
  • Latency stacking — input buffer + network RTT + render interpolation adds up; profile end-to-end, not per layer in isolation.

Key takeaways

  • Snapshot input once per frame at the top of the game loop before simulation runs.
  • Use action mapping to decouple devices from gameplay — enables rebinding, AI, and clean networking.
  • Apply dead zones on analog sticks; buffer latest pointer positions from high-frequency events.
  • Coyote time and input buffers cheaply improve platformer feel on all devices.
  • In multiplayer, send timestamped action enums; keep simulation deterministic for server replay.
  • Support remapping, toggle options, and focus-loss cleanup for accessibility.

Related reading