Skip to content

Custom Data

The SaveLoadManager class keeps track of all savable objects in the scene and collects their state in a background process so when the Save() method is invoked, it contains all the information required to successfully perfom the oepration.

In order to let the SaveLoadManager know what objects it needs to keep track of the developers need to implement the IGameSave interface on each object that contains data to save.

As soon as the object is available, it must call the Subscribe(reference: IGameSave, priority: int) method. Likewise, when the object is destroyed it should call Unsubscribe(reference: IGameSave).

The IGameSave interface

The IGameSave interface requires to fill the following methods and properties:

  • string SaveID: Gives an id that uniquely identifies this data
  • bool IsShared: Tells whether this data is shared across all save games
  • Type SaveType: Returns the type of the object to be serialized and stored
  • object SaveData: Returns the instance of the object that's going to be saved
  • LoadMode LoadMode: Define whether loading happens following a Greedy or a Lazy format
  • void OnLoad(object value): Callback for when the game is loaded

In order to understand better how this works, it's better to demonstrate this with an example.

Let's say that in our game we have one single chest in a scene that the player can only open once.

public class MyChest: MonoBehaviour
{
    public bool hasBeenOpened = false;

    public void OnOpen()
    {
        Debug.Log("Do something, like giving a potion to player");
        this.hasBeenOpened = true;
    }
}

In order to keep track of whether the chest has been opened or not, we implement the IGameSave interface on the component that defines the behavior of the chest:

public class MyChest: MonoBehaviour, IGameSave
{
    public bool hasBeenOpened = false;

    public void OnOpen()
    {
        if (this.hasBeenOpened) return;

        Debug.Log("Do something, like giving a potion to player");
        this.hasBeenOpened = true;
    }

    // The id for this save game is 'my-chest'
    public string SaveID => "my-chest";

    // This save should not be shared across multiple slots
    public bool IsShared => false;

    // The object type we're going to be saving
    public Type SaveType => typeof(bool);

    // The value we're going to store
    public object SaveData => this.hasBeenOpened;

    // The loading mode should be set as lazy
    public LoadMode LoadMode => LoadMode.Lazy;

    // When loading the game, restore the state
    public void OnLoad(object value)
    {
        this.hasBeenOpened = (bool)value;
    }
}

Most fields should be self explanatory. It is important to highlight though, that it's up to the developer to implement how the state is restored. The OnLoad(object value) is called when a game is loaded, and the value parameter is the value from a previously saved game. It's the developer's responsibility to cast the object value to a valid type and assign the values to whichever fields are necessary.

The Load Mode is a tricky concept. It's an enum that allows to choose between two options:

  • Lazy: This should be the default option for 90% of the cases. When this option is selected, the save and load system will restore the state of an object when this object is created. Not before.
  • Greedy: This requires a persistent object that survives cross-scene transitions (set as DontDestroyOnLoad() method). Most commonly used with singleton patterns, this mode forces the load as soon as the event is triggered.

Subscription

Now, all that's left to do is tell the SaveLoadManager to keep track of this component as soon as it's initialized, and unsubscribe from it when the component is destroyed. Following the previous example, we implement the OnEnable() and OnDisable() Unity methods to subscribe and unsubscribe respectively:

public class MyChest: MonoBehaviour, IGameSave
{
    public bool hasBeenOpened = false;

    void OnEnable()
    {
        _ = SaveLoadManager.Subscribe(this);
    }

    void OnDisable()
    {
        _ = SaveLoadManager.Unsubscribe(this);
    }

    // IGameSave implementation below
    // ...
}

This gives all the necessary information to the save and load system about the life-cycle of this object so it can keep track of its state progress. If your object is never destroyed and survives scene transitions, you can skip the un-subscription.

To wrap things up, here's the full script of the example:

public class MyChest: MonoBehaviour, IGameSave
{
    public bool hasBeenOpened = false;

    public void OnOpen()
    {
        if (this.hasBeenOpened) return;

        Debug.Log("Do something, like giving a potion to player");
        this.hasBeenOpened = true;
    }

    void OnEnable()
    {
        _ = SaveLoadManager.Subscribe(this);
    }

    void OnDisable()
    {
        _ = SaveLoadManager.Unsubscribe(this);
    }

    public string SaveID => "my-chest";
    public bool IsShared => false;

    public Type SaveType => typeof(bool);
    public object SaveData => this.hasBeenOpened;

    public LoadMode LoadMode => LoadMode.Lazy;

    public void OnLoad(object value)
    {
        this.hasBeenOpened = (bool)value;
    }
}

The hasBeenOpened property will always return false if the OnOpen() method has never been executed, but will return true if it has at some point. If the user saves and loads back the game, its value will be kept.