Chapter 10. The Application Life Cycle

This chapter deals with an area that you will almost certainly want to factor into your game in order for it to work as your users will expect. It covers what happens when your game loses focus on the device.

One of the more controversial decisions that Microsoft made about the Windows Phone 7 operating system is regarding its multitasking capabilities. Clearly, it is capable of multitasking, and some system applications run in the background the whole time, but third-party applications such as those that we can write are not permitted to multitask. When they lose focus, they completely stop executing.

The situation is slightly worse even than this; they don't simply stop executing, but they are actually closed. Managed applications written in .NET do not persist in the background at all. This clearly saves device resources and prevents unexpected battery drain, but it is a real nuisance in terms of maintaining the state of a game.

If people playing your game are interrupted by a phone call or have to quickly switch over to the calendar to set up an appointment, the chances are that they will want to return to where they were in your game and continue from where they left off. Unfortunately, the game was closed as soon as they navigated away from it. The onus is upon us to perform whatever steps are necessary to restore the game to its previous state once it is reactivated.

This state management requirement is a reasonably complex area of functionality, but is essential for consideration in your games. It is also something best implemented into your game as soon as possible so that it can be constantly tested during development. Trying to add this feature into a game that has already been written can be time-consuming at best, and nearly impossible at worse.

The Effects of Losing Focus

Let's start by understanding the problem. Load up the HighScores example project from the previous chapter and start it running (on the emulator or on a real device). Once it has launched, click the phone's hardware Back button and observe what happens. The phone returns to the home screen, and the Visual Studio IDE breaks out of run mode and returns to edit mode. Clearly, the game has stopped running. You can see, therefore, that clicking the Back button while in your game explicitly closes the application.

Tip

If you are working in the emulator, you can use its keyboard shortcuts to activate the hardware buttons instead of clicking with the mouse. F1 clicks the Back button, F2 clicks the Windows button, and F3 clicks the Search button.

Now relaunch the project from the IDE. Once it is running and displaying the blue Game over screen, click the Windows hardware button. The phone once again returns to the home screen, but this time the IDE stays in run mode, even though the game looks like it has closed. Now click the hardware Back button. After a brief pause, the "Game Over" screen reappears.

But hold on. Didn't we just say that Windows Phone 7 applications always close when they lose the focus? This certainly isn't what appears to have just happened. Both the phone and Visual Studio appear to have indicated that the game was suspended and then restarted. What is happening?

The answer is that the game was closed. Visual Studio recognizes that the user has not explicitly closed it, and that it was closed because it lost focus. To help reconnect to the game when the user navigates back to it through the historical application stack on the device, it remains in run mode. When the user does navigate back, the application relaunches on the phone, and Visual Studio detects this and connects to the relaunched application. This gives the impression that it is the same application session that has been reconnected.

The important fact is that the application was still closed. When Visual Studio reconnects to the application on the device, it is connecting to an entirely new instance of the application. All data that was stored in the previous session is lost, and unless we do something to persist it, it cannot be recovered.

You can see this more clearly in the HighScores example project by tapping the screen when prompted and entering your name so that the orange Highscores screen is displayed. Now press the Windows button, and then the Back button. The game has clearly lost track of what it was doing because it has reset back to the blue Game over screen.

This procedure of deactivating and then restarting an application is known as tombstoning. When the user navigates away from the game, Windows Phone leaves a marker (a tombstone) behind to identify the game on the application stack. When the Back button is pressed so that the user navigates back through this application stack, Windows Phone 7 will find this marker and use it to determine that your game should be reactivated.

To allow the game to resume from where it left off, we need to get our code to save away any important state information inside this tombstone and then retrieve this data when the game relaunches. In this section, we will discuss the mechanisms around this goal and explore a strategy for achieving it.

Note

In some games, it is not practical to completely maintain the state of all objects because their complexity might simply be too great to manage or they might contain internal data that is not accessible to your game code. Under these circumstances, the best that you can do is allow regular checkpoints (the game state is recorded at a point where it can be reasonably reconstructed). The player might lose a small amount of game progress as a result, but will still return to a recent known point within the game. Be aware of the potential for cheating that this might introduce, however: navigating away as soon as they lose a life and then returning to a prior checkpoint might make the game easier than you expected!

Life Cycle Events

Let's start by taking a close look at how we can hook into the four application life cycle events. These events cover the application starting up, closing down, deactivating, and reactivating. The code from this section can be found in the Tombstoning example project accompanying this chapter.

