Reading and Writing Files Using Storage, Windows Phone Isolated Storage

0
232

Using Windows Phone Isolated Storage

XNA potentially supports many storage devices across all the game systems it targets, but on the WP7, there is only one type: isolated storage. Any type of game asset can be imported into the Content Manager through an existing or user-created content importer, and this is the preferred way to read game data that does not change. For data that might change, such as generated data files or user-created game levels, or saved games, we can access the file system to stream data to our game from isolated storage. Any type of data file can be opened and written to or read from using Storage.IO classes, which we will learn about here. Since XML is recognized as a resource file by Content Manager, we will use XML for our example in this hour. XML has the benefit of being versatile and human readable at the same time. But binary and text files can be used as well.

Saving a Data File

We will first learn to create a new file in isolated storage and then read the data back out of the file afterward. The first thing that must be done is to add the library System.Xml.Serialization to the project via the references. The Serialization library makes it very easy to convert a class or struct into a file and read it back again without our having to decode the file manually (by setting individual properties one at a time). Let’s add it to the project.

Adding XML Support to the Project

  1. Right-click References in the Content project and choose Add Reference from the pop-up context menu.
  2. Locate the library called System.Xml.Serialization in the list, as shown in Figure 19.1.

    The System.Xml. Serialization library.
    FIGURE 19.1 The System.Xml. Serialization library.

Now that the reference is set, we can use XML files in the project more easily.

Isolated Storage

To access a file in isolated storage, we have to create a file object using the IsolatedStorageFile class:

[code]
IsolatedStorageFile storage =
IsolatedStorageFile.GetUserStoreForApplication();
[/code]

IsolatedStorageFile.GetUserStoreForApplication() is a rather verbose method that creates the new storage object with linkage to the application’s (or game’s) private storage area, making it available for accessing files, directories, and so on. If the object doesn’t need to be made global to the project, a shorthand declaration can be used:

[code]
var storage = IsolatedStorageFile.GetUserStoreForApplication();
[/code]

Creating a New Directory

Next, a required step must be taken: A directory must be created for the application to store files in. The private or isolated storage area has room for dictionary-style key/value data as well as SQL database tables, so we can’t just toss files in there like one large file system—we have to create a directory. If you don’t create a directory first, an exception error will occur when you try to create a new file. We will use the storage object to create a directory. The IsolatedStorageFile class has a method called DirectoryExists() that returns true if a passed directory exists. CreateDirectory() is used to create a new directory. So, if the directory doesn’t already exist, we want to create it:

[code]
const string directory = “StorageDemo”;
if (!storage.DirectoryExists(directory))
storage.CreateDirectory(directory);
[/code]

Creating a New File

Now, we can create a file inside the directory. First, we have to check to see whether the file exists. WP7 does not support the FileMode.CreateNew option, which is supposed to overwrite a file if it already exists. Trying to do this generates an exception error, even though it works on Windows and Xbox 360. So, we have to delete the file first before creating it again. Usually this is not a problem because savegame data tends to be rather simple for most games. If you are working on a large, complex game, like an RPG, and there’s a lot of data, of course the game might support multiple savegame files, and you’ll have a mini file manager built into the game. But we’re just learning the ropes here, so we’ll do it the simple way to get it working. We use the FileExists() and DeleteFile() methods to get rid of the old save file:

[code]
const string filename = directory + “\savegame.dat”;
if (storage.FileExists(filename))
storage.DeleteFile(filename);
[/code]

Now we’re ready to create a new savegame file and write data to it. This is done with the IsolatedStorageFileStream() class:

[code]
var fstream = new IsolatedStorageFileStream(
filename, FileMode.CreateNew, storage);
[/code]

The FileMode enumeration has these values:

  • CreateNew = 1
  • Create = 2
  • Open = 3
  • OpenOrCreate = 4
  • Truncate = 5
  • Append = 6

Writing Data to the File with Serialization

Although any type of data file can be created, XML is quite easy to use, and an entire class or struct variable (full of game data) can be written to the file with only a couple lines of code. If you want to just write binary or text data to the file, that will work also at this point, but it’s so much easier to use serialization! Here is a simple struct we can use for this example:

