> For the complete documentation index, see [llms.txt](https://series-1.gitbook.io/rundot-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://series-1.gitbook.io/rundot-docs/v5.21.0/readme/gamepad.md).

# Gamepad API

Read hardware controllers (gamepads) with a single normalized model that works identically across desktop web, the Steam Deck / Steam shell, and mobile hardware controllers (Backbone on iOS and Android). Write your input code once and `RundotGameAPI.gamepad` reports the same button names, axis signs, and analog trigger values on every surface.

The host decides how input is sourced for you — either a zero-latency direct read of the browser's Web Gamepad API, or a host-mediated stream where the platform lacks a reliable Web Gamepad API (some mobile WebViews) or remaps input through Steam Input on the Deck. Your game cannot tell which path served it.

## Quick Start

```typescript
import RundotGameAPI from '@series-inc/rundot-game-sdk/api'

if (RundotGameAPI.gamepad.isSupported()) {
  const onConnected = RundotGameAPI.gamepad.onConnected((event) => {
    console.log(`controller ${event.index} connected: ${event.id}`)
  })

  const onDisconnected = RundotGameAPI.gamepad.onDisconnected((event) => {
    console.log(`controller ${event.index} disconnected`)
  })

  // Poll inside your own requestAnimationFrame loop:
  function tick(): void {
    for (const pad of RundotGameAPI.gamepad.getGamepads()) {
      if (pad.buttons.a.pressed) jump()
      move(pad.axes.leftX, pad.axes.leftY)
      accelerate(pad.buttons.rightTrigger.value) // analog 0..1
    }
    requestAnimationFrame(tick)
  }
  requestAnimationFrame(tick)

  // On teardown:
  // onConnected.unsubscribe(); onDisconnected.unsubscribe()
}
```

`getGamepads()` is synchronous and never throws — poll it from your own render loop. It returns only currently-connected pads (no `null`/disconnected slots), so an empty array means no controller is attached.

## Capability Detection

`isSupported()` reflects **capability**, not whether a pad is currently connected. It is `true` on any controller-capable surface (even with no pad attached) and `false` on a surface that cannot read controllers (or on a host build that predates this feature). Use it to decide whether to show controller-specific UI.

```typescript
if (!RundotGameAPI.gamepad.isSupported()) {
  // Fall back to touch/keyboard controls.
}
```

`getGamepads()` returns `[]` and the connect/disconnect events never fire when input is unsupported — there is never an error or a hang.

## Connect / Disconnect Events

```typescript
const sub = RundotGameAPI.gamepad.onConnected((event) => {
  // event: { index: number; id: string; source: GamepadSource }
})
// Later:
sub.unsubscribe()
```

A controller already attached when your game starts still fires `onConnected`, so you don't need a separate "scan on startup" path.

## Normalized Model

Every surface reports the same `GamepadSnapshot`. Button names follow the W3C standard layout (Xbox naming); analog triggers report `value` in `0..1` and sticks report each axis in `-1..1`.

```typescript
interface GamepadSnapshot {
  index: number
  id: string
  connected: boolean
  source: GamepadSource
  standardMapping: boolean // false = best-effort mapping
  buttons: Record<GamepadButtonName, { pressed: boolean; value: number }>
  axes: { leftX: number; leftY: number; rightX: number; rightY: number }
  sourceTimestamp: number // when the device was sampled
  deliveredTimestamp: number // when the SDK served it (>= sourceTimestamp)
}
```

### Button map

| `GamepadButtonName`                           | Standard control                             |
| --------------------------------------------- | -------------------------------------------- |
| `a`, `b`, `x`, `y`                            | Face buttons                                 |
| `leftBumper`, `rightBumper`                   | Shoulder buttons (L1 / R1)                   |
| `leftTrigger`, `rightTrigger`                 | Analog triggers (L2 / R2), `value` in `0..1` |
| `select`, `start`, `guide`                    | Back / Start / Home                          |
| `leftStick`, `rightStick`                     | Stick clicks (L3 / R3)                       |
| `dpadUp`, `dpadDown`, `dpadLeft`, `dpadRight` | D-pad                                        |

### Axis map

| Axis     | Range   | Notes                                    |
| -------- | ------- | ---------------------------------------- |
| `leftX`  | `-1..1` | Left stick X (left = `-1`, right = `+1`) |
| `leftY`  | `-1..1` | Left stick Y (up = `-1`, down = `+1`)    |
| `rightX` | `-1..1` | Right stick X                            |
| `rightY` | `-1..1` | Right stick Y                            |

### Sources

`source` tells you where the snapshot came from. It does not change how you read the data — the model is identical regardless.

| `GamepadSource`      | Surface                                  |
| -------------------- | ---------------------------------------- |
| `web-gamepad`        | Desktop web / browser Web Gamepad API    |
| `steam-input`        | Steam Input (Steam Deck + remapped pads) |
| `ios-gamecontroller` | iOS GameController (Backbone, MFi)       |
| `android-input`      | Android InputDevice                      |

## Latency Telemetry

Each snapshot carries `sourceTimestamp` (when the provider sampled the device) and `deliveredTimestamp` (when the SDK served it). On the zero-latency direct path the two are equal; on the host-mediated path `deliveredTimestamp` is larger by the transport cost, so `deliveredTimestamp - sourceTimestamp` is the added input latency.

`gamepad.__debug.getLatencyStats()` returns `{ p50, p95, max, sampleCount }` in ms over a rolling window. It is **off by default** (returns `sampleCount: 0`) and only records when the input debug flag is enabled, in which case the host also logs a structured `gamepad.latency` event to its observability pipeline.

```typescript
const { p95, sampleCount } = RundotGameAPI.gamepad.__debug.getLatencyStats()
```

## Best Practices

* Poll `getGamepads()` from your own `requestAnimationFrame` loop — never store the array, read it fresh each frame.
* Gate controller UI on `isSupported()`, and always keep a touch/keyboard fallback for surfaces without a controller.
* Unsubscribe from `onConnected`/`onDisconnected` when your game tears down.
* Treat `value` on triggers as analog (`0..1`) and apply your own dead-zone to sticks; raw axes are reported untouched in `-1..1`.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://series-1.gitbook.io/rundot-docs/v5.21.0/readme/gamepad.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
