This blog is no longer updated. Feel free to copy any useful information to other blogs or wikis as this site may not exist for much longer. Thanks.


Storage Device Management 2.0

July 27th, 2009 | 4 comments

Since I’m on a roll (read: all of my games keep crashing so I have nothing else to do) with blog posts, I figured I’d do another lengthy write up of my latest iteration of StorageDevice management code. My last long post was lost in the switch from nickontech.com to the new domain (odd since every other post, both before and after it, transferred just fine).

Anyway, the old system was pretty flawed and the very popular StorageDeviceManager class on XNAWiki.com has its own set of issues. However both suffer from one problem: trying to use a single class to wrap both player-specific and player-agnostic StorageDevices. As I was working on making my code more and more friendly and robust, I realized that this was just a silly task. There’s really no need to put both types into one class, given that they inherently have different purposes and subtly different data associated (e.g. a player-specific storage device knows exactly who to prompt when it’s disconnected whereas a player-agnostic storage device has no idea).

To that end I started writing what is my current storage code. This is going to be a long post and there will be plenty of code and explanation. So if you’re interested in understanding and using the system, click on through to keep reading.

First up I wanted to design the system such that a game should have absolutely no access to the StorageDevice or StorageContainer objects. Zero. None. Off limits. The idea is that a game shouldn’t need access to either of those because our system is taking care of it. In addition, if we give the game access to those objects, there is a possibility that they can accidentally (or intentionally) dispose of them, which is obviously no good.

Our game should care about data and that’s it. So I started off defining an interface that represents game data that can be saved, loaded, and deleted:

/// <summary>
/// Defines the interface for a file utilized by the SaveDevice.
/// </summary>
public interface ISaveData
{
	/// <summary>
	/// The name of the file this data will be stored in.
	/// </summary>
	string FileName { get; }

	/// <summary>
	/// Saves the data.
	/// </summary>
	/// <param name="stream">A Stream used to save the data.</param>
	void Save(Stream stream);

	/// <summary>
	/// Loads the data.
	/// </summary>
	/// <param name="stream">A Stream used to load the data.</param>
	void Load(Stream stream);
}

Pretty basic. We have a property we use to get the file name and then two methods for loading and saving the data. Both our methods take in a stream so that the implementing class has only to use the stream how they need to read or write the data.

With that little interface defined, we move on to the next few little pieces we need to set up before getting to our main SaveDevice class. We want to set up a few enumerations and EventArg types for various things. We start with SaveDevicePromptState enumeration. This enumeration simply defines the state of the SaveDevice in regards to prompting the user:

/// <summary>
/// The various states of the SaveDevice.
/// </summary>
internal enum SaveDevicePromptState
{
	/// <summary>
	/// The SaveDevice is doing nothing.
	/// </summary>
	None,

	/// <summary>
	/// The SaveDevice needs to show the StorageDevice selector.
	/// </summary>
	ShowSelector,

	/// <summary>
	/// The SaveDevice needs to prompt the user because a
	/// StorageDevice selector was canceled.
	/// </summary>
	PromptForCanceled,

	/// <summary>
	/// The SaveDevice needs to force the user to choose a
	/// StorageDevice because a StorageDevice selector was canceled.
	/// </summary>
	ForceCanceledReselection,

	/// <summary>
	/// The SaveDevice needs to prompt the user because a
	/// StorageDevice was disconnected.
	/// </summary>
	PromptForDisconnected,

	/// <summary>
	/// The SaveDevice needs to force the user to choose a
	/// StorageDevice because a StorageDevice was disconnected.
	/// </summary>
	ForceDisconnectedReselection
}

I’m assuming with the comments explain that well enough. Moving on we have the SaveDeviceEventResponse enumeration. This defines how we want to respond to the user canceling the storage device selector prompt or disconnecting their device. Again, the comments can explain it pretty well:

/// <summary>
/// Responses for a user canceling the StorageDevice selector
/// or disconnecting the StorageDevice.
/// </summary>
public enum SaveDeviceEventResponse
{
	/// <summary>
	/// Take no action.
	/// </summary>
	Nothing,

