Farseer Physics Platformer 3.3.1 Update
It sucks when updates to a project break existing tutorials out there, so I'm posting the 3.3.1 update to @RoyTries XNA Farseer Platform Physics Tutorial that he wrote. @RabidLionGames updated it when the 3.2 update came out and a few things got broken in the 3.3.1 update so hopefully this helps out anyone who's looking for a working version in 3.3.1!
This post is just going to show the breaking changes from 3.2 to 3.3.1. Make sure you complete the original tutorial to get the project setup and read the 3.2 update to see the changes made in that version. Make sure to read the part about the unit conversion from 2.1.3 to 3.2 otherwise you'll have massively huge objects on your screen! After those 2 are done and set up there's a handful of changes below that you can make below to have a working version with 3.3.1.
Changes from 3.2 to 3.3.1:
PhysicsObject.cs
Find the following variable and remove it altogether:
public Fixture fixture;
Here is what the 3.2 version of our SetupPhysics method looked like:
fixture = FixtureFactory.CreateRectangle(world, ConvertUnits.ToSimUnits(width), ConvertUnits.ToSimUnits(height), mass, ConvertUnits.ToSimUnits(position)); body = fixture.Body; fixture.Body.BodyType = BodyType.Dynamic; fixture.Restitution = 0.3f; fixture.Friction = 0.5f;
In 3.3.1 we have a BodyFactory that will create a Body and attach a Fixture to it so we don't have a need for the explicit 'fixture' object that was declared at the top (that's why we removed it!). Your new SetupPhysics method should now look like the following:
body = BodyFactory.CreateRectangle(world, ConvertUnits.ToSimUnits(width), ConvertUnits.ToSimUnits(height), mass, ConvertUnits.ToSimUnits(position)); body.BodyType = BodyType.Dynamic; body.Restitution = 0.3f; body.Friction = 0.5f;
In StaticPhysicsObject.cs find the following line:
fixture.Body.BodyType = BodyType.Static;
and replace it with this (once again, 'fixture' is gone so we update it):
body.BodyType = BodyType.Static;
In CompositePhysicsObject.cs the following:
revJoint = JointFactory.CreateRevoluteJoint(world, physObA.fixture.Body, physObB.fixture.Body, ConvertUnits.ToSimUnits(relativeJointPosition)); physObA.fixture.CollisionFilter.IgnoreCollisionWith(physObB.fixture); physObB.fixture.CollisionFilter.IgnoreCollisionWith(physObA.fixture);
Gets changed to:
revJoint = JointFactory.CreateRevoluteJoint(world, physObA.body, physObB.body, ConvertUnits.ToSimUnits(relativeJointPosition)); physObA.body.IgnoreCollisionWith(physObB.body); physObB.body.IgnoreCollisionWith(physObA.body);
Once again, we simply removed anything that referred to the 'fixture' object since we're not using it.
In SpringPhysicsObject.cs find the following line:
springJoint = JointFactory.CreateAngleJoint(world, physObA.fixture.Body, physObB.fixture.Body);
and replace it with this (guess what... no 'fixture' object so we cut it out!):
springJoint = JointFactory.CreateAngleJoint(world, physObA.body, physObB.body);
Character.cs remains the same as the 3.2 version.
Now for the bread and butter, CompositeCharacter.cs. Change:
public Fixture wheel;
to:
public Body wheel;
Inside of the SetupPhysics method you can remove these two lines:
fixture = FixtureFactory.CreateRectangle(world, (float)ConvertUnits.ToSimUnits(width), (float)ConvertUnits.ToSimUnits(upperBodyHeight), mass / 2); body = fixture.Body;
and replace them with this line:
body = BodyFactory.CreateRectangle(world, (float)ConvertUnits.ToSimUnits(width), (float)ConvertUnits.ToSimUnits(upperBodyHeight), mass / 2);
a little further down in the SetupPhysics method you'll find these lines:
wheel = FixtureFactory.CreateCircle(world, (float)ConvertUnits.ToSimUnits(width / 2), mass / 2); wheel.Body.Position = body.Position + ConvertUnits.ToSimUnits(Vector2.UnitY * (upperBodyHeight / 2)); wheel.Body.BodyType = BodyType.Dynamic;
and you can change them to this (using the BodyFactory to create the Body and attach a Fixture. Since wheel is a Body instead of a Fixture now, we remove the extra ".Body" found in the bottom two lines):
wheel = BodyFactory.CreateCircle(world, (float)ConvertUnits.ToSimUnits(width / 2), mass / 2); wheel.Position = body.Position + ConvertUnits.ToSimUnits(Vector2.UnitY * (upperBodyHeight / 2)); wheel.BodyType = BodyType.Dynamic;
The last change near the bottom of the SetupPhysics method is to update these two lines since we're not using the fixture variable anymore:
wheel.CollisionFilter.IgnoreCollisionWith(fixture); fixture.CollisionFilter.IgnoreCollisionWith(wheel);
becomes:
wheel.IgnoreCollisionWith(body); body.IgnoreCollisionWith(wheel);
You can download a ZIP file of all of these updated classes here to check for differences if something didn't work.
Adding melee to PSK
Update: Went through the entire tutorial in a fresh solution and fixed one bug where the enemies no longer killed you. The tutorial has been updated (UpdateEnemies(...) method in Level.cs) and now works as-is. If you have problems, make sure to download the file just below and compare it with yours to see where you may have messed up!
You can download a working version below.
Here's a video showing off the final product of this tutorial:
Here is what I used for his attack animation as a placeholder, feel free to use it. Place this in the same place as the other animations: “HighResolutionContent>Sprites>Player”. Also make sure to add it to your project: Right click on the Player folder and Add>Existing Item>Attack.png
Player.cs
Find the following:
private float jumpTime;
After it add the following:
// Attacking state public bool isAttacking; const float MaxAttackTime = 0.33f; public float AttackTime;
Next find the following:
public Rectangle BoundingRectangle
{
get
{
int left = (int)Math.Round(Position.X - sprite.Origin.X) + localBounds.X;
int top = (int)Math.Round(Position.Y - sprite.Origin.Y) + localBounds.Y;
return new Rectangle(left, top, localBounds.Width, localBounds.Height);
}
}After that add the following:
public Rectangle MeleeRectangle
{
get
{
int left = (int)Math.Round(Position.X - sprite.Origin.X) + localBounds.X;
int top = (int)Math.Round(Position.Y - sprite.Origin.Y) + localBounds.Y;
if (flip == SpriteEffects.FlipHorizontally)
return new Rectangle(
(left + localBounds.Width),
top,
localBounds.Width,
localBounds.Height);
else
return new Rectangle(
(left - localBounds.Width),
top,
localBounds.Width,
localBounds.Height);
}
}
At the top of Player.cs find the following line:
private Animation dieAnimation;
After that add the following line:
private Animation attackAnimation;
Now find the LoadContent method in Player.cs and find the following line:
dieAnimation = new Animation(
Level.Content.Load<Texture2D>("Sprites/Player/Die"),
0.1f,
false);After that add the following:
attackAnimation = new Animation(
Level.Content.Load<Texture2D>("Sprites/Player/Attack"),
0.1f,
false);
Find the DoJump method and add a new method after it:
private void DoAttack(GameTime gameTime)
{
// If the player wants to attack
if (isAttacking)
{
// Begin or continue an attack
if (AttackTime > 0.0f)
{
AttackTime -= (float)gameTime.ElapsedGameTime.TotalSeconds;
}
else
{
isAttacking = false;
}
}
else
{
//Continues not attack or cancels an attack in progress
AttackTime = 0.0f;
}
}
Now our attack is setup but we need a keybind so we can actually use it! At the very top of our player class add just the KeyboardState line below:
class Player
{
KeyboardState previousKeyboardState = Keyboard.GetState();Now add the following in the GetInput method:
if (previousKeyboardState.IsKeyUp(Keys.F) && keyboardState.IsKeyDown(Keys.F))
{
if (AttackTime != MaxAttackTime)
{
isAttacking = true;
AttackTime = MaxAttackTime;
}
}
Now onto the Update method, after the GetInput() call add the following:
DoAttack(gameTime);
Find the following in the Update method:
if (IsAlive && IsOnGround)
{
if (Math.Abs(Velocity.X) - 0.02f > 0)
{
sprite.PlayAnimation(runAnimation);
}
else
{
sprite.PlayAnimation(idleAnimation);
}
}And change it to look like the following:
if (IsAlive && IsOnGround)
{
if (isAttacking)
{
sprite.PlayAnimation(attackAnimation);
}
else
{
if (Math.Abs(Velocity.X) - 0.02f > 0)
{
sprite.PlayAnimation(runAnimation);
}
else
{
sprite.PlayAnimation(idleAnimation);
}
}
}
BUILD THE SOLUTION AT THIS POINT TO MAKE SURE YOU DON’T HAVE ANY ERRORS.
Enemy.cs
In Enemy.cs find the following:
// Animations private Animation runAnimation; private Animation idleAnimation; private AnimationPlayer sprite;
and add the following after it:
private Animation dieAnimation;
// Sounds
private SoundEffect killedSound;
public bool IsAlive { get; private set; }
private const float deathTimeMax = 1.0f;
public float deathTime = deathTimeMax;
Here we added a death animation, death sound, and an IsAlive variable that will allow our enemies to die. We also added a timer so we can make our enemy disappear a certain amount of time after he dies. If you want to change how long they're alive simply change the deathTimerMax variable.
To initialize the IsAlive variable find the constructor for our Enemy, and after,
this.position = position;
add:
this.IsAlive = true;
First we need to load the sound into the project because it isn’t there by default. In the solution explorer go to “SharedContent” right click on Sounds>Add>Existing Item and add “MonsterKilled”. Right click on MonsterKilled and go to Properties. In the Properties window find ContentProcessor and from the drop down menu to the right of that choose Sound Effect – XNA Framework. At the very top add another using statement:
using Microsoft.Xna.Framework.Audio;
Once you add the sound, you'll need to change it's properties because it's considered a 'Song' instead of a 'Sound Effect'. Right click on the sound after you add it to your solution and hit properties. Under 'Content Processor' change it from Song to Sound Effect. We need to load the new sound so find the LoadContent method and add the following at the bottom:
//Load sounds.
killedSound = Level.Content.Load<SoundEffect>("Sounds/MonsterKilled");While we’re in the LoadContent method add the following by the other animations: (NOTE: You also need to add this animation to your solution because it isn't there by default. Navigate to HighResContent>Sprites and then in each folder (MonsterA, MonsterB, etc) you'll have to add the existing item like you did above with the sound)
dieAnimation = new Animation(Level.Content.Load<Texture2D>(spriteSet + "Die"), 0.07f, false);
Inside the Update method add this block of code.
if (!IsAlive)
deathTime -= (float)gameTime.ElapsedGameTime.TotalSeconds;
Inside of the Draw method let's change the code a little bit. Change the if/else statement to look like the following:
// play death animation if we're dead, stop running
//when the game is paused or before turning around.
if (deathTime < deathTimeMax)
sprite.PlayAnimation(dieAnimation);
else if (!Level.Player.IsAlive ||
Level.ReachedExit ||
Level.TimeRemaining == TimeSpan.Zero ||
waitTime > 0)
{
sprite.PlayAnimation(idleAnimation);
}
else
{
sprite.PlayAnimation(runAnimation);
}
Under the Draw method add a new method:
public void OnKilled(Player killedBy)
{
IsAlive = false;
killedSound.Play();
}
Level.cs
Next in Level.cs find the following in the UpdateEnemies method (and add the bold part)
// Touching an enemy instantly kills the player
if (enemy.BoundingRectangle.Intersects(Player.BoundingRectangle))
{
if(enemy.IsAlive)
OnPlayerKilled(enemy);
}Now add this if statement after the one above:
if (enemy.IsAlive && enemy.BoundingRectangle.Intersects(Player.MeleeRectangle))
{
if(Player.isAttacking)
OnEnemyKilled(enemy, Player);
}
Now under the UpdateEnemies method add a new method:
private void OnEnemyKilled(Enemy enemy, Player killedBy)
{
enemy.OnKilled(killedBy);
}
Now in the Draw(...) method find the block of code to draw the enemies and change it to this:
foreach (Enemy enemy in enemies)
if(enemy.IsAlive || enemy.deathTime > 0)
enemy.Draw(gameTime, spriteBatch);
And that's it! Give it a try and see if you can finally kill those bad guys!
Double Jump in PSK
Double jumping is pretty common in most platformers so I thought I'd share some code I use for it.
Here's a video showing the final product:
In Player.cs add the following underneath the other jumping code. (MaxJumpTime, JumpControlPower, etc.)
private int numberOfJumps = 0;
Now find this line in Update(...)
isJumping = false;
and add this after it:
if (isOnGround)
numberOfJumps = 0;
Now in the DoJump(...) method find this block of code
// If we are in the ascent of the jump
if (0.0f < jumpTime && jumpTime <= MaxJumpTime)
{
// Fully override the vertical velocity with a power curve that
//gives players more control over the top of the jump
velocityY =
JumpLaunchVelocity * (1.0f - (float)Math.Pow(jumpTime / MaxJumpTime, JumpControlPower));
}
else
{
// Reached the apex of the jump
jumpTime = 0.0f;
}and change it to this:
// If we are in the ascent of the jump
if (0.0f < jumpTime && jumpTime <= MaxJumpTime)
{
// Fully override the vertical velocity with a power curve that
//gives players more control over the top of the jump
velocityY =
JumpLaunchVelocity * (1.0f - (float)Math.Pow(jumpTime / MaxJumpTime, JumpControlPower));
}
else
{
// Reached the apex of the jump and has double jumps
if (velocityY > -MaxFallSpeed * 0.5f && !wasJumping && numberOfJumps < 1)
{
velocityY =
JumpLaunchVelocity * (0.5f - (float)Math.Pow(jumpTime / MaxJumpTime, JumpControlPower));
jumpTime += (float)gameTime.ElapsedGameTime.TotalSeconds;
numberOfJumps++;
}
else
{
jumpTime = 0.0f;
}
}
And that's it! You're able to double jump slightly after the first jump and whenever you're on the ground your jump count gets reset so you're able to double jump again.
Saving/Loading with EasyStorage using XNA 3.1
This tutorial uses EasyStorage version (57440).
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. If you plan on releasing your game on the 360 you also have to make sure it doesn't crash while saving/loading if someone pulls out the memory card. I'm using Nick Gravelyn's EasyStorage which you'll have to download in order to follow along. I'm using a project that has the Platform Starter Kit combined with the Game State Management for this tutorial. You must have the Game State Management sample incorporated into your project in order to follow this tutorial. You could just use a blank copy of the GSM if you want to wanted to get this working, then try to move it into your own project.
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 GS3.1 > EasyStorage in the download above) to the very base of your project. Your base folder should look like this after the move. NOTE: I'm still using XNA 3.1 in this tutorial so don't let the name in the image through you for a loop (it's from the 4.0 tutorial).

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_3.1'"). 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 an "EasyStorage [Xbox]" project alongside the others.
Let's add the reference now. In the solution explorer expand your main project ("EasyStorage_3.1" 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
Now right-click on the Saving folder and click Add>Class and name it 'Global.cs'. The purpose of this file is to have a set of global variables so we can access them from wherever we want in our code. It should look like the following but you need to change the namespace to match the namespace of YOUR game project. Also change fileName1 to the name of your game.
using System;
using System.Collections.Generic;
using System.Text;
using EasyStorage;
namespace YOUR_NAMESPACE_HERE!
{
public class Global
{
// A generic EasyStorage save device
public static ISaveDevice 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";
}
}
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 on my 'Press start to begin' screen, when you press start, which should be a part of any game because you can also find out which controller is controlling everything. The struct above our Global class has the variables we'll need to save, replace them with anything that you'll want to save in your own game. You need to make a new class (preferably in the Screens folder) called PressStartScreen.cs and replace the contents of that with the class below. Make sure you change YOUR_NAMESPACE_HERE and YOUR_GAME_NAME.
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using EasyStorage;
namespace YOUR_NAMESPACE_HERE!
{
class PressStartScreen : MenuScreen
{
ISaveDevice 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.English);
// 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.
//http://blog.nickgravelyn.com/2009/03/basic-handling-of-multiple-controllers/
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
}
}
}
Now that you have that in your game you'll need to go into Game.cs and find the line that says
screenManager.AddScreen(new MainMenuScreen(), null);
and change it to
screenManager.AddScreen(new PressStartScreen(), null);
This will now start your game with a press start to begin screen and when you continue it prompts you to choose a storage device (if you have more then just the hard drive connected) and the main menu screen is created.
A few notes about the above code, because there's a lot going on that will confuse you if you don't understand it. If you're playing on the PC then your SaveDevice is chosen automatically and your save file will be saved in My Documents/SavedGames I believe. Otherwise it makes a call to PromptMe() which does all the heavy lifting of choosing a 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 as well. 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. This 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 if you try saving later you'll get errors.
- 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!
The first line (from the 2 options we're talking about) happens when you cancel the device selector. The 2nd line happens if you disconnect the storage device at some point after choosing it.
Finally, we set our Global.SaveDevice to the sharedSaveDevice so we have access to it later when we want to save/load.
NOTE: I'm using SharedSaveDevice in this example, so anything you save is accessible to anyone playing the game on your console. If you want to save data and attach it to a certain player, as most AAA games do, you'll want to look into PlayerSaveDevice instead, but it's a little bit more work.
Player.cs
So now onto saving. The Platform Starter Kit has a Player.cs class, and that's where I do my saving. We need to add the saving/loading methods. You'll also need to add a using statement: using System.IO;
public void SaveGame()
{
// make sure the file exists
if (Global.SaveDevice.FileExists(Global.containerName, Global.fileName_game))
{
// save a file asynchronously. this will trigger IsBusy to return true
// for the duration of the save process.
Global.SaveDevice.Save(
Global.containerName,
Global.fileName_game,
stream =>
{
using (StreamWriter writer = new StreamWriter(stream))
{
writer.WriteLine(lives);
writer.WriteLine(doIHaveTheKey);
writer.WriteLine(score);
}
});
}
}
Take note of the order you save stuff. Since the variable "lives" is the first to be saved, it MUST be the first to be loaded!
So here's our load methods which look much the same as SaveGame() but instead of writing to a file, we're reading from it.
public void LoadGame()
{
if (Global.SaveDevice.FileExists(Global.containerName, Global.fileName_game))
{
Global.SaveDevice.Load(
Global.containerName,
Global.fileName_game,
stream =>
{
using (StreamReader reader = new StreamReader(stream))
{
lives = int.Parse(reader.ReadLine());
doIHaveTheKey = bool.Parse(reader.ReadLine());
score = int.Parse(reader.ReadLine());
}
});
}
}
And that's everything! When we want to save our game we make a call to SaveGame() in our Player.cs class (or whatever you used) and when I load up my game I call LoadGame() in my LoadContent() method.
I know this can be pretty confusing at first, but once you finally get it, it'll be smooth sailing from there on!
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:
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.
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.
//http://blog.nickgravelyn.com/2009/03/basic-handling-of-multiple-controllers/
//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
screenManager.AddScreen(new MainMenuScreen(), null);
and change it to
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):
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.
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!
