Skip to content

Stateful Gamepad ​

StatefulGamepad wraps the XR Gamepad to provide edge‑triggered events and helpful axes utilities based on the active WebXR Input Profile.

Why use it? ​

  • Edge events: getButtonDown/Up() so you don’t hand‑roll previous/next button arrays.
  • Mapping by component id: call getButtonPressed('xr-standard-squeeze') instead of hard‑coding indices.
  • Axes helpers: 2D magnitude, per‑direction enter/leave (Up/Down/Left/Right) with a threshold.

Accessing gamepads ​

XRInputManager exposes lazily‑created stateful pads for the current primary source on each side.

ts
const leftPad = xrInput.gamepads.left;
const rightPad = xrInput.gamepads.right;

// Example: trigger a use action on select edge
if (rightPad?.getSelectStart()) doUse();
if (rightPad?.getSelectEnd()) stopUse();

Pads are re‑created when the primary input source changes or when a gamepad appears/disappears on that source. Always null‑check.

Mappings from input profiles ​

When first created, the pad resolves the active profile and builds:

  • buttonMapping: Map<string, number> — component id → button index.
  • axesMapping: Map<string, {x,y}> — component id → axis indices.

Common component ids include:

  • 'xr-standard-trigger', 'xr-standard-squeeze'
  • 'xr-standard-thumbstick', 'xr-standard-touchpad'
  • 'a-button', 'b-button', 'x-button', 'y-button', 'thumbrest', 'menu'

API cheatsheet ​

ts
// Buttons by id or raw index
pad.getButtonPressed('xr-standard-trigger');
pad.getButtonDown('xr-standard-squeeze');
pad.getButtonUpByIdx(3);
pad.getButtonValue('xr-standard-trigger'); // 0..1

// Select convenience (from layout.selectComponentId)
pad.getSelectStart();
pad.getSelectEnd();
pad.getSelecting();

// 2D axes (thumbstick/touchpad)
const v = pad.getAxesValues('xr-standard-thumbstick'); // { x, y }
const mag = pad.get2DInputValue('xr-standard-thumbstick'); // 0..√2

// Directional state machine (with threshold)
pad.axesThreshold = 0.8; // default
pad.getAxesEnteringUp('xr-standard-thumbstick');
pad.getAxesLeavingRight('xr-standard-thumbstick');

Directional states are one of Default, Up, Down, Left, Right and update each pad.update() tick. The input manager calls update() for you when the gamepad is present.

Patterns ​

Smooth locomotion with thumbstick ​

ts
const pad = xrInput.gamepads.left;
if (pad) {
  const { x, y } = pad.getAxesValues('xr-standard-thumbstick')!;
  // Move in grip space forward/right
  moveRigFromGrip(xrInput.xrOrigin.gripSpaces.left, x, y, dt);
}

Snap turn on entering Left/Right ​

ts
const pad = xrInput.gamepads.right;
if (pad?.getAxesEnteringLeft('xr-standard-thumbstick')) snapTurn(-30);
if (pad?.getAxesEnteringRight('xr-standard-thumbstick')) snapTurn(30);

Press‑to‑hold interactions ​

ts
const pad = xrInput.gamepads.right;
if (pad?.getButtonDown('xr-standard-trigger')) startLaser();
if (pad?.getButtonUp('xr-standard-trigger')) stopLaser();

Troubleshooting ​

  • getSelectStart() never fires: ensure the active layout’s selectComponentId matches your device profile, and that you’re checking the pad for the primary side.
  • Axes events feel jittery: increase axesThreshold above 0.8 to reduce accidental cardinal transitions.

Privacy | Terms