Sensors

Microsoft::Devices & Microsoft::Devices::Sensors — Accelerometer, Gyroscope, Compass, Motion, VibrateController

ⓘ

Implementation status: Accelerometer and Gyroscope use a real sensor whenever the selected CNA platform reports one (the SDL3 sensor API on the default platform); Compass and Motion are real on Android and report NotSupported elsewhere; VibrateController uses SDL haptics. This snapshot has 37 Devices/Devices-EXT test sources with 530 statically discoverable GoogleTest-family definitions (alpha.1: 36 and 525), including injectable backends for headless CI. Hardware-dependent tests self-skip when the required device is absent.

ⓘ

Build flag: the Microsoft::Devices sensors documented here are built in every configuration: the devices module is added unconditionally and is part of the CNA umbrella target. The CNA_DEVICES CMake option, which defaults to OFF, gates only the separate CNA-specific device extensions (CNA::Devices in the devices-ext module: clipboard, dialogs, camera and so on); configure with -DCNA_DEVICES=ON to compile those. The path-filtered devices-tests.yml workflow builds both Microsoft::Devices and CNA::Devices tests with UBSan on relevant pushes and pull requests and runs the suites named in its two test filters (which miss seven Microsoft::Devices suites and, by naming renamed dialog suites, four CNA::Devices ones), and is also manually dispatchable; that is desktop/headless evidence, not a physical-sensor laboratory.

Overview

XNA 4.0 introduced these sensor classes as part of its Windows Phone support, providing a straightforward API for tilt, rotation, heading and shake detection. CNA maps them onto two platform integrations: the selected platform's sensor service (the SDL sensor subsystem on the default SDL3 platform) for acceleration and angular velocity, and, on Android, the NDK <android/sensor.h> interface for magnetometer-based heading and fused orientation. This is separate from the selected graphics renderer.

The sensors are instances, not static state snapshots. Create a sensor object, call Start(), and read getCurrentValueProperty() once per frame (or subscribe to CurrentValueChanged / ReadingChanged if you prefer events); call Stop() when you are done. The static getIsSupportedProperty() tells you beforehand whether the selected platform has the sensor, and getStateProperty() reports a SensorState (NotSupported, Ready, Initializing, NoData, NoPermissions, Disabled). There is no AccelerometerState type and no GetState() on the sensors.

Where a sensor genuinely has no hardware or platform backing, the API is still present and callable — it reports NotSupported rather than fabricating data, so code that guards on getIsSupportedProperty() degrades gracefully without preprocessor guards. Microsoft::Devices::Environment::getDeviceTypeProperty() (new in this snapshot) returns Device on mobile targets and Emulator elsewhere, matching XNA's Microsoft.Devices.Environment.DeviceType.

Sensor classes at a glance

Class Backend Where it is real Status
Accelerometer Platform sensor service (SDL3 sensor API) Android and desktop (Linux, Windows, macOS) wherever the platform reports an accelerometer Functional
Gyroscope Platform sensor service (SDL3 sensor API) Android and desktop (Linux, Windows, macOS) wherever the platform reports a gyroscope Functional
Compass Android NDK <android/sensor.h> Android only; NotSupported elsewhere Android-only
Motion Android NDK fusion of 5 sensors Android only; NotSupported elsewhere Android-only
VibrateController SDL haptics Any device with an SDL haptic device (gamepads excluded by design) Functional

The Compass/Motion restriction is a platform limitation, not an unimplemented stub: there is a complete Android implementation, and no equivalent magnetometer/fusion source is wired on other platforms, so those platforms answer honestly instead of returning zeros.

Accelerometer class

Accelerometer is an ordinary class: construct one, Start() it, read it, Stop() it. It derives from SensorBase<AccelerometerReading>, and up to ten instances may exist per application. Its members:

Member Type Description
Accelerometer::getIsSupportedProperty() bool (static) true when the current target is Android, iOS or desktop and the selected CNA platform reports a real accelerometer — for example a 2-in-1 laptop or a phone. Browser builds are excluded by policy unless keyboard emulation is on (below), which makes it true on desktop and in the browser. false when no such sensor is found.
Start() / Stop() void Begin and end data acquisition. Start() throws AccelerometerFailedException if acquisition is already running or the platform cannot start it.
getCurrentValueProperty() AccelerometerReading The latest reading. Throws System::InvalidOperationException if the sensor is not supported.
getIsDataValidProperty() bool true once a valid reading has arrived.
getStateProperty() SensorState (enum) NotSupported, Ready, Initializing, NoData, NoPermissions or Disabled. Read under the sensor subsystem's lock.
CurrentValueChanged, ReadingChanged events Raised when a new reading arrives; use them instead of polling if you prefer.
getTimeBetweenUpdatesProperty() / set… System::TimeSpan The requested update interval.

AccelerometerReading members

AccelerometerReading is the value returned by getCurrentValueProperty(). It carries two members:

Member Type Description
getAccelerationProperty() const Vector3& Current acceleration in G-forces. See axis breakdown below.
getTimestampProperty() System::DateTimeOffset When the reading was taken.

Acceleration axis conventions

The acceleration vector uses the same coordinate conventions as XNA 4.0 on Windows Phone, measured in G-forces (1 G ≈ 9.81 m/s²). SDL3 reports raw acceleration in m/s², and CNA applies the m/s²→G conversion so that readings match XNA's units rather than SDL's:

Component Physical meaning Typical value (device flat)
getAccelerationProperty().X Left/right tilt — positive toward the right edge of the device 0.0
getAccelerationProperty().Y Forward/back tilt — positive toward the top edge of the device 0.0
getAccelerationProperty().Z Up/down (gravity component) — approximately 1.0 when the screen faces up and the device is stationary ~1.0

When the device is held upright (portrait orientation), gravity shifts from Z into the Y axis, so the Y component approaches −1.0 and the Z component approaches 0.0. A shake produces transient spikes across all three axes.

Keyboard emulation (CNAEXT, off by default)

Accelerometer::setKeyboardEmulationEnabledEXT(true) replaces the primary accelerometer with a named software sensor driven by the arrow keys, so a tilt-controlled game can be tried on a desktop or in a browser without hardware or motion permission. A reading is (Right − Left, Up − Down, −1) normalised to one g; no keys reads (0, 0, −1), and an unfocused game reads neutral. Readings arrive when Game pumps input, once per frame. The switch is process-wide (getKeyboardEmulationEnabledEXT() reads it); stop every running accelerometer before changing it, or it throws InvalidOperationException. Gyroscopes and controller sensors keep their physical behaviour. The window-orientation counterpart, getWindowProperty().setKeyboardOrientationEmulationEnabledEXT(true), maps Up, Left and Right to Portrait, LandscapeLeft and LandscapeRight, honouring SupportedOrientations.

Platform support

Platform Accelerometer / Gyroscope Compass / Motion VibrateController
Android Live data via the platform sensor service (SDL3 sensor API) Live data via the NDK sensor fusion path SDL haptic rumble
Desktop (Linux / Windows / macOS) Live data wherever the selected platform reports a sensor device NotSupported SDL haptic rumble on a real haptic device

Android is wired through CMake (the NDK toolchain, with the sensor code linking android), but there is no Android CI job and no Android CMake preset — Android builds are a manual step today.

Concurrency hardening

The platform sensor path is not a thin wrapper. Each sensor class guards its registrations, callback lifetime and state reads with its own subsystem mutex, native subsystem acquisition and release go through the platform (the SDL3 platform serializes them under its process-wide state lock), and the implementation tracks callbacks in flight so that Dispose() can be re-entered from a callback without a use-after-free. This matters because the same underlying sensor device can be reached from more than one sensor object.

VibrateController

VibrateController is a real implementation rather than a no-op: it borrows the selected platform's haptics service, and on the SDL3 platform that service initialises rumble with SDL_InitHapticRumble, plays it with SDL_PlayHapticRumble and drives a genuine SDL_HAPTIC_LEFTRIGHT effect where the device supports one; the headless and terminal platforms provide none.

