Chapter 9. Enhancing Your Game

There are many different styles and varieties of games, and they can be diverse in the ways that they play and function. Some game features are useful in many different games, though, and in this chapter, we'll examine a couple of useful pieces of functionality that fall into this category. These game enhancements will be created to be easily reusable so that they can be quickly and easily dropped into any game that would benefit from them.

The enhancements that we will create are as follows:

  • A settings class, allowing us to easily set and retrieve configuration values, and save them to files so that they can be retrieved the next time the game is executed.

  • A high score table, providing a very easy mechanism for adding and displaying scores that the player has achieved.

These features will be created within the game framework and can be easily accessed by calling into the methods of the appropriate class.

Along the way, we will also examine some other mechanisms that will be very useful within your games, such as handling the Back button, reading and writing data from and to isolated storage, and working with multiple sets of game objects.

Let's take a look at what each of these enhancements does in more detail, how they are used, and how they work internally. The internal workings are only discussed fairly briefly here as they are intended as end user components, but are described in case you want to make any changes to how they work or to lift any of the code out for use in other projects. The full source code is, of course, available in the GameFramework project for this chapter in the accompanying download.

Managing Game Settings

Most games and applications will want to offer the user control over how some of the features of the program function. In a game they will include things such as settings for the game (difficulty level, different game modes), settings for the game environment (sound effect and music volume levels, graphic options), or settings that are controlled by the application itself (such as remembering the date the game was last played or the name that the player last entered into a high score table).

There is nothing particularly difficult about managing this information, but as with everything else we have done with the game framework, our objective is to make this as simple to manage as possible. To look after all of this for us, we will add a new class, SettingsManager, to the GameFramework project. This class will allow us to easily set and query game settings, and to save them to the device and retrieve them once again.

There are a number of parts to this class, and also some things to consider when it comes to actually using it, so let's take a look at how it all fits together. All the code for this section is present in the Settings example project that accompanies this chapter and this chapter's updated GameFramework.

Class Structure

The SettingsManager class is declared as a public class, but with a constructor whose scope is internal. Setting the scope in this way prevents code outside of the game framework itself from being able to instantiate the class, and so we can ensure that just a single instance is created by the framework and used throughout the game. Limiting the class to a single instance ensures that all parts of the game see the same settings without having to pass an instance of the class around between functions.

The single object instance is accessed using the GameHost.SettingsManager property.

Setting and Retrieving Values

The main purpose of the SettingsManager class is to store values that we give to it, and to allow those values to be retrieved later. These two actions can be accomplished by calling the SetValue and GetValue methods.

A number of different overloads of these two functions are provided for different data types, with versions available for string, int, float, bool, and DateTime types. Internally they are all stored as strings, but the overloads ensure that the values are encoded and decoded properly so this internal storage mechanism is transparent to the calling code.

Whenever a value is retrieved using one of the GetValue methods, a defaultValue parameter is provided. This parameter serves two purposes. First, it allows for a sensible value to be returned if the requested setting is unknown and doesn't exist within the object. This simplifies the use of the class, as shown in Listing 9-1. If we try to retrieve the sound effects volume level and it hasn't already been set, we default it to 100 so that its initial setting is at full volume, even though no setting value actually exists within the object.

Example 9.1. Retrieving a setting value from the SettingsManager Object

int volumeLevel;

        // Retrieve the volume level from the game settings
        volumeLevel = SettingsManager.GetValue("SoundEffectsVolume", 100);

The second role that the defaultValue parameter performs is identifying the expected return type of the GetValue function. If the defaultValue is passed as an int, the return value will be an int, too. This provides a convenient variable type mechanism, avoiding the need to cast and convert values to the appropriate types.

In addition to setting and getting values, the class also allows existing settings to be deleted. The DeleteValue function will remove a setting from the class, causing any subsequent calls to GetValue to return the provided default once again. To remove all the stored settings, the ClearValues function can be called. All the settings that have been previously written will be erased.

Inside the SettingsManager class, the values that are passed in to be set are routed into a single version of the SetValue function, which expects its value to be of type string. Within this function, the value is written into a special collection provided by the phone called ApplicationSettings, contained within the System.IO.IsolatedStorage namespace. ApplicationSettings is a dictionary collection, and every item added to the dictionary is immediately written away into isolated storage, a special data storage system used within the phone. We will look at the implications of isolated storage in more detail in the "Reading and Writing Files in Isolated Storage" section later in this chapter.

Note

As of this writing, when running on the emulator, the isolated storage is reset every time the emulator is closed. This can be frustrating because stored data does not persist from one emulator session to the next, but can also be useful as it presents an opportunity to test your game in an environment that has no previously written files or settings.

The SetValue function's code to add the setting values to the settings dictionary is shown in Listing 9-2.

Example 9.2. Writing settings values into the ApplicationSettings dictionary

