GPS 101
Let’s spend a few minutes to just learn the basics of GPS, in order to better use it in our code. What GPS boils down to—the nitty-gritty—are two floating-point numbers representing the X and Y position on the Earth. The X value has traditionally been called longitude, and the Y value has been known as the latitude. From a game programming perspective, this is an easier way to grasp the terms, but a naval veteran would scoff at the overly simplistic way this is being presented. We’ll gloss over issues of precision in order to grasp the concepts first.
Longitude represents the “X” or horizontal coordinate on the surface of the Earth, running east or west from the zero point.
Latitude represents the “Y” or vertical coordinate on the surface of the Earth, running north or south from the zero point.
The origin (0,0) is located about 400 miles off the western coast of Africa, southwest of Nigeria and south of Ghana. From that origin point, longitude increases to the right (east), and decreases to the left (west); latitude increases up (north), and decreases down (south). In other words, it is oriented exactly like the Cartesian coordinate system we’ve been using all along for our trig-heavy examples. This makes translating GPS coordinates for the purpose of making a reality game a cinch!
To help make sense of the coordinate system, Table 17.1 shows the approximate latitude and longitude values of several major cities in the world, formatted in a way that makes sense to game programmers (such that longitude comes before latitude— remember, we aren’t navigating here). Note that these are far from precise, just rough estimates to present the general location of each city. More precise GPS coordinates will include up to six decimal places of increasing precision, down to just 10 feet or less in granularity.
If you want to learn more about latitude and longitude coordinates, there is an interactive world map available online at http://itouchmap.com/latlong.html.
Windows Phone Location Services
XNA provides us with a geographic location service in a library located in a namespace called System.Device.Location. This library is not included in the project’s references by default, so we must add it to use this library in our program.
Adding the Location Services Library
- Right-click References in the Solution Explorer, and then choose Add Reference.
- In the dialog box that comes up, there is a list with the .NET tab already in view, as shown in Figure 17.1. Select System.Device from the list and click the OK button.
- The geographic location services library is in a namespace called System.Device.Location, which must be added with a using statement to any program that needs these services:
[code]
using System.Device.Location;
[/code]
Using the Location Services
To read the current device’s GPS location, we create an object using the GeoCoordinateWatcher class:
[code]
GeoCoordinateWatcherSim watcher;
[/code]
It is okay to create the watcher object in Initialize() or LoadContent(), or in response to a user event:
[code]
watcher = new GeoCoordinateWatcher(GeoPositionAccuracy.Default);
[/code]
At this point, the object is created but is not yet receiving any GPS data. We have to create an event handler to handle position status change events. The trigger that causes such an event is movement of the device, which can be fine-tuned with the MovementThreshold property:
[code]
watcher.MovementThreshold = 20;
[/code]
The first event we’ll tap into is StatusChanged. A new event method will need to be created to correspond with the name of the method passed to this new event object. In this case, the example is using a string called statusText, which can be printed out from the main Draw() call. Optionally, a programmer-defined status could be set here and used elsewhere in the game:
[code]
watcher.StatusChanged += new EventHandler
<GeoPositionStatusChangedEventArgs>(watcher_StatusChanged);
void watcher_StatusChanged(object sender,
GeoPositionStatusChangedEventArgs e)
{
switch (e.Status)
{
case GeoPositionStatus.Disabled:
statusText += “Location service has been disabledn”;
break;
case GeoPositionStatus.Initializing:
statusText += “Location service is initializingn”;
break;
case GeoPositionStatus.NoData:
statusText += “Location service is not returning any datan”;
break;
case GeoPositionStatus.Ready:
statusText += “Location service is receiving datan”;
break;
}
}
[/code]
The actual movement of the GPS device triggers position change events that we can tap into with the PositionChanged event. A similar event method will have to be created for this event as well. In this example, a GeoCoordinate variable called coord is set using the passed parameter that contains the GPS location data:
[code]
watcher.PositionChanged += new EventHandler
<GeoPositionChangedEventArgs<GeoCoordinate>>(watcher_PositionChanged);
void watcher_PositionChanged(object sender,
GeoPositionChangedEventArgs<GeoCoordinate> e)
{
coord = e.Position.Location;
}
[/code]
Simulating Position Changes
The WP7 emulator does not have a GPS receiver, and even if your PC has one, the emulator doesn’t know how to use it—the emulator is a self-contained system that only uses the networking of your PC to simulate connectivity. I say “simulate” because in a real WP7 device, that Internet connection would come through the airwaves, presumably G3 or G4, depending on what the service provider supports.
There is a workaround for the limitation. If you want to create a game that uses location services, it’s a given you must be able to test it extensively, and even with a real WP7 device, testing GPS code can be a challenge. So, even with hardware, it may be preferred to develop this code with a GPS simulation rather than the real thing. With a simulation, you can define the location data yourself and write the gameplay code to respond to location data in a predictable way. Only the final testing stages of the game would need to be done “in the field.”
So, a question arises: How do we simulate GPS data?
The solution is to write a class that inherits from GeoLocationWatcher and then fill in data events with a timer that generates real-time updates via GeoLocation events. Voilà!
GeoLocationSim
There are three classes involved in the geographic location simulator. The first is GeoLocationSim, which inherits directly from GeoCoordinateWatcher, the main GPS class in XNA. There are quite a few properties, events, and methods defined in this abstract class that are required to pass this off as a legitimate GeoLocation class so that it works with normal GeoLocation code, but we don’t need all of that for testing purposes. Nevertheless, they are all required. In the sample project for this hour, I have added all three classes in a source file called GeoLocationSim.cs. First, take a look at Listing 17.1, the code for the sim class.
LISTING 17.1 Base GeoLocation Simulation Class
[code]
abstract public class GeoLocationSim : GeoCoordinateWatcher
{
private GeoPosition<GeoCoordinate> current;
private Timer timer;
public GeoLocationSim()
{
current = new GeoPosition<GeoCoordinate>();
Status = GeoPositionStatus.Initializing;
RaiseStatusChanged();
}
private void RaiseStatusChanged()
{
GeoPositionStatusChangedEventArgs args =
new GeoPositionStatusChangedEventArgs(Status);
if (StatusChanged != null)
{
StatusChanged(this, args);
}
}
private void RaisePositionChanged()
{
GeoPositionChangedEventArgs<GeoCoordinate> args =
new GeoPositionChangedEventArgs<GeoCoordinate>(current);
if (PositionChanged != null)
PositionChanged(this, args);
}
public void OnTimerCallback(object state)
{
try
{
if (Status == GeoPositionStatus.Initializing)
{
Status = GeoPositionStatus.NoData;
RaiseStatusChanged();
}
StartGetCurrentPosition();
TimeSpan next = GetNextInterval();
timer.Change(next, next);
}
catch (Exception)
{
throw;
}
}
protected void UpdateLocation(double longitude, double latitude)
{
GeoCoordinate location = new GeoCoordinate(latitude, longitude);
if (!location.Equals(current.Location))
{
current = new GeoPosition<GeoCoordinate>(
DateTimeOffset.Now, location);
if (Status != GeoPositionStatus.Ready)
{
Status = GeoPositionStatus.Ready;
RaiseStatusChanged();
}
RaisePositionChanged();
}
}
abstract protected TimeSpan GetNextInterval();
abstract protected void StartGetCurrentPosition();
//override base property
public GeoPositionPermission Permission
{
get { return GeoPositionPermission.Granted; }
}
//override base property
public GeoPosition<GeoCoordinate> Position
{
get { return current; }
}
//override base event
public event EventHandler<GeoPositionChangedEventArgs
<GeoCoordinate>> PositionChanged;
//override base method
public void Start(bool suppressPermissionPrompt)
{
Start();
}
//override base method
public void Start()
{
TimeSpan span = GetNextInterval();
timer = new Timer(OnTimerCallback, null, span, span);
}
//override base property
public GeoPositionStatus Status
{
get;
protected set;
}
//override base event
public event EventHandler
<GeoPositionStatusChangedEventArgs> StatusChanged;
//override base method
public void Stop()
{
timer.Change(Timeout.Infinite, Timeout.Infinite);
Status = GeoPositionStatus.Disabled;
RaiseStatusChanged();
}
//override base method
public bool TryStart(bool suppressPermissionPrompt, TimeSpan timeout)
{
Start();
return true;
}
}
[/code]
Filling in GPS Data with Timing
SampleGeoCoord is a helper class that is used to fill in GPS position data with timing. Each position coordinate corresponds to a one-second interval at which the position update event is triggered. So, this class supplies longitude, latitude, and time.
[code]
public class SampleGeoCoord
{
public double Longitude { get; set; }
public double Latitude { get; set; }
public TimeSpan Time { get; set; }
public SampleGeoCoord(double Longitude, double Latitude, int seconds)
{
this.Longitude = Longitude;
this.Latitude = Latitude;
this.Time = new TimeSpan(0, 0, seconds);
}
}
[/code]
GeoCoordinateWatcherSim
The GeoCoordinateWatcherSim is our main workhorse simulation class, inheriting directly from GeoLocationSim. This class puts the GeoLocationSim properties, methods, and events to work using data populated within an array of SampleGeoCoord objects. In the example coming up that uses this class, I’ve centered the coordinates around Los Angeles, with 60 seconds of random locations within a radius of about 100 miles around the city coordinates (-118, 34). Listing 17.2 contains the code for the GeoCoordinateWatcherSim class.
LISTING 17.2 Usable GeoCoordinateWatcherSim Worker Class
[code]
public class GeoCoordinateWatcherSim : GeoLocationSim
{
List<SampleGeoCoord> events;
int currentEventId;
Random rand = new Random();
public GeoCoordinateWatcherSim(GeoPositionAccuracy accuracy)
{
currentEventId = 0;
events = new List<SampleGeoCoord>();
//create random coordinates in Los Angeles
for (int n = 1; n < 60; n++)
{
double Long = -118 – rand.Next(2) – rand.NextDouble();
double Lat = 33 + rand.Next(2) + rand.NextDouble();
events.Add(new SampleGeoCoord(Long, Lat, n));
}
}
private SampleGeoCoord Current
{
get
{
return events[currentEventId % events.Count];
}
}
protected override void StartGetCurrentPosition()
{
this.UpdateLocation(Current.Longitude, Current.Latitude);
currentEventId++;
}
protected override TimeSpan GetNextInterval()
{
return Current.Time;
}
}
[/code]
Creating the Geo Position Demo
Let’s write a program to demonstrate the GeoCoordinateWatcherSim class in action. The example requires only a font, because it just prints out the longitude and latitude of the geographical coordinate data and the status of the watcher. The code for the Geo Position Demo program is found in Listing 17.3, and Figure 17.2 shows the program running. Note that this example will work on a WP7 device without the simulated data with a single line change, from
[code]
watcher = new GeoCoordinateWatcherSim(…);
[/code]
to
[code]
watcher = new GeoCoordinateWatcher(…);
[/code]
LISTING 17.3 The Geo Position Demo Program
[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
TouchLocation oldTouch;
Random rand;
SpriteFont font;
string statusText = ““;
GeoCoordinateWatcherSim watcher = null;
GeoCoordinate coord = null;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
oldTouch = new TouchLocation();
}
protected override void Initialize()
{
base.Initialize();
StartGeoLocation();
}
protected override void LoadContent()
{
rand = new Random();
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>(“WascoSans”);
}
protected override void UnloadContent()
{
base.UnloadContent();
watcher.Stop();
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin(SpriteSortMode.FrontToBack,
BlendState.AlphaBlend);
spriteBatch.DrawString(font, “Latitude: “ +
coord.Latitude.ToString(“0.000”),
new Vector2(100, 10), Color.White);
spriteBatch.DrawString(font, “Longitude: “ +
coord.Longitude.ToString(“0.000”),
new Vector2(100, 30), Color.White);
spriteBatch.DrawString(font, statusText,
new Vector2(100, 100), Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
void StartGeoLocation()
{
coord = new GeoCoordinate();
//try to create geo coordinate watcher
if (watcher == null)
{
statusText += “Starting location service…n”;
watcher = new GeoCoordinateWatcherSim(
GeoPositionAccuracy.Default);
watcher.MovementThreshold = 20;
watcher.StatusChanged += new EventHandler
<GeoPositionStatusChangedEventArgs>(
watcher_StatusChanged);
watcher.PositionChanged += new EventHandler
<GeoPositionChangedEventArgs<GeoCoordinate>>
(watcher_PositionChanged);
watcher.Start();
}
}
void watcher_StatusChanged(object sender,
GeoPositionStatusChangedEventArgs e)
{
switch (e.Status)
{
case GeoPositionStatus.Disabled:
statusText += “Location service has been disabledn”;
break;
case GeoPositionStatus.Initializing:
statusText += “Location service is initializingn”;
break;
case GeoPositionStatus.NoData:
statusText += “Location service is not returning any datan”;
break;
case GeoPositionStatus.Ready:
statusText += “Location service is receiving datan”;
break;
}
}
void watcher_PositionChanged(object sender,
GeoPositionChangedEventArgs<GeoCoordinate> e)
{
coord = e.Position.Location;
}
}
[/code]
There are many uses for GPS tracking, not to mention potential multiplayer games, but one thing to keep in mind is that GPS only provides location data, but there’s no transmitting of that data. After the location is received, that’s it—it’s data, and it’s not transmitted anywhere. GPS is read-only. So, if you have in mind a game, there must still be a network infrastructure connecting all the players, wherein each player will transmit his or her GPS location to the other players over the network. The WP7 platform supports Xbox Live for networking, so that is likely the next subject to study if you’re interested in making a networked game.