	/// <summary>
	/// Displays a message box to choose whether to select a new
	/// device and shows the selector if appropriate.
	/// </summary>
	Prompt,

	/// <summary>
	/// Displays a message that the user must choose a new device
	/// and shows the device selector.
	/// </summary>
	Force,
}

Next we’re on to the event arguments. First we have the SaveDeviceEventArgs which we use whenever the user cancels a storage prompt or disconnects their storage device, allowing the game to inform the SaveDevice how to respond.

/// <summary>
/// Event arguments for the SaveDevice class.
/// </summary>
public sealed class SaveDeviceEventArgs : EventArgs
{
	/// <summary>
	/// Gets or sets the response to the event. The default response is to prompt.
	/// </summary>
	public SaveDeviceEventResponse Response { get; set; }

	/// <summary>
	/// Gets or sets the player index of the controller for which the message
	/// boxes should appear. This does not change the actual selection of the
	/// device but is merely used for the MessageBox displays.
	/// </summary>
	public PlayerIndex PlayerToPrompt { get; set; }
}

You can see that we leave both properties with a public set which means that the event handler in the game is fully intended to set these values as necessary. Those values are how the SaveDevice knows what to do after the event has fired.

Next we have a simple event argument class to handle one specific case. If the game sets the Response in SaveDeviceEventArgs to SaveDeviceEventResponse.Prompt, the user is given a message box prompt asking whether they’d like to select a new StorageDevice. When the user has made a selection, the SaveDevice fires off another event for the game, though this time it is for informational purposes only.

/// <summary>
/// Event arguments for the SaveDevice after a MessageBox prompt
/// has been closed.
/// </summary>
public sealed class SaveDevicePromptEventArgs : EventArgs
{
	/// <summary>
	/// Gets whether or not the user has chosen to select a new
	/// StorageDevice.
	/// </summary>
	public bool ShowDeviceSelector { get; internal set; }
}

The ShowDeviceSelector property is used simply to tell the game whether or not the user has chosen to select a new device. One note here is my use of the internal keyword on the set property. Since all of my storage code is in a separate assembly from my games, the internal lets my SaveDevice class set this value based on the user response, yet prevents the game from altering it (not that it really matters; the SaveDevice doesn’t use this value for anything after the event). The only reason for using internal is so that my game is forced to have it be read only while still allowing the SaveDevice to have just a single SaveDevicePromptEventArgs field that it can reuse.

FINALLY we are done with the preparations. We are ready to start our SaveDevice class. The SaveDevice is the base class that we will use to encapsulate the shared functionality between a player-specific and player-agnostic storage device. We also will implement some methods for handling the ISaveDevice objects. So let’s not wait any longer and jump right into it. We start with a simple declaration of the class with the fields and constants we’re going to need:

/// <summary>
/// A base class for an object that maintains a StorageDevice.
/// </summary>
public abstract class SaveDevice : GameComponent
{
	private const string promptForCancelledMessage = "No storage device was selected. You can continue without a device, but you will not be able to save. Would you like to select a storage device?";

	private const string forceCanceledReselectionMessage = "No storage device was selected. A storage device is required to continue.";

	private const string promptForDisconnectedMessage = "The storage device was disconnected. You can continue without a device, but you will not be able to save. Would you like to select a storage device?\n\nNote: if you only have one remaining storage device on the system (e.g. only a hard drive) the device will be selected automatically and no prompt will appear.";

	private const string forceDisconnectedReselectionMessage = "The storage device was disconnected. A storage device is required to continue.\n\nNote: if you only have one remaining storage device on the system (e.g. only a hard drive) the device will be selected automatically and no prompt will appear.";

	private const string deviceRequiredTitle = "Storage Device Required";
	private const string deviceOptionalTitle = "Reselect Storage Device?";
	private static readonly string[] deviceOptionalOptions = new[]
	{
		"Yes. Select new device.",
		"No. Continue without device."
	};
	private static readonly string[] deviceRequiredOptions = new[] { "Ok" };

