[HOWTO] How To: Write Custom Savers

Announcements, support questions, and discussion for Quest Machine.
Post Reply
User avatar
Tony Li
Posts: 20680
Joined: Thu Jul 18, 2013 1:27 pm

[HOWTO] How To: Write Custom Savers

Post by Tony Li »

You can add your own saver scripts to the Pixel Crushers save system. This is a brief discussion of how to write custom saver scripts.

To do so, duplicate the script Plugins / Pixel Crushers / Common / Templates / SaverTemplate.cs. Move it outside the Plugins folder. A good place is in the same folder as the script whose data you want to save.


Simple Bool Saver
Here's an example of saving a bool on a custom script. The script:

Code: Select all

public class Lightswitch : MonoBehaviour
{
    public bool isOn;
}
The saver:

Code: Select all

using UnityEngine;
using PixelCrushers;
[RequireComponent(typeof(Lightswitch))]
public class LightswitchSaver : Saver
{
    public override string RecordData()
    {
        // Return "1" if isOn is true, "0" otherwise.
        return GetComponent<Lightswitch>().isOn ? "1" : "0";
    }
    
    public override void ApplyData(string s)
    {
        if (string.IsNullOrEmpty(s)) return; // If we didn't receive any save data, exit immediately.
        GetComponent<Lightswitch>().isOn = (s == "1");
    }
}

Saving Multiple Variables
Here's a more complex example that uses a serializable class named Data to save multiple variables:

Code: Select all

public class PlayerStats : MonoBehaviour
{
    public int health;
    public int mana;
}
The saver:

Code: Select all

using System;
using UnityEngine;
using PixelCrushers;
[RequireComponent(typeof(PlayerStats))]
public class PlayerStatsSaver : Saver
{
    [Serializable]
    public class Data
    {
        public int health;
        public int mana;
    }
    
    public override string RecordData()
    {
        var playerStats = GetComponent<PlayerStats>();
        var data = new Data();
        data.health = playerStats.health;
        data.mana = playerStats.mana;
        return SaveSystem.Serialize(data);
    }
    
    public override void ApplyData(string s)
    {
        if (string.IsNullOrEmpty(s)) return; // If we didn't receive any save data, exit immediately.
        var data = SaveSystem.Deserialize<Data>(s);
        if (data == null) return; // If save data is invalid, exit immediately.
        var playerStats = GetComponent<PlayerStats>();
        playerStats.health = data.health;
        playerStats.mana = data.mana;
    }
}
Alternatively, you can make your script a subclass of Saver and implement the RecordData() and ApplyData() methods in them:

Code: Select all

using System;
using UnityEngine;
using PixelCrushers;

public class PlayerStats : Saver
{
    public int health;
    public int mana;
    
    [Serializable]
    public class SaveData
    {
        public int health;
        public int mana;
    }
    
    public override string RecordData()
    {
        var data = new SaveData();
        data.health = health;
        data.mana = mana;
        return SaveSystem.Serialize(data);
    }
    
    public override void ApplyData(string s)
    {
        if (string.IsNullOrEmpty(s)) return; // If we didn't receive any save data, exit immediately.
        var data = SaveSystem.Deserialize<SaveData>(s);
        if (data == null) return; // If save data is invalid, exit immediately.
        health = data.health;
        mana = data.mana;
    }
}
However, I usually prefer to keep the responsibilities separate. This keeps the code of each script shorter and easier to read.


Serialization Notes
By default, the save system uses Unity's JSON serialization, which can serialize an object (such as an instance of a class) into a string, and later deserialize that string back into an object. (You can switch to other serialization methods by swapping out the Save System GameObject's JsonDataSerializer component with a different DataSerializer component.)

Unity's JSON serialization can serialize primitive types (bool, int, string, etc.) and lists. It can't serialize ScriptableObject references. That means it can serialize this:

Code: Select all

[System.Serializable] // Mark this class as serializable.
public class InventorySaveData
{
    public List<string> itemNames;
}
but it can't serialize this:

Code: Select all

[System.Serializable] // Mark this class as serializable.
public class InventorySaveData // Note: This class won't work with save system.
{
    public List<ItemScriptableObject> items; //<-- This will be serialized as a list of NULLs.
}
Say you have an inventory system that has a list of ItemScriptableObject objects and an amount of money. You can't save the ItemScriptableObject list directory. But, in your saver's RecordData() method, you can save a list of the names of those items and the money amount:

Code: Select all

[System.Serializable] // Mark this class as serializable.
public class InventorySaveData
{
    public int money;
    public List<string> itemNames;
}

public override string RecordData()
{
    var inventory = GetComponent<Inventory>(); // Example -- get your inventory system script.
    var saveData = new InventorySaveData();
    saveData.money = inventory.money;
    saveData.itemNames = new List<string>();
    foreach (var item in inventory.items)
    {
        saveData.itemNames.Add(item.name);
    }
    return SaveSystem.Serialize(saveData); // Return serialized data.
}
To restore your inventory in the ApplyData() method, you need a way to get a ScriptableObject item from its name. For example, you could put all your item SOs in a Resources folder and use Resources.Load:

Code: Select all

public override void ApplyData(string s)
{
    if (string.IsNullOrEmpty(s)) return; // If this isn't valid save data, skip.
    var inventory = GetComponent<Inventory>();
    var saveData = SaveSystem.Deserialize<InventorySaveData>(s);
    if (saveData == null) return; // If we fail to deserialize it, skip.
    inventory.money = saveData.money;
    inventory.items = new List<ItemScriptableObject>();
    foreach (var itemName in saveData.itemNames)
    {
        var item = Resources.Load<ItemScriptableObject>(itemName);
        inventory.items.Add(item);
    }
}
This is a simplified example to illustrate the basic approach to saver scripts. An actual implementation will probably need to record more data. For example, your inventory system might have a quantity for each item in the inventory, so you'd need to save that also.
Post Reply