[code]
public struct SaveGameData
{
public string Name;
public int Score;
}
[/code]

A new SaveGameData variable is created and the two properties are filled with data. This is where you would store actual game data in the properties in order to restore the game to this gameplay state later when the savegame file is loaded:

[code]
savedata = new SaveGameData();
savedata.Name = “John Doe”;
savedata.Score = rand.Next(500, 5000);
[/code]

Now, to write the data to the file, we have to create an XmlSerializer object, and then write the serialized object out to the file:

[code]
XmlSerializer serializer = new XmlSerializer(typeof(SaveGameData));
serializer.Serialize(fstream, savedata);
[/code]

At this point, the file has been created and data has been written to it that was contained in the savedata struct variable.

Loading a Data File

Loading a serialized XML file is very similar to the writing process. Of course, you may read a simple text or binary file and parse the data if that is more suitable for your needs, but I’m using serialization and XML because it’s so easy and likely to be the approach most game developers take with WP7 savegame data. The same storage object is created, but we don’t need any of the code to create a directory or delete the existing file (obviously), so the code to load the savegame file is much simpler:

[code]
var storage = IsolatedStorageFile.GetUserStoreForApplication();
[/code]

Likewise, the IsolatedStorageFileStream object is created in the same way:

[code]
var fstream = new IsolatedStorageFileStream(
filename, FileMode.CreateNew, storage);
[/code]

There is a second way to create the fstream file object variable: by creating the object in a using statement and then adding code that uses the object in the bracketed code block:
[code]
using (var fstream = new IsolatedStorageFileStream(
filename, FileMode.Open, storage)) { }
[/code]

The XmlSerializer object is created in a similar manner:

[code]
XmlSerializer serializer = new XmlSerializer(typeof(SaveGameData));
[/code]

The only difference really is a call to Deserialize() instead of Serialize(), and this method returns our savegame data as an object:

[code]
data = (SaveGameData)serializer.Deserialize(fstream);
[/code]

Just for curiosity’s sake, here is what the XML file looks like that is created by our code. If you were to serialize a more complex data type, like a Vector4, then the parameters within that class or struct would become sub-items in the XML structure.

[code]
<?xml version=”1.0”?>
<SaveGameData xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”
xmlns:xsd=”http://www.w3.org/2001/XMLSchema”>
<Name>John Doe</Name>
<Score>1245</Score>
</SaveGameData>
[/code]

Creating the Storage Demo Example

We will go over a complete program that demonstrates how to save data to a save game file and then load it again, based on some rudimentary user input. Two buttons are created and displayed using our familiar Button class (which inherits from Sprite). This class requires a bitmap file called button.png, so be sure that it exists in the content project.

To verify that the example is working, we will want to run the program, save the data, close the program, and then rerun it and choose the load option to see that the data is still there. So, the example should read and write the data only when the user chooses to, not automatically. When the emulator is being used, exiting the program still preserves it in memory, but closing the emulator will erase all traces of the program and data files.

Closing the WP7 emulator will wipe the storage memory, including data files created by our example here, and any programs previously loaded from Visual Studio. But closing the program and rerunning it will reveal an intact file system. This happens because the emulator creates a new emulation state system when it is run, and that is not saved when it closes.

Figure 19.2 shows the output of the Storage Demo program.

The Storage Demo example shows how to read and write data.
FIGURE 19.2 The Storage Demo example shows how to read and write data.

Button Class

Just for the sake of clarity, Listing 19.1 shows the source code for the Button class. We have seen the code before, but it is required by the Storage Demo and is included again for clarity.

LISTING 19.1 Source Code for the Button Class

