Tutorial 50: Accelerometer and Sensors
What you’ll learn
- Starting the
Accelerometerand reading anAccelerometerReading. - The axis conventions, and which way is up.
- Smoothing raw samples with a low-pass filter before using them as input.
Before you start — Tutorial 49: Touch Input and Gestures — sensors are the other half of mobile input. The Microsoft::Devices sensor layer is always built and is part of the CNA target — no CMake option is needed (-DCNA_DEVICES=ON is a different, optional layer; see the note below). Real readings need hardware that has the sensor and the default SDL3 platform implementation.
Sensors are not Android-only. Accelerometer and Gyroscope accept Android, iOS and desktop targets (a browser build is excluded unless you turn on keyboard emulation, below) and ask the selected CNA platform implementation whether a real sensor exists. Only the SDL3 platform (the default, CNA_PLATFORM=SDL3) implements that query, through SDL’s sensor subsystem; with HEADLESS or TERMINAL getIsSupportedProperty() is simply false. One opt-in exception: Accelerometer::setKeyboardEmulationEnabledEXT(true) (a CNAEXT switch, off by default, to be set while no accelerometer is started) replaces the primary accelerometer with arrow-key tilt readings, and getIsSupportedProperty() then answers true on desktop and in the browser without any hardware. On an SDL3 desktop build, a laptop or 2-in-1 with a motion sensor, or a controller that reports one, gives real readings. Whether you get data therefore depends on the machine and the platform choice: always check getIsSupportedProperty() before calling Start(), and keep a keyboard fallback for machines with no sensor. Compass and Motion are Android-only — their implementations are compiled behind __ANDROID__. To tell a phone from a desktop at run time, Microsoft::Devices::Environment::getDeviceTypeProperty() returns DeviceType::Device on Android and iOS targets and DeviceType::Emulator on desktop and browser targets.
Two things to keep straight. The sensor classes (Microsoft::Devices::Sensors) need no build option; the -DCNA_DEVICES=ON switch (default OFF) only gates the separate CNA-specific CNA::Devices headers (battery, clipboard, locale, message boxes and similar), which this tutorial does not use. And Android sensor paths, while implemented, are covered by no automatic CI — validate on a real device before shipping.
Accelerometer class
#include "Microsoft/Devices/Sensors/Accelerometer.hpp"
using namespace Microsoft::Devices::Sensors;
// Support is a static query; sampling is per-instance.
if (!Accelerometer::getIsSupportedProperty()) {
// Fall back to keyboard/gamepad tilt simulation
return;
}
Accelerometer accelerometer; // keep this alive as a member
// Start sampling
accelerometer.Start();
// ... in Update():
AccelerometerReading reading = accelerometer.getCurrentValueProperty();
Vector3 g = reading.getAccelerationProperty();
// Stop when no longer needed (saves battery on mobile)
accelerometer.Stop();
Keep the support check in front of every read: getCurrentValueProperty() throws System::InvalidOperationException on an instance whose sensor is not supported, exactly as the XNA/Windows Phone API did. getStateProperty() returns the SensorState enum, and there is a CurrentValueChanged event if you prefer callbacks to polling.
AccelerometerReading
AccelerometerReading is a class, not a plain struct: the acceleration comes back as a single Vector3 through a property accessor.
AccelerometerReading reading = accelerometer.getCurrentValueProperty();
// 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
Vector3 acceleration = reading.getAccelerationProperty();
// Timestamp of the reading, as a DateTimeOffset
auto timestamp = reading.getTimestampProperty();
// True once at least one sample has arrived from the driver
bool valid = accelerometer.getIsDataValidProperty();
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 vary between Android OEM implementations, and desktop SDL sensors have their own conventions again. Always test on target hardware and apply a dead-zone filter.
The other sensors
| Type | Where it works |
|---|---|
Accelerometer | Android, iOS and desktop, wherever the SDL3 platform finds an accelerometer |
Gyroscope | Android, iOS and desktop, wherever the SDL3 platform finds a gyroscope |
Compass | Android only |
Motion | Android only |
All four follow the same shape as Accelerometer: a static getIsSupportedProperty(), a per-instance Start()/Stop(), and a getCurrentValueProperty() returning a reading class.
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::getIsSupportedProperty()) return;
AccelerometerReading raw = accelerometer_.getCurrentValueProperty();
Vector3 rawTilt = raw.getAccelerationProperty();
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
#include "Microsoft/Devices/Sensors/Accelerometer.hpp"
using namespace Microsoft::Devices::Sensors;
class MarbleGame final : public Game {
public:
MarbleGame() : graphics_(this) {
setIsFixedTimeStepProperty(true);
setTargetElapsedTimeProperty(TimeSpan::FromSeconds(1.0 / 60.0));
}
protected:
void Initialize() override {
Game::Initialize();
marblePos_ = Vector2(400, 300);
marbleVel_ = Vector2::Zero;
useTilt_ = Accelerometer::getIsSupportedProperty();
if (useTilt_)
accelerometer_.Start();
}
void LoadContent() override {
spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
// A 1x1 white pixel, stretched to the marble's size when drawn
marbleTex_ = std::make_unique<Texture2D>(getGraphicsDeviceProperty(), 1, 1);
const Color white = Color::White;
marbleTex_->SetData(&white, 1);
}
void UnloadContent() override {
if (useTilt_)
accelerometer_.Stop();
}
void Update(GameTime& gameTime) override {
Game::Update(gameTime);
const float dt = static_cast<float>(
gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty());
Vector2 gravity = Vector2::Zero;
if (useTilt_) {
// Tilt the device to roll the marble
Vector3 a = accelerometer_.getCurrentValueProperty().getAccelerationProperty();
filteredX_ = filteredX_ * 0.8f + a.X * 0.2f;
filteredY_ = filteredY_ * 0.8f + a.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().getViewportProperty();
const float r = 20.0f; // marble radius in pixels
marblePos_.X = MathHelper::Clamp(marblePos_.X, r, vp.getWidthProperty() - r);
marblePos_.Y = MathHelper::Clamp(marblePos_.Y, r, vp.getHeightProperty() - r);
// Bounce off walls
if (marblePos_.X <= r || marblePos_.X >= vp.getWidthProperty() - r) marbleVel_.X *= -0.6f;
if (marblePos_.Y <= r || marblePos_.Y >= vp.getHeightProperty() - 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();
// No Present(): Game presents the frame after Draw() returns.
}
private:
GraphicsDeviceManager graphics_;
Microsoft::Devices::Sensors::Accelerometer accelerometer_;
std::unique_ptr<SpriteBatch> spriteBatch_;
std::unique_ptr<Texture2D> marbleTex_;
Vector2 marblePos_;
Vector2 marbleVel_;
bool useTilt_ = false;
float filteredX_ = 0.0f;
float filteredY_ = 0.0f;
};
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Sensors and vibration: delivery, math and lifetime — Which thread delivers a CNA sensor reading and in which units, the Android compass and motion mathematics, the landscape remap, VibrateController semantics, the open Dispose(bool) defect and the evidence limits.