public void SetValue(string settingName, string value)
    {
        // Convert the setting name to lower case so that names are case-insensitive
        settingName = settingName.ToLower();

        // Does a setting with this name already exist?
        if (IsolatedStorageSettings.ApplicationSettings.Contains(settingName))
        {
            // Yes, so update its value
            IsolatedStorageSettings.ApplicationSettings[settingName] = value;
        }
        else
        {
            // No, so add it
            IsolatedStorageSettings.ApplicationSettings.Add(settingName, value);
        }
    }

The function first converts the settingName parameter into lowercase. This makes the settings case-insensitive, which can help avoid obscure problems later on. Once this is done, the ApplicationSettings dictionary is checked to see whether a setting with the specified name already exists. If it does, it is updated with the new value; otherwise, a new dictionary item is added.

Reading the settings is equally straightforward. The GetValue function, shown in Listing 9-3, checks to see whether an item with the specified settingName exists. If it does, its value is returned; otherwise, the provided defaultValue is returned instead.

Example 9.3. Reading settings values back from the ApplicationSettings dictionary

public string GetValue(string settingName, string defaultValue)
    {
        // Convert the setting name to lower case so that names are case-insensitive
        settingName = settingName.ToLower();

        // Does a setting with this name exist?
        if (IsolatedStorageSettings.ApplicationSettings.Contains(settingName))
        {
            // Yes, so return it
            return IsolatedStorageSettings.ApplicationSettings[settingName].ToString();
        }
        else
{
            // No, so return the default value
            return defaultValue;
        }
    }

We can now set, update, and interrogate the settings for our game, but then we come to the next question: how do we allow the player to view and change the settings?

Displaying a Settings Screen

XNA has no built-in graphical user interface components, but we can easily create a simple user interface for maintaining settings using simple text labels. We can represent them using the GameFramework.TextObject class and can use code that we have already written to detect when the user clicks them. Each click can cycle the setting on to its next value. This is primitive but workable; you can enhance this system in your own games to add more sophisticated user interaction if you need to.

The problem that presents itself with this approach is what to do with all the other game objects that are already active. If we have a spaceship and showers of aliens and missiles scattered all over the screen, how do we suddenly switch from this environment into an entirely different screen displaying game settings?

The answer is to allow multiple sets of game objects to be maintained at once. When we are ready to switch to the settings screen, we tell the game framework to suspend its list of game objects, allowing us to set up another list—one that displays the text for each of the settings. Once we have finished with the settings screen, we tell the game framework to restore the suspended game objects, reactivating them so that the game continues from exactly where we left it.

Note

Use this technique only when you actually need to return to the set of suspended game objects. If your game has finished and there is no need to return to it, simply clear the GameObjects collection instead. This saves .NET from having to keep all the obsolete objects alive. The exception is if you are going to recycle the suspended objects, in which case suspending them can save the need to create more objects later.

This is implemented in the game framework by allowing a stack of suspended game objects to be maintained. If we want to suspend the current set of objects (for example, the game itself) so that we can temporarily display another set (the objects that form the settings screen), we can push the main game's list of objects onto the stack. This clears the GameObjects collection so that it is empty, ready for objects for the settings screen to be added. Once we are done with the settings screen, we pop the stack, discarding the current game objects list (containing the objects for the settings screen) and restoring those objects that were placed on the stack earlier.

In this way, we can interrupt the active objects as many times as we want and always return back to them later. It is similar in effect to clicking a link in a web browser to reach a second page and then clicking the Back button to return to the previous page.

.NET provides a generic Stack class, and we will use this to implement the game object stack. As the game objects are implemented as a list of GameObjectBase classes, the stack will be a stack of lists of GameObjectBase. Each item on the stack holds a complete collection of game objects, rather than the individual objects themselves. The declaration for the stack, taken from the top of the GameHost class, is shown in Listing 9-4.

Example 9.4. The stack variable onto which collections of game objects will be pushed

// A stack of gameobjects lists so that we can work with multiple object sets
    private Stack<List<GameObjectBase>> _gameObjectsStack;

To provide access to the stack, we provide two related methods inside GameHost. The first of these, PushGameObjects, places the current GameObjects list on to the top of the stack and creates a new list for the game to work with. The second, PopGameObjects, discards the current GameObjects list and replaces it with the list on the top of the stack. These methods can be seen in Listing 9-5.

Example 9.5. Pushing and popping the game object list stack

public void PushGameObjects()
    {
        // Push the current GameObjects list on to the stack
        _gameObjectsStack.Push(GameObjects);
        // Create a new empty list for the game to work with
        GameObjects = new List<GameObjectBase>();
    }

    public void PopGameObjects()
    {
        // Pop the list from the top of the stack and set it as the GameObjects list
        GameObjects = _gameObjectsStack.Pop();
    }