	private bool deviceWasConnected;
	private SaveDevicePromptState state = SaveDevicePromptState.None;
	private readonly AsyncCallback storageDeviceSelectorCallback;
	private readonly AsyncCallback forcePromptCallback;
	private readonly AsyncCallback reselectPromptCallback;
	private readonly SaveDevicePromptEventArgs promptEventArgs =
		new SaveDevicePromptEventArgs();
	private readonly SaveDeviceEventArgs eventArgs = new SaveDeviceEventArgs();
	private StorageDevice storageDevice;

	/// <summary>
	/// Gets the name of the StorageContainer used by this SaveDevice.
	/// </summary>
	public string StorageContainerName { get; private set; }

	/// <summary>
	/// Gets whether the SaveDevice has a valid StorageDevice.
	/// </summary>
	public bool HasValidStorageDevice
	{
		get { return storageDevice != null && storageDevice.IsConnected; }
	}

	/// <summary>
	/// Invoked when a StorageDevice is selected.
	/// </summary>
	public event EventHandler DeviceSelected;

	/// <summary>
	/// Invoked when a StorageDevice selector is canceled.
	/// </summary>
	public event EventHandler<SaveDeviceEventArgs> DeviceSelectorCanceled;

	/// <summary>
	/// Invoked when the user closes a prompt to reselect a StorageDevice.
	/// </summary>
	public event EventHandler<SaveDevicePromptEventArgs> DeviceReselectPromptClosed;

	/// <summary>
	/// Invoked when the StorageDevice is disconnected.
	/// </summary>
	public event EventHandler<SaveDeviceEventArgs> DeviceDisconnected;
}

Hopefully most of that explains itself with the comments. We have some constants we use for various message boxes, some state fields, some AsyncCallback fields so that we can assign our various callbacks in the constructor and avoid any garbage from creating them over and over, a property to get the name used for opening the StorageContainer, and then a set of events that can be used to notify the game of what’s going on with the StorageDevice. All pretty simple so far.

Next we’ll move on and write our constructor which is simple as well:

/// <summary>
/// Creates a new SaveDevice.
/// </summary>
/// <param name="game">The current Game instance.</param>
/// <param name="storageContainerName">The name to use when opening a StorageContainer.</param>
protected SaveDevice(Game game, string storageContainerName)
	: base(game)
{
	storageDeviceSelectorCallback = StorageDeviceSelectorCallback;
	reselectPromptCallback = ReselectPromptCallback;
	forcePromptCallback = ForcePromptCallback;
	StorageContainerName = storageContainerName;
} 

Pretty simple, but at this point it won’t compile since we’re referencing methods we’ve yet to create. We’ll get to those in due time. First let’s write a few of our more simple methods. We’ll start with a method that lets the game tell the SaveDevice that we need it to prompt for a device.

/// <summary>
/// Flags the SaveDevice to prompt for a storage device on the next Update.
/// </summary>
public void PromptForDevice()
{
	If (state == SaveDevicePromptState.None)
		state = SaveDevicePromptState.ShowSelector;
}

Next we’ll hammer out the methods that will take our ISaveData objects and actually save, load, and delete them:

/// <summary>
/// Saves a set of ISaveData object.
/// </summary>
/// <param name="saveData">The objects to save.</param>
public void Save(params ISaveData[] saveData)
{
	if (!HasValidStorageDevice)
		throw new InvalidOperationException("StorageDevice is not valid.");

	using (var container = storageDevice.OpenContainer(StorageContainerName))
	{
		foreach (var save in saveData)
		{
			var path = Path.Combine(container.Path, save.FileName);
			using (StreamWriter writer = new StreamWriter(path))
				save.Save(writer.BaseStream);
		}
	}
}

