XNA applications aren’t normally
built around pages like Silverlight applications. If you wanted,
however, you could certainly implement your own page-like structure
within an XNA
program. You’ll recall that the state of the phone’s Back button is
checked during every call to the standard Update override. You can use this logic for
navigational purposes as well as for terminating the program. But that’s
something I’ll let you work out on your own.
An XNA program can also make
use of the same PhoneApplicationService class used by Silverlight programs for saving
transient state information during tombstoning. An XNA program can also
use this class to install handlers for the four PhoneApplicationService events: Launching, Activated, Deactivated, and Closing. You’ll need references both to the
Microsoft.Phone library (for PhoneApplicationService
itself) and System.Windows (for the IApplicationService
interface that PhoneApplicationService
implements). Within the Game1.cs file you’ll want a using directive for Microsoft.Phone.Shell.
In the constructor of the Game1
class you can obtain the PhoneApplicationService
instance associated with the application through the static PhoneApplicationService.Current property.
The Game class also
defines a couple handy virtual methods named OnActivated
and OnDeactivated
that are also useful for handling tombstoning. The OnActivated
method is called during launching and re-activation, and OnDeactivated is
called during deactivation and program closing, much like the OnNavigatedTo and OnNavigatedFrom
virtual methods of a Silverlight page.
The program uses the PhoneApplicationService
events for saving and restoring application settings (a Color), and overrides of
the OnDeactivated and OnActivated
events for retaining transient data (the number of taps).
But I went a little
further in providing a more generalized solution for application
settings. I gave the XnaTombstoning
project a dedicated Settings class that uses the more generalized
features of isolated storage that involve real files rather than just
simple settings. You’ll need a reference to System.Xml.Serialization
library for this class as well using
directives for the System.IO,
System.IO.IsolatedStorage, and System.Xml.Serialization
namespaces.
Example 1. Silverlight
Project: XnaTombstoning File: Settings.cs (excerpt)
public class Settings { const string filename = "settings.xml";
// Application settings public Color BackgroundColor { set; get; }
public Settings() { BackgroundColor = Color.Navy; }
public void Save() { IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForApplication(); IsolatedStorageFileStream stream = storage.CreateFile(filename); XmlSerializer xml = new XmlSerializer(GetType()); xml.Serialize(stream, this); stream.Close(); stream.Dispose(); }
public static Settings Load() { IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForApplication(); Settings settings;
if (storage.FileExists(filename)) { IsolatedStorageFileStream stream = storage.OpenFile("settings.xml", FileMode.Open); XmlSerializer xml = new XmlSerializer(typeof(Settings)); settings = xml.Deserialize(stream) as Settings; stream.Close(); stream.Dispose(); } else { settings = new Settings(); }
return settings; } }
|
The idea here is that an instance
of this Settings
class itself is serialized and saved in isolated storage in the Save method, and
then retrieved from isolated storage and deserialized in the Load method. Notice that the Load method is
static and returns an instance of the Settings
class.
When an instance of this Settings class is serialized, all its public properties
are serialized. This class has exactly one public property of type Color named BackgroundColor but it
would be very easy to add more properties to this class as the
application develops and gets more sophisticated.
In the Save method, the area of isolated storage
reserved for this application is obtained from the static IsolatedStorageFile.GetUserStoreForApplication method. This method returns an object of type IsolatedStorageFile
but the name is a little misleading. This IsolatedStorageFile
object is closer in functionality to a file
system than a file. You use the object to maintain directories, and
to create and open files. A call to CreateFile
returns an IsolatedStorageFileStream
which here is used with an XmlSerializer
object to serialize and save the file.
The Load method is a bit more complex because it’s
possible that the program is being run for the very first time and the
settings.xml file does not exist. In that case, the Load method
creates a new instance of Settings.
Notice the constructor
that initializes the properties to their default values, which in this
case only involves the single public property named BackgroundColor. If
you add a second public property for another application setting at
some point, you’ll want to also specify a default value of that property
in the constructor. The first time you run the new version of the
program, that new property will be initialized in the constructor, but
the Load
method will retrieve a file that doesn’t have that property, so the new
version smoothly integrates with the previous version.
Here’s another
consideration: This scheme only works if the properties representing
application settings are serializable. For a more complex program, that might not be
the case. For objects that are not serializable but still must be saved
to isolated storage, you can still include a property for that object
in this file but you’ll want to flag that property definition with the [XmlIgnore]
attribute. The property will be ignored for serialization purposes.
Instead you’ll need to handle that property with special code in the Save and Load
methods.
The remainder of the XnaTombstoning project lets you tap the screen and
responds by displaying a new random background color and a count of the
number of taps. The background color is treated as an application
setting (as is evident by its inclusion in the Settings
class) and the number of taps is a transient setting.
Here’s an excerpt of the Game1
class showing the fields, constructor, and PhoneApplicationService
events:
Example 2. Silverlight
Project: XnaTombstoning File: Game1.cs (excerpt)
public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch;
Settings settings; SpriteFont segoe14; Viewport viewport; Random rand = new Random(); StringBuilder text = new StringBuilder(); Vector2 position; int numTaps = 0;
public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content";
// Frame rate is 30 fps by default for Windows Phone. TargetElapsedTime = TimeSpan.FromTicks(333333);
TouchPanel.EnabledGestures = GestureType.Tap;
PhoneApplicationService appService = PhoneApplicationService.Current; appService.Launching += OnAppServiceLaunching; appService.Activated += OnAppServiceActivated; appService.Deactivated += OnAppServiceDeactivated; appService.Closing += OnAppServiceClosing; }
. . .
void OnAppServiceLaunching(object sender, LaunchingEventArgs args) { settings = Settings.Load(); }
void OnAppServiceActivated(object sender, ActivatedEventArgs args) { settings = Settings.Load(); }
void OnAppServiceDeactivated(object sender, DeactivatedEventArgs args) { settings.Save(); }
void OnAppServiceClosing(object sender, ClosingEventArgs args) { settings.Save(); } }
|
A Settings object
named settings is saved as a field. The
constructor attaches handlers for the four
events of PhoneApplicationService and it is in the handlers for these events that
the application settings are saved and
loaded.
The LoadContent
override contains nothing surprising:
Example 3. Silverlight
Project: XnaTombstoning File: Game1.cs (excerpt)
protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); segoe14 = this.Content.Load<SpriteFont>("Segoe14"); viewport = this.GraphicsDevice.Viewport; }
|
The Update method
reads taps, updates the numTaps field, determines a new random color, and also
prepares a StringBuilder object for displaying the number of taps:
Example 4. Silverlight
Project: XnaTombstoning File: Game1.cs (excerpt)
protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit();
while (TouchPanel.IsGestureAvailable) if (TouchPanel.ReadGesture().GestureType == GestureType.Tap) { numTaps++; settings.BackgroundColor = new Color((byte)rand.Next(255), (byte)rand.Next(255), (byte)rand.Next(255)); }
text.Remove(0, text.Length); text.AppendFormat("{0} taps total", numTaps); Vector2 textSize = segoe14.MeasureString(text.ToString()); position = new Vector2((viewport.Width - textSize.X) / 2, (viewport.Height - textSize.Y) / 2);
base.Update(gameTime); }
|
Notice that the new color is
saved not as a field, but as the BackgroundColor
property of the Settings instance.
That property is then referenced in the Draw
override:
Example 5. Silverlight
Project: XnaTombstoning File: Game1.cs (excerpt)
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(settings.BackgroundColor);
spriteBatch.Begin(); spriteBatch.DrawString(segoe14, text, position, Color.White); spriteBatch.End();
base.Draw(gameTime); }
|
The transient value of the numTaps field is saved to and restored from the State dictionary
of the PhoneApplicationService in
overrides of OnActivated and OnDeactivated:
Example 6. Silverlight
Project: XnaTombstoning File: Game1.cs (excerpt)
protected override void OnActivated(object sender, EventArgs args) { if (PhoneApplicationService.Current.State.ContainsKey("numTaps")) numTaps = (int)PhoneApplicationService.Current.State["numTaps"];
base.OnActivated(sender, args); }
protected override void OnDeactivated(object sender, EventArgs args) { PhoneApplicationService.Current.State["numTaps"] = numTaps; base.OnDeactivated(sender, args); }
|
It might seem a little
arbitrary to save and restore application settings in one set of event handlers, and
save and restore transient settings in another set of overrides to
virtual methods, and in a practical sense it is arbitrary. The program
will get a call to OnActivated about the same time the Launching
and Activated events are fired, and a
call to OnDeactivated about the same
time the Deactivated and Closing events are fired.
The differentiation is more conceptual in that OnActivated
and OnDeactivated are associated with
the Game
instance, so they should be used for properties associated with the
game rather than overall application settings.
It’s possible that you’ll need
to save an unserializable
object as a transient setting, but because it’s not serializable, you
can’t use the State dictionary of the PhoneApplicationService
class. You’ll need to use isolated storage for such an object, but you
don’t want to accidently retrieve that object and reuse it when the
program is run again. In this case, you’ll use a flag in the State
dictionary indicating whether you need to load the transient object from
isolated storage.