Tutorial 50: Accelerometer and Sensors
Platform support. Accelerometer support in CNA depends on the underlying SDL3 sensor API (SDL_Sensor). On Android: sensor access is implemented but requires hardware validation — test on a real device. On Desktop (Linux/Windows): the accelerometer API is not available unless the hardware exposes a compatible SDL sensor (some laptops with motion sensors qualify). Always check Accelerometer::IsSupported before calling Start().
Accelerometer class
#include "Microsoft/Xna/Framework/Input/Accelerometer.hpp"
using namespace Microsoft::Xna::Framework::Input;
// Check platform support
if (!Accelerometer::IsSupported()) {
// Fall back to keyboard/gamepad tilt simulation
return;
}
// Start sampling
Accelerometer::Start();
// ... in Update():
AccelerometerReading reading = Accelerometer::GetCurrentReading();
// Stop when no longer needed (saves battery on mobile)
Accelerometer::Stop();
AccelerometerReading
struct AccelerometerReading {
// Acceleration in each axis, expressed in units of g (9.81 m/s^2)
// When the device lies flat on a table: X ≈ 0, Y ≈ 0, Z ≈ -1 (or +1 depending on OS)
// When tilted left: X negative
// When tilted right: X positive
// When tilted toward user: Y positive
double Acceleration_X;
double Acceleration_Y;
double Acceleration_Z;
// Timestamp of the reading (nanoseconds since boot on Android)
long long Timestamp;
};
Axis conventions
| Axis | Device flat | Device tilted right | Device face-down |
|---|---|---|---|
| X | 0 | +1 g | 0 |
| Y | 0 | 0 | 0 |
| Z | −1 g | −1 g | +1 g |
Actual sign conventions can vary between Android OEM implementations. Always test on target hardware and apply a dead-zone filter.
Low-pass filter for stable tilt readings
Raw accelerometer data includes vibration noise. A simple exponential low-pass filter smooths it:
const float kAlpha = 0.2f; // 0 = no update, 1 = raw value
Vector3 filteredTilt_ = Vector3::Zero; // member variable
void Update(GameTime&) override {
if (!Accelerometer::IsSupported()) return;
AccelerometerReading raw = Accelerometer::GetCurrentReading();
Vector3 rawTilt(
static_cast<float>(raw.Acceleration_X),
static_cast<float>(raw.Acceleration_Y),
static_cast<float>(raw.Acceleration_Z));
filteredTilt_ = Vector3::Lerp(filteredTilt_, rawTilt, kAlpha);
}
Use cases
- Tilt controls — roll a marble, steer a vehicle, aim a weapon
- Shake detection — detect rapid acceleration spikes to trigger an action
- Step counter — count oscillations in the Y axis magnitude
- Orientation detection — determine portrait vs landscape without the display rotation API
Code example: tilt-based marble game input
class MarbleGame final : public Game {
public:
MarbleGame() : graphics_(this) {
setIsFixedTimeStep(true);
setTargetElapsedTime(TimeSpan::FromSeconds(1.0 / 60.0));
}
protected:
void Initialize() override {
Game::Initialize();
marblePos_ = Vector2(400, 300);
marbleVel_ = Vector2::Zero;
useTilt_ = Accelerometer::IsSupported();
if (useTilt_)
Accelerometer::Start();
}
void UnloadContent() override {
if (useTilt_)
Accelerometer::Stop();
}
void Update(GameTime& gameTime) override {
const float dt = static_cast<float>(
gameTime.ElapsedGameTime.TotalSeconds());
Vector2 gravity = Vector2::Zero;
if (useTilt_) {
// Tilt the device to roll the marble
AccelerometerReading r = Accelerometer::GetCurrentReading();
filteredX_ = filteredX_ * 0.8f + static_cast<float>(r.Acceleration_X) * 0.2f;
filteredY_ = filteredY_ * 0.8f + static_cast<float>(r.Acceleration_Y) * 0.2f;
// Dead zone: ignore tiny tilts
const float kDeadZone = 0.05f;
float gx = (std::abs(filteredX_) > kDeadZone) ? filteredX_ : 0.0f;
float gy = (std::abs(filteredY_) > kDeadZone) ? filteredY_ : 0.0f;
// Gravity in screen space: tilt right → marble goes right (+X)
// tilt toward user → marble goes down (+Y)
gravity = Vector2(gx, -gy) * 400.0f; // scale to pixels/s^2
} else {
// Keyboard fallback
auto kb = Keyboard::GetState();
if (kb.IsKeyDown(Keys::Left)) gravity.X -= 400.0f;
if (kb.IsKeyDown(Keys::Right)) gravity.X += 400.0f;
if (kb.IsKeyDown(Keys::Up)) gravity.Y -= 400.0f;
if (kb.IsKeyDown(Keys::Down)) gravity.Y += 400.0f;
}
// Integrate velocity and position
marbleVel_ = marbleVel_ + gravity * dt;
marbleVel_ = marbleVel_ * 0.97f; // friction
marblePos_ = marblePos_ + marbleVel_ * dt;
// Clamp to screen
auto& vp = getGraphicsDeviceProperty().getViewport();
const float r = 20.0f; // marble radius in pixels
marblePos_.X = MathHelper::Clamp(marblePos_.X, r, vp.Width - r);
marblePos_.Y = MathHelper::Clamp(marblePos_.Y, r, vp.Height - r);
// Bounce off walls
if (marblePos_.X <= r || marblePos_.X >= vp.Width - r) marbleVel_.X *= -0.6f;
if (marblePos_.Y <= r || marblePos_.Y >= vp.Height - r) marbleVel_.Y *= -0.6f;
}
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color::DarkGray);
spriteBatch_->Begin();
// Draw marble centred on marblePos_
Rectangle dest(
static_cast<int>(marblePos_.X - 20),
static_cast<int>(marblePos_.Y - 20),
40, 40);
spriteBatch_->Draw(*marbleTex_, dest, Color::White);
spriteBatch_->End();
gd.Present();
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<SpriteBatch> spriteBatch_;
Texture2D* marbleTex_ = nullptr;
Vector2 marblePos_;
Vector2 marbleVel_;
bool useTilt_ = false;
float filteredX_ = 0.0f;
float filteredY_ = 0.0f;
};