1.9 将数据保存到文件,从文件读取数据

问题

你需要在游戏中实现保存功能。解决方案虽然保存功能通常是创建游戏的最后一步,但你肯定需要在游戏中实现这个功能。首先,XNA使用默认的.NET文件I/O 功能,这意味着创建/打开/删除文件是很容易的。接着,使用XmlSerializer类能非常容易地将数据保存到文件并在以后加载数据。

唯一的问题是找到在PC平台和Xbox360平台都能保存的位置,要解决这个问题,你可以使用StorageDevice,它必须首先被建立。

注意创建一个StorageDevice的原理可能很复杂,但别被吓倒,因为本教程的其余部分(从“将数据保存到磁盘”) 非常简单和有效,而且这个方法并不局限与在XNA上使用,因为它使用的是默认的.NET功能。

工作原理

在将数据保存到磁盘之前,你需要一个可用的位置进行写入。这可以通过创建一个StorageDevice实现,StorageDevice会询问Xbox360平台上的用户将数据保存到哪儿。

异步创建一个StorageDevice

在这个过程中会打开Xbox Guide并终止程序直到用户关闭Guide。要解决这个问题,可以让这个过程异步处理,原理在教程8-5中解释。

Guide需要将GamerServicesComponent添加到游戏中 (见教程8-1),所以要将下列代码添加到Game1构造函数中:

public Game1()
{
    graphics = new GraphicsDeviceManager(this); 
    Content.RootDirectory = "Content"; 
    Components.Add(new GamerServicesComponent(this)); 
} 

下一步,当调用Update方法时,你会检查用户是否想保存数据,如果是,则调用Guide. BeginShowStorageDeviceSelector,它会打开一个对话框让用户选择在哪里保存数据。程序并不会在此终止,但会暂停程序直到用户关闭对话框。Guide. BeginShowStorageDeviceSelector方法要求你指定另一个FindStorageDevice方法作为第一个参数,一旦用户关闭对话框这个方法就会被调用:

KeyboardState keyState = Keyboard.GetState(); 
if (!Guide.IsVisible)
    if (keyState.IsKeyDown(Keys.S)) 
        Guide.BeginShowStorageDeviceSelector(FindStorageDevice, null); 

如果用户按下S键就会打开对话框,但代码并不会终止直到用户关闭对话框。

当对话框显示在屏幕上时你的程序仍在运行,一旦用户做出选择并关闭对话框,作为第一个参数的FindStorageDevice方法就会被调用,这意味着你应事先定义这个方法,否则编译器会报错:

private void FindStorageDevice(IAsyncResult result)
{
    StorageDevice storageDevice = Guide.EndShowStorageDeviceSelector(result); 
        if (storageDevice != null) 
            SaveGame(storageDevice); 
} 

BeginShowStorageDeviceSelector的输出结果作为这个方法的参数,如果你将结果传递到Guide. EndShowStorageDeviceSelector方法,就可以获取data storage。但是如果用户取消操作,结果将会是null,所以你应该事先检查。如果StorageDevice可用,你将它传递到SaveGame方法,这个方法一会儿后就会定义。

但如果当用户指定数据位置时允许用户进行第二种操作,例如加载数据,那么就需要定义第二个方法,比方说FindStorageDeviceForLoading。但清晰的方法是指定一个identifier,在FindStorageDevice方法中可以检查这个identifier。你的Update方法应该包含下列代码块:

KeyboardState keyState = Keyboard.GetState(); 
if (!Guide.IsVisible)
{
    if (keyState.IsKeyDown(Keys.S)) 
        Guide.BeginShowStorageDeviceSelector(FindStorageDevice, "saveRequest"); 
    if (keyState.IsKeyDown(Keys.L)) 
        Guide.BeginShowStorageDeviceSelector(FindStorageDevice, "loadRequest"); 
} 

如你所见,在这两种情况下对话框都会显示,当对话框关闭后就会调用FindStorageDevice方法。但这次的区别是你指定了一个identifier并在FindStorageDevice方法中检查这个identifier:

private void FindStorageDevice(IAsyncResult result)
{
    StorageDevice storageDevice = Guide.EndShowStorageDeviceSelector(result); 
    if (storageDevice != null)
    {
        if (result.AsyncState == "saveRequest") 
            SaveGame(storageDevice); 
        else if (result.AsyncState == "loadRequest") 
            LoadGame(storageDevice); 
    }
} 

