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().
#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.
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.