Tutorial 30: GamePad and Controller Input
GamePad support in CNA is provided through SDL3's joystick and gamepad subsystem. Xbox-style controllers, DualShock, and most SDL3-compatible gamepads are supported on Linux and Windows. Availability may vary by backend — the SDL3-based backend has the fullest support.
GamePad.GetState(PlayerIndex)
Call GamePad::GetState(PlayerIndex) once per frame in your Update method. It returns a GamePadState snapshot by value — cheap to copy. CNA supports up to four simultaneous controllers via PlayerIndex::One through PlayerIndex::Four.
Always check IsConnected before reading any state. Reading state from a disconnected controller returns safe defaults (all buttons released, sticks at zero).
void Update(GameTime& gameTime) override {
auto state = GamePad::GetState(PlayerIndex::One);
if (!state.IsConnected) return;
// safe to read buttons, sticks, triggers here
}
GamePadState Struct
GamePadState groups its data into four sub-structs:
| Member | Type | Description |
|---|---|---|
Buttons | GamePadButtons | Face buttons, shoulder buttons, Start, Back |
ThumbSticks | GamePadThumbSticks | Left and right stick as Vector2 |
Triggers | GamePadTriggers | Left and right trigger as float 0–1 |
DPad | GamePadDPad | D-pad directions as ButtonState |
IsConnected | bool | Whether the controller is plugged in |
Buttons (A/B/X/Y/Start/Back/Shoulder)
Each button in GamePadButtons is a ButtonState: either ButtonState::Pressed or ButtonState::Released.
Available buttons: A, B, X, Y, Start, Back, LeftShoulder, RightShoulder, LeftStick, RightStick (stick click).
To detect a single press (not held), compare the current frame against the previous frame:
bool IsJustPressed(ButtonState current, ButtonState previous) {
return current == ButtonState::Pressed && previous == ButtonState::Released;
}
// In your Update:
bool attackPressed = IsJustPressed(
state.Buttons.A,
prevState_.Buttons.A
);
Thumbsticks (Left/Right as Vector2)
state.ThumbSticks.Left and state.ThumbSticks.Right each return a Vector2 with components in the range [-1, 1]. In XNA convention, Y positive is up — the opposite of screen coordinates. When using a stick for 2D movement, negate Y to get screen-down as positive.
Vector2 move = state.ThumbSticks.Left;
// Convert to screen space: negate Y
playerPos_.X += move.X * speed * dt;
playerPos_.Y -= move.Y * speed * dt; // negate for screen coords
Triggers (float 0-1)
state.Triggers.Left and state.Triggers.Right return a float between 0.0 (fully released) and 1.0 (fully depressed). Ideal for analog actions such as acceleration, zoom, or charge attacks.
float shieldStrength = state.Triggers.Left; // 0 = no shield, 1 = full
float accelerator = state.Triggers.Right; // 0 = coasting, 1 = full throttle
Vibration
Call GamePad::SetVibration(PlayerIndex, leftMotor, rightMotor) to start rumble. Both motors accept a float in [0, 1]. Left motor is low-frequency (heavy thud), right is high-frequency (sharp buzz). Always call SetVibration with zeros to stop the effect — the motors keep running until told otherwise.
// Hit feedback: strong thud for 200 ms
void OnPlayerHit() {
GamePad::SetVibration(PlayerIndex::One, 0.8f, 0.3f);
vibrateTimer_ = 0.2f;
}
void Update(GameTime& gameTime) override {
float dt = (float)gameTime.getElapsedGameTime().TotalSeconds();
if (vibrateTimer_ > 0.0f) {
vibrateTimer_ -= dt;
if (vibrateTimer_ <= 0.0f)
GamePad::SetVibration(PlayerIndex::One, 0.0f, 0.0f);
}
}
Deadzone Handling
Physical thumbsticks never rest exactly at (0, 0). Always apply a deadzone to avoid unwanted drift. A radial deadzone checks the stick's total length — cleaner than checking X and Y independently (axial deadzone):
static constexpr float DEADZONE = 0.18f;
Vector2 ApplyRadialDeadzone(Vector2 stick) {
float len = stick.Length();
if (len < DEADZONE) return Vector2::Zero;
// Rescale so the live range starts at 0 outside the deadzone
float scale = (len - DEADZONE) / (1.0f - DEADZONE);
return Vector2::Normalize(stick) * scale;
}
// Axial deadzone (simpler, less accurate):
Vector2 ApplyAxialDeadzone(Vector2 stick) {
if (std::abs(stick.X) < DEADZONE) stick.X = 0.0f;
if (std::abs(stick.Y) < DEADZONE) stick.Y = 0.0f;
return stick;
}
Multiple Controllers
CNA supports up to four simultaneous controllers. Each player reads from their own PlayerIndex:
void Update(GameTime& gameTime) override {
static const PlayerIndex indices[] = {
PlayerIndex::One, PlayerIndex::Two,
PlayerIndex::Three, PlayerIndex::Four
};
for (int i = 0; i < 4; ++i) {
auto state = GamePad::GetState(indices[i]);
if (!state.IsConnected) continue;
UpdatePlayer(i, state);
}
}
Complete Example
A full game that moves a player rectangle with the left thumbstick, attacks with A (single-press), uses the left trigger for a shield, and vibrates on hit:
#include <memory>
#include <cmath>
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/GameTime.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"
#include "Microsoft/Xna/Framework/Vector2.hpp"
#include "Microsoft/Xna/Framework/Input/GamePad.hpp"
#include "Microsoft/Xna/Framework/Input/PlayerIndex.hpp"
#include "Microsoft/Xna/Framework/Input/ButtonState.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include "Microsoft/Xna/Framework/Graphics/Texture2D.hpp"
using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;
using namespace Microsoft::Xna::Framework::Input;
static constexpr float DEADZONE = 0.18f;
static constexpr float PLAYER_SPD = 250.0f;
static Vector2 ApplyRadialDeadzone(Vector2 stick) {
float len = stick.Length();
if (len < DEADZONE) return Vector2::Zero;
float scale = (len - DEADZONE) / (1.0f - DEADZONE);
return Vector2::Normalize(stick) * scale;
}
class GamePadDemo final : public Game {
public:
GamePadDemo() : graphics_(this) {
graphics_.setPreferredBackBufferWidth(800);
graphics_.setPreferredBackBufferHeight(600);
}
protected:
void LoadContent() override {
spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
// 1x1 white pixel used as a coloured rectangle
pixel_ = std::make_unique<Texture2D>(getGraphicsDeviceProperty(), 1, 1);
Color white = Color::White;
pixel_->SetData(&white, 1);
}
void Update(GameTime& gameTime) override {
float dt = (float)gameTime.getElapsedGameTime().TotalSeconds();
auto state = GamePad::GetState(PlayerIndex::One);
// Vibration timer
if (vibrateTimer_ > 0.0f) {
vibrateTimer_ -= dt;
if (vibrateTimer_ <= 0.0f)
GamePad::SetVibration(PlayerIndex::One, 0.0f, 0.0f);
}
if (!state.IsConnected) {
noController_ = true;
return;
}
noController_ = false;
// Movement with left thumbstick + radial deadzone
Vector2 move = ApplyRadialDeadzone(state.ThumbSticks.Left);
playerPos_.X += move.X * PLAYER_SPD * dt;
playerPos_.Y -= move.Y * PLAYER_SPD * dt; // Y-flip for screen space
// Clamp to screen
playerPos_.X = std::clamp(playerPos_.X, 0.0f, 760.0f);
playerPos_.Y = std::clamp(playerPos_.Y, 0.0f, 560.0f);
// Attack: A button — single press only
bool aPressed = state.Buttons.A == ButtonState::Pressed
&& prevState_.Buttons.A == ButtonState::Released;
if (aPressed) {
attacking_ = true;
attackTimer_ = 0.15f;
// Vibrate: sharp buzz on attack
GamePad::SetVibration(PlayerIndex::One, 0.2f, 0.9f);
vibrateTimer_ = 0.12f;
}
if (attackTimer_ > 0.0f) {
attackTimer_ -= dt;
if (attackTimer_ <= 0.0f) attacking_ = false;
}
// Shield: left trigger (analog)
shieldStrength_ = state.Triggers.Left;
prevState_ = state;
}
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color(30, 30, 40, 255));
spriteBatch_->Begin();
if (noController_) {
// Draw a grey placeholder when no controller connected
spriteBatch_->Draw(*pixel_,
Rectangle(300, 260, 200, 80), Color(80, 80, 80, 255));
} else {
// Shield aura (blue tint, proportional to trigger)
if (shieldStrength_ > 0.01f) {
int alpha = (int)(shieldStrength_ * 180);
spriteBatch_->Draw(*pixel_,
Rectangle((int)playerPos_.X - 6, (int)playerPos_.Y - 6, 52, 52),
Color(60, 120, 255, alpha));
}
// Player body
Color bodyCol = attacking_ ? Color(255, 200, 50, 255)
: Color(50, 200, 100, 255);
spriteBatch_->Draw(*pixel_,
Rectangle((int)playerPos_.X, (int)playerPos_.Y, 40, 40),
bodyCol);
}
spriteBatch_->End();
gd.Present();
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<SpriteBatch> spriteBatch_;
std::unique_ptr<Texture2D> pixel_;
Vector2 playerPos_ = { 380.0f, 280.0f };
GamePadState prevState_{};
float shieldStrength_ = 0.0f;
float vibrateTimer_ = 0.0f;
float attackTimer_ = 0.0f;
bool attacking_ = false;
bool noController_ = false;
};
int main() {
GamePadDemo game;
game.Run();
return 0;
}
Key Points
- Always check
IsConnectedbefore using any state members. - Use a radial deadzone on thumbsticks to avoid diagonal bias.
- Detect single presses by comparing current and previous
GamePadState. - Always reset vibration to
0, 0— motors stay on until explicitly stopped. - Y on thumbsticks is positive-up; negate when mapping to screen Y (positive-down).