The
accelerometer can be an entertaining and engaging method of input,
especially for game development with XNA Game Studio or Silverlight. We
all have seen the car racing games on mobile phone or mobile gaming
devices where the user is tilting the device like a steering wheel.
1. Understanding How It Works
The Accelerometer
sensor detects acceleration in all three axis's, X, Y, Z to form a 3D
vector. You may wonder in what direction and magnitude does the vector
point?Collect a few accelerometer readings using this line of code
System.Diagnostics.Debug.WriteLine(AccelerometerHelper
.Current2DAcceleration.ToString());
The following are a few samples from the Output window when debugging:
{X:0.351 Y:-0.002 Z:0.949} (Magnatude is approximately 1.02)
{X:0.401 Y:0.044 Z:0.984} (Magnatude is approximately 1.06)
{X:0.378 Y:0.04 Z:1.023} (Magnatude is approximately 1.09)
{X:0.386 Y:0.022 Z:0.992} (Magnatude is approximately 1.06)
{X:0.409 Y:0.03 Z:0.992} (Magnatude is approximately 1.07)
You can calculate the magnitude of the vector using the Pythagorean Theorem, which is the Sqrt(X2+Y2+Z2)
= magnitude of the vector. The value should be about one but as you can
see from the above samples, it can vary by location or could possibly
be an error deviation. Either way, this is why applications like a level
suggest that you calibrate the level against a known flat surface
before using the virtual level.
If you run the application
in the emulator, this reading is returned every time: {X:0 Y:0 Z:-1}.
Holding the phone flat in my unsteady hand yields similar values with Z near one and X, and Y near zero.
{X:0.039 Y:0.072 Z:-1.019}
{X:0.069 Y:0.099 Z:-1.047}
{X:0.012 Y:0.056 Z:-1.008}
{X:0.016 Y:0.068 Z:-1.019}
This suggests that the
vector is oriented to point towards the center of the earth, which for
the above readings is the bottom of the phone or a negative Z when the
phone is lying flat on its back. Flipping the phone in my hand yields
the following values:
{X:-0.043 Y:0.08 Z:1.019}
{X:-0.069 Y:0.111 Z:1.093}
{X:-0.069 Y:0.099 Z:1.093}
{X:-0.039 Y:0.107 Z:1.031}
This time the vector is
pointing out from the glass toward the ground, because the phone is
lying face down. This information is useful if you need to determine how
the phone is oriented in the users hand when say taking a photograph.
Figure 1
shows the accelerometer coordinate system. This is important because
developers must translate readings into the coordinate system for the
application.
As an example, in the
XNA Framework, the default 2D coordinate system has positive Y
goingdown, not up, so you cannot just take the Y component of
acceleration and apply it to the Y value for a game object in 2D XNA.
NOTE
The default coordinate system for 3D in the XNA Framework has positive Y going up.
With this background in hand, the next section covers development with the accelerometer sensor.
2. Programming with the Accelerometer
Accessing the Accelerometer sensor is pretty straightforward. We start with an XNA project, adding a reference to the Microsoft.Devices.Sensors assembly, and declaring instance of the Accelerometer
class. In the Game1.Initialize() method, create an instance of the
Accelerometer and call the Start() method to generate readings.
NOTE
Turn off the Accelerometer if not needed to save battery power.
Create an event handler for ReadingChanged as well. The following is the code to create the event handler:
accelerometer = new Accelerometer();
accelerometer.Start();
accelerometer.ReadingChanged +=
new EventHandler<AccelerometerReadingEventArgs>(accelerometer_ReadingChanged);
The accelerometer_ReadingChanged event handler event arguments AccelerometerReadingEventArgs class exposes acceleration in three dimensions via the X, Y, and Z member variables of type double. There is also a TimeStamp variable to allow measurement of acceleration changes over time.
A private member Vector3 variable named AccelerometerTemp is added to the Game1 class to collect the reading so that the code in the event handler does not have to new up a Vector3 each time a reading is collected. We create a helper static class named AccelerometerHelper that takes the accelerometer reading and assigns it to a Vector3 property named Current3DAcceleration. The following is the ReadingChanged event handler:
private Vector3 AccelerometerTemp = new Vector3();
void accelerometer_ReadingChanged(object sender, AccelerometerReadingEventArgs e)
{
//
AccelerometerTemp.X = (float)e.X;
AccelerometerTemp.Y = (float)e.Y;
AccelerometerTemp.Z = (float)e.Z;
AccelerometerHelper.Current3DAcceleration = AccelerometerTemp;
AccelerometerHelper.CurrentTimeStamp = e.Timestamp;
}
The AccelerometerHelper
class takes the Vector3 and parses the values up in the "setter"
function for the Current3DAcceleration property into the class members
listed in Figure 2.
Most of the code in the AccelerometerHelper class is properties and private member variables to hold values.Private backing variables are optional with the {get ; set ;} construct in C# but we use them here in order for all other member variables to be derived from just setting the Current3DAcceleration property. Listing 1 has the code for the AccelerometerHelper class.
Example 1. AccelerometerHelper Class Code File
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
namespace AccelerometerInputXNA
{
static public class AccelerometerHelper
{
static private Vector3 _current3DAcceleration;
static public Vector3 Current3DAcceleration
{
get
{
return _current3DAcceleration;
}
set
{
//Set previous to "old" current 3D acceleration
_previous3DAcceleration = _current3DAcceleration;
//Update current 3D acceleration
//Take into account screen orientation
//when assigning values
switch (Orientation)
{
case DisplayOrientation.LandscapeLeft:
_current3DAcceleration.X = -value.Y;
_current3DAcceleration.Y = -value.X;
_current3DAcceleration.Z = -value.Z;
break;
case DisplayOrientation.LandscapeRight:
_current3DAcceleration.X = value.Y;
_current3DAcceleration.Y = value.X;
_current3DAcceleration.Z = value.Z;
break;
case DisplayOrientation.Portrait:
_current3DAcceleration.X = value.X;
_current3DAcceleration.Y = value.Y;
_current3DAcceleration.Z = value.Z;
break;
}
//Update current 2D acceleration
_current2DAcceleration.X = _current3DAcceleration.X;
_current2DAcceleration.Y = _current3DAcceleration.Y;
//Update previous 2D acceleration
_previous2DAcceleration.X = _previous3DAcceleration.X;
_previous2DAcceleration.Y = _previous3DAcceleration.Y;
//Update deltas
_xDelta = _current3DAcceleration.X - _previous3DAcceleration.X;
_yDelta = _current3DAcceleration.Y - _previous3DAcceleration.Y;
_zDelta = _current3DAcceleration.Z - _previous3DAcceleration.Z;
}
}
static private Vector2 _current2DAcceleration;
static public Vector2 Current2DAcceleration
{
get
{
return _current2DAcceleration;
}
}
static private DateTimeOffset _currentTimeStamp;
static public DateTimeOffset CurrentTimeStamp
{
get
{
return _currentTimeStamp;
}
set
{
_previousTimeStamp = _currentTimeStamp;
_currentTimeStamp = value;
}
}
static private Vector3 _previous3DAcceleration;
static public Vector3 Previous3DAcceleration
{ get { return _previous3DAcceleration; } }
static private Vector2 _previous2DAcceleration;
static public Vector2 Previous2DAcceleration
{ get { return _previous2DAcceleration; } }
static private DateTimeOffset _previousTimeStamp;
static public DateTimeOffset PreviousTimeStamp
{ get { return _previousTimeStamp; } }
static private double _xDelta ;
static public double XDelta { get { return _xDelta;} }
static private double _yDelta;
static public double YDelta { get { return _yDelta; } }
static private double _zDelta;
static public double ZDelta { get { return _zDelta; } }
public static DisplayOrientation Orientation { get; set; }
}
}
|
Notice in the setter function for the Current3DAcceleration
property, there is a switch statement that flips the sign as needed
based on device orientation, whether landscape left or landscape right,
because the accelerometer coordinate system is fixed. This ensures that
behavior is consistent when the XNA Framework flips the screen based on
how the user is holding the device
To test the helper class, we copy over the GameObject class and assets, StickMan and the font, from the GesturesTouchPanelXNA
project as well as the code to load up the assets and draw on the
screen. The gesture code is not copied over since the input for this
project is the Accelerometer. As before, the Game1.Update()
method in the game loop is the place to handle input and apply it to
objects. This line of code is added to the Update method to apply
acceleration to the StickMan GameObject instance:
StickManGameObject.Velocity += AccelerometerHelper.Current2DAcceleration;
Run the application and the application behaves as expected: tilt the phone left, and the StickMan
slides left, and vice versa when holding the phone in landscape
orientation. If you tilt the phone up far enough, the screen flips to
either DisplayOrientation.LandscapeLeft or DisplayOrientation.LandscapeRight and the behavior remains consistent.
The main issue with this calculation of just adding the Current2DAcceleration to the StickMan's velocity results in a very slow acceleration. This can be easily remedied by scaling the value like this
StickManGameObject.Velocity +=30* AccelerometerHelper.Current2DAcceleration;
The UI "feels" much better
with this value and is more fun to interact with. Depending on the game
object's desired behavior, you could create a ratchet effect by having
fixed positions when the accelerometer values are between discrete
values instead of the smooth application of accelerometer values to
position in this code sample.
The
Accelerometer sensor works equally well in Silverlight. The difference
is mapping Accelerometer changes to X/Y position values directly
(instead of applying Vectors) using a CompositeTransform
object just like what was done in the Silverlight sample with
Manipulations in the previous section on multi-touch. Next up is the
Location sensor.