docs

Gamepad Input

Forward game controller connections, button presses, and axis movement to pages.

On this page

Pages read game controllers through the standard Gamepad API. The library never polls the OS— you describe each controller, report connections and disconnections, and forward stick and button changes as they happen.

AppCore doesn't forward controllers either, so every application manages gamepad input this way. If you haven't created a renderer yet, see Integrating into a Game Engine.

Describing a Controller

To describe a controller, call Renderer::SetGamepadDetails() with its slot index, a name string, and its axis and button counts.

Controllers belong to the Renderer, not an individual View.

Controller Slots and Bounds

The slot index determines the controller's position in the page's navigator.getGamepads() array. Always number controllers starting from 0. If you assign a controller to slot 1 without registering slot 0, slot 0 reads null.

The device ID appears on the page as gamepad.id. Later button and axis events must stay within the declared counts— any index outside those counts is ignored.

If a controller's layout or name changes, call Renderer::SetGamepadDetails() again for that slot. Disconnect and reconnect the controller to apply the update— pages keep seeing the old description while the controller stays connected.

Connecting and Disconnecting

To connect or disconnect a controller, dispatch a GamepadEvent to Renderer::FireGamepadEvent().

Setting the event type to GamepadEvent::kType_GamepadConnected makes the controller visible to pages. The page's window receives a gamepadconnected event immediately, without waiting for the user to press a button first.

When a controller is unplugged, fire another event with GamepadEvent::kType_GamepadDisconnected. The page's window receives a gamepaddisconnected event, and that slot in navigator.getGamepads() returns to null.

📘 Registration Order

Describe a controller first, fire its connection event second, and then forward input changes. Connecting a slot that was never described has no effect.

Forwarding Sticks and Buttons

To forward stick movement and button presses, dispatch a GamepadAxisEvent to Renderer::FireGamepadAxisEvent() and a GamepadButtonEvent to Renderer::FireGamepadButtonEvent().

C++
#include <Ultralight/Ultralight.h>

using namespace ultralight;

RefPtr<Renderer> renderer;

///
/// Called by our engine when a controller is plugged in.
///
void OnControllerConnected(uint32_t slot, const char* name, uint32_t axes,
                           uint32_t buttons) {
  ///
  /// Describe the controller first (its name shows up as gamepad.id).
  ///
  renderer->SetGamepadDetails(slot, name, axes, buttons);

  ///
  /// Now tell pages it's connected.
  ///
  GamepadEvent evt;
  evt.type = GamepadEvent::kType_GamepadConnected;
  evt.index = slot;
  renderer->FireGamepadEvent(evt);
}

///
/// Called by our engine whenever a stick or trigger moves.
///
void OnControllerAxis(uint32_t slot, uint32_t axis, double value) {
  GamepadAxisEvent evt;
  evt.index = slot;
  evt.axis_index = axis;
  evt.value = value; // Normalized to [-1.0, 1.0].
  renderer->FireGamepadAxisEvent(evt);
}

///
/// Called by our engine whenever a button's value changes.
///
void OnControllerButton(uint32_t slot, uint32_t button, double value) {
  GamepadButtonEvent evt;
  evt.index = slot;
  evt.button_index = button;
  evt.value = value; // 0.0 is released, 1.0 is fully pressed.
  renderer->FireGamepadButtonEvent(evt);
}

Pages treat any reading greater than zero as pressed— analog triggers can send fractional values to represent partial presses.

🚧 Call on the Renderer's Thread

All gamepad calls must run on the Renderer's thread. If you read controllers on a background thread, forward each change with Renderer::PostTask(). See Updating and Rendering.

Reading Controllers on the Page

JavaScript reads controller state by polling navigator.getGamepads() inside an animation loop.

JavaScript
function pollControllers() {
  for (const pad of navigator.getGamepads()) {
    if (!pad)
      continue;

    // Axis values run from -1.0 to 1.0. Each button has a 'pressed' flag
    // and a 'value' from 0.0 to 1.0.
    if (pad.buttons[0].pressed)
      console.log(pad.id + ' says: Ultralight rocks! (axis 0 is ' +
                  pad.axes[0] + ')');
  }

  requestAnimationFrame(pollControllers);
}

requestAnimationFrame(pollControllers);

Each controller object exposes its id, an axes array, and a buttons array where each button provides a boolean pressed flag and a numeric value. For details on standard browser properties, see the MDN Gamepad API reference.

Gamepad state is process-wide— every page across every View sees every controller you forward. This differs from keyboard and mouse events, which reach only the specific View they are fired on. See Keyboard Input.