Unity
How to Save & Load your Game in Unity?
Building a Persistence System
This blog is a quick guide on how I built a save & load mechanism for my upcoming game, Slime Hunter.
There are several ways of building a persistence system in Unity. The most simplest solution provided by Unity for this is to use PlayerPrefs. PlayerPrefs can be used to store string, float, and integer values in a local registry, without encryption. This information persists between game sessions and hence can be used to save some small preference data. For example, you can store the settings adjusted by player, like volume, graphics settings, etc.
To build a full fledged Save & Load mechanism, you need to build a Persistence System.
Pre-requisites
Basic understanding of Unity and a knack for learning :)
The Idea
I was building this Persistence System for a 3D open-world action RPG game called Slime Hunter. Like any other game, this game also consists of a lot of different Components. Each component contains different type of data. A singleton "PersistenceManager" can not possibly know how to save each and every component. So, the idea was to build the Persistent System such that each Component can save & load information relevant to it by itself.
When it is time to Save:
PersistenceManager asks each PersistentObject to generate a "save" file.
PersistenceManager saves this file in the file storage. Each PersistentObject saves a separate file (reasoning explained later -- UUID).
When it is time to Load:
PersistenceManager loads each data file from the file storage.
Each PersistentObject gets its relevant data file and loads data from it.
IPersistent Interface Class
Each component in Unity inherits from MonoBehaviour to get the component properties. I created an interface class called IPersistent and used it to create a class called PersistentObject.
The interface class simply ensures that each PersistentObject class defines what to save.
Code: (ThatDNS) Github: Persistence/IPersistent.cs
public interface IPersistent
{
// Serialize the relevant data and return it
public byte[] GetSaveData();
// Parse the serialized data and load it into object properties
public void LoadSaveData(byte[] data);
}
PersistentObject Abstract Class
PersistentObject inherits from MonoBehaviour and IPersistent. Any component can become a PersistentObject.
Features of PersistentObject:
Generate a UUID for uniquely identifying each persistent object instance in the game.
(IMPORTANT) Each persistent object saves a different file. To uniquely identify this saved file, I've used UUID.
How do I store this UUID? This part is clever. I use Unity's meta files by using the inspector window :)
Unity can store values of serialized properties of a Component in the Scene's meta file. This value never changes in the game, unless changed manually. Whenever a PersistentObject gets added to a GameObject, we'll set its UUID to a unique value and this value will always persist and stay unique. Simple!Ensure that PersistenceManager knows about this PersistentObject.
PersistenceManager is a Singleton. In Awake(), we store the PersistentObject in the PersistenceManager. On Destroy, we remove it.
Code: (ThatDNS) Github: Persistence/PersistentObject.cs
public abstract class PersistentObject : MonoBehaviour, IPersistent
{
[SerializeField] string uuid; // Don't forget to set this in the Inspector!
protected virtual void Awake()
{
if (string.IsNullOrEmpty(uuid))
{
uuid = Guid.NewGuid().ToString();
}
PersistenceManager.Instance.RegisterPersistent(this);
}
protected virtual void OnDestroy()
{
PersistenceManager.Instance.UnregisterPersistent(this);
}
public abstract byte[] GetSaveData();
public abstract void LoadSaveData(byte[] data);
}
PersistenceManager
PersistenceManager is a simple Singleton class which contains a list of PersistentObject.
It contains 2 important methods:-
SaveGame
This function ends up asking each PersistentObject to save itself. I'm saving the files in binary format to protect these files. You can go ahead and encrypt them but I don't think that is necessary. Using my approach, your game can easily save hundreds of small save files. For someone trying to hack your game, finding the relevant saved file among these can be a nightmare!
public void SaveGame()
{
foreach (PersistentObject obj in persistentGameObjects)
{
byte[] data = obj.GetSaveData();
string path = Path.Combine(Application.persistentDataPath, SAVE_FOLDER, $"{obj.GetUUID()}.dat");
File.WriteAllBytes(path, data);
}
}
LoadGame
This function uses each PersistentObject's UUID to determine its saved file. It loads the file and passes the contents to the PersistentObject. Rest all responsibility of actually loading the data falls upon the relevant PersistentObject.
(Note that this is not "actually" a PersistentObject object. It is a derived class of PersistentObject.)
public void LoadGame()
{
foreach (PersistentObject obj in persistentGameObjects)
{
string path = Path.Combine(Application.persistentDataPath, SAVE_FOLDER, $"{obj.GetUUID()}.dat");
if (File.Exists(path))
{
byte[] data = File.ReadAllBytes(path);
obj.LoadSaveData(data);
}
}
}
Code: (ThatDNS) Github: PersistenceManager.cs
You can call SaveGame() with a Coroutine to auto-save your game at regular intervals.
This system currently allows only 1 saved game in a PC, but this can be easily extended to allow multiple saved games. Just add another subfolder after the "SAVE_FOLDER" for each new saved game.
And, that is it! Now you have a pretty robust save and load mechanism. So go ahead and build something awesome! 😁