Tutorial 50: Accelerometer and Sensors

CNA — C++ XNA 4.0 reimplementation

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

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