Saving/Loading using EasyStorage with XNA 4.0
This tutorial is made with XNA version 4.0. If you're still using 3.1 you can use this tutorial (which is a little outdated (Nov '09) but it should work).
Trying to get saving and loading to work on a 360 can be a pain at first, but it's pretty easy as long as you set it up correctly. I'm using Nick Gravelyn's EasyStorage which you'll have to download in order to follow along (I'm using changeset 57440 for this tutorial so grab that version for best compatibility). I'm going to create a mock-up game using the Xbox 360 Game State Management (4.0 version) to try and make this more accessible to everyone instead of using the Platformer Starter Kit like in the previous tutorial. You should be able to rip all of the saving/loading code straight from here and drop it into your own game.A good suggestion if you're on the newer side is to go through this tutorial and get it working in a blank copy of the Xbox 360 Game State Management example BEFORE you try to just drop it into your game. Once you get it working you'll get a little confidence boost and you'll have some understanding of how it works.
Adding the files to your project
First off, we need to transfer all of the necessary files to our blank copy of the GSM (game state management). All you have to do is drag the EasyStorage folder (found at GS4 > EasyStorage in the download above) to the very base of your project. Your base folder should look like this after the move. I named the blank copy of the Xbox 360 GSM "EasyStorage_4.0" so whatever you named your project will show up there instead.

Inside of Visual Studio we need to add the project and add a reference to it. In the solution explorer right click on the VERY TOP item (mine's named "Solution 'EasyStorage_4.0'"). Navigate to Add > Existing Project... > (find the location of the EasyStorage folder that you moved in the step above) EasyStorage > EasyStorage [Xbox].csproj. When you select that you should see a "EasyStorage [Xbox]" project along side the others.
Let's add the reference now. In the solution explorer expand your main project ("EasyStorage_4.0" for me) and right click on the "References" folder. Click on Add Reference and when the popup opens click on the Projects tab at the top. You should see 'EasyStorage [Xbox]' and just add that. We can move onto setting up our game now.
NOTE: The EasyStorage folder also contains projects for windows and windows phone but we're just focusing on Xbox for this tutorial. If you're also making a windows or windows phone copy of the game you'll have to repeat the step above and add the specific projects and references.
Global.cs
I like to create a Global.cs class and store the save device in there so I can save/load wherever in my code I want. Right click on your main project and Add > Class. Name it Global.cs and replace everything in the file with the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | using System; using System.Collections.Generic; using System.Linq; using System.Text; using EasyStorage; namespace YOUR_NAMESPACE_HERE! { public class Global { // A generic EasyStorage save device public static IAsyncSaveDevice SaveDevice; //We can set up different file names for different things we may save. //In this example we're going to save the items in the 'Options' menu. //I listed some other examples below but commented them out since we //don't need them. YOU CAN HAVE MULTIPLE OF THESE public static string fileName_options = "YourGame_Options"; //public static string fileName_game = "YourGame_Game"; //public static string fileName_awards = "YourGame_Awards"; //This is the name of the save file you'll find if you go into your memory //options on the Xbox. If you name it something like 'MyGameSave' then //people will have no idea what it's for and might delete your save. //YOU SHOULD ONLY HAVE ONE OF THESE public static string containerName = "YourGame_Save"; } } |
The comments should explain what each value does. The basic gist is that you should only have one container but you can have multiple files within that save container which allows your code to be much more organized.
PressStartScreen.cs
Now we have the framework laid out and we just have to set our SaveDevice before we can use it. I have the following code in my 'Press start to begin' screen which should be a part of any game because you can also find out which controller is controlling everything. Nick Gravelyn has an awesome example of how to support multiple controllers here. You need to make a new class in your Screens folder called "PressStartScreen.cs" and replace the contents of that with the class below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using EasyStorage; namespace YOUR_NAMESPACE_HERE! { class PressStartScreen : MenuScreen { IAsyncSaveDevice saveDevice; public PressStartScreen() : base("") { MenuEntry startMenuEntry = new MenuEntry("Press A to start"); startMenuEntry.Selected += StartMenuEntrySelected; MenuEntries.Add(startMenuEntry); } void StartMenuEntrySelected(object sender, PlayerIndexEventArgs e) { PromptMe(); } private void PromptMe() { // we can set our supported languages explicitly or we can allow the // game to support all the languages. the first language given will // be the default if the current language is not one of the supported // languages. this only affects the text found in message boxes shown // by EasyStorage and does not have any affect on the rest of the game. EasyStorageSettings.SetSupportedLanguages(Language.French, Language.Spanish); // on Windows Phone we use a save device that uses IsolatedStorage // on Windows and Xbox 360, we use a save device that gets a //shared StorageDevice to handle our file IO. #if WINDOWS_PHONE saveDevice = new IsolatedStorageSaveDevice(); Global.SaveDevice = saveDevice; // we use the tap gesture for input on the phone TouchPanel.EnabledGestures = GestureType.Tap; #else // create and add our SaveDevice SharedSaveDevice sharedSaveDevice = new SharedSaveDevice(); ScreenManager.Game.Components.Add(sharedSaveDevice); // make sure we hold on to the device saveDevice = sharedSaveDevice; // hook two event handlers to force the user to choose a new device if they cancel the // device selector or if they disconnect the storage device after selecting it sharedSaveDevice.DeviceSelectorCanceled += (s, e) => e.Response = SaveDeviceEventResponse.Force; sharedSaveDevice.DeviceDisconnected += (s, e) => e.Response = SaveDeviceEventResponse.Force; // prompt for a device on the first Update we can sharedSaveDevice.PromptForDevice(); sharedSaveDevice.DeviceSelected += (s, e) => { //Save our save device to the global counterpart, so we can access it //anywhere we want to save/load Global.SaveDevice = (SaveDevice)s; //Once they select a storage device, we can load the main menu. //You'll notice I hard coded PlayerIndex.One here. You'll need to //change that if you plan on releasing your game. I linked to an //example on how to do that but here's the link if you need it. //<a href="http://blog.nickgravelyn.com/2009/03/basic-handling-of-multiple-controllers/">http://blog.nickgravelyn.com/2009/03/basic-handling-of-multiple-controllers/</a> //We need to perform a check to see if we're on the Press Start Screen. //If a storage device is selected NOT from this page, we don't want to //create a new Main Menu screen! (Thanks @FreelanceGames for the mention) if(this.IsActive) ScreenManager.AddScreen(new MainMenuScreen(), PlayerIndex.One); }; #endif #if XBOX // add the GamerServicesComponent ScreenManager.Game.Components.Add( new Microsoft.Xna.Framework.GamerServices.GamerServicesComponent(ScreenManager.Game)); #endif } } } |
Once you have this new file in your game you'll need to go into Game.cs and find the line that says
1 | screenManager.AddScreen(new MainMenuScreen(), null); |
and change it to
1 | screenManager.AddScreen(new PressStartScreen(), null); |
Now when your game starts you'll be presented with a 'Press Start to begin' screen. When you press A or Start it prompts you to choose a storage device and the main menu screen is created. NOTE!! If you only have ONE storage device connected (Ex: you have a hard drive and no memory cards) then you won't get a popup at all, it will automatically choose the storage device found in the background (unaware to you).
A few notes about the above code, because there's a lot going on that will confuse you if you don't understand it. When you hit Start or A it calls the "PromptMe()" method which handles setting up the save device. If you have only the hard drive connected to your Xbox then it will seem like nothing happened because it chooses that automatically. If you have multiple storage devices plugged in (hard drive, memory card) then you will get a pop-up asking you to choose a storage device. Look at the 2 lines in the PromptMe method that end with SaveDeviceEventResponse.Force. SaveDeviceEventResponse has 3 different arguments that you can use:
- Nothing means nothing happens if you cancel the device selector screen. No save device will be chosen and you won't be able to save anything later on.
- Prompt means it will ask you to either choose a storage device, or continue without saving.
- Force means that you MUST choose a save device in order to continue. No ifs, ands or butts about it!
If you chose Prompt or Force and you remove the memory card (if that's the storage device you chose) you'll get a popup saying something along the lines of "reselect your storage device." If you chose Nothing and remove the memory card your game may crash when it tries to save again.
Finally, when we select the save device (sharedSaveDevice.DeviceSelected) we assign Global.SaveDevice to the save device so we have access to it later when we want to save/load. We also load the main menu when they choose a device.ANOTHER NOTE! If you change the SaveDeviceEventResponse to Prompt or Nothing and you don't choose a save device the menu won't load. This is because we have the code for loading the main menu inside the block of code for when our save device gets selected. You'll have to modify this if you want to allow users to continue w/o a save device.
MORE NOTES: I've always used SharedSaveDevice which means 1 save per Xbox. If you want different gamertags to have their own save games you'll have to use a PlayerSaveDevice instead of SharedSaveDevice.
OptionsMenuScreen.cs
If you're working in a copy of the Xbox 360 Game State Management then you should have a file called "OptionsMenuScreen.cs" in your Screens folder. Open that up because we're going to change it up so you can save your game options. I'm just going to use the default options they give us but in reality you might have options in here to change the volume of your sounds or select some preferences.
Inside of the constructor (public OptionsMenuScreen()...) method let's add this block of code to the VERY bottom (we can also remove the FIRST "SetMenuEntryText();" call since we call it after we try loading previous values):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | if (Global.SaveDevice.FileExists(Global.containerName, Global.fileName_options)) { Global.SaveDevice.Load( Global.containerName, Global.fileName_options, stream => { using (StreamReader reader = new StreamReader(stream)) { currentLanguage = int.Parse(reader.ReadLine()); frobnicate = bool.Parse(reader.ReadLine()); elf = int.Parse(reader.ReadLine()); } }); } SetMenuEntryText(); |
You'll also need to add "using System.IO;" to the top.
Since everything that we're trying to read is a string we need to convert it to the same type as the variable we're setting it to. That's where values like "int.Parse(...)" and "bool.Parse(...)" come from. The first one converts the string to an integer while bool.Parse(...) converts the string to a bool.
Now that we can load values, we need to add our save functionality! We're going to save when the user backs out of this screen (by pressing B or pressing the Back button) so we need to override the OnCancel method. Add this in the Handle Input region if you still have it, or just add it underneath the constructor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | protected override void OnCancel(PlayerIndex playerIndex) { // make sure the device is ready if (Global.SaveDevice.IsReady) { // save a file asynchronously. this will trigger IsBusy to return true // for the duration of the save process. Global.SaveDevice.SaveAsync( Global.containerName, Global.fileName_options, stream => { using (StreamWriter writer = new StreamWriter(stream)) { writer.WriteLine(currentLanguage); writer.WriteLine(frobnicate); writer.WriteLine(elf); } }); } base.OnCancel(playerIndex); } |
NOTE: I didn't deal with saving/loading the "Ungulate" variable so that will also default to the initial value when you try it.
And that's everything! Here's a couple suggestions to keep everything organized.
- When saving/loading stuff we're writing/reading from a file. The first value you write MUST be the first value you read. When writing this I accidentally wrote 4 lines to the file (saving) and when I tried reading them (loading) the order was messed up. I had to delete my save file and fix the order to get it to work again.
- When you move to saving game data make a new file name in the Global.cs file (I have a couple examples commented out in there) and just move the saving/loading examples in OptionsMenuScreen.cs into the file where you're working with your game code. All you'll have to do is change "Global.fileName_options" in the save/load arguments to your new name and change what you're reading/writing.
I know this can be pretty confusing at first, but once you finally get it, it'll be smooth sailing from there on!
July 19th, 2011 - 15:50
This was super helpful–the first actually understandable reader I’ve seen so far! Thanks so much!
The only thing I would suggest would be to have the project name be something like TUTORIAL GAME or something. Naming it Easy Storage and having to drag the Easy Storage 4.0 files into the folder made the image very confusing–which was I dragging where?
July 20th, 2011 - 09:51
The name of the project is EasyStorage4.0 and you drag the Easy Storage folder into that from wherever you downloaded it from. I’ll have to go through it sometime and make the naming convention a little easier to understand.
July 21st, 2011 - 11:09
Jeff–Yes, I understood the differences between the two, it just took a little while to figure whether your solution was Easy Storage or Easy Storage 4.0. Either way, this was still by far the clearest tutorial on how to save on the xbox that I’ve seen yet. Thanks for doing this!
July 25th, 2011 - 22:05
Would this work for XNA development on PC, besides the XBox and Windows phone?
July 25th, 2011 - 23:02
Yep, EasyStorage works with all 3 platforms. I haven’t done any work with the PC side of it though so you’ll likely find more help over at the EasyStorage web page: EasyStorage.codeplex.com.
September 13th, 2011 - 13:33
Most helpful documentation on understanding how to save/load files. I was wondering, could we not have used e.PlayerIndex in PressStartScreen.cs to figure out which player is the one starting the game. like so:
{
PromptMe(e.PlayerIndex);
}
private void PromptMe(PlayerIndex playerIndex)
{
…
ScreenManager.AddScreen(new MainMenuScreen(), playerIndex);
…
}
September 13th, 2011 - 17:01
You sure could (and should!). I just went for the easy route for ease-of-use. I usually use this snippet which allows me to easily set up multiple players if needed: http://blog.nickgravelyn.com/2009/03/basic-handling-of-multiple-controllers/
October 21st, 2011 - 11:57
Hi, nice article I’m trying to implement this and am getting the following error:
Object reference not set to an instance of an object.
on this line
if (Global.SaveDevice.FileExists(Global.containerName, Global.fileName_options))
Also – I’m confused as to what the containerName is – is this a folder in the savedevice which the file is saved in? how is this assigned outside the global.cs?
public static string containerName = “YourGame_Save”;
Thanks.
October 21st, 2011 - 19:35
The containerName is the name of the save file so when you look for the save, it’ll show up as “YourGame_Save” so it’s probably a good idea to change it. As for that error, it sounds like you’re not assigning something properly. Does your Global.cs file have a ‘containerName’ and ‘fileName_options’ variable? You can feel free to change the name of those variables to something that’s more understandable for you, just make sure you change it wherever else you wrote it.
October 22nd, 2011 - 11:42
Thanks I see, so ‘containerName’ is the actual file (e.g. txt) and ‘fileName_options’ is basically a (e.g. string) header name within that file?
You’re right on the error is wasn’t assigned prior to use, I was also running/looking for the file on PC but realised this works on XBOX – I can see the file now on the memory stick.
I’m trying to get this to load/save from a struct of high score data – playername, score, level – like a top 20 list and then display on screen using spritebatch – do you have any tips? How do I pass data lists and then read and display.
April 30th, 2012 - 07:04
Arrggghhh…….why so compicated, why XNA does not just have a class with to methods:
InputStream RedaFile(string filename)
void WriteFile(string filename, OutputStream outp)
and all stuff like “disconnected USB while running game” will be handeled correctly by default, and if you wish u could change it , but if you don’t want to loose time you just can use theese two methods
April 30th, 2012 - 13:53
That’s why there’s tutorials like this one.
Anyways, a simple, built in, component like you mentioned would still need customizing/modifying to work properly in everyones’ games. Doing the implementation/research yourself makes sure you know how it works fundamentally so if you encounter a problem you have an idea on how/why it happened so you can fix it.
May 4th, 2012 - 20:27
Hi Jeff, first of all thanks for these great tutorials, you’ve taught me alot and really helped me get into and understand XNA.
My question is in regards to the game crashing if the player removes the MU after being forced to choose a storage device, would that crash mean that the game gets a fail in peer review? If so how would you go about solving that problem?
May 4th, 2012 - 21:06
A crash in any location will cause your game to fail. It sounds like you might be saving/loading something and not using a Try/Catch block when you remove the MU. Whenever you attempt to save or load data make sure to use something like the following:
try{
//attempt to save data
}
catch ()
{
//if something went wrong, we end up in here
}