Skip to main content

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

SourceWhat to read there
../games/hosanna-ui/src/hosanna-game/core/GameInput.tsGameButton, GameInputDeviceType, GameInputAxis, constructor defaults, Roku key mapping, and helpers.
../games/hosanna-ui/src/hosanna-game/input/GameInputAdapterManager.tsAdapter registration, _tick, dispatchInput, and drainInputs.
../games/hosanna-ui/src/hosanna-game/input/RokuGameInputAdapter.tsRoku remote events converted into GameInput.fromRoEvent.
../games/hosanna-ui/src/hosanna-game/input/web/WebKeyboardGameInputAdapter.tsKeyboard mapping, press/held/release phases, and held duration.
../games/hosanna-ui/src/hosanna-game/input/web/WebGamepadInputAdapter.tsBrowser gamepad buttons, left-stick axes, device identity, and player index.
../games/hosanna-ui/src/hosanna-game/input/web/WebPointerGameInputAdapter.tsCanvas-relative mouse/touch coordinates and pointer capture.
../games/hosanna-ui/src/hosanna-game/control/Hs2dControlInput.tsHs2d gameplay helper functions and axis thresholding.
../games/hosanna-ui/src/hosanna-game/control/Hs2dControlInputManager.tsPaired-controller messages converted into GameInput.
../hosanna-ui-game-samples-public/src/hosanna-game-examples/native-shoot-em-up/VerticalShooterLevelScene.tsReference scene handling loading input, pause, arrows, left stick, and gameplay actions.
../hosanna-ui-game-samples-public/src/hosanna-game-examples/diagnostics/ControllerRigController.tsPer-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.

FieldUse
buttonCodeRaw numeric code from the adapter or synthetic protocol. Keep this for diagnostics, not gameplay decisions.
buttonNormalized button name such as up, down, left, right, ok, back, play, a, b, x, y, start, or select.
pressFirst-frame press event when the adapter can tell.
heldRepeated/continuous input or active axis.
releaseControl release.
heldTimeMsHeld duration where supported.
x, yPointer, stick, or directional coordinates.
sourceAdapterAdapter name such as rokuGameInput or pairedControlsInput.
deviceTyperemote, keyboard, gamepad, mouse, touch, or unknown.
deviceIdPhysical or paired device identity where available.
playerIndexPlayer slot for paired or gamepad input. Host remote input is usually unassigned.
timestampAdapter-provided timestamp.
axisAnalog axis payload, usually for leftStick or rightStick.
motionOptional raw orientation, rotation, acceleration, interval, and screen-orientation sample from a motion-capable controller.
controlOriginal 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:

CodeButton
0back
2up
3down
4left
5right
6ok
7replay
8rewind
9fastForward
10options
13play

Use input.isButton('ok') and input.isDirectionalArrow() instead of comparing raw key codes.

Phases

Use phases according to the action:

Action typeRecommended phase handling
One-shot commands such as pause, confirm, restart, or open menuIgnore releases and fire once on a non-release event. Prefer press when the adapter reliably sends it.
Continuous movement from arrowsMaintain direction state on non-release and clear that axis on release.
Analog movementRead axis, apply a threshold, and clear movement on release or below-threshold values.
DiagnosticsShow 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';
HelperBehavior
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

DoDon'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.
Talk to us