Sometimes a game needs to save a lot of state information. This is very commonly done with a xml or json parser, however text based parsing can be fairly slow and messy as it continues to grow with more complex projects. The solution I have for save and load is entirely parsed as binary objects, and data lookup is done through references to a Unity ScriptableObject. This solution has the advantage of being 100% backwards compatible between different versions of save data with no extra version migration work needed, while also being very easily extensible as the project grows.
Another benefit of tying anything to a ScriptableObject in Unity is that you are now able to create object instances natively in the engine UI. For example, this is a context menu from my Unity projects:

So if I want to have a boolean flag in my save data, I right click and go to Phantom->SaveData->Flag and create my save data object. This new flag would then look like this in the UI:

The two important things to point out here are “Guid” and “Default Value.” Default value is pretty self explanatory, but in addition; every save data object contains a Guid that is generated at object creation. This key is used for easy serialization and lookup at runtime. For serialization purposes we need a unique identifier, so I went with a generated Guid to ensure that the identifier will always be guaranteed unique.
OK, now we have a save data object and can pass it around to our various scripts as a serialized parameter. Just to show how easy these are to use, we may have a class with code that looks something like this:
[SerializeField]
private SaveFlag _myFlag = null;
public bool MyFlagValue
{
get
{
return _myFlag.Value;
}
set
{
_myFlag.Value = value;
}
}
SaveFlag itself does all the heavy lifting in terms of saving and loading actual cache information of our boolean flag. These operations are not necessarily free, as we saw earlier we are using a guid as a save data key, with the underlying implementation here being a Dictionary. However, the high level usage of the save data interfaces is all this is, not much else to be done when making use of it within a project.
With the high level interface out of the way, let’s take a look at what is happening underneath, starting with what “Value” on the SaveFlag is actually doing:
public bool Value
{
get
{
return (SaveDataSerializer.GetSaveData(this) as FlagData).Value;
}
set
{
(SaveDataSerializer.GetSaveData(this) as FlagData).Value = value;
}
}
As you can quickly see from the above code sample, this “Value” property is actually quite a bit more complicated than it may appear. SaveDataSerializer is a singleton that actually maintains runtime values and ties them to our earlier Guid keys. Since a ScriptableObject cannot contain mutable information, runtime values for a boolean flag are actually saved as a separate FlagData type that is being accessed through SaveDataSerializer instead.
The final interesting parts of this system are the actual serialization. How do we write our save data to a file, and how do we read it back in. Writing is fairly straightforward and I won’t go into too much detail, but in short, we need to create a file somewhere and write out all the contents of our persistent data table. The data that we write out is exactly what we need to recreate the table at runtime. That means we need our Guid, we need the data itself, and some kind of identifier that tells us the runtime class that stores this data. For the identifier, I have a simple enum of possible data types and save that information by casting relevant enum values to an int.
When it comes to loading that data back in, it’s not a whole lot more complicated. We know the exact size of our Guid and data type identifier, so we load those bits first, then allow the data storage class to parse out what it needs immediately after.
// binReader is a C# BinaryReader instance
// nameLength should a constant for string length of our Guid's
// name length appears to be good, load save data name identifier
string name = new string(binReader.ReadChars(nameLength));
// determine the type of this save data blob
int type = binReader.ReadInt32();
// create corresponding in memory instance of save data
bool errorOccurred = false;
SaveVariable.Data data = null;
switch((SaveDataType)type)
{
// create an instance of our flag data and
// serialize flag specific data all at once
case SaveDataType.Flag:
errorOccurred = !SaveVariable.CreateSaveData<SaveFlag.FlagData>(
binReader,
out data
);
break;
// add any other data types as similar
}
if (!errorOccurred)
{
AddSaveData(name, data);
}
This entire example has been done with a binary save file, however there’s no reason it couldn’t use the more popular xml, json, or other special formats like sql underneath. The main idea was to keep this very simple and allow users to easily track exactly how much memory the final save file takes up. Some platforms demand preallocating save data on the disc at install time, so being able to easily track that with a stable binary format can save you from a lot of headaches up front; as opposed to a text based format like xml or json where the size may not always be the same.