The Launching Event

When a game launches completely from scratch, the launching event will be triggered. This event can be used as an opportunity to load or initialize any data that is only required when the game is being started without any previous tombstoned session being present.

Note

The launching event is fired after the Initialize and LoadContent methods have both been called, so you can safely refer to any data that those methods have initialized in your launching event code.

The Closing Event

The closingevent fires when your game is closing down. This is in response to the game properly exiting because the user pressed the Back button while the game was playing, resulting in a call to the Game.Exit function (such as the call that is by default present at the beginning of the Update function within the main game class).

This event can be used to store away any persistent data that will be needed the next time the game is run.

Warning

You cannot rely on the closing event always being called. It is possible for the user to navigate away from your game and for it to never be reactivated. Any essential data that needs to be retained should either be stored into isolated storage as soon as it changes (as we have done for both the SettingsManager and HighScore object data) or should be written by both the closing event and the deactivated event.

You might want to store the complete game state to isolated storage in this event so that it can be resumed the next time the player launches your game. Some types of games will benefit from this; others can happily restart in a default state with any previous game data discarded.

The Deactivated Event

When the user navigates away from your game (or is taken away by an external event, such as an incoming phone call), the deactivated event will fire. This event indicates that your game is about to be tombstoned and gives you an opportunity to prepare.

There are two ways in which data can be stored in this event, and you might want to use both of them.

The first storage mechanism is for persistent data that is shared between consecutive sessions of the game and that is written by your game code to isolated storage. Any data that needs to be retrieved later on, even if the game is never reactivated, should be stored in this way.

The second mechanism is for transient data (data that is required only by this particular instance of the game should it be reactivated). Instead of writing this data to isolated storage, it can be written into a state object, the contents of which can then be recovered if the game is reactivated (though there is no guarantee that it ever will be). There are restrictions around the type of data that can be placed into the state object, as you will see shortly.

However you store your data, the most important thing is to store it quickly when a game is deactivating. If your game takes too long, the operating system will kill it, even if it is still working. Deactivation is not the time to perform any calculations or other unnecessary tasks; the event code needs to focus solely on storing state and finishing as fast as it can.

