Input
InputState in @g2w/core is a plain, DOM-free state machine. Browser adapters feed it normalized events throughhandle* methods; game and scene code read it through query methods. It holds no references to window, document, DOM events, or any renderer — which is exactly why unit tests can drive it deterministically without a browser.
Wire the adapter
DomInput from @g2w/input-dom adapts real browser events — keyboard, pointer, and multi-touch through Pointer Events — onto your InputState. All DOM handling lives in the adapter, never in game code:
const input = new InputState();
const domInput = new DomInput(input, {
canvas,
keyTarget: window,
blurTarget: window,
// pointer + key coords in the game's logical space
logicalSize: { width: VIEW_WIDTH, height: VIEW_HEIGHT },
});
// in G2WGame's onUpdate — exactly one boundary per frame:
onUpdate: (deltaTime) => {
scenes.update(deltaTime);
input.endFrame(); // after every consumer has read the state
},input.endFrame() exactly once per game frame, after every consumer has read the state. It resets all pressed/released/touch edges at one explicit boundary; held (down) state is never affected.Read it in game code
Held state is continuous: isKeyDown /isButtonDown report whether the input is currently down.Pressed/released are per-frame edges, set by events and reset together at endFrame():
update(deltaTime) {
// held state is continuous
if (input.isKeyDown("KeyD")) player.moveRight(deltaTime);
// per-frame edges — true from the event until endFrame()
if (input.wasKeyPressed("Enter")) scenes.goTo("gameplay");
if (input.wasButtonPressed(0)) shoot();
// pointer position in canvas-relative coordinates
crosshair.moveTo(input.pointerX, input.pointerY);
}Two details worth knowing: a quick press-and-release between twoendFrame calls is not lost — the key reports both edges in the same frame while isKeyDown is already falseagain. And OS key repeat while a key is held creates no extra press edges.
Focus and pointer policies
Focus loss releases every held key and button with the corresponding released edges, so game logic observes the same release it would have seen from real events. Pointer leaving the canvas releases held buttons the same way — the matching pointerup may have happened off-canvas. The last pointer position is kept in both cases.
Multi-touch
Beyond the single-pointer view, adapters feed individual touch contacts keyed by a stable id (the DOM pointerId). Several simultaneous contacts are supported, and cancellation has its own edge so you can distinguish an intentional lift from the browser claiming the gesture for scrolling:
for (const id of input.activeTouchIds()) {
const pos = input.getTouchPosition(id)!;
if (input.wasTouchStarted(id)) spawnStick(id, pos);
if (input.wasTouchCancelled(id)) releaseStick(id); // system gesture
}Gamepads
@g2w/input-gamepad adapts the Gamepad API onto the DOM-freeGamepadState with an injectable polling source. Unlike DOM events, gamepads are polled: the host drives sampling withpoll(), once per frame, before any read:
import { GamepadState } from "@g2w/core";
import { GamepadInput } from "@g2w/input-gamepad";
const gamepad = new GamepadInput(new GamepadState());
// the host drives sampling — once per frame, before reads
gamepad.poll();
if (gamepad.isButtonDown(0)) jump();Next steps
See Audio for sound that follows the same ownership discipline, orarchitecture notes for the full adapter boundary rules.