Monday, 29 November 2010

Tombstoning in Phone 7 XNA games

One of my pet peeves about the new Windows phone 7 platform is incorrect or insubstantial use of "tombstoning" of applications, especially when it comes to games. Tombstoning is the terminology Microsoft use to describe the hibernation of apps when they lose focus (ie. for an incoming call, or the start button being pressed).

In a way, some of the worst culprits are the official Xbox Live games on the Phone 7 marketplace. Very few of them will resume the game at the exact place they lost focus. Take one of my favourite titles, Rocket Riot, for example. It's one that almost gets it right, but falls at the last hurdle.

Each of Rocket Riot's levels requires that the player kill X number of enemies, 50+ in the late game. If the game loses and then regains focus (ie. it is tombstoned), it remembers which level was being played, but not the number of enemies that had already been killed, or the progress of a boss fight on boss levels. The player is effectively forced to restart from the beginning of the level, and is in a way punished for taking a phone call or accidentally brushing against the start or search hardware buttons.

Some of the Xbox Live games don't even behave that well. Having to sit through splash screens and 30 second long loading screens just to get back to a game that simply lost focus is just not good enough. And as responsible XNA developers, we can make sure our games behave, and it needn't be a complete hassle to achieve seamless tombstoning.

The XNA team give us a great starting point in the Phone 7 Gamestate Management sample. There's even more help available in the article on Tombstoning in WP7 Games.

I use a modified version of the Gamestate Management sample as a starting point for all my XNA projects, whichever platform I'm working on at the time. I found the Phone 7 version of the sample to be very useful, although in its current incarnation, it doesn't quite push the tombstoning quite far enough.

I changed the sample to recognise the right startup event for restoring state upon re-activation. There's a handy table in the Tombstoning in WP7 Games article. The Gamestate sample does not check the PhoneApplicationService.Current.StartupMode property, and instead attempts to restore from a tombstoned state every time the game is run. To avoid this, we simply need to add a check in Game.cs as follows:

Replace:

// attempt to deserialize the screen manager from disk. if that
// fails, we add our default screens.
if (!screenManager.DeserializeState())
{
// Activate the first screens.
screenManager.AddScreen(new BackgroundScreen(), null);
screenManager.AddScreen(new MainMenuScreen(), null);
}

With:

// attempt to deserialize the screen manager from disk. if that
// fails, we add our default screens.
bool deserialized = true;

if (PhoneApplicationService.Current.StartupMode == StartupMode.Activate)
{
if (!screenManager.DeserializeState())
{
deserialized = false;
}
}
else
{
deserialized = false;
}

if (!deserialized)
{
screenManager.AddScreen(new BackgroundScreen(), null);
screenManager.AddScreen(new MainMenuScreen(), null);
}

You'll need to add a reference to Microsoft.Phone, and also System.Windows (the latter is required for the IApplicationService interface). You'll then need to add the corresponding using statement in Game.cs:

using Microsoft.Phone.Shell;

Great! now the Gamestate sample will only attempt to restore from a tombstoned state upon re-activation, not starting afresh. Now onto the tricky stuff.

The Gamestate sample rather cleverly serializes the state of all the opened GameScreens when the game is Deactivated. This means we don't have to do anything extra to get back to the screen (most likely GameplayScreen) that was in use at the time of deactivation. If the Xbox Live games mentioned earlier are anything to go by, you've done enough at this point! Go straight to marketplace, do not collect £200.

But in order to cater for the more discerning phone user, we need to do more work. We need all of our game objects to be saved as they are. We want our main character to come back exactly where we left him. We want to be on the right level, and with the same enemies in the same places. Now all of this is going to depend on your game and how you intend to handle game saves, but I will give an example of just one way to do it, using XmlSerializer.

The GameScreen base class has two overridable methods, Serialize() and Deserialize(), which are called when the screen is removed or added during application Deactivation and Activation respectively. So, in our GameplayScreen, we need to add both these methods:

public override void Serialize()
{
base.Serialize();
}

public override void Deserialize()
{
base.Deserialize();
}

Now, let's assume that you have a game hero class, Hero. Your GameplayScreen might instantiate the Hero class:

Hero gameHero;

public GameplayScreen()
{
gameHero = new Hero();
}

And during the game, the hero's fields might be updated, such as gameHero.Position, gameHero.Level and so on. We need to save the state of the Hero class when the game is deactivated, and we're going to use XML serialization to do it, because it requires very little work to get up and running.

Let's extend our earlier GameplayScreen.Serialize method to store down our Hero when GameplayScreen is serialized (the game is being tombstoned):