/// <summary>
/// Loads a set of ISaveData object.
/// </summary>
/// <param name="saveData">The objects to load.</param>
public void Load(params ISaveData[] saveData)
{
	if (!HasValidStorageDevice)
		throw new InvalidOperationException("StorageDevice is not valid.");

	using (var container = storageDevice.OpenContainer(StorageContainerName))
	{
		foreach (var save in saveData)
		{
			var path = Path.Combine(container.Path, save.FileName);
			using (StreamReader reader = new StreamReader(path))
				save.Load(reader.BaseStream);
		}
	}
}

/// <summary>
/// Deletes a set of ISaveData object.
/// </summary>
/// <param name="saveData">The objects to delete.</param>
public void Delete(params ISaveData[] saveData)
{
	if (!HasValidStorageDevice)
		throw new InvalidOperationException("StorageDevice is not valid.");

	using (var container = storageDevice.OpenContainer(StorageContainerName))
		foreach (var save in saveData)
			File.Delete(Path.Combine(container.Path, save.FileName));
} 

Again, I hope most of this makes sense. The methods just verify that the device is valid before opening up a container, iterating the save data objects, and performing the proper operation.

Do note, however, that these methods are not guaranteed safe. I intentionally did not wrap any try/catch in there because the SaveDevice has no idea what you’d want to do in that case. Swallowing the exception is a poor decision since your game should really be informed if there’s a problem with saving or loading data. So my advice is that you put any call to Load, Save, or Delete in a try/catch and handle any exceptions. If for no other reason in case the InvalidOperationException gets thrown by us, but also in case the OpenContainer method or StreamWriter or StreamReader constructors throw.

Next we’re going to write an abstract method and a virtual method that our subclasses can use to customize the behavior of the device.

/// <summary>
/// Derived classes should implement this method to call the
/// Guide.BeginShowStorageDeviceSelector method with the desired parameters,
/// using the given callback.
/// </summary>
/// <param name="callback">The callback to pass to Guide.BeginShowStorageDeviceSelector.</param>
protected abstract void GetStorageDevice(AsyncCallback callback);

/// <summary>
/// Prepares the SaveDeviceEventArgs to be used for an event.
/// </summary>
/// <param name="args">The event arguments to be configured.</param>
protected virtual void PrepareEventArgs(SaveDeviceEventArgs args)
{
	args.Response = SaveDeviceEventResponse.Prompt;
	args.PlayerToPrompt = PlayerIndex.One;
}

The GetStorageDevice method is where a subclass would make a call to Guide.BeginShowStorageDeviceSelector, passing in the given callback as the callback to that method. This lets each subclass handle which overload of BeginShowStorageDeviceSelector to show. The PrepareEventArgs method simply lets subclasses change how the event arguments are initialized before firing the event. Our PlayerSaveDevice overrides this to set the PlayerToPrompt to default to the player that owns the storage device.

Next we have the rather large Update method. Though large, it is fairly straightforward:

/// <summary>
/// Allows the component to update itself.
/// </summary>
/// <param name="gameTime">The current game timestamp.</param>
public override void Update(GameTime gameTime)
{
	bool deviceIsConnected = HasValidStorageDevice;

	if (!deviceIsConnected && deviceWasConnected)
	{
		// if the device was disconnected, fire off the event and handle result
		PrepareEventArgs(eventArgs);

		if (DeviceDisconnected != null)
			DeviceDisconnected(this, eventArgs);

		HandleEventArgResults();
	}
	else if (!deviceIsConnected)
	{
		try
		{
			if (!Guide.IsVisible)
			{
				switch (state)
				{
					case SaveDevicePromptState.ShowSelector:
						state = SaveDevicePromptState.None;
						GetStorageDevice(storageDeviceSelectorCallback);
						break;

					case SaveDevicePromptState.PromptForCanceled:
						Guide.BeginShowMessageBox(
							eventArgs.PlayerToPrompt,
							deviceOptionalTitle,
							promptForCancelledMessage,
							deviceOptionalOptions,
							0,
							MessageBoxIcon.None,
							reselectPromptCallback,
							null);

						break;

					case SaveDevicePromptState.ForceCanceledReselection:
						Guide.BeginShowMessageBox(
							eventArgs.PlayerToPrompt,
							deviceRequiredTitle,
							forceCanceledReselectionMessage,
							deviceRequiredOptions,
							0,
							MessageBoxIcon.None,
							forcePromptCallback,
							null);

						break;

					case SaveDevicePromptState.PromptForDisconnected:
						Guide.BeginShowMessageBox(
							eventArgs.PlayerToPrompt,
							deviceOptionalTitle,
							promptForDisconnectedMessage,
							deviceOptionalOptions,
							0,
							MessageBoxIcon.None,
							reselectPromptCallback,
							null);
						break;

					case SaveDevicePromptState.ForceDisconnectedReselection:
						Guide.BeginShowMessageBox(
							eventArgs.PlayerToPrompt,
							deviceRequiredTitle,
							forceDisconnectedReselectionMessage,
							deviceRequiredOptions,
							0,
							MessageBoxIcon.None,
							forcePromptCallback,
							null);
						break;

					default:
						break;
				}
			}
		}
		catch (GuideAlreadyVisibleException)
		{
			// swallow the exception
		}
	}

	deviceWasConnected = deviceIsConnected;
}

