Tutorial 30: GamePad and Controller Input
What you’ll learn
- Reading a
GamePadStateperPlayerIndex. - Buttons, thumbsticks as
Vector2, and triggers as floats. - Applying a deadzone so a resting stick reads as zero.
- Vibration, and handling more than one controller.
Before you start — Tutorial 10: Handling Keyboard Input — gamepads use the same poll-and-compare-with-last-frame pattern.
GamePad support comes from CNA's platform layer, independently of which graphics renderer you compiled in. With the default SDL3 platform it is provided through SDL3's joystick and gamepad subsystem: Xbox-style controllers, DualShock, and most SDL3-compatible gamepads work on Linux, Windows and macOS, with rumble where the device offers it; controller mappings come from SDL’s own gamecontrollerdb. The TERMINAL and HEADLESS platforms expose no gamepad at all: there getIsConnectedProperty() is always false, so make sure your game is still usable without one. To test pad code without a pad, CNA has an opt-in CNAEXT switch, GamePad::setKeyboardEmulationEnabledEXT(true) (off by default), which makes player one a connected pad driven by the keyboard (WASD for the left stick, the arrow keys for the right stick, K/L/J/I for A/B/X/Y, Enter and Escape for Start and Back); it merges with a real pad and leaves the keys visible to Keyboard.
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 getIsConnectedProperty() 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.getIsConnectedProperty()) return;
// safe to read buttons, sticks, triggers here
}
GamePadState Struct
GamePadState groups its data into four sub-structs. As everywhere in CNA’s C++ API, each XNA member is read through an accessor (getButtonsProperty(), and so on):
| Member | Type | Description |
|---|---|---|
getButtonsProperty() | GamePadButtons | Face buttons, shoulder buttons, Start, Back |
getThumbSticksProperty() | GamePadThumbSticks | Left and right stick as Vector2 |
getTriggersProperty() | GamePadTriggers | Left and right trigger as float 0–1 |
getDPadProperty() | GamePadDPad | D-pad directions as ButtonState |
getIsConnectedProperty() | 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, each read with a getter such as getAProperty(): 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.getButtonsProperty().getAProperty(),
prevState_.getButtonsProperty().getAProperty()
);
Thumbsticks (Left/Right as Vector2)
state.getThumbSticksProperty().getLeftProperty() and getRightProperty() 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.getThumbSticksProperty().getLeftProperty();
// 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.getTriggersProperty().getLeftProperty() and getRightProperty() 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.getTriggersProperty().getLeftProperty(); // 0 = no shield, 1 = full
float accelerator = state.getTriggersProperty().getRightProperty(); // 0 = coasting, 1 = full throttle
Vibration
Call GamePad::SetVibration(PlayerIndex, leftMotor, rightMotor) to start rumble; it returns false if the controller (or the platform) has no vibration support. 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.getElapsedGameTimeProperty().getTotalSecondsProperty();
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), so a deadzone is needed to avoid unwanted drift. Unlike some other frameworks, XNA (and so CNA) already applies one for you: GamePad::GetState(PlayerIndex) uses GamePadDeadZone::IndependentAxes, which compares X and Y with the threshold separately. You choose the mode with the second overload:
// Built-in options
auto s1 = GamePad::GetState(PlayerIndex::One); // IndependentAxes (default)
auto s2 = GamePad::GetState(PlayerIndex::One, GamePadDeadZone::Circular); // radial, better for aiming
auto s3 = GamePad::GetState(PlayerIndex::One, GamePadDeadZone::None); // raw values
Circular checks the stick's total length — cleaner than checking X and Y independently. If you want to control the threshold and the rescaling yourself, ask for the raw values with GamePadDeadZone::None and process them, so the two deadzones do not stack:
auto state = GamePad::GetState(PlayerIndex::One, GamePadDeadZone::None);
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.getIsConnectedProperty()) 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 <algorithm>
#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/PlayerIndex.hpp"
#include "Microsoft/Xna/Framework/Input/ButtonState.hpp"
#include "Microsoft/Xna/Framework/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_.setPreferredBackBufferWidthProperty(800);
graphics_.setPreferredBackBufferHeightProperty(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.getElapsedGameTimeProperty().getTotalSecondsProperty();
// Raw stick values: the radial deadzone below does the processing
auto state = GamePad::GetState(PlayerIndex::One, GamePadDeadZone::None);
// Vibration timer
if (vibrateTimer_ > 0.0f) {
vibrateTimer_ -= dt;
if (vibrateTimer_ <= 0.0f)
GamePad::SetVibration(PlayerIndex::One, 0.0f, 0.0f);
}
if (!state.getIsConnectedProperty()) {
noController_ = true;
return;
}
noController_ = false;
// Movement with left thumbstick + radial deadzone
Vector2 move = ApplyRadialDeadzone(state.getThumbSticksProperty().getLeftProperty());
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.getButtonsProperty().getAProperty() == ButtonState::Pressed
&& prevState_.getButtonsProperty().getAProperty() == 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.getTriggersProperty().getLeftProperty();
prevState_ = state;
}
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color(30, 30, 40, 255));
spriteBatch_->Begin(); // AlphaBlend (premultiplied), see the shield colour below
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 = static_cast<int>(shieldStrength_ * 180);
spriteBatch_->Draw(*pixel_,
Rectangle(static_cast<int>(playerPos_.X) - 6,
static_cast<int>(playerPos_.Y) - 6, 52, 52),
Color::FromNonPremultiplied(60, 120, 255, alpha));
}
// Player body
Color bodyCol = attacking_ ? Color(255, 200, 50, 255)
: Color(50, 200, 100, 255);
spriteBatch_->Draw(*pixel_,
Rectangle(static_cast<int>(playerPos_.X),
static_cast<int>(playerPos_.Y), 40, 40),
bodyCol);
}
spriteBatch_->End();
// Game::EndDraw() presents the frame after Draw() returns.
}
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
getIsConnectedProperty()before using any state members, and keep your game playable when no controller exists (several platform layers have none). - Use a radial deadzone (
GamePadDeadZone::Circular, or your own on raw values) 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).
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Gamepads, joysticks, haptics and host power — How CNA maps controllers to the four XNA slots, normalizes and dead-zones axes, what each GamePad extension does, and how the joystick, haptics, power and sensor classes of CNA::Input behave.