The tools for switching between sets of objects are now available, so our games need to know how to use them. Switching can be implemented by keeping track of what the game is currently doing. Is it playing, is it showing the settings, or is it doing something else (perhaps showing a title screen or high score table)? We can track this by creating an enumeration with an item for each of these modes, and updating it as the player moves from one mode to another.

The code required to manage this can be seen in the Settings example project. The enumeration and mode variables are declared at the top of the SettingsGame class and can be seen in Listing 9-6. The mode defaults to GameModes.Playing because this is the state in which the game begins.

Example 9.6. Keeping track of the current game mode

// An enumeration containing the game modes that we can support
    private enum GameModes
    {
        Playing,
        Settings
    };
    // The active game mode
    private GameModes _gameMode = GameModes.Playing;

The game is initialized and set up exactly as normal, remembering to load any existing settings but ignoring the presence of the settings mode. Within the Update and Draw methods, however, we execute a switch statement against the current mode, and call off to separate functions for each game mode. This allows us to clearly separate the functionality that each mode requires from each of the other modes.

When the game is playing, its update function (Update_Playing) waits for the user to touch the screen. When this happens, it calls into a function named EnterSettingsMode so that the settings screen can be initialized. EnterSettingsMode does three things: it pushes the game objects list on to the stack, updates the _gameMode variable to indicate that it is in Settings mode, and then adds its own game objects required for the settings page.

As soon as this happens, the game switches to displaying the settings. All drawing and updating of the original game objects is suspended as they are no longer present in the GameObjects list.

On the new screen, the user can now amend the settings as required. But how do they leave the settings page? The model used by Windows Phone 7 is to press the hardware Back button to leave a page and go back to the previous page, and so this is the mechanism that we will use.

This is very easy to implement. The whole time we have been working with XNA, the small piece of code shown in Listing 9-7 has been present at the beginning of our Update function. This monitors the Back button, and when it detects that it has been pressed, it exits the game.

Example 9.7. Closing the game when the Back button is pressed

// Allows the game to exit
        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
        {
            this.Exit();
        }

It is essential that pressing the Back button from certain game screens does actually close the game, as without this the game will fail Microsoft's certification guidelines required for submission to the Windows Phone Marketplace (which we will discuss, along with more detail about Back button handling, in Chapter 15). If the user has navigated into a sub-screen such as the settings screen, however, we can use this button to allow backward navigation to the earlier screen—again, exactly like in a web browser.

We therefore check for this button within the Update_Settings function. When it is detected, instead of closing the game we call a function named LeaveSettingsMode. This function reads the user's new settings values back into the SettingsManager object, saves them to isolated storage, sets the game mode back to Playing, and pops the game objects from the stack. Everything is exactly back to how it was before the user entered the settings screen. This can be seen in Listing 9-8.

Example 9.8. Handling the Back button when in Settings mode

// Allows the user to return to the main game
        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
        {
            LeaveSettingsMode();
        }

Creating the Settings User Interface

XNA offers a huge amount of functionality, but one of the things that it lacks is a user interface. Fortunately for the settings page we can keep things fairly simple and implement our own primitive user interface using TextObject instance.

Each instance will be configured with a position, a value, and a set of available values. Each time the object is tapped on the screen, it will cycle to the next available value, looping past the final value back to the beginning.

Clearly, this is not at all sophisticated and could be enhanced to support all sorts of other features, but this is left as an exercise for the reader. These simple classes will suffice for this example and also for many games.

So that the game framework can identify settings objects (as we will see in a moment), they are implemented as a new class in the game framework. SettingsItemObject inherits from TextObject, as shown in Figure 9-1.

SettingsItemObject's position in the game framework object hierarchy

Figure 9.1. SettingsItemObject's position in the game framework object hierarchy

SettingsItemObject adds the following four public properties to those provided by the base TextObject:

  • Name is the name of the setting as it will be stored in SettingsManager.

  • Title is the display name for the setting.

  • Values is an array of strings that form the permitted list of values for the setting.

  • SelectedValue is the currently selected value.

All these properties are also required to be passed as parameters to the class constructor.

The class also exposes a new public method: SelectNextValue. Each time it is called, the object cycles to the next value within the Values array, looping back to the start once the final item is passed.

To use the settings screen, we need to add three new pieces of functionality to the game: one to initialize and display the settings screen, another to allow the user to tap the settings items to update them, and a final piece to save the settings and return to the game. Let's look at how they are implemented.

Opening the Settings Screen

For our example we will simply tap the screen to open the settings screen. This won't be good enough for a real game, but we could implement a menu system, as described shortly in "Planning a Game's Navigation Model" to provide a more realistic system of access.