We’ll walk through this code. First we check if we have a valid device and store that for use throughout the method. Next we check to see if the device was valid last frame but is invalid this frame. If that is the case, we prepare our event arguments and fire off the Disconnected event. We then call the HandleEventArgResults method to handle that response.

Next up we have the condition for the device being disconnected, but was also previously disconnected. This is figured by being an else to the first if statement. Inside here we basically do a try/catch (to watch for those dreaded GuideAlreadyVisibleExceptions) and a switch inside that to determine what we’re going to do. If we need to show the prompt, we call our abstract GetStorageDevice method. Otherwise we run through the other states and handle them appropriately. Most are fairly similar so I’ll leave you to just read through and see what each is doing.

Then at the bottom we just store the state of the device from this frame so we can compare with it next frame. Easy peasy.

Lastly we just have our set of callback and the HandleEventArgsResult method to implement. Let’s run through these individually:

private void StorageDeviceSelectorCallback(IAsyncResult result)
{
	storageDevice = Guide.EndShowStorageDeviceSelector(result);

	if (storageDevice != null && storageDevice.IsConnected)
	{
		if (DeviceSelected != null)
			DeviceSelected(this, null);
	}
	else
	{
		PrepareEventArgs(eventArgs);

		if (DeviceSelectorCanceled != null)
			DeviceSelectorCanceled(this, eventArgs);

		HandleEventArgResults();
	}
}

First the callback that fires when the user responds to the BeginShowStorageDeviceSelector prompt. If a device was selected, we store it and fire the DeviceSelected event. If a device was not selected, we prepare some event arguments and fire the DeviceSelectorCanceled event to see what the game wants to do about that.

private void ForcePromptCallback(IAsyncResult ar)
{
	// just end the message and instruct the SaveDevice to show the selector
	Guide.EndShowMessageBox(ar);
	state = SaveDevicePromptState.ShowSelector;
}

private void ReselectPromptCallback(IAsyncResult ar)
{
	int? choice = Guide.EndShowMessageBox(ar);

	// get the device if the user chose the first option
	state = choice.HasValue && choice.Value == 0
		? SaveDevicePromptState.ShowSelector
		: SaveDevicePromptState.None;

	// fire an event for the game to know the result of the prompt
	promptEventArgs.ShowDeviceSelector = state == SaveDevicePromptState.ShowSelector;
	if (DeviceReselectPromptClosed != null)
		DeviceReselectPromptClosed(this, promptEventArgs);
}

Ok, so that’s not individually, but they’re both pretty simple so I’m putting both together. The ForcePromptCallback is invoked when the user is told they have to reselect a storage device. We just set the state to ShowSelector so that next frame we’ll prompt them for a new device.