根据指定的identity,你将调用SaveGame或LoadGame方法。

将数据保存到磁盘

一旦有了一个正确的StorageDevice,你就可以很容易地为写入数据的文件指定一个名称:

private void SaveGame(StorageDevice storageDevice)
{
    StorageContainer container = storageDevice.OpenContainer("BookCodeWin"); 
    string fileName = Path.Combine(container.Path, "save0001.sav"); 
    FileStream saveFile = File.Open(fileName, FileMode.Create); 
} 

这将创建一个叫做save0001. Sav的文件,如果这个文件已经存在将会被覆盖。

注意在PC上这个文件将会创建在My Documents\SavedGames文件夹中。

一旦你有了正确的文件名并已经打开了这个文件,你就可以使用默认的.NET 方法保存文件,假设你将存储的数据结构如下:

public struct GameData
{
    public int ActivePlayers; 
    public float Time; 
} 

你需要创建一个XmlSerializer,它能够将你的数据转换为XML并保存到磁盘:

XmlSerializer xmlSerializer = new XmlSerializer(typeof(GameData));
xmlSerializer.Serialize(saveFile, gameData); 
saveFile.Close(); 

XmlSerializer能够串行化GameData对象,之后使用一个命令就可以将GameData对象的数据流写到文件中。别忘了关闭文件流,否则程序会锁定文件流。

你需要添加System. IO和System.Xml. Serialization命名空间,只需在代码顶部添加下面的代码:

using System.IO; 
using System.Xml.Serialization; 

最后一行代码需要你添加对System. Xml的引用,要添加这个引用,可以打开Project菜单并选择Add Reference,选择System.Xml,如图1-5所示,然后点击OK。

2

图1-5 添加对System. Xml的引用

从磁盘加载数据

加载数据的方法与保存数据的方法相同,顺序正好相反。你检查文件是否存在,如果存在就打开它。你仍要创建一个XmlSerializer,但这次你使用XmlSerializer从文件流中反串行化GameData对象,下面的代码加载从文件加载所有数据并把数据转换为GameData对象:

private void LoadGame(StorageDevice storageDevice) 
{
    StorageContainer container = storageDevice.OpenContainer("BookCodeWin"); 
    string fileName = Path.Combine(container.Path, "save0001.sav"); 
    if (File.Exists(fileName)) 
    {
        FileStream saveFile = File.Open(fileName, FileMode.Open); 
        XmlSerializer xmlSerializer = new XmlSerializer(typeof(GameData)); 
        gameData = (GameData)xmlSerializer.Deserialize(saveFile); 
        saveFile.Close(); 
    }
} 
代码

Update方法检查用户是想保存还是加载文件并打开对话框:

protected override void Update(GameTime gameTime)
{
    GamePadState gamePadState = GamePad.GetState(PlayerIndex.One); 
    if (gamePadState.Buttons.Back == ButtonState.Pressed) 
        this.Exit(); 
    KeyboardState keyState = Keyboard.GetState(); 
    if (!Guide.IsVisible)
    {
        if (keyState.IsKeyDown(Keys.S)) 
            Guide.BeginShowStorageDeviceSelector(FindStorageDevice, "saveRequest"); 
        if (keyState.IsKeyDown(Keys.L)) 
            Guide.BeginShowStorageDeviceSelector(FindStorageDevice, "loadRequest"); 
    }
    gameData.Time += (float)gameTime.ElapsedGameTime.TotalSeconds; 
    base.Update(gameTime); 
} 

当用户关闭Guide时就会调用FindStorageDevice方法,这个方法根据异步调用的identity调用SaveData或LoadData方法。你可以在前面的代码中看到整个FindStorageDevice方法,只漏了SaveGame 方法:

private void SaveGame(StorageDevice storageDevice) 
{
    StorageContainer container = storageDevice.OpenContainer("BookCodeWin"); 
    string fileName = Path.Combine(container.Path, "save0001.sav"); 
    FileStream saveFile = File.Open(fileName, FileMode.Create); 
    XmlSerializer xmlSerializer = new XmlSerializer(typeof(GameData)); 
    XmlSerializer.Serialize(saveFile, gameData); 
    saveFile.Close(); 
    log.Add("Game data saved!"); 
} 
3