It deliberately excludes gamepads from the devices it will open. That is intentional: gamepad rumble belongs to GamePad::SetVibration(), and letting VibrateController claim the same device would mean two APIs fighting over one motor. If you want a controller to rumble, use GamePad::SetVibration().

Known gap

⚠

Compass reports TrueHeading (getTrueHeadingProperty()) as equal to MagneticHeading. CNA has no magnetic-declination data source, so the two headings cannot be distinguished. If your game needs true north, apply your own declination correction.

Code examples

Checking support before use

Always guard sensor use with Accelerometer::getIsSupportedProperty() so that your game degrades gracefully on desktop builds without a sensor:

#include "Microsoft/Devices/Sensors/Accelerometer.hpp"
using namespace Microsoft::Devices::Sensors;

// In Game::Initialize() or your platform detection pass
if (Accelerometer::getIsSupportedProperty()) {
    // Sensor is available; enable tilt controls
    accel_ = std::make_unique<Accelerometer>();
    try {
        accel_->Start();
        tiltControlsEnabled = true;
    } catch (const AccelerometerFailedException&) {
        tiltControlsEnabled = false;   // fall back to keyboard/gamepad controls
    }
}

Reading the accelerometer each frame (tilt-to-move)

A typical tilt-to-move mechanic reads the sensor once in Update() and maps the X/Y axes directly to horizontal and vertical forces on a physics body or velocity vector:

// Declared in your Game class
std::unique_ptr<Accelerometer> accel_;

// In Update(GameTime& gameTime)
if (tiltControlsEnabled && accel_->getIsDataValidProperty()) {
    AccelerometerReading reading = accel_->getCurrentValueProperty();
    const Vector3& g = reading.getAccelerationProperty();

    // Map tilt to movement — dead-zone to filter hand tremor
    const float deadZone = 0.1f;
    float tiltX = g.X;
    float tiltY = g.Y;

    if (std::abs(tiltX) < deadZone) tiltX = 0.0f;
    if (std::abs(tiltY) < deadZone) tiltY = 0.0f;

    // Apply as velocity (scale to your game's units per second)
    float speed = 200.0f;
    playerVelocity.X = tiltX * speed;
    playerVelocity.Y = tiltY * speed;  // positive Y = forward on the device
}

Shake detection

A shake produces a spike in the acceleration magnitude above the normal gravity baseline of ~1 G. Compare the length of the vector against a threshold:

if (tiltControlsEnabled && accel_->getIsDataValidProperty()) {
    float magnitude = accel_->getCurrentValueProperty().getAccelerationProperty().Length();
    const float shakeThreshold = 2.5f;  // 2.5 G — adjust per game feel

    if (magnitude > shakeThreshold) {
        // Player shook the device — trigger action
        OnShakeDetected();
    }
}

Stop the sensor when you are done

// In UnloadContent() or your shutdown path
if (accel_) accel_->Stop();

Gyroscope

Gyroscope is implemented on the same real SDL3 sensor path as Accelerometer, with the same concurrency hardening, and is available on Android and desktop (Linux, Windows, macOS). It reports angular velocity around each axis, which enables more precise orientation tracking and complements the accelerometer for robust tilt controls. It follows the same instance model: Gyroscope::getIsSupportedProperty(), Start(), getCurrentValueProperty().getRotationRateProperty() (a Vector3), Stop().

Compass and Motion

Compass exposes a magnetic heading and Motion exposes a fused device attitude. Both are backed on Android by a real NDK implementation that fuses five underlying sensors through <android/sensor.h>. Both follow the same instance model as the other sensors (Compass: getCurrentValueProperty().getMagneticHeadingProperty(); Motion: getCurrentValueProperty(), then getAttitudeProperty(), getGravityProperty() and friends).

On every non-Android platform they report NotSupported. Check getIsSupportedProperty() before use and provide a fallback if your game needs a heading on desktop or web.