public override void Serialize()
{
using (IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForApplication())
{
if (!storage.DirectoryExists("TombstoneData"))
{
storage.CreateDirectory("TombstoneData");
}
using (IsolatedStorageFileStream stream = storage.CreateFile("TombstoneData\\hero.dat"))
{
XmlSerializer xmls = new XmlSerializer(typeof(Hero));
xmls.Serialize(stream, gameHero);
}
}
base.Serialize();
}

It's as simple as that! We open up the application's IsolatedStorageFile, create a directory in it, and serialize the gameHero instance of the Hero class to XML, which we then write out to the hero.dat file stream. And because all .NET 4 classes are automatically marked as Serializable, we don't need to do any extra work to save out most of the fields on the class.

We can now do the opposite to load our hero back in when the game is activated, and the screens are Deserialized:

public override void Deserialize()
{
using (IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForApplication())
{
try
{
using (IsolatedStorageFileStream stream = storage.OpenFile("TombstoneData\\hero.dat", FileMode.Open, FileAccess.Read))
{
XmlSerializer xmls = new XmlSerializer(typeof(Hero));
gameHero = (Hero)xmls.Deserialize(stream);
}
}
catch (Exception ex)
{
if (storage.DirectoryExists("TombstoneData"))
{
string[] files = storage.GetFileNames("TombstoneData\\*");
foreach (string file in files)
{
storage.DeleteFile(Path.Combine(loadFolder, file));
}
}
}
}

base.Deserialize();
}

We add the extra try/catch to handle any errors that may occur whilst loading our data back in. If it fails, we simply delete any remaining files to clean up the TombstoneData folder for next time.

Now, it's important to know in which order the GameplayScreens initialization methods get called when returning from tombstone. The ones we're concerned about (in order of execution) are:

  • The constructor GameplayScreen()
  • Deserialize()
  • LoadContent()
So we should instantiate our game classes in the screen's constructor, load their saved state in Deseralize(), then load any graphical content in LoadContent(). You'll probably have a Hero.LoadContent() method, which you'd call from GameplayScreen.LoadContent().

Of course, there's a lot more to tombstoning than simply serializing your main classes, but hopefully it'll give you a starting point to help improve your game's behaviour. There are issues with the performance of the XmlSerializer and you may choose to look to Binary serialization for instance.

I'm only just scratching the surface whilst developing the as-yet-unannounced Team Mango RPG for the phone, so I'll likely blog more about my final tombstoning solution as it's quite a black art that unfortunately even the big studios are failing to get right.

6 comments:

Simon (Darkside) Jackson said...

Make sure you cover all aspects of how Tombstoning is supposed to be implemented. Only leave what you need to actually need to store at the last minute to tombstoning events to get arround the performance issues with XML serialisation.
Store as you go along in your game wherever you can (so long as it doesn't impact in game perf :D ).
check out the Tombstoning articles on XNA-UK.net for such info.

YaTaF said...

When I was replacing the old deserialization code with yours I'm getting an error: "The name 'StartupMode' does not exist in the current context". I'm not sure why I'm getting this, I have the using statement for Microsoft.Phone.Shell and a reference to Microsoft.Phone.

James Stephenson said...

So I am new to the XNA game scene. My first game only kept track of the level you were on, mainly because I did not want people to come back into a huge array of shots being fired at them. However, I may actually change it after reading your blog.

But to my question. I am making a card game, boring, I know, but I want a spades game for my phone. Anyway, when I deserialize my classes that contain a class for card, the cards are coming back as 2 of diamonds, all of them. Do I need to do something special in my deserialize of the hand and player class that has a list of card classes contained within?

Thanks.

Gareth said...

So I need to come back and revisit this post in the future, and perhaps put together a sample project. There's a few problems with the code I've posted, but nothing too serious.

James, if it was me I would have a Player class that had a public List<Card> generic list field on it. I'd then serialise the Player class when my GameplayScreen Serialize method is called.

What you'd need to watch out for is the event order for when the screens are reloaded. LoadContent is called after Deserialize, so you'd either need to initialise your Player class in the GameplayScreen constructor, or set a flag on Deserialisation and check it in LoadContent, and not initialize the Player class if you're coming back from tombstone.

James Stephenson said...

Well my problem is the card.class... I look at the appservice data when it writes out to it and it all looks fine.

When I reload it, all the cards default to 0,0 which in this case 2 of diamonds. So I thought I would just write the cards individually for each player than readd them to the player class upon load. That does not look like it worked, although only had a few minutes this morning before I had to leave for my real job to see if I was even close there.

Anyway, thanks for the reply.

James Stephenson said...

I figured it out. I read somewhere else that the List and ArrayList does not serialize properly in XNA. Well that is how I store the cards for the player and the hands, List so I just need to strip those lists down and do a good job of readding them to the players.

Thanks for the help though.