The ReselectPromptCallback is invoked when the user is given a message box asking whether they’d like to select a storage device or not. If the user chooses to reselect, we show them the device selector again. Either way we fire the DeviceReselectPromptClosed event so that the game knows whether the user chose to continue without a StorageDevice or not.

/// <summary>
/// Handles reading from the eventArgs to determine what action to take.
/// </summary>
private void HandleEventArgResults()
{
	// clear the Device reference
	storageDevice = null;

	// determine the next action...
	switch (eventArgs.Response)
	{
		// will have the manager prompt the user with the option
		// of reselecting the storage device
		case SaveDeviceEventResponse.Prompt:
			state = deviceWasConnected
	        	? SaveDevicePromptState.PromptForDisconnected
	        	: SaveDevicePromptState.PromptForCanceled;
			break;

		// will have the manager prompt the user that the device must be selected
		case SaveDeviceEventResponse.Force:
			state = deviceWasConnected
	        	? SaveDevicePromptState.ForceDisconnectedReselection
	        	: SaveDevicePromptState.ForceCanceledReselection;
			break;

		// will have the manager do nothing
		default:
			state = SaveDevicePromptState.None;
			break;
	}
}

Lastly the HandleEventArgsResult method. This is used to read in the event arguments from various events and determine what the SaveDevice should do. It’s pretty basic so I’ll just leave it at that.

And there we go. That’s the entire SaveDevice. We’re done… oh wait, we’re not. This was just the base class. :) Thankfully the subclasses are incredibly short. We’ll start with the SharedSaveDevice which represents our player-agnostic SaveDevice. This is a tiny class. So tiny that the XML doc comments actually take more lines of code. :p

/// <summary>
/// A SaveDevice used for non player-specific saving of data.
/// </summary>
public sealed class SharedSaveDevice : SaveDevice
{
	/// <summary>
	/// Creates a new SaveDevice.
	/// </summary>
	/// <param name="game">The current Game instance.</param>
	/// <param name="storageContainerName">
	/// The name to use when opening a StorageContainer.
	/// </param>
	public SharedSaveDevice(Game game, string storageContainerName)
		: base(game, storageContainerName) {}

	/// <summary>
	/// Derived classes should implement this method to call the
	/// Guide.BeginShowStorageDeviceSelector method with the desired parameters,
	/// using the given callback.
	/// </summary>
	/// <param name="callback">
	/// The callback to pass to Guide.BeginShowStorageDeviceSelector.
	/// </param>
	protected override void GetStorageDevice(AsyncCallback callback)
	{
		Guide.BeginShowStorageDeviceSelector(callback, null);
	}
}

You can see we just have a constructor to call down to the base class constructor along with our GetStorageDevice method which simply calls the basic BeginShowStorageDeviceSelector method with the specified callback. Real easy. Next the PlayerSaveDevice:

/// <summary>
/// A SaveDevice used for saving player-specific data.
/// </summary>
public sealed class PlayerSaveDevice : SaveDevice
{
	/// <summary>
	/// Gets the PlayerIndex of the player for which the data will be saved.
	/// </summary>
	public PlayerIndex Player { get; private set; }

	/// <summary>
	/// Creates a new PlayerSaveDevice for a given player.
	/// </summary>
	/// <param name="game">The current Game instance.</param>
	/// <param name="storageContainerName">
	/// The name to use when opening a StorageContainer.
	/// </param>
	/// <param name="player">The player for which the data will be saved.</param>
	public PlayerSaveDevice(Game game, string storageContainerName, PlayerIndex player)
		: base(game, storageContainerName)
	{
		Player = player;
	}

	/// <summary>
	/// Derived classes should implement this method to call the
	/// Guide.BeginShowStorageDeviceSelector method with the desired parameters,
	/// using the given callback.
	/// </summary>
	/// <param name="callback">
	/// The callback to pass to Guide.BeginShowStorageDeviceSelector.
	/// </param>
	protected override void GetStorageDevice(AsyncCallback callback)
	{
		Guide.BeginShowStorageDeviceSelector(Player, callback, null);
	}