[code]
public class Button : Sprite
{
public string text;
private SpriteBatch p_spriteBatch;
private SpriteFont p_font;
public Button(ContentManager content, SpriteBatch spriteBatch,
SpriteFont font)
: base(content, spriteBatch)
{
p_spriteBatch = spriteBatch;
p_font = font;
Load(“button”);
text = ““;
color = Color.LightGreen;
}
public void Draw()
{
base.Draw();
Vector2 size = p_font.MeasureString(text);
Vector2 pos = position;
pos.X -= size.X / 2;
pos.Y -= size.Y / 2;
p_spriteBatch.DrawString(p_font, text, pos, color);
}
public bool Tapped(Vector2 pos)
{
Rectangle rect = new Rectangle((int)pos.X, (int)pos.Y, 1, 1);
return Boundary().Intersects(rect);
}
}
[/code]

Storage Demo Source

Here in Listing 19.2, we have the source code for the Storage Demo program, with the definition of the SaveGameData class as well.

LISTING 19.2 Source Code for the Storage Demo Program

[code]
public struct SaveGameData
{
public string Name;
public int Score;
}
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont font;
Random rand;
TouchLocation oldTouch;
Button[] buttons;
int current = -1;
bool loaded = false;
SaveGameData savedata;
const string directory = “StorageDemo”;
const string filename = directory + “\savegame.dat”;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
oldTouch = new TouchLocation();
rand = new Random();
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>(“WascoSans”);
//create save button
buttons = new Button[2];
buttons[0] = new Button(Content, spriteBatch, font);
buttons[0].text = “Save”;
buttons[0].position = new Vector2(100, 100);
//create load button
buttons[1] = new Button(Content, spriteBatch, font);
buttons[1].text = “Load”;
buttons[1].position = new Vector2(300, 100);
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
//get state of touch input
TouchCollection touchInput = TouchPanel.GetState();
if (touchInput.Count > 0)
{
TouchLocation touch = touchInput[0];
if (touch.State == TouchLocationState.Pressed)
{
current = -1;
int n = 0;
foreach (Button b in buttons)
{
int x = (int)touch.Position.X;
int y = (int)touch.Position.Y;
if (b.Boundary().Contains(x, y))
{
current = n;
break;
}
n++;
}
}
oldTouch = touch;
}
if (current == 0)
{
savedata = new SaveGameData();
savedata.Name = “John Doe”;
savedata.Score = rand.Next(500, 5000);
SaveData(savedata);
loaded = false;
current = -1;
}
else if (current == 1)
{
savedata = LoadData();
loaded = true;
current = -1;
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin(SpriteSortMode.FrontToBack,
BlendState.AlphaBlend);
print(0, 0, “Storage Demo”, Color.White);
foreach (Button b in buttons)
b.Draw();
if (loaded)
{
print(100, 200, “Loaded data:nn” +
“Name: “ + savedata.Name + “n” +
“Score: “ + savedata.Score.ToString() + “n”,
Color.White);
}
spriteBatch.End();
base.Draw(gameTime);
}
void print(int x, int y, string text, Color color)
{
var pos = new Vector2((float)x, (float)y);
spriteBatch.DrawString(font, text, pos, color);
}
private void SaveData(SaveGameData data)
{
var storage = IsolatedStorageFile.GetUserStoreForApplication();
//create directory for data
if (!storage.DirectoryExists(directory))
storage.CreateDirectory(directory);
//delete any existing file
if (storage.FileExists(filename))
storage.DeleteFile(filename);
//create new savegame file
using (var fstream = new IsolatedStorageFileStream(filename,
FileMode.CreateNew, storage))
{
XmlSerializer serializer = new XmlSerializer(
typeof(SaveGameData));
serializer.Serialize(fstream, data);
}
}
private SaveGameData LoadData()
{
SaveGameData data;
var storage = IsolatedStorageFile.GetUserStoreForApplication();
using (var fstream = new IsolatedStorageFileStream(filename,
FileMode.Open, storage))
{
XmlSerializer serializer = new XmlSerializer(
typeof(SaveGameData));
data = (SaveGameData)serializer.Deserialize(fstream);
}
return data;
}
}
[/code]

We now have the ability to create a savegame file and load it again! This greatly enhances the replay value of a game that would otherwise appear to have been freshly installed every time it is run. Use this feature to store game settings, player names, and high score lists, as well as generated game levels and anything else that needs to be remembered by the game for the next time.