There is no guarantee that a deactivated game will ever be reactivated again. The game can be relaunched from the start menu (in which case the deactivated game's tombstone would be discarded), or the user can navigate far enough away from the tombstone that it is considered too old and purged from memory.

The Activated Event

The final life cycle event is the activated event, which is triggered when the user returns to a previously deactivated game by pressing the Back button until the tombstoned game comes back into focus.

During this event, data can be read from isolated storage and/or from the state object in order to put the game back into a usable state ready to continue execution.

The activated event and the launching event are mutually exclusive; only one or the other will ever fire when your game is starting up.

Note

Under certain conditions, Windows Phone 7 may deactivate your game and then reactivate it without actually closing it in between. You should ensure that your game is written so that its activation will work even if the game state is still present.

Handling the Life Cycle Events

All four of these events can be intercepted by adding event handlers in your game code. The event handlers are added to the Microsoft.Phone.Shell.PhoneApplicationService.Current object, as shown in Listing 10-1, whose code is taken from the TombstoneEvents example project (unrelated code has been removed from this listing so that we can focus on just the event handling code).

Example 10.1. Setting up the life cycle event handlers

using Microsoft.Phone.Shell;

namespace TombstoneEvents
{
    public class TombstoneEventsGame : GameHost
    {
        public TombstoneEventsGame()
        {
            // Set up application life cycle event handlers
            PhoneApplicationService.Current.Launching += GameLaunching;
            PhoneApplicationService.Current.Closing += GameClosing;
            PhoneApplicationService.Current.Deactivated += GameDeactivated;
            PhoneApplicationService.Current.Activated += GameActivated;
        }
private void GameLaunching(object sender, LaunchingEventArgs e)
        {
            System.Diagnostics.Debug.WriteLine("Game launching");
        }

        private void GameClosing(object sender, ClosingEventArgs e)
        {
            System.Diagnostics.Debug.WriteLine("Game closing");
        }

        private void GameDeactivated(object sender, DeactivatedEventArgs e)
        {
            System.Diagnostics.Debug.WriteLine("Game deactivated");
        }

        private void GameActivated(object sender, ActivatedEventArgs e)
        {
            System.Diagnostics.Debug.WriteLine("Game activated");
        }
    }
}

Note

In order to use the Microsoft.Phone.Shell classes, you will need to add a reference in your game project to both Microsoft.Phone and to System.Windows. Without these references, your code will not compile.

The event handlers are called each time one of the life cycle events is triggered. We will integrate some of this functionality into the game framework shortly and make it easier to respond to these events as a result, but the code in Listing 10-1 shows the underlying mechanism to access the events.

Seeing the Events in Action

Let's run up the TombstoneEvents example project and spend a little time experimenting with the sequence in which the life cycle events fire. Each event writes to the Visual Studio debug output stream, so it is easy to see what is happening.

The first time you launch the project, it displays the text Game launching as the launching event is fired. At this stage, you can click the Back button to exit the program; this time it displays Game closing to indicate that the game is shutting down rather than being tombstoned. Visual Studio returns to edit mode as the project stops running.

Now try running the project again, but this time press the Windows button after it starts up. Once again, the phone returns to the start page, but this time we navigated away from the game rather than closing it. The debug stream now shows Game deactivated instead of Game closing. The game has been tombstoned. It is not running (it was terminated just the same as before), but the tombstone marker has been left in the application stack.

Warning

Notice that the Visual Studio IDE is still in run mode when the game is tombstoned. It is there to make it easier to reconnect to the game if and when it is reactivated. It does not mean that the game is still running (it is not; it was still closed completely and has lost all its state). Don't let the fact that the IDE continues in run mode confuse you into thinking that the game is running in the background.

To return to the tombstoned game, click the Back button. The phone navigates back through the application stack, finds the game's tombstone marker and reactivates the game. This time, the game shows Game activated instead of Game launching. The game has still started absolutely from scratch, though. Because the Activated event fired instead of the Launching event, it is our cue to read the tombstoned data back into the application, and you will see how this is done shortly.

One last thing to try is launching the game and tombstoning it once more by pressing the Windows button. This time, instead of pressing Back to return to the game, relaunch another instance of the game by clicking the circled arrow icon from the phone's start page and then clicking the game's icon in the application list, as shown in Figure 10-1.

Starting a new instance of the tombstoned game

Figure 10.1. Starting a new instance of the tombstoned game

When a new instance launches like this, the existing tombstoned instance is completely discarded, and the new instance starts by firing its Launched event rather than its Activated event. Visual Studio returns to edit mode, and the tombstoned game instance cannot be reactivated by repeatedly using the Back button. This first instance is an example of a deactivated application that will never be reactivated.

Now that we have seen how the application life cycle works, let's look at how to use it to allow state to persist between sessions when the game is tombstoned.

Persisting Session State

So what can we do to allow our game to pick up where it left off after it is reactivated? Clearly we can hook into the Deactivate and Activate events and use them to store information about what our game is doing, but what is the best way to do this?

One option is to write all the information about our game to a file in isolated storage. When the game is activated, we could check for the presence of this file; if found, load it and read all the game data within it.

That process would certainly work, but there is another way: storing the game state in the PhoneApplicationService object. This second approach is recommended by Microsoft because it is faster to execute. Execution speed is important because if your game takes too long, the phone will decide that it isn't working and will abort the reactivation (and this problem will also cause your game to fail Microsoft's application certification guidelines required for entry into the Windows Phone Marketplace).

The PhoneApplicationService object (also found inside the Microsoft.Phone reference) contains a generic Dictionary, keyed by strings and storing objects, into which we can write values that are to be transferred from the deactivating session into the later activated session. And the good news is that we can simply drop objects into this dictionary in the Deactivate event, and then read them back in the Activate event. The bad news is that there are some important restrictions on the objects that can be placed within the dictionary, as we will see shortly.

Let's try this out first and see how it works. Inside the TombstoneEvents project, the Deactivated event contains a small amount of code that generates a random number into a variable named rand. It then puts the number into the PhoneApplicationService.Current.State dictionary. The code is shown in Listing 10-2.

Example 10.2. Placing a randomly generated number into state storage in the Deactivated event

private void GameDeactivated(object sender, DeactivatedEventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Game deactivated");

        // Generate a random number
        int rand = GameHelper.RandomNext(10000);
        // Write the number to the state object
        PhoneApplicationService.Current.State.Clear();
        PhoneApplicationService.Current.State.Add("RandomNumber", rand);
    }

Warning

Always remember to clear the State dictionary before you begin writing values to it, as shown in this listing. If an application is tombstoned for a second time, any content added from the first deactivation will still be present, resulting in key collisions or unexpected behavior.

