Gamepad
Read game controllers from C#: buttons, analogue sticks and triggers, plus rumble on the pads that support it - polled in the browser's frame loop so your Blazor code only hears about actual input.
@inject Bit.Butil.Gamepad gamepadMDN reference
GetGamepads returns an empty array even with a pad plugged in -
that is not a bug, and the fix is to ask the user to press a button.
Returns true when the runtime exposes navigator.getGamepads. During prerender/SSR the check returns false rather than throwing, so defer it to OnAfterRenderAsync.
@inject Bit.Butil.Gamepad gamepad
var supported = await gamepad.IsSupported();A point-in-time read of every connected pad. Axes are floats from -1 to 1; buttons carry an analogue Value as well as Pressed and Touched, because triggers on a standard-mapping pad are buttons rather than axes. Mapping is 'standard' when the browser recognised the layout - otherwise the indices are device-specific.
var pads = await gamepad.GetGamepads();
foreach (var pad in pads)
{
// pad.Mapping == "standard": axes 0/1 are the left stick, 2/3 the right
var leftX = pad.Axes.ElementAtOrDefault(0);
var aPressed = pad.Buttons.ElementAtOrDefault(0)?.Pressed ?? false;
var trigger = pad.Buttons.ElementAtOrDefault(7)?.Value ?? 0; // analogue
}Gamepads have no input events - polling is the only way to read them. SubscribeChanges polls on requestAnimationFrame (which also means it stops while the tab is hidden) and only crosses into .NET when the state actually differs, at most once per minIntervalMs. Analogue sticks jitter constantly, so that floor is what keeps a controller sitting still from calling into C# sixty times a second.
private ButilSubscription? _watch;
_watch = await gamepad.SubscribeChanges(
pads => InvokeAsync(() =>
{
_live = pads;
StateHasChanged();
}),
minIntervalMs: 50);
// later:
await _watch.DisposeAsync();Fires when a pad is plugged in or unplugged. Both callbacks get the full list of connected pads as it is after the change, not just the one that changed, so you can rebind your UI from a single argument.
_connection = await gamepad.SubscribeConnection(
onConnected: pads => InvokeAsync(() => { /* ... */ }),
onDisconnected: pads => InvokeAsync(() => { /* ... */ }));Plays a dual-rumble effect on a pad's two motors: strongMagnitude drives the low-frequency (heavy) one, weakMagnitude the high-frequency (light) one, both from 0 to 1. Returns false when the pad is gone or has no vibration actuator - check HasVibration on a snapshot first.
var played = await gamepad.Vibrate(
index: 0,
durationMs: 300,
strongMagnitude: 1.0,
weakMagnitude: 0.4);
// cut an effect short:
await gamepad.ResetVibration(0);API reference
ValueTask<bool> IsSupported()ValueTask<GamepadState[]> GetGamepads()ValueTask<ButilSubscription> SubscribeConnection(Action<GamepadState[]>? onConnected = null, Action<GamepadState[]>? onDisconnected = null)ValueTask<ButilSubscription> SubscribeChanges(Action<GamepadState[]> handler, int minIntervalMs = 50)ValueTask<bool> Vibrate(int index, int durationMs = 200, double strongMagnitude = 1, double weakMagnitude = 1, int startDelayMs = 0)ValueTask ResetVibration(int index)ValueTask DisposeAsync()Index, Id, Connected, Mapping, Timestamp, Axes, Buttons, HasVibrationPressed, Touched, Value