	/// <summary>
	/// Prepares the SaveDeviceEventArgs to be used for an event.
	/// </summary>
	/// <param name="args">The event arguments to be configured.</param>
	protected override void PrepareEventArgs(SaveDeviceEventArgs args)
	{
		// the base implementation sets some aspects of the arguments,
		// so we let it do that first
		base.PrepareEventArgs(args);

		// we then default the player to prompt to be the player that
		// owns this storage device. we assume the game will leave this
		// untouched so that the correct player is prompted, but we also
		// allow the game to change it if there's a reason to.
		args.PlayerToPrompt = Player;
	}
}

There’s a little more going on here. First is that our constructor requires the PlayerIndex of the player that this SaveDevice is for. Next we have the GetStorageDevice implementation that just calls the basic BeginShowStorageDeviceSelector overload for a player-specific device. Lastly we override the PrepareEventArgs method to set a different default value for the PlayerToPrompt value.

Lastly I just want to show a basic implementation of the ISaveData type that can be used for some generic XML saving. It’s also nice just to round everything off and show how you’d set up a class to work in the system for saving data:

public class XmlSaveData<T> : ISaveData
{
	private readonly XmlSerializer serializer = new XmlSerializer(typeof(T));

	public string FileName { get; private set; }
	public T Data { get; set; }

	public XmlSaveData(string fileName)
	{
		FileName = fileName;
	}

	public void Save(Stream stream)
	{
		serializer.Serialize(stream, Data);
	}

	public void Load(Stream stream)
	{
		Data = (T)serializer.Deserialize(stream);
	}
}

Now, that’s just one way to do it, but that’s the beauty of the interface; you can implement the whole thing however you want.

Fhew! That’s a lot of stuff in there. Hopefully between my commentary and the comments in the code you have a decent understanding of how it all fits together and works. And if not, I suppose it’s not entirely vital that you understand the internal workings; the public interface is relatively simple and straightforward.

This works really well for my needs, but might not be ideal for everyone at this point. But hopefully this has given you some ideas of how to structure your own file management code if it doesn’t work as-is for you.


Possibly Related Posts

(Automatically Generated)
EasyStorage Released
Using interpolators and timers
Extending GamePadState
How To Make A Better Community Game
How To Test If A Player Can Purchase Your Game

  1. conkerjo
    July 27th, 2009 at 05:34
    Quote | #1

    Looks good, not had chance to try it yet but I will. Unless i missed it it would be nice to have a zip containing the code to try out.

  2. CarlosNYM
    July 29th, 2009 at 12:23
    Quote | #2

    Hmm, I really want to get Data Storage working on my game and I tried this out. I copied and pasted the code exactly and I got 67 errors. They are mostly — does not exist errors so I am not sure if I put stuff in the right place. For ezample, in the StorageDeviceSelectorCallback method, storageDevice and DeviceSelected can not be found in the entire method. I put this method in the Game1.cs File. not sure if thats where it’s supposed to go.

  3. ethorad
    July 31st, 2009 at 12:17
    Quote | #3

    Sorry for the tangential comment, but I’ve just switched to c# with XNA and gone through your tile game tutorial videos. Absolutely love them, found it very useful seeing the process you go through in coding – something you can’t get from reading a book and being presented with a fully working piece of code where I end up wondering how to write something like it.

    One question though, when you were doing the code in the last video to save out the tile layers in xml documents, you were using the XmlDocument classes and attaching nodes etc. The code looks pretty long and complex to me. What’s the advantage in doing that over say

    doc.write(“”);
    {
    doc.write(“”, filename, index);
    }
    doc.write(“”);

    (the code probably is incorrect, but you get the idea. it’s a lot shorter than creating nodes and attributes etc etc)

  4. ethorad
    July 31st, 2009 at 12:19
    Quote | #4

    Humm, sorry wordpress has stripped out the xml. Removing the angle brackets the code should be

    doc.write(“Textures”);
    {
    doc.write(“Texture File={0} ID={1}”, filename, index);
    }
    doc.write(“/Textures”);

Comments are closed.