[HOWTO] How To: Write Custom Savers
Posted: Thu Aug 12, 2021 7:41 am
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:
The saver:
Saving Multiple Variables
Here's a more complex example that uses a serializable class named Data to save multiple variables:
The saver:
Alternatively, you can make your script a subclass of Saver and implement the RecordData() and ApplyData() methods in them:
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:
but it can't serialize this:
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:
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:
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.
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;
}
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;
}
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;
}
}
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;
}
}
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;
}
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.
}
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.
}
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);
}
}