intermediate · 24 minute study
Actions outlive individual input devices
Supporting keyboard, touch and controllers becomes fragile when every mechanic knows device-specific buttons. More devices should add adapters, not duplicate gameplay rules.
Useful preparation · 1 prerequisite
Use this as a map, not a lock. If these ideas are familiar, continue.
- A game is state evolving through time — A movement rule that feels correct on your laptop can double its speed on a faster display. Separate elapsed time from the number of drawings before building more mechanics.
First, build a mental model
A device says what physically happened. An action says what the player intends. The simulation reads move, aim, interact and pause; it should not care whether an arrow key, thumbstick or touch control produced them.
- Separate continuous levels from edges. Holding move-right is a level sampled each update. Opening the pause menu is usually a press edge, so one held button does not toggle the menu every frame.
- Track independent sources. If two fingers or a key and pointer hold fire, releasing one source must not release the others. A set of source IDs represents this ownership more accurately than one shared boolean.
- Pointer capture can keep receiving a drag outside its original element. Clear ownership on pointerup, pointercancel and lostpointercapture. Blur, hidden-tab transitions and controller disconnection also need a release policy.
- Gamepad axes are analog values. Apply a documented dead zone, clamp the result, and inspect the standard mapping before assuming button indices. Normalize a two-axis movement vector when diagonal keyboard input must not move faster.
- Limit preventDefault to focused game controls. Menus, editable inputs and keyboard navigation still need their normal behavior. A controller connection alone should not seize input focus from a touch user.
const fireSources = new Set();
const press = source => fireSources.add(source);
const release = source => fireSources.delete(source);
const readFire = () => fireSources.size > 0;
press("keyboard:Space");
press("pointer:7");
release("pointer:7");
console.log(readFire()); // true: Space still owns fire
release("keyboard:Space");
console.log(readFire()); // false
// A device adapter supplies source IDs. A blur handler clears the set.- Acquire
Space and pointer 7 create two independent holds. The action is active because at least one owner exists.
- Release one
Pointer 7 ends. Removing its source leaves Space's ownership intact; the action remains active.
- Release all
Space ends or the game loses focus. When no owners remain, the next input sample reports fire = false.
Production thinking
Take this beyond the example.
Test combinations: key plus touch, two pointers, drag outside the button, pointer cancellation, controller disconnect, menu focus and repeated resume. Provide remapping and visible control labels where practical. Input sanitization is part of playability; it is not a substitute for server-side validation of competitive results.
Sources and further reading
Your evidence
unseen is a local learning signal, not a credential or proof of production competence.
Explain the mental model, then explicitly acknowledge it. Opening a page is not evidence.
Supported retries can complete this lesson. Independent evidence and delayed review determine the skill stage.
Next lesson · Movement is a vector; collision is a query →