Fun with the Accelerometer on Your Surface RT

One of the cool things about Microsoft’s Surface RT is that it comes with a rather complete array of sensors. Among those sensors are an accelerometer, which allows software running on the device to sense the acceleration along the X, Y, and Z axes in real time. As a practical matter, that means software can determine the device’s 3D orientation in space and respond to changes in orientation as well. Want to build a simulated carpenter’s level or a Labyrinth-type game in which the user rolls a ball on a tabletop by tilting the device? Accelerometers make all this and more possible. And the accelerometer API in WinRT means that accelerometer data is never more than a method call away.

If you’re familiar with the accelerometer API in Windows Phone 7, you’re a step ahead of the rest in writing accelerometer-based Windows Store apps. But there’s an important difference between the APIs. On a Windows phone, Accelerometer.ReadingChanged events fire continuously at 20 millisecond intervals. In Windows 8, the same events fire at intervals you specify through the Accelerometer.ReportInterval property, which defaults to 16 milliseconds on a Surface RT. However, in Windows 8, these events don’t come in a steady stream, but instead fire only when an acceleration vector changes. Accelerometer-based apps ported from the Windows phone must take this account and not rely on ReadingChanged events for periodic UI updates.

To demonstrate, I built a Labyrinth-type Windows Store app. I began with a single-page “Blank App” project and added a simple UI consisting of an image of a steel ball and a wooden tabletop to MainPage.xaml:

<Grid>
    <Image Source="Images/Wood.png" />
    <Image x:Name="Ball" Width="80" Height="80" Source="Images/Ball.png">
        <Image.RenderTransform>
            <TranslateTransform x:Name="BallTransform" />
        </Image.RenderTransform>
    </Image>
</Grid>

Running the app in the Windows simulator produces the following opening screen:

image

Next, I opened MainPage.xaml.cs and modified the MainPage class as follows:

public sealed partial class MainPage : Page
{
    private const double _gravity = 32.2; // Acceleration of gravity (ft/sec2)
    private const double _damping = 0.6;  // Damping factor for boundary collisions
    private const double _mul = 128.0;    // Motion multiplier (sensitivity)

    private Accelerometer _accel;
    private double _sx = 0.0;
    private double _sy = 0.0;
    private double _vx = 0.0;
    private double _vy = 0.0;
    private double _ax = 0.0;
    private double _ay = 0.0;
    private double _time;

    private double _width, _height;
        
    public MainPage()
    {
        this.InitializeComponent();
    }

    /// <summary>
    /// Invoked when this page is about to be displayed in a Frame.
    /// </summary>
    /// <param name="e">Event data that describes how this page was reached.  The Parameter
    /// property is typically used to configure the page.</param>
    protected async override void OnNavigatedTo(NavigationEventArgs e)
    {
        _width = (Window.Current.Bounds.Width - Ball.Width) / 2.0;
        _height = (Window.Current.Bounds.Height - Ball.Height) / 2.0;

        _accel = Accelerometer.GetDefault();

        if (_accel == null)
            await new MessageDialog("No accelerometer detected").ShowAsync();
        else
        {
            _time = DateTime.Now.Ticks;

            CompositionTarget.Rendering += (s, args) =>
                {
                    var reading = _accel.GetCurrentReading();

                    // Compute number of seconds elapsed during this time interval
                    var ticks = DateTime.Now.Ticks;
                    double time = (ticks - _time) / 10000000.0;

                    // Compute average accelerations during this time interval
                    double ax = ((reading.AccelerationX * _gravity) + _ax) / 2.0;
                    double ay = ((reading.AccelerationY * _gravity) + _ay) / 2.0;

                    // Compute velocities at end of this time interval
                    double vx = _vx + (ax * time);
                    double vy = _vy + (ay * time);

                    // Compute new ball position
                    double sx = _sx + ((((_vx + vx) / 2.0) * time) * _mul);
                    double sy = _sy - ((((_vy + vy) / 2.0) * time) * _mul);

                    // Check for boundary collisions and "bounce" if necessary
                    if (sx < -_width)
                    {
                        sx = -_width;
                        vx = -vx * _damping;
                    }
                    else if (sx > _width)
                    {
                        sx = _width;
                        vx = -vx * _damping;
                    }

                    if (sy < -_height)
                    {
                        sy = -_height;
                        vy = -vy * _damping;
                    }
                    else if (sy > _height)
                    {
                        sy = _height;
                        vy = -vy * _damping;
                    }

                    // Save the latest motion parameters
                    _time = ticks;
                    _ax = ax;
                    _ay = ay;
                    _vx = vx;
                    _vy = vy;
                    _sx = sx;
                    _sy = sy;

                    // Update the ball position
                    BallTransform.X = sx;
                    BallTransform.Y = sy;
                };
        }
    }
}

The gist of it is that I hooked CompositionTarget.Rendering events, which in Windows 8 as in Silverlight fire from the rendering engine each time the runtime is ready to render a new frame, and used it to call Accelerometer.GetCurrentReading to get the latest acceleration vectors from the accelerometer. Then I plugged the X and Y acceleration vectors (these are normalized vectors, with 1.0 representing the pull of gravity in a specified direction) into equations of motion to compute a new position for the ball based on its current momentum and the amount that the device is tilted in the X and Y directions. The result? Run it on your Surface and the ball smoothly follows the tilt of the table. The field named _mul is a sensitivity factor, so if the table’s too sensitive for you, you can lower _mul to lessen the sensitivity of the ball.

You can download a zip file containing the entire Visual Studio project from my Downloads folder on SkyDrive. Build the project and deploy it to your Surface to see first-hand how it feels. Note that you WILL need a real device to run the app, because the Windows simulator, unlike the Windows Phone emulator, doesn’t include a simulated accelerometer.

Stay Informed

Sign up for the latest blogs, events, and insights.

We deliver solutions that accelerate the value of Azure.
Ready to experience the full power of Microsoft Azure?

Atmosera is thrilled to announce that we have been named GitHub AI Partner of the Year.

X