Next to this data storage code is another piece of code inside the Activated event that reads the value back from the dictionary, as shown in Listing 10-3.

Example 10.3. Reading a value back from state storage in the Activated event

private void GameActivated(object sender, ActivatedEventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Game activated");

        // Recover the random number
        int rand = (int)PhoneApplicationService.Current.State["RandomNumber"];
    }

Place breakpoints into these two functions so that you can trace through their execution and then start the project running. Once it opens, press the Windows button to navigate away from the game. The Deactivated event fires, and the random number is generated and added to the dictionary. Make a note of the number and then allow the project to continue running to completion.

On the phone, press the Back button to return to the tombstoned project. The breakpoint in the Activated event fires, and the value is retrieved back from the dictionary. The number that was generated is present and is restored back into the reactivated game.

This is the basis on which we will build our strategy for surviving being tombstoned.

The dictionary is not limited to numbers, of course: it can accept any object at all. However, only objects that are serializable can be added to the dictionary without errors occurring. Any non-serializable object will result in a failure to reactivate the game.

Objects can be serialized provided that

  • they contain a public, parameterless constructor (though other constructors can be present, too).

  • they only publicly expose fields and properties that use simple value types (including structs and enums) or other serializable objects.

Serialization recurses through all the public properties and fields within the object being serialized and then builds them all up into a representation that it can store away as a piece of data. When the data is later processed for deserialization, the object (and any subobjects it uses, and so on) is re-created just as it was before.

For many of the object properties that we use in our games, this will work very nicely: object positions, colors, vectors, matrices, and so on will serialize without any issues. However, all our game object classes have properties that contain objects that cannot be serialized (textures, fonts, and models); they all contain nonserializable data and will break if we try to add them to the state dictionary.

In order to be able to serialize these objects we need to overcome two problems: how to exclude the nonserializable elements from the serialization process and how to repopulate these nonserializable properties once the object is re-created. Let's see how these problems can be addressed.

Controlling Serialization

By default, objects that are placed into the state dictionary will have all their public fields and properties serialized. .NET provides a mechanism whereby instead of everything being included by the serialization operation, we can flag individual properties and fields that should be included. Those properties and fields that are not flagged will be excluded from serialization. By applying this mechanism to our classes, we can omit the flag from properties that return nonserializable objects such as textures, and the rest of the object will then be successfully persisted.

Note

Fields and properties with private or internal scope are always ignored by the serialization process; only public fields and properties are included. Because it is usually a good idea to avoid public fields (it is impossible to later change them into public properties without breaking the binary interface), it is best practice to ensure that all values that need to be serialized are implemented using properties.

The inclusion and exclusion of properties within a class is enabled by applying the DataContract attribute to the class to be serialized. This attribute can be found within the System.Runtime.Serialization namespace, and to access it a reference must first be added to System.Runtime.Serialization, too.

Once this has been set up and the attribute applied to the class, the serialization process will only observe properties and fields that have had the DataMember attribute applied. Properties and fields that do not have this attribute will be excluded from serialization.

Listing 10-4 shows a very simple class definition that contains two public properties: a position vector and a Texture2D. The class has the DataContract attribute applied and the position has the DataMember attribute, but the texture property does not. This class can be serialized successfully, and will store the position away ready to be deserialized. The texture will not be serialized, and after deserialization will remain at its default null value.

Example 10.4. Controlling which properties are included by the serialization process

[DataContract]
public class TestClass
{
    // Constructor
    public TestClass()
    {
    }

    // Public properties
    [DataMember]
    public Vector2 Position { get; set; }

    public Texture2D Texture { get; set; }
}

DataContracts and Inheritance

There are some important details about using the DataContract attribute with inheritance. If these details are incorrectly implemented in your game, you will find that serialization stops working properly, so it is important that you understand how .NET serializes classes under these conditions.

The DataContract attribute is not inheritable. If you apply it to a base class and then derive another class from that base class, the derived class will not have the DataContract attribute applied. Its default behavior therefore will be that all public properties defined within the derived class will be serialized, and all properties in the base class marked with the DataMember attribute will be serialized.

If you want the derived class to allow only some of its properties to be serialized, you must apply the DataContract to the derived class and then add the DataMember attribute to the appropriate properties. Specifying DataMember attributes without the class DataContract attribute will have no effect.

Derived classes cannot apply the DataContract attribute unless their base class also applies it.

