Tutorial 50: Accelerometer and Sensors

CNA — C++ XNA 4.0 reimplementation

What you’ll learn

  • Starting the Accelerometer and reading an AccelerometerReading.
  • The axis conventions, and which way is up.
  • Smoothing raw samples with a low-pass filter before using them as input.

Before you startTutorial 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

AxisDevice flatDevice tilted rightDevice face-down
X0+1 g0
Y000
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

TypeWhere it works
AccelerometerAndroid plus desktop Linux/Windows/macOS, wherever SDL3 finds an accelerometer
GyroscopeAndroid plus desktop Linux/Windows/macOS, wherever SDL3 finds a gyroscope
CompassAndroid only
MotionAndroid 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;
};