I should really write a bare bones version of what I did so I can share it easily, but here's the concept:
I have two things going on here. The first is a Struct of serializable objects that I turn into XML and save using IsolatedStorage. The second is a class that I use to hold "SaveFile" data, like knowing if a slot is empty or not.
Here's the first:
using System.IO.IsolatedStorage;
using System.Xml.Serialization;
XmlSerializer serializer;
Player player;
int scoreTemp, experienceTemp, clownsFreedTemp;
Boolean playerHasShoesOnTemp;
List<SoccerBallWrapper> ownedSoccerBallsTemp;
//SoccerBallWrapper would be a custom wrapper class that I wrote to store data.
//Basically just a stripped down class that holds information about a GameObject that isn't serializable.
//For example, if you wanted to save the player's inventory of soccer balls, but the SoccerBall class
//has a bunch of methods and stuff. The wrapper would hold only data that needs to be saved
string containerName = "GameSaves";
SaveFile selectedSaveFile;
[Serializable]
public struct SaveGame
{
int score, experience, clownsFreed;
Boolean playerHasShoesOn;
List<SoccerBallWrapper> ownedSoccerBalls;
}
public SaveLoadManager(Player p)
{
player = p;
serializer = new XmlSerializer(typeof(SaveGame));
ownedSoccerBallsTemp = new List<SoccerBallWrapper>();
}
void SaveToDevice()
{
var dataFile = Global.FileManager.GetDefaultIsolatedStorageFileStore();
IsolatedStorageFileStream isolatedFileStream = null;
//Copy all game data that you want to save over to the Temp variables you created above.
this.scoreTemp = player.score;
this.experienceTemp = player.experience;
//Loop through player.ownedSoccerBalls and create a wrapper for it, then add it to ownedSoccerBallsTemp
//...
//...
//Add it to a new SaveGame object
SaveGame SaveData = new SaveGame()
{
experience = experienceTemp,
score = scoreTemp,
ownedSoccerBalls = ownedSoccerBallsTemp,
...
}
//Save it
if (dataFile.FileExists(selectedSaveFile.fileName))
{
dataFile.DeleteFile(selectedSaveFile.fileName);
}
using (isolatedFileStream = dataFile.CreateFile(selectedSaveFile.fileName))
{
// Set the position to the begining of the file.
isolatedFileStream.Seek(0, SeekOrigin.Begin);
// Serialize the new data object.
serializer.Serialize(isolatedFileStream, SaveData);
// Set the length of the file.
isolatedFileStream.SetLength(isolatedFileStream.Position);
}
dataFile.Close();
isolatedFileStream.Dispose();
}
void LoadFromDevice()
{
var dataFile = Global.FileManager.GetDefaultIsolatedStorageFileStore();
IsolatedStorageFileStream isolatedFileStream = null;
if (dataFile.FileExists(selectedSaveFile.fileName))
{
// Open the file using the established file stream.
using (isolatedFileStream = dataFile.OpenFile(selectedSaveFile.fileName, FileMode.Open, FileAccess.ReadWrite))
{
// Store the deserialized data object.
SaveGame SaveData = (SaveGame)serializer.Deserialize(isolatedFileStream);
//Extract the save data
player.score = SaveData.score;
player.experience = SaveData.experience;
//Loop through SaveData.ownedSoccerBalls and use the wrapper data to recreate the player's ownedSoccerBalls
}
dataFile.Close();
isolatedFileStream.Close();
}
}
That should take care of saving and loading. I did a lot of redundant things there to make the concept more clear. There's no reason to create temporary variables for basic types like integers or booleans, or anything you can save directly, really. You can just copy them directly to the SaveGame struct.
As for the second part, the SaveFile class:
public class SaveFile
{
public Boolean empty = true;
public String fileName;
public int score, experience, whatever;
public DateTime rawDateTime;
public void PrepareSavePreview(SaveGame SaveData)
{
// Use this to set your stats (score, experience, whatever).
// this.score = SaveData.score
}
}
I use this class to tell if a save slot is empty and to display some basic stats on the Save/Load game screen. Then I create a List in the first section I showed above.
If you want 3 Save Slots in your game, you would create 3 SaveFiles to add to that list and give them fileNames like "1.sav", "2.sav", "3.sav". When a player starts a new game you set the "selectedSaveFile" attribute from that first section to whatever slot they chose. Then you can save to the "selectedSaveFile.fileName". This way, when they play the game later, you can loop through however many save slots your game has...
foreach(SaveFile file in SaveFiles)
{
if(file.empty == false)
//Show some save data on screen and let them load it!
else
//Empty slot!
}
Welp, that was a lot. Hope it helps. I'll clarify anything if you need it. By the way, I based this originally off of https://gavindraper.com/2010/11/25/how-to-loadsave-game-state-in-xna/ which I read years ago. Then I modified it to work better with Monogame and my own purposes.
Edit: I should also add I have no idea how well this would work with mobile ports. We had to change some stuff for UWP and I'll most likely have to change things for other consoles. It's a start, anyway.