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.

Monday, 22 November 2010

Shrinking Dysnomia

In my last post, I teased Dysnomia running on Windows Phone 7. It's an interesting challenge, shrinking the game down to run on the phone. I did a lot of optimization on the 360 version, mostly around garbage collection and memory management while the gameplay was running.

What I did not optimize at all was RAM usage. I chose to load all game assets up-front, even before the title screen appeared. For the 360, the amount of overall RAM used was trivial, and the load times were fast. I loaded all animation spritesheets, all sounds, all music and all popup screen content before the first level was started.

The only items I chose to load per-level were the tilesheets, as they were by far the largest textures used in the game, and I needed to be able to free up the RAM they used between levels.

On the phone however, the RAM usage story is completely different. Microsoft recommends that an application shall not exceed 90mb of total RAM usage, that is to say peak usage. It's a simple matter to check how much memory your app is using:

var curuse = (long)DeviceExtendedProperties.GetValue("ApplicationCurrentMemoryUsage");
var memuse = (long)DeviceExtendedProperties.GetValue("ApplicationPeakMemoryUsage");
var maxmem = (long)DeviceExtendedProperties.GetValue("DeviceTotalMemory");
curuse /= 1024 * 1024;
memuse /= 1024 * 1024;
maxmem /= 1024 * 1024;

Will return current memory usage, peak memory usage, and total RAM available (in MB) respectively.


So when I hooked up a debug method to output the RAM usage to the screen, I discovered that Dysnomia was using 190MB at peak (with the map screen open), and 170MB during normal gameplay. That's twice the usage limit for passing app certification!

I began by simply cutting down on front-loaded assets. Later on, I'll implement per-level loading of required spritesheets, but for now I've simply eliminated loading of everything not needed for the first level. That alone got me down to around 110MB peak usage.

Next up, I began resizing all of the sprite and tilesheet textures. An across-the-board resize of 75% got me down to around 80MB peak. Several larger spritesheets could be downsized even further. Next up, I stopped loading variations of HUD and player sprites for different player colour options and for player two, as there will be only one player on the phone.

After all that, I'm currently down to 75MB in-game, and 80MB or so on the map screen. I'm still investigating further reductions, including using DXT compression for spritesheets. My target is 65MB in-game usage before reinstating sound effects and boss fights, which will require a good 10-15MB of RAM per level on top of what's used already.

I'm not going to officially confirm Dysnomia for WP7 until I'm sure I can make it work, but so far I'm impressed with the virtual sticks (emulating left/right thumbsticks via the touchscreen) I've implemented, so I'm sure it will control quite nicely if I can just get everything squeezed into RAM.

Game 4

Can't wrap up this post without a quick update on Game 4. I've resumed work on the next Team Mango game, working on it alongside Dysnomia for WP7 and Run! for XBLIG. It's an RPG, and the next job I needed to complete was getting an enemy manager up and running. The manager controls all of the enemies loaded on a map, including their spawn points.

As the game is based upon my XNA Tile Engine solution, I'm using the Tiled editor to lay out the area maps. To depict enemy spawns, I'm using an Object layer. The screenshot below shows a typical map layout with spawns. Each rectangle represents one enemy's patrol area, with the spawn point in the center. When the enemy is loaded, a point within its rectangle is chosen at random, and the enemy begins to move toward that point. When it reaches its destination (or collides with a wall), it chooses a new destination point from within its rectangle.



And the result, Squirrels!

Monday, 15 November 2010

Moving House and Team Mango projects for the next year

I'm almost done with the house move. That is to say - we're in, but still living out of boxes while we wait for a few bits and pieces to come together. It's not been the smoothest of moves, especially having our garage broken into no more than two days after spending our first night in the new place. Nothing too essential gone, but all of my guitar amps and effects were taken. I was planning to sell them next year to fund a new PC, so it looks like that may be on hold for the time being.

Anyway, troubles aside the new house is looking great and I now have a spare room studio setup for working on all the Team Mango projects that I've been planning for the better part of half a year while all the househunting and moving was taking place. Here's a dark picture of my workstation:


I need to do a bit of housekeeping in this post, starting with an update on 7Cache, my Phone 7 Geocaching app. I've decided to suspend the project for now, because Groundspeak released their official Geocaching app for the phone, and it seems to have everything most 'cachers would need including offline caching. It's quite well presented and seems to work well, though I have yet to get out and test it properly on a full day's caching. It certainly has more features than I planned for 7Cache, apart from the Cache Radar function. I rarely used that in Geocache Navigator anyway.

I may revisit this decision further down the line if there's a niche for features that Groundspeak won't add to their app, or if they make the Geocaching.com API publicly available. For the time being, I have game projects to work on and that's where I'd rather spend my very limited personal time. Speaking of which...





That's Run!, the third game from Team Mango. It's been in the works since February, just before Dysnomia hit Xbox Live Indie Games. I've been working on it sporadically, but it's close to being ready now.

I'd describe it as "Skyroads with Avatars", and it offers 1-4 players local competetive play as well as online best times, using the same highscore sharing method employed by many XBLIGs. I'm looking forward to finally getting it out the door after a busy few months away from all things Indie Games.

The fourth Team Mango game remains under wraps, but I can say it's an action-RPG, and will be released for Phone 7, XBLIG and Windows sometime in 2011. It should also be pretty damn funny, but I guess that's subjective!

Also,