And then there is one final complication that can cause all sorts of problems and confusion. If the class that is actually instantiated and serialized does not contain at least one serializable property (a public property marked for serialization and with a getter and a setter so that it can be both read and written), the object will not be serialized, even if its base classes do contain serializable properties. An override of a property from the base class does not count. Providing the derived class directly contains at least one serializable property of its own, it will then be fully serialized, including all the properties from the base classes.

If you create a derived game object class that doesn't have any properties that need to be serialized, you will need to add a dummy property so that the serialization process doesn't ignore the class. This process can be simple, as shown in Listing 10-5, but is essential for your objects to survive tombstoning.

Example 10.5. Creating a dummy property to force serialization of a derived class with no other serializable properties

/// <summary>
    /// A dummy property to force serialization of this class
    /// </summary>
    [DataMember]
    public bool _SerializeDummy { get; set; }

If you are unsure as to whether your class is being serialized or not, put breakpoints on both the get and set parts of one of its serializable properties. If serialization is working, the get breakpoint will fire when the game is being deactivated, and the set breakpoint will fire when it is reactivated.

Also ensure that you have the Visual Studio IDE set to break on all exceptions because it can otherwise close your game during either of these events without an explanation. The IDE can be set in this way be selecting Debug/Exceptions from the main menu, and then checking the Common Language Runtime Exceptions Thrown box, as shown in Figure 10-2.

Setting the IDE to break on all thrown exceptions

Figure 10.2. Setting the IDE to break on all thrown exceptions

Persisting Nonserializable Data

The DataContract attribute gets us past the problem of serializing the object, but we have lost the nonserializable data. When our object is restored, it will fail as soon as it tries to use the missing content.

We work around this by storing an identifier that represents the object rather than the object itself. For example, in the game framework, all the textures that we use are stored in the GameHost's Textures dictionary. Instead of serializing the texture, we can add an additional property that returns the name of the texture rather than the texture itself. The name will then be serialized, and will be provided to the deserialized object when the game is reactivated. Once the object knows the name, it can look it up in the dictionary and retrieve the actual texture object once again.

We're not quite there yet, however, because there is an additional complication: the deserialization process takes place before the game's Initialize or LoadContent methods fire. The game objects are therefore unable to read from the dictionaries because they haven't yet been populated. And in fact, the game object doesn't even have a reference to the GameHost object; this would normally be passed as a parameter to the class constructor, but because deserialization uses the parameterless constructor, this object is not available.

Instead we have to just store the content object name away as a string and retrieve the actual object later on once the game has had a chance to finish initializing itself.

Tombstoning in the Game Framework

There's a lot of complexity involved in getting a game to seamlessly resume after tombstoning, and unfortunately we can't really escape that. What we can do, though, is put as much of the complexity as possible into the game framework, allowing us to minimize the problem in our games.

The main task that the framework sets out to accomplish is the serialization of everything inside the GameObjects collection. A significant proportion of the game functionality is likely to be contained within these objects, so an easy and usable mechanism for persisting them will solve a large proportion of the problems that a game faces when it is tombstoned.

Allowing the game to become serializable in this way requires a number of changes to the game framework classes; most of them are fairly small, but some involve a little more complexity. Let's address each of the changes so that we can understand how the game framework helps to support this process.

All the code described in this section can be found in the Tombstoning example project.

Setting up the GameHost Event Handlers

To save us having to add event handlers for the four application lifecycle events, the GameHost does this for us. It provides four virtual functions that can be overridden in the game itself to respond to the events when necessary. This is a minor enhancement, but does simplify the event processing.

Listing 10-6 shows the overrides required to produce the same debug output functionality present in the earlier TombstoneEvents project.

Example 10.6. Controlling which properties are included by the serialization process

protected override void GameLaunching()
    {
        System.Diagnostics.Debug.WriteLine("Game launching");
        base.GameLaunching();
    }
    protected override void GameClosing()
    {
        System.Diagnostics.Debug.WriteLine("Game closing");
        base.GameClosing();
    }
    protected override void GameDeactivated()
    {
        System.Diagnostics.Debug.WriteLine("Game deactivated");
        base.GameDeactivated();
    }
    protected override void GameActivated()
    {
        System.Diagnostics.Debug.WriteLine("Game activated");
        base.GameActivated();
    }

Preparing the Classes for Serialization

For convenience, all the GameObjectBase-derived classes that we have created have used parameterized constructors into which various essential pieces of information have been passed. For the objects to be serialized, they must have a parameterless constructor, so such a constructor has been added to all the game object classes.

