-
-
Notifications
You must be signed in to change notification settings - Fork 2
Gamepad Input Quick Start
A compact reference for controller buttons, analog sticks, triggers, XInput, and SDL2 in Gondwana.
- Choose a Backend
- The Basic Pattern
- Show the A Button Being Pressed
- Register Every Connected Controller
- Read an Analog Stick
- Read Triggers
- Handle Held-Button Repeat
- Support Hot-Plugged Controllers
- Pause or Remove Bindings
- Cleanup
- Cheat Sheet
- Common Problems
- Further Reading
Gondwana currently provides two common gamepad backends.
| Backend | Best fit | Initialization |
|---|---|---|
| XInput | Xbox-compatible controllers on Windows | Engine.InitializeXInputGamepadManager() |
| SDL2 | Cross-platform controller support | Engine.InitializeSdlGamepadManager() |
A WinForms game host initializes XInput by default.
Avalonia and Blazor hosts leave gamepad backend selection to the game.
Gamepad setup has four parts:
Initialize manager
↓
Discover connected adapters
↓
Register a button for each GamepadId
↓
Handle ButtonDown
The manager owns devices. The event poller owns registered button events.
This WinForms/XInput example registers the A button on existing controllers.
using Gondwana.Input.Gamepad;protected override void OnGamepadManagerInitialized()
{
var manager = Engine.Input.GamepadManager;
var poller = Engine.Input.GamepadEventPoller;
if (manager is null || poller is null)
return;
// Perform one initial discovery/state refresh.
manager.Update();
poller.ButtonDown += OnGamepadButtonDown;
foreach (var gamepad in manager.ConnectedAdapters)
{
poller.StartMonitoringButton(
gamepad.GamepadId,
button: "A");
}
}private void OnGamepadButtonDown(
GamepadButtonDownEventArgs e)
{
if (e.Config.Button == "A")
{
Console.WriteLine(
$"A is down on {e.Adapter.GamepadId}.");
}
}For XInput, controller IDs look like:
XInput_0
XInput_1
XInput_2
XInput_3
private void RegisterButtons(
IGamepadAdapter gamepad)
{
var poller =
Engine.Input.GamepadEventPoller!;
poller.StartMonitoringButton(
gamepad.GamepadId,
"A");
poller.StartMonitoringButton(
gamepad.GamepadId,
"B");
poller.StartMonitoringButton(
gamepad.GamepadId,
"Start");
}foreach (var gamepad in manager.ConnectedAdapters)
{
RegisterButtons(gamepad);
}Button names come from the selected backend.
A
B
X
Y
Start
Back
DPadUp
DPadDown
DPadLeft
DPadRight
LeftShoulder
RightShoulder
SDL2 names mirror its enum values:
SDL_CONTROLLER_BUTTON_A
SDL_CONTROLLER_BUTTON_B
SDL_CONTROLLER_BUTTON_X
SDL_CONTROLLER_BUTTON_Y
A portable game should map backend-specific names to game actions.
Analog sticks are continuous state. Read them from the adapter.
private void ReadLeftStick(
IGamepadAdapter gamepad)
{
var stick = gamepad.LeftStick?
.WithDeadzone(0.20f);
if (stick is not { } value)
return;
if (!value.IsEngaged())
return;
Console.WriteLine(
$"Stick: {value.X:0.00}, {value.Y:0.00}");
}Apply it to movement:
var direction = new Vector2(
value.X,
-value.Y);
player.Movement.SetVelocity(
direction * moveSpeed);The Y sign may need to be inverted because many game worlds use positive Y downward while controller APIs commonly describe up as positive.
StickDirection direction =
value.Direction(0.20f);
if (direction.HasFlag(
StickDirection.Left))
{
MoveLeft();
}float left =
gamepad.LeftTrigger;
float right =
gamepad.RightTrigger;Typical values are normalized:
0.0 = not pressed
1.0 = fully pressed
Example:
if (gamepad.RightTrigger > 0.5f)
{
Accelerate();
}For continuous trigger behavior, read the adapter state during your update path rather than relying on a button event.
ButtonDown may fire repeatedly while the button remains held.
Control the interval when registering:
poller.StartMonitoringButton(
gamepad.GamepadId,
"A",
timeBetweenEvents: 0.20);This is useful for menu navigation.
For a one-time action, track your own down state:
private readonly HashSet<string>
_activeButtons = new();private void OnGamepadButtonDown(
GamepadButtonDownEventArgs e)
{
string key =
$"{e.Adapter.GamepadId}:{e.Config.Button}";
if (!_activeButtons.Add(key))
return;
Jump();
}To implement true press/release edge semantics for gamepad buttons, compare PressedButtons snapshots in a game-specific controller layer. The core GamepadEventPoller currently exposes held-compatible button-down events rather than a released event.
A controller may connect after initial setup.
Track registered IDs:
private readonly HashSet<string>
_registeredGamepads = new();private void RegisterNewGamepads()
{
var manager =
Engine.Input.GamepadManager;
if (manager is null)
return;
foreach (var gamepad in manager.ConnectedAdapters)
{
if (!_registeredGamepads.Add(
gamepad.GamepadId))
{
continue;
}
RegisterButtons(gamepad);
}
}Call this from an appropriate game update or periodic timer.
Do not call manager.Update() in a separate unbounded loop. The engine already refreshes it at the engine frame rate.
Pause every gamepad event:
poller.PauseAllInput = true;Resume:
poller.PauseAllInput = false;Pause one configuration:
poller
.AllButtonConfigsByGamepadId[gamepadId]["A"]
.IsPaused = true;Stop one button:
poller.StopMonitoringButton(
gamepadId,
"A");Stop all buttons for one controller:
poller.StopMonitoringAllButtons(
gamepadId);protected override void UnhookEvents()
{
if (Engine.Input.GamepadEventPoller is { } poller)
poller.ButtonDown -= OnGamepadButtonDown;
}The engine also removes registered button configurations during disposal.
Engine.InitializeXInputGamepadManager();Engine.InitializeSdlGamepadManager();poller.StartMonitoringButton(
gamepad.GamepadId,
"A");if (e.Config.Button == "A")
{
}var stick =
gamepad.LeftStick?
.WithDeadzone(0.20f);float left = gamepad.LeftTrigger;
float right = gamepad.RightTrigger;string id = gamepad.GamepadId;Perform one initial manager update:
manager.Update();The engine handles ongoing updates afterward.
Confirm the button name for the active backend.
XInput:
A
SDL2:
SDL_CONTROLLER_BUTTON_A
ButtonDown can repeat while held. Increase timeBetweenEvents or implement game-level edge tracking.
Apply a deadzone:
gamepad.LeftStick?
.WithDeadzone(0.20f);Register the buttons for its new GamepadId.
The manager refresh and event polling occupy distinct parts of the engine cycle. Treat controller state as frame-sampled input and avoid assumptions about native-event immediacy.
- Home
- Make Your First Game in 30 Minutes
- Engine Architecture Overview
- Gondwana Engine Lifecycle
- Gondwana CLI Cheatsheet
- Assets Files
- Tilesheets
- Scenes and SceneLayers
- Sprites
- Views, Cameras, and Viewports
- DirectDrawing
- Game State Files
- Logging
- Movement and Controllers
- Input Handling
- Collision Detection
- Timers and Engine Timing
- Using the Effects System
- Engine Configuration