When the game detects a tap on the screen (in the Update_Playing function), it calls into the EnterSettingsMode function, shown in Listing 9-9 (though the code shown here omits several of the items for brevity—the full list can be seen in the example project's SettingsGame class).

Example 9.9. Entering Settings mode

private void EnterSettingsMode()
  {
      // Push the game's objects onto the object stack
      PushGameObjects();

      // Add the title
      GameObjects.Add(new TextObject(this, Fonts["WascoSans"], new Vector2(10, 10),
                                                                      "Game Settings"));

      // Add some settings
      GameObjects.Add(new SettingsItemObject(this, new Vector2(30, 90), Fonts["WascoSans"],
                              0.9f, SettingsManager, "Speed", "Speed",
                              "1", new string[] { "1", "2", "3" }));
      GameObjects.Add(new SettingsItemObject(this, new Vector2(30, 140), Fonts["WascoSans"],
                              0.9f, SettingsManager, "Difficulty", "Difficulty",
                              "Medium", new string[] { "Easy", "Medium", "Hard" }));
      GameObjects.Add(new SettingsItemObject(this, new Vector2(30, 190), Fonts["WascoSans"],
                              0.9f, SettingsManager, "MusicVolume", "Music volume",
                              "Medium", new string[] { "Off", "Quiet", "Medium", "Loud" }));

      // Set the new game mode
      _gameMode = GameModes.Settings;
  }

As this code shows, there are several new SettingsItemObject instances created, and a number of parameters are passed into the class constructor each time. These parameters are as follows:

  • game is the game's GameHost instance.

  • position is the display position for the settings item.

  • font is the font to use to display the text.

  • scale is a scaling value to apply to the text.

  • settingsManager is the game's SettingsManager instance. The value of the setting will be automatically retrieved from this object before the item is displayed.

  • name is the name of the setting within the SettingsManager.

  • title is the name for the item to display onscreen (note the difference between this and the item's name for the music volume setting).

  • defaultValue is used if no existing setting value can be found in the SettingsManager object; this value will be used as an initial value for the setting.

  • values is an array of string values that will be cycled through when the user taps the settings item.

The result of this function is that the running game objects are all placed on to the stack, and a new set of objects is created to display the settings screen. All the actual settings items are created as SettingsItemObject instances. The game mode is changed from Playing to Settings so that the game knows that the settings screen is now active. The Update_Settings and Draw_Settings functions are now called in the game class each cycle, resulting in the settings page appearing as shown in Figure 9-2.

The Settings page

Figure 9.2. The Settings page

Updating the Settings Values

The user can change the value of any of the settings by simply tapping on its text onscreen. This is easily implemented by asking the GameHost to identify the selected object for each screen tap. If an object is found and is a SettingsItemObject, we can simply call its SelectNextValue method to move to the next available value.

The code to achieve this is performed in the Update_Settings function, shown in Listing 9-10.

Example 9.10. Part of the Update_Settings function, allowing object values to be updated

GameObjectBase obj;
        TouchCollection tc;

        // Has the user touched the screen?
        tc = TouchPanel.GetState();
        if (tc.Count == 1 && tc[0].State == TouchLocationState.Pressed)
        {
            // Find the object at the touch point
            obj = GetSpriteAtPoint(tc[0].Position);
            // Did we get a settings option object?
            if (obj is GameFramework.SettingsItemObject)
            {
                // Yes, so toggle it to the next value
                ((SettingsItemObject)obj).SelectNextValue();
            }
        }
    }

Nothing more is required to allow the objects to update themselves.

Leaving the Settings Screen

Once the settings screen is up and running, the player can change the settings as required and can then press the hardware Back button to leave the settings screen and return to the game. This is handled by calling the game's LeaveSettingsMode function, shown in Listing 9-11.

Example 9.11. Leaving the Settings screen and returning to the game

private void LeaveSettingsMode()
    {
        // Get and store the settings values
        SettingsManager.RetrieveValues();

        // Retrieve the previous game objects
        PopGameObjects();
        // Set the new game mode
        _gameMode = GameModes.Playing;

        // Update the speed of each ball
        foreach (GameObjectBase obj in GameObjects)
        {
            if (obj is BallObject)
            {
                ((BallObject)obj).Speed = SettingsManager.GetValue("Speed", 1);
            }
        }
    }

The first thing that the function needs to do is retrieve the updated values back from the settings screen. This is easily achieved by calling the SettingsManager.RetrieveValues function. Each settings item is identifiable because it is of type (or is derived from type) SettingsItemObject, so RetrieveValues loops through the GameObjects collection looking for objects of this type. Each time it finds one, it reads out its setting name and value (both of which we provided as parameters to the object's constructor) and uses this to write the setting into the SettingsManager class.

Once this is done, the main game objects are then restored by calling PopGameObjects, and the game mode set back to Playing. The remainder of the code applies the modified Speed setting, updating the speed of all the balls in the game.

Note

Because this is just an example, the Speed setting is the only one that is actually observed by the project. In a real game, all the values would be considered and put into action in the LeaveSettingsMode function.

If you give the example project a try, you will see that the balls all observe whatever speed is selected in the settings screen. The settings are saved to storage when the game closes and automatically restored when it starts again. When you leave the settings screen, all the balls are present in exactly the positions they were in before the settings were displayed.

Planning a Game's Navigation Model

As has already been noted, tapping the main game screen is unlikely to be a satisfactory mechanism for entering the settings screen in a real game. Exactly how you structure navigation between different parts of the game is, of course, up to you and the needs of your game, but here is an approach that may work.

Open the game with a title page, displaying a welcome message and a game logo. Also on the page is a menu allowing the player to start the game, edit the settings, exit the game, and perhaps perform other tasks, too (such as viewing the high scores or the game's credits).

The player can select any of these items, and the game will react as requested. Wherever users end up, they can press the hardware Back button to return to the title page. They can then access settings and the other pages once again.

When returning to the title page from within an active game, the game should be suspended by pushing its objects on to the stack. Instead of a "start game" option, the menu should now offer "resume game" and "new game." This way, players can amend their settings and then return to the game, or can abandon the current game and start a new one.

Pressing the Back button while the title page is open will close the game and return the player to the phone's operating system pages.

This navigation model provides easy access to all areas of the game in a consistent and predictable fashion.

Adding a High Score Table

Another common requirement across many games is the ability to keep track of the highest scores that players have achieved. .NET provides some useful features to implement high scores (such as sortable collections into which we can place all our scores), but actually implementing the feature into a game requires a little effort, most of which is fairly unexciting—not what we want to be spending time addressing when we could be being creative instead.

To reduce the amount of work required, we can build support for high scores into the game framework. We won't make things too complicated, but our implementation will have the facility for keeping track of multiple tables at once if the game requires such a feature (for different difficulty levels, for example) and a very simple API so that the game doesn't require very much code to interact with the high scores.

The final feature that we will add is the ability to save the score tables to storage each time they are updated and load them back in each time the game is launched. Scores can therefore be stored indefinitely, providing a constant challenge to the player to better his or her earlier efforts.

All the code for maintaining and manipulating the high score table can be found in the GameFramework project for this chapter. Alongside this project is an example project named HighScores that demonstrates some user interaction with the high score table. A screenshot of a populated score table is shown in Figure 9-3.

An example high score table display

Figure 9.3. An example high score table display

Implementing the High Score Table

The high scores are implemented using three new classes in the game framework: HighScores, HighScoreTable and HighScoreEntry.

The first of these classes, HighScores, provides the top-level API for working with high scores. Its functions include loading and saving scores to and from storage, creating and obtaining individual high score tables, and a simple mechanism for displaying the scores from a table on the screen.

The HighScoreTable class represents a single table of scores (of which we may maintain several, as already mentioned). Its responsibilities are around maintaining the individual tables, allowing scores to be added and interrogated.

The final class, HighScoreEntry, represents a single score item within a high score table. It keeps track of the score and the player's name, as well as the date the score was achieved (the reason for which we will discuss shortly).

All three of these classes are declared with internal scope on their class constructors, preventing code outside the game framework from instantiating them. Instead, a single instance of HighScores can be accessed from the GameHost.HighScores property, and functions within this class and the HighScoreTable class allow instances of the other two objects to be created and manipulated. The relationships of the classes within the rest of the game framework can be seen in Figure 9-4.

The high score classes within the game framework

Figure 9.4. The high score classes within the game framework

Defining Tables

Before the high score classes can be used for storing game scores, the tables within the high score environment must first be defined. This must be done before any scores are added, and also before scores are loaded from storage.

The tables are added by calling the HighScores.InitializeTable function. There are two overloads for this function: the first overload expects a table name to be supplied along with the number of score entries that it is to hold; the second overload also expects a description of the table.

If you decide to implement multiple tables for your game, each one must be given a different name. You may want to base these on your game's difficulty levels (for example, Easy, Normal, and Difficult) or on some other factor that is significant to the game. These values will be used as a key into a dictionary of HighScoreTable objects, so will need to be provided again later when you want to update or read one of the tables.

If you provide a description, it will be stored alongside the table for later retrieval.

Each table can store an arbitrary number of score entries, depending on the needs of your game. Many games will only store ten entries per table, but there is nothing stopping you from storing hundreds if you want.

References to the individual tables can be obtained by calling the HighScores.GetTable function, passing in the name of the table to be returned. This returns a HighScoreTable object, with which the scores inside the named table can be manipulated.

Working with High Score Tables

Having created a table and retrieved a reference to its object, the next set of functions is within the HighScoreTable class.

New scores can be added by calling the AddEntry function, passing in the name and the score. Providing the score is high enough to make it into the table, a new HighScoreEntry object will be added into the class's private _scoreEntries list, and the entry object returned back to the calling procedure. If the score is not good enough to qualify, the entry list is left alone, and the function returns null. The table will always be sorted by score, so there is no need for external code to sort the entries.

All high scores are date stamped, so if there is a collision between the new score and an existing score, the existing score will be given precedence and will appear higher in the list than the new entry.

Accompanying this function is another useful function, ScoreQualifies, which returns a boolean value that indicates whether the supplied score is good enough to make it on to the table. The return value from this function can be used to determine whether users should be prompted to enter their name or not.

A read-only collection of all the scores can be retrieved by querying the Entries property. The description of the table (if one was set when it was initialized) can be retrieved from the Description property.

High Score Entries

Each entry into the high score table is represented as an instance of the HighScoreEntry class. This class stores the name, score, and date for each entry, allowing the contents of the table to be read and displayed.

In addition to storing these properties, the class also implements the .NET IComparer interface. This interface is used to provide a simple mechanism for the HighScoreTable class to sort the entries in its table. The Compare method first sorts the items by score and then by date, ensuring that higher scores appear first, and for matching scores, older entries appear first.

Clearing Existing High Scores

If you decide that you want to clear the high scores (preferably at the user's request), you can call the Clear method of either the HighScores object (which will clear all entries from all tables) or an individual HighScoreTable object (which will clear that table alone).

Loading and Saving Scores

The final part of the HighScores data API is the ability to load and save the scores to and from the device so that they persist from one game session to the next. These functions are accessed using the LoadScores and SaveScores methods of the HighScores class.

Prior to calling either of these functions, however, the game can choose to set the file name within which the scores will be loaded and saved via the FileName property. If no file name is specified, the class defaults to using a name of Scores.dat. Being able to store high scores in different files allows for different sets of scores to be written should this be of benefit.

The high score data is written as an XML document, the structure of which is shown in Listing 9-12. A new <table> element is created for each defined table, and inside is an <entries> element containing an <entry> for each score within the table.

Example 9.12. The content of a stored high scores file

<?xml version="1.0" encoding="utf-16"?>
<highscores>
    <table>
        <name>Normal</name>
        <entries>
            <entry>
                <score>14302</score>
                <name>Helen</name>
                <date>2010-09-21T21:27:46</date>
            </entry>
            <entry>
                <score>330</score>
                <name>Olly</name>
                <date>2010-09-21T21:27:46</date>
            </entry>
            <entry>
                <score>60</score>
                <name>Martin TSM</name>
                <date>2010-09-21T21:27:46</date>
            </entry>
        </entries>
    </table>
    <table>
        <name>Difficult</name>
        <entries />
    </table>
    </highscores>

The code within SaveScores required to generate this XML is fairly straightforward, looping through the tables and the score entries, and adding the details of each into the XML output. The output is constructed with an XmlWriter object by the code shown in Listing 9-13.

Example 9.13. Building the XML for the high score file

StringBuilder sb = new StringBuilder();
        XmlWriter xmlWriter = XmlWriter.Create(sb);
        HighScoreTable table;

        // Begin the document
        xmlWriter.WriteStartDocument();
        // Write the HighScores root element
        xmlWriter.WriteStartElement("highscores");

        // Loop for each table
        foreach (string tableName in _highscoreTables.Keys)
        {
            // Retrieve the table object for this table name
            table = _highscoreTables[tableName];

            // Write the Table element
            xmlWriter.WriteStartElement("table");
            // Write the table Name element
            xmlWriter.WriteStartElement("name");
xmlWriter.WriteString(tableName);
            xmlWriter.WriteEndElement();    // name

            // Create the Entries element
            xmlWriter.WriteStartElement("entries");

            // Loop for each entry
            foreach (HighScoreEntry entry in table.Entries)
            {
                // Make sure the entry is not blank
                if (entry.Date != DateTime.MinValue)
                {
                    // Write the Entry element
                    xmlWriter.WriteStartElement("entry");
                    // Write the score, name and date
                    xmlWriter.WriteStartElement("score");
                    xmlWriter.WriteString(entry.Score.ToString());
                    xmlWriter.WriteEndElement();    // score
                    xmlWriter.WriteStartElement("name");
                    xmlWriter.WriteString(entry.Name);
                    xmlWriter.WriteEndElement();    // name
                    xmlWriter.WriteStartElement("date");
                    xmlWriter.WriteString(entry.Date.ToString("yyyy-MM-ddTHH:mm:ss"));
                    xmlWriter.WriteEndElement();    // date
                    // End the Entry element
                    xmlWriter.WriteEndElement();    // entry
                }
            }

            // End the Entries element
            xmlWriter.WriteEndElement();    // entries

            // End the Table element
            xmlWriter.WriteEndElement();    // table
        }

        // End the root element
        xmlWriter.WriteEndElement();    // highscores
        xmlWriter.WriteEndDocument();

        // Close the xml writer, which will put the finished document into the stringbuilder
        xmlWriter.Close();

Tip

The comments present on each call to WriteEndElement help to clarify exactly which element is ending. Reading the code later on becomes much easier.

We will look at where the data is actually written to in a moment, but first let's take a quick look at the code that processes the XML after reading it back in. It uses the LINQ to XML functions to quickly and easily parse the content. LINQ to XML (LINQ being short for Language INtegrated Query and pronounced as link) allows the XML elements to be interrogated using a syntax similar in many ways to a database SQL statement, and it provides a simple mechanism for us to loop through each setting element and read out the name and value contained within.

The LINQ to XML code for loading the scores is shown in Listing 9-14. This code loops through all the entry elements so that we obtain an item in the result collection for each entry in the file. Besides reading out the details of the entry itself (from the name, score, and date elements), it also reads the entry's table name. This is achieved by looking at the parent node of the entry (the entries node), looking at its parent (the table node), and then retrieving the value of its name element. Once these four values have been identified, the foreach loop that follows can easily add each score into the appropriate high score table.

Example 9.14. Loading saved high score data from the stored XML document

// Parse the content XML that was loaded
            XDocument xDoc = XDocument.Parse(fileContent);
            // Create a query to read the score details from the xml
            var result = from c in xDoc.Root.Descendants("entry")
                            select new
                            {
                                TableName = c.Parent.Parent.Element("name").Value,
                                Name = c.Element("name").Value,
                                Score = c.Element("score").Value,
                                Date = c.Element("date").Value
                            };
            // Loop through the resulting elements
            foreach (var el in result)
            {
                // Add the entry to the table.
                table = GetTable(el.TableName);
                if (table != null)
                {
                    table.AddEntry(el.Name, int.Parse(el.Score), DateTime.Parse(el.Date));
                }
            }

Reading and Writing Files in Isolated Storage

Everything is now present to create the high score file, parse the file, and retrieve its contents, but there is still the issue of actually reading from and writing to files on the device. Let's address that now.

Windows Phone 7, unlike Windows Mobile before it, has a closed storage system. Applications have access to multiple files and directories just like on a desktop PC, but they can access only files that they have created. Files from other applications or from the operating system are inaccessible.

This system is called isolated storage, and its purpose is to prevent applications from causing problems for one another or performing actions within the device that they are not permitted to perform. It is the same system that Silverlight uses when running on desktop PCs; isolated storage allows applications to save information that they need to disk, but prevents them from accessing the user's private files or any other information that may be stored on the computer.

In most cases, this storage mechanism won't present any problems for Windows Phone 7 games, but one significant implication is that the System.IO.File namespace that you might be familiar with using from programming .NET on the desktop cannot be used from the phone.

Instead, file access is performed using another class within the System.IO.IsolatedStorage namespace. The class is called IsolatedStorageFile, instances of which are obtained by calling the class's static GetUserStoreForApplication method.

Once an instance has been obtained, it can be used to perform the file-based operations that you would expect to be able to use: creating files, reading and writing files, checking whether files exist, creating directories, reading lists of file and directory names, and so on. The MSDN documentation contains plenty of information on this class if you want to learn more about it.

We will thus call on this class to read and write our high score XML data. Listing 9-15 shows the remainder of the SaveScores function, writing the constructed XML (in the StringBuilder object named sb) to the device's isolated storage.

Example 9.15. Writing the settings XML document to isolated storage

// Get access to the isolated storage
        using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication())
        {
            // Create a file and attach a streamwriter
            using (StreamWriter sw = new StreamWriter(store.CreateFile(FileName)))
            {
                // Write the XML string to the streamwriter
                sw.Write(sb.ToString());
            }
        }
    }

The code required to load the high score file from isolated storage is shown in Listing 9-16. This listing includes a simple check to see whether the file actually exists prior to loading it.

Example 9.16. Checking for and loading the XML document from isolated storage

string settingsContent;

        // Get access to the isolated storage
        using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication())
        {
            if (!store.FileExists(FileName))
            {
                // The score file doesn't exist
                return;
            }
            // Read the contents of the file
            using (StreamReader sr = new StreamReader(
                                                store.OpenFile(FileName, FileMode.Open)))
            {
                fileContent = sr.ReadToEnd();
            }
        }

We can now update, load, and save the high scores for our game. All we need to do now is actually use them, and this is what we will look into next.

Using the HighScore Classes in a Game

All the data structures are present and correct, but they still need to be actually wired up into a game. The HighScores example project shows how you can do this.

It begins by initializing the high score tables and loading any existing scores in its InitializeMethod, as shown in Listing 9-17.

Example 9.17. Initializing the high score table

// Initialize and load the high scores
        HighScores.InitializeTable("Normal", 20);
        HighScores.InitializeTable("Difficult", 20);
        HighScores.LoadScores();

Next, the game begins. The current game mode is managed using a class-level variable named _gameMode, exactly as we discussed with regard to the game settings screen earlier in this chapter. In our example, the game is fairly short-lived, however, and it immediately ends. A random score is assigned to the player to help you see how the high score table works.

Inside ResetGame, after creating the random score, the code adds some TextObject instances to the game to tell the player that the game is over and provide some information about the score. It calls into the HighScoreTable.GetScore function to determine whether the player's score is good enough, and displays an appropriate message onscreen.

The update loop then waits for the user to tap the screen before continuing. When Update_Playing detects this tap, it calls the EnterHighScoresMode function to prepare the game for displaying and updating the high score table.

EnterHighScoresMode, shown in Listing 9-18, first updates the game mode to indicate that we are now working with the high score table and then checks again to see whether the score qualifies for a new high score entry. If it does, it opens the text entry dialog, as described in the "Prompting the User to Enter Text" section in Chapter 4. The dialog is given some suitable captions and also reads the default text value from the game settings. We'll look at this again in a moment.

Alternatively, if the score isn't good enough, it calls into the ResetHighscoreTableDisplay function, which initializes the scores for display onscreen. You will see the code for this function shortly.

Example 9.18. Preparing the game to display or update the high score table

private void EnterHighScoresMode()
    {
        // Set the new game mode
        _gameMode = GameModes.HighScores;

        // Did the player's score qualify?
        if (HighScores.GetTable("Normal").ScoreQualifies(_score))
        {
            // Yes, so display the input dialog
            // Make sure the input dialog is not already visible
            if (!(Guide.IsVisible))
            {
                // Show the input dialog to get text from the user
                Guide.BeginShowKeyboardInput(PlayerIndex.One, "High score achieved",
                                             "Please enter your name",
                                             SettingsManager.GetValue("PlayerName", ""),
                                             InputCallback, null);
            }
        }
else
        {
            // Show the highscores now. No score added so nothing to highlight
            ResetHighscoreTableDisplay(null);
        }
    }

If the text entry dialog was displayed, it calls into the InputCallback function after text entry is complete, as shown in Listing 9-19. Assuming that a name was entered, the callback function adds the name to the high score table and retains the HighScoreEntry object that is returned before saving the updated scores. Once this is done, the ResetHighscoreTableDisplay function is called to show the high scores, passing the newly added entry as its parameter.

Example 9.19. Responding to the completion of the text entry dialog

void InputCallback(IAsyncResult result)
    {
        string sipContent = Guide.EndShowKeyboardInput(result);
        HighScoreEntry newEntry = null;

        // Did we get some input from the user?
        if (sipContent != null)
        {
            // Add the name to the highscore
            newEntry = HighScores.GetTable("Normal").AddEntry(sipContent, _score);
            // Save the scores
            HighScores.SaveScores();
            // Store the name for later use
            SettingsManager.SetValue("PlayerName", sipContent);        }
        // Show the highscores now and highlight the new entry if we have one
        ResetHighscoreTableDisplay(newEntry);
    }

Notice that the name that was entered is stored into SettingsManager for later use. This is a nice little feature that allows players' names to be remembered between gaming sessions, saving them having to type it in again the next time they play. This is also an example of using SettingsManager for system settings that aren't directly altered by the player via a settings screen.

Finally, there is the ResetHighscoreTableDisplay function that was called twice in the previous listings. It sets up the GameObjects collection to show all the scores for the high score table. It uses a function named CreateTextObjectsForTable inside the HighScores class to assist with this. This function simply adds a series of text objects containing the score details, but it accepts among its parameters a start and end color (used for the first and last score items, with the items in between fading in color between the two); a highlight entry (when the player has just added a new score, pass its HighScoreEntry object here to highlight the entry within the table); and a highlight color. There is no requirement to use this function, and the high score classes can be used purely as a data manipulation structure if desired, but it can be a time saver if its functionality is sufficient for your game.

Example 9.20. Responding to the completion of the text entry dialog

private void ResetHighscoreTableDisplay(HighScoreEntry highlightEntry)
    {
        // Clear any existing game objects
        GameObjects.Clear();

        // Add the title
GameObjects.Add(new TextObject(this, Fonts["WascoSans"],
                                                      new Vector2(10, 10), "High Scores"));

        // Add the score objects
        HighScores.CreateTextObjectsForTable("Normal", Fonts["WascoSans"], 0.8f, 80, 30,
                                    Color.White, Color.Blue, highlightEntry, Color.Yellow);
    }

Reusing Game Components

It is very easy to find yourself rewriting sections of code over and over again as you develop games, and each time you write things slightly differently or change how things work. Creating reusable components such as those in this chapter can save you the effort of reinventing these same concepts in each new project that you create.

Even though writing reusable components such as these may require a little extra work, you'll be pleased that you made the effort when you plug them into your future projects because the overall amount of development time will be decreased (plus you'll have consistency in the appearance and functionality of your games).

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

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