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. Requires -DCNA_DEVICES=ON: the Microsoft::Devices sensor layer is gated behind that CMake option, which defaults to OFF. Real readings additionally need hardware that has the sensor.
Sensors are not Android-only. Accelerometer and Gyroscope run a real SDL3 hardware probe on desktop Linux, Windows and macOS as well as on Android — getIsSupportedProperty() genuinely opens the SDL sensor subsystem and looks for an SDL_SENSOR_ACCEL / SDL_SENSOR_GYRO device. On a laptop with a motion sensor, or a controller that reports one, you get real readings on the desktop. Whether you get data therefore depends on the machine, not on the operating system: 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__.
Two things must be true before any of this compiles or runs. The Microsoft::Devices layer is gated behind -DCNA_DEVICES=ON, which defaults to OFF, so a stock build has no sensor types at all. 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();
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 plus desktop Linux/Windows/macOS, wherever SDL3 finds an accelerometer |
Gyroscope | Android plus desktop Linux/Windows/macOS, wherever SDL3 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 UnloadContent() override {
if (useTilt_)
accelerometer_.Stop();
}
void Update(GameTime& gameTime) override {
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();
gd.Present();
}
private:
GraphicsDeviceManager graphics_;
Microsoft::Devices::Sensors::Accelerometer accelerometer_;
std::unique_ptr<SpriteBatch> spriteBatch_;
Texture2D marbleTex_;
Vector2 marblePos_;
Vector2 marbleVel_;
bool useTilt_ = false;
float filteredX_ = 0.0f;
float filteredY_ = 0.0f;
};