To provide control over which properties are serialized, GameObjectBase has the DataContract attribute set. Each of the derived object classes has the attribute set too. All the appropriate serializable properties are marked with the DataMember attribute. These include the UpdateCount property in GameObjectBase; the position, origin, scale, color, angle, and layer depth properties of SpriteObject; and so on through the other object classes. The properties that cannot be serialized (those that store textures, models, fonts, sound effects, and songs) do not have the DataMember attribute; we will look at how these are handled next.

With the classes configured in this way, instances are ready to be serialized and restored.

Persisting Content References

Next up, we need to deal with how content objects are persisted during tombstoning. As we have already discussed, although we cannot serialize the content itself, we can serialize its name. Taking SpriteObject as an example (though this approach is applied to all the objects that have content reference), we add a new SpriteTextureName property that accepts or returns a string value, and can therefore be easily serialized.

Unfortunately, there is nothing inside the sprite's Texture2D object that actually tells us its name. It does have a Name property, but it returns an empty string. We could set the name when the content object is loaded, but doing this has the potential to cause errors because if we should fail to set the object name, we would not be able later to identify the object.

Instead, we can interrogate the dictionaries and ask them to perform a reverse lookup—searching for the object that we are using and telling us its key. We can easily iterate through all the items in a dictionary by using a foreach loop, iterating a KeyValuePair object across the values.

The GameHost class contains a number of dictionaries, however, and they are all of different types. While we could set up a different function for each, we can save some repetitive coding by creating a generic dictionary scanning function instead. We can pass into this the dictionary to be scanned (e.g., the Textures dictionary) and the type of object it contains (Texture2D objects in this case) and it will return back the object's key.

The generic implementation of this function is shown in Listing 10-7.

Example 10.7. Finding the key for an object inside any generic dictionary with a string-based key

public string GetContentObjectName<T>(Dictionary<string, T> objectDictionary,
                                                                            T contentObject)
    {
        // Loop for each key/value pair
        foreach (KeyValuePair<string, T> dictItem in objectDictionary)
        {
            // Is this item's value the object that we are searching for?
            if (EqualityComparer<T>.Default.Equals(dictItem.Value, contentObject))
            {
                return dictItem.Key;
            }
        }
        // Couldn't find the requested object
        return null;
    }

The function is called as shown in Listing 10-8.

Example 10.8. Finding the name of a sprite's texture object

textureName = Game.GetContentObjectName<Texture2D>(Game.Textures, SpriteTexture);

This gives us the ability to return a content object's name ready for serialization. That's the first part of the problem covered.

Next we need to be able to recover the name when the object is deserialized. .NET will pass the name string to the object, but the object cannot yet retrieve the actual content object because it has no reference to the GameHost object, and the content has not yet been loaded.

Instead, we simply store the name away in a private string variable. When the game has been fully initialized and is ready to actually use the texture, we will retrieve it from the GameHost object then.

The full implementation of the SpriteObject.SpriteTextureName property is in Listing 10-9.

Example 10.9. Setting and returning the name of the sprite's texture object

/// <summary>
    /// The name of the texture being used by this sprite
    /// </summary>
    private string _textureName;
    /// <summary>
    /// Sets or returns the sprite texture name.
    /// This can be serialized when the game is tombstoned.
    /// </summary>
    [System.Runtime.Serialization.DataMember]
    public virtual string SpriteTextureName
    {
        get
        {
            // Return the texture name
            return _textureName;
        }
        set
        {
            // Is this texture name different to the one we are already using?
            if (_textureName != value)
            {
                // Store the new name
                _textureName = value;
                // Clear the stored texture so that the name is processed the name
                // time the SpriteTexture property is called
                _spriteTexture = null;
            }
        }
    }

Each time the property is set to a new value, it removes any existing SpriteTexture object reference by setting the property's backing variable (_spriteTexture) to null. This ensures that the SpriteTexture property (whose modified definition we will look at next) will observe any changes to the SpriteTextureName that can be made during normal execution of the game at times other than deserialization. Without this, setting the name within our game code would have no effect on the texture actually being used.

The texture name can now be persisted through tombstoning and set back into the object when the game is reactivated. The final step is to get the object to actually restore the texture object when it needs it.

This step is achieved by adding some further code to the SpriteTexture property, shown in Listing 10-10. Whereas before this property was a simple automatically implemented property, we now need to expand it out with a little code and provide another private backing variable to store the actual texture object in.

Example 10.10. The updated SpriteTexture property

/// <summary>
    /// The underlying texture being used by this sprite
    /// </summary>
    private Texture2D _spriteTexture;
    /// <summary>
    /// A reference to the default texture used by this sprite
    /// </summary>
    public Texture2D SpriteTexture
    {
        get
        {
            // Do we have a texture name but no actual texture?
            // This will be the case after recovering from being tombstoned.
            if (_spriteTexture == null && !String.IsNullOrEmpty(_textureName) && Game != null)
            {
                // Get the texture back from the Textures collection
                _spriteTexture = Game.Textures[_textureName];
            }
            // Return the sprite texture
            return _spriteTexture;
        }
        set
        {
            if (_spriteTexture != value)
            {
                // Set the sprite texture
                _spriteTexture = value;
                // Set the texture name
                _textureName = Game.GetContentObjectName<Texture2D>(Game.Textures, value);
            }
        }
    }

Let's look at the set part of the property. Each time a new texture is set, the backing variable is updated and then the _textureName variable is updated with the name of the texture, which ensures that the texture name is always up to date. The _textureName variable is updated directly rather than through the SpriteTextureName property to avoid the SpriteTextureName set part clearing the _spriteTexture variable back to null again.

In the get part of the property, the code checks to see whether its _spriteTexture backing variable is null (which it will be after a tombstoned game is reactivated). If it is, and if the object has a non-blank texture name and a reference to the GameHost, it retrieves the texture from the Textures dictionary.

This is inescapably more complex than the implementation that we had for the SpriteTexture property. This implementation is able to survive tombstoning however, by always ensuring that it knows the texture name, and by retrieving the required texture if it finds that a name is set but the texture is not.

As complex as it is, all this functionality is packaged away inside the GameFramework.SpriteObject class and is therefore completely transparent to any derived classes or external code that is using the class. Any code that doesn't know the details of the internal implementation can use it just like the simple property that we implemented before.

This same property model is repeated for all the game framework classes that deal with content objects and allows them all to survive the tombstoning process.

Automatic Storage and Retrieval of Game Objects in the State Dictionary

The game objects are capable of being tombstoned, but we have a little more work still to do to ensure that they are actually added to the state dictionary during the Deactivated event and then retrieved in the Activated event. Once again we can simplify this work by moving the code into the game framework.

The first of the new functions is the GameHost.WriteGameObjectsToPhoneState function, shown in Listing 10-11. It first clears the existing state dictionary and then loops through all the items in the GameObjects list, adding each to the dictionary.

Example 10.11. Adding the game objects into the state dictionary

protected void WriteGameObjectsToPhoneState()
   {
       // Clear any previous state
       PhoneApplicationService.Current.State.Clear();

       // Track the index so we can generate a unique dictionary key for each object
       int objectIndex = 0;
       foreach (GameObjectBase obj in GameObjects)
       {
           // Is this object interested in being added to the phone state?
           if (obj.WriteToPhoneState)
           {
               // Generate a name and add the object
               PhoneApplicationService.Current.State.Add("_obj" + objIndex.ToString(), obj);
               // Move to the next object index number
               objIndex += 1;
           }
       }
   }

It might be that there are some game objects that do not need to be maintained when the game is deactivated. One example is particle objects. They are usually present purely for short-term visual effect, and if they were discarded during the tombstoning process they would have negligible effect on the game when it reawakens. Ignoring these objects can reduce the amount of data that needs to be serialized, speeding up the whole process.

To allow us to control which objects are serialized and which are not, a new property named WriteToPhoneState has been added to GameObjectBase. This is a virtual property with just a get part, and by default it simply returns true.

Classes that want to exclude some or all of their objects from serialization can override this property and return false instead. This is exactly what the MatrixParticleObjectBase class does, so by default all particle objects are excluded from serialization. If you have a particle object class that you do want to be included, you will need to override in your class and return true once again.

The WriteGameObjectsToPhoneState function can be simply called from your GameDeactivated function to put all the game objects into the state dictionary.

Note

Always call WriteGameObjectsToPhoneState before adding any other items to the state dictionary because it clears the dictionary items before adding the game objects.

That's everything set up ready for deactivation, so how do we reactivate things when the game restarts? This is achieved by calling the GameHost.ReadGameObjectsFromPhoneState function. This function loops through all the objects in the state dictionary, pulling out every object that is derived from GameObjectBase. For each object that is found, it is given back its Game property reference to the GameHost and then added back into the GameObjects collection. The function is shown in Listing 10-12.

Example 10.12. Reinstating objects that were stored in the state dictionary

protected void ReadGameObjectsFromPhoneState()
    {
        GameObjectBase obj;
        // Loop for each state item key/value
        foreach (KeyValuePair<string, object> stateItem in
                                                     PhoneApplicationService.Current.State)
        {
            // Is the value a game object?
            if (stateItem.Value is GameObjectBase)
            {
                // It is. Retrieve a reference to the object
                obj = (GameObjectBase)stateItem.Value;
                // Set its Game property so that it can see the GameHost
                obj.Game = this;
                // Add the object back into the GameObjects collection
                GameObjects.Add(obj);
            }
        }
    }

This function is simply called from the Activated event and together with the changes in the "Persistent Content References" from the previous section will result in all the game objects being reinstated to their original condition.

Identifying Specific Objects After Tombstoning

The approach described so far repopulates the GameObjects collection after reactivation, but what if we had stored individual references to specific objects in class-level variables? We will frequently do this so that, for example, we know which game object is the player's object, or which object is displaying the player's score.

To provide a simple mechanism for reobtaining access to specific objects, we introduce another new property to GameObjectBase, this time named Tag. The Tag property can be set to contain any string value, and will be included in object serialization.

After the game has been reactivated, references to tagged objects can be obtained by calling GameHost.GetObjectByTag, passing in the tag string. If a matching object is found, it will be returned.

This can be seen in action inside the Tombstoning example project. Within the game class' GameActivated, the _scoreText object is repopulated by looking for the object tagged with the string _scoreText. This object tag is initially set when the text object is created inside ResetGame.

Game Initialization

In many of our examples, we have called in to the ResetGame function at the end of LoadContent. This has served us well in our simple examples, but is no longer adequate when tombstoning becomes a consideration.

LoadContent is called every time the game initializes, regardless of whether it is launching a new instance or activating a tombstoned instance. If we call ResetGame from the end of LoadContent, it will be called after reactivation, erasing all the game objects we have painstakingly restored and resetting the game back to its initial state. That's not quite the result we were looking for!

To make everything work as required, simply move the call to ResetGame into the GameLaunching function instead. This will then be called only when a new instance of the game is starting up.

One final detail to be aware of is that the first call to the game's Update might occur prior to the Launching or Activated events being called. If you have any code inside Update that expects the game to have been initialized, it might fail as a result. The simplest fix to this is to set a class-level boolean variable from the end of your Launching and Activated events, and return from any calls to Update if this variable has not yet been set.

Troubleshooting Serialization

You need to ensure that the derived game object classes you create in your game conform to the requirements for serialization in order for them to be able to survive tombstoning.

Here is a checklist of things to look for if your object serialization doesn't seem to be working:

  • Ensure that the scope of your class is public (if no scope is specified, it will default to private).

  • Ensure you have a public, parameterless constructor.

  • Ensure that your class either has the DataContract attribute and that the appropriate properties within the class have the DataMember attribute, or that the class does not have the DataContract attribute and all the public properties in the class are serializable.

  • Ensure that your class has at least one direct serializable public property (defined within the class itself, not inherited).

If any of these conditions is not met, your game will either throw an exception when its objects are being serialized (a potentially confusing SecurityException is one such possibility), your game will simply fail to relaunch when you navigate back to it, or some of your objects will be missing from the game when it is reactivated.

Tracking down and managing problems in this area can be a challenge, and this is one of the reasons why attending to tombstoning early on in your game's development process can save time and frustration later on.

Returning from the Grave

The Tombstoning project demonstrates everything that we have discussed in this section, including successful deserialization of several sprite and text objects, reobtaining a reference to a specific object, and also putting additional content (the player's score) into the state dictionary.

If you launch the game, press the Windows button, and then finally press the Back button, you will see that everything picks up exactly where it had left off.

A more complex example is in the VaporTrailTS project. This returns to the VaporTrail example from Chapter 8, and includes serialization and deserialization of a scene containing 3D objects, models, particles, a skybox, a custom camera, and a user-selectable view mode. Everything within the scene survives tombstoning except for the smoke particles, which are deliberately excluded.

A fair amount of effort is required within the game framework to support tombstoning, but the game itself can usually take advantage of this without too much extra work. In many cases, the end result will make the implementation of tombstoning support time well spent.

This chapter concludes our work with XNA. While we will refer back to it in the coming chapters (and even use some of its functionality), it is time to step across into a different technology now and examine the games that can be made using Silverlight.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.237.77