Game Input
GameInput is the normalized input object passed into Hs2d scenes. It hides platform differences between Roku remotes, keyboards, gamepads, touch controls, and paired controllers so gameplay code can handle a shared button and axis vocabulary.
Controller pairing is a producer of GameInput objects. The pairing stack itself is documented in Controller Pairing.
Source References
| Source | What to read there |
|---|---|
../games/hosanna-ui/src/hosanna-game/core/GameInput.ts | GameButton, GameInputDeviceType, GameInputAxis, constructor defaults, Roku key mapping, and helpers. |
../games/hosanna-ui/src/hosanna-game/input/GameInputAdapterManager.ts | Adapter registration, _tick, dispatchInput, and drainInputs. |
../games/hosanna-ui/src/hosanna-game/input/RokuGameInputAdapter.ts | Roku remote events converted into GameInput.fromRoEvent. |
../games/hosanna-ui/src/hosanna-game/input/web/WebKeyboardGameInputAdapter.ts | Keyboard mapping, press/held/release phases, and held duration. |
../games/hosanna-ui/src/hosanna-game/input/web/WebGamepadInputAdapter.ts | Browser gamepad buttons, left-stick axes, device identity, and player index. |
../games/hosanna-ui/src/hosanna-game/input/web/WebPointerGameInputAdapter.ts | Canvas-relative mouse/touch coordinates and pointer capture. |
../games/hosanna-ui/src/hosanna-game/control/Hs2dControlInput.ts | Hs2d gameplay helper functions and axis thresholding. |
../games/hosanna-ui/src/hosanna-game/control/Hs2dControlInputManager.ts | Paired-controller messages converted into GameInput. |
../hosanna-ui-game-samples-public/src/hosanna-game-examples/native-shoot-em-up/VerticalShooterLevelScene.ts | Reference scene handling loading input, pause, arrows, left stick, and gameplay actions. |
../hosanna-ui-game-samples-public/src/hosanna-game-examples/diagnostics/ControllerRigController.ts | Per-player diagnostic routing for paired controllers. |
Shape
export interface GameInputOptions {
buttonCode: number;
button?: GameButton;
press?: boolean;
held?: boolean;
release?: boolean;
heldTimeMs?: number;
x?: number;
y?: number;
sourceAdapter?: string;
deviceType?: GameInputDeviceType;
deviceId?: string;
playerIndex?: number;
timestamp?: number;
axis?: GameInputAxis;
motion?: GameInputMotion;
control?: string;
}
The constructor fills conservative defaults: booleans default to false, coordinates default to 0, sourceAdapter defaults to an empty string, deviceType defaults to unknown, and timestamp defaults to 0.
| Field | Use |
|---|---|
buttonCode | Raw numeric code from the adapter or synthetic protocol. Keep this for diagnostics, not gameplay decisions. |
button | Normalized button name such as up, down, left, right, ok, back, play, a, b, x, y, start, or select. |
press | First-frame press event when the adapter can tell. |
held | Repeated/continuous input or active axis. |
release | Control release. |
heldTimeMs | Held duration where supported. |
x, y | Pointer, stick, or directional coordinates. |
sourceAdapter | Adapter name such as rokuGameInput or pairedControlsInput. |
deviceType | remote, keyboard, gamepad, mouse, touch, or unknown. |
deviceId | Physical or paired device identity where available. |
playerIndex | Player slot for paired or gamepad input. Host remote input is usually unassigned. |
timestamp | Adapter-provided timestamp. |
axis | Analog axis payload, usually for leftStick or rightStick. |
motion | Optional raw orientation, rotation, acceleration, interval, and screen-orientation sample from a motion-capable controller. |
control | Original controller-defined control name. Defaults to the normalized button. |
Button Vocabulary
GameButton covers Roku remote buttons and broader controller/gamepad controls:
back up down left right ok replay rewind fastForward options play
a b x y leftShoulder rightShoulder leftTrigger rightTrigger
start select leftStick rightStick unknown
Roku remote key codes are mapped in GameInput:
| Code | Button |
|---|---|
0 | back |
2 | up |
3 | down |
4 | left |
5 | right |
6 | ok |
7 | replay |
8 | rewind |
9 | fastForward |
10 | options |
13 | play |
Use input.isButton('ok') and input.isDirectionalArrow() instead of comparing raw key codes.
Phases
Use phases according to the action:
| Action type | Recommended phase handling |
|---|---|
| One-shot commands such as pause, confirm, restart, or open menu | Ignore releases and fire once on a non-release event. Prefer press when the adapter reliably sends it. |
| Continuous movement from arrows | Maintain direction state on non-release and clear that axis on release. |
| Analog movement | Read axis, apply a threshold, and clear movement on release or below-threshold values. |
| Diagnostics | Show press, held, release, sourceAdapter, deviceType, deviceId, and playerIndex. |
The vertical shooter accepts play while ready to push its pause scene, accepts back while loading to return to the menu, and maintains inputX/inputY state for arrows and left-stick axes.
Adapter Flow
GameInputAdapterManager owns input producers:
const inputManager = new GameInputAdapterManager({ now: () => Date.now() });
inputManager.registerAdapter(new RokuGameInputAdapter());
inputManager.setAdapterEnabled('rokuGameInput', true);
inputManager._tick(deltaMs);
const inputs = inputManager.drainInputs();
Adapters call manager.dispatchInput(input). The manager queues those inputs until the runtime drains them. drainInputs() swaps two retained buffers, so the returned array is borrowed only until the next drain; copy data needed by history or asynchronous work. Registering another adapter with the same name detaches the old one before attaching the new one.
Hs2d Helper Functions
Import gameplay helpers from hosanna-game/control:
import {
getHs2dInputAxisDirection,
getHs2dMotionAxis,
hs2dInputButton,
isHs2dGameplayInputForPlayer,
isHs2dInputButton,
isHs2dSinglePlayerInput,
} from '@hs-src/hosanna-game/control';
| Helper | Behavior |
|---|---|
isHs2dGameplayInputForPlayer(input, playerIndex, allowUnassigned) | Accepts input for a specific player. If allowUnassigned is true, also accepts inputs without playerIndex. |
isHs2dSinglePlayerInput(input) | Accepts player 0 and unassigned host input. |
hs2dInputButton(input) | Aliases paired-controller a to ok and start to play. |
isHs2dInputButton(input, button) | Compares against the Hs2d button vocabulary after aliasing. |
getHs2dInputAxisDirection(input, threshold = 0.35) | Converts left-stick axis or arrow buttons into { x, y } with -1, 0, or 1 components. |
getHs2dMotionAxis(input, control) | Returns the normalized { x, y } axis only when the input belongs to the requested game-defined motion control. |
Use these helpers in game scenes instead of duplicating player and alias rules.
Pointer, Motion, And Custom Controls
WebPointerGameInputAdapter maps a primary mouse or touch pointer from the displayed canvas bounds into logical game coordinates. Pointer events use button: 'ok', control: 'pointer', and deviceType: 'mouse' | 'touch'; inspect x, y, and the normal press/held/release flags.
Paired controllers may emit arbitrary control names and raw motion samples. Prefer isControl() when a game presentation defines actions beyond GameButton:
for (const input of inputs) {
if (input.isControl('attack') && !input.release) {
attack(input.playerIndex);
}
if (input.isControl('steering') && input.axis) {
steering = input.axis.x ?? input.axis.value;
}
if (input.motion) {
lastRoll = input.motion.orientationGamma ?? 0;
}
}
A custom control may normalize to button: 'unknown', so comparing only button would discard it. Treat raw motion as optional device data; games should still provide a button, stick, keyboard, or remote path where the product requires it.
Single-Player Scene Pattern
protected override handleInputs(inputs: GameInput[]): boolean {
if (!this.isReady) {
for (const input of inputs) {
if (!isHs2dSinglePlayerInput(input)) continue;
if (isHs2dInputButton(input, 'back') && !input.release) {
this.context.navigateToMenu();
return true;
}
}
return false;
}
for (const input of inputs) {
if (!isHs2dSinglePlayerInput(input)) continue;
if (isHs2dInputButton(input, 'play') && !input.release) {
this.game?.pushScene(new MyPauseScene(this.pauseOptions));
return true;
}
if (input.axis && input.axis.name === 'leftStick') {
const axis = getHs2dInputAxisDirection(input);
this.inputX = input.release ? 0 : axis.x;
this.inputY = input.release ? 0 : axis.y;
continue;
}
if (isHs2dInputButton(input, 'left')) {
this.inputX = input.release ? 0 : -1;
} else if (isHs2dInputButton(input, 'right')) {
this.inputX = input.release ? 0 : 1;
} else if (isHs2dInputButton(input, 'up')) {
this.inputY = input.release ? 0 : -1;
} else if (isHs2dInputButton(input, 'down')) {
this.inputY = input.release ? 0 : 1;
} else if (isHs2dInputButton(input, 'ok') && !input.release) {
this.firePrimary();
}
}
return false;
}
For single-player games, accept unassigned host input because a physical Roku remote usually has no playerIndex.
Per-Player Routing
Multiplayer scenes should route by slot:
for (const input of inputs) {
for (let playerIndex = 0; playerIndex < playerCount; playerIndex++) {
if (!isHs2dGameplayInputForPlayer(input, playerIndex, false)) {
continue;
}
if (isHs2dInputButton(input, 'ok') && !input.release) {
this.players[playerIndex].attack();
}
const direction = getHs2dInputAxisDirection(input);
this.players[playerIndex].setDirection(direction.x, direction.y);
}
}
Use allowUnassigned = false for true multiplayer so the host remote does not accidentally control every player. If a game intentionally lets the host remote control player 0, make that rule explicit.
Do And Don't
| Do | Don't |
|---|---|
Use normalized button and helper functions in gameplay. | Branch gameplay on raw Roku key codes. |
Check release before one-shot actions. | Toggle pause or restart on both press and release. |
| Accept unassigned input for single-player host controls. | Require playerIndex === 0 for a Roku remote-only game. |
Route multiplayer by playerIndex. | Let unassigned input drive every player by accident. |
Use getHs2dInputAxisDirection for left-stick and arrow parity. | Treat analog stick axes and arrows as separate gameplay systems unless the game needs that distinction. |
| Keep UI focus navigation separate from gameplay input handling. | Use UI focus search as the game control loop. |