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:
- Update input state — poll devices or drain event queues into a normalized structure (e.g.
{ jump: true, moveX: -1 }). - 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
KeyboardEventstate tracking ornavigator.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: noneon the game canvas after informing the player. - Multi-touch — track each
touch.identifierseparately 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
preventDefaulton 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
- Game loop and frame timing — where input fits in update vs render
- Game state machines — FSMs for player states and jump transitions
- Multiplayer netcode — client prediction and input authority
- Web accessibility (a11y) — keyboard navigation and inclusive design patterns