Curious case of serialization, Unity and refactoring

In my experience, the best way to solve this is to change the way you think about how saving works, and acknowledge the fact that runtime state is distinct from persistent state. Once you acknowledge that, and store your persistent state separately from your runtime state, it becomes a lot easier to figure out how to save.

Hopefully, I can make this concrete and easy for the Unity use case.

TL;DR: Split up your save data and your runtime data, and take advantage of how easy it is to save and load ScriptableObject.

Right now, you likely have gameplay state that lives in your MonoBehaviour component classes, right? Stored in their private fields, and whatnot. This data is your RUNTIME state. It's the state of the app that's in memory at the moment.

All of your data at the moment, according to your description, fits this model. That is, you aren't making a distinction in your code between runtime state and persistent state. That makes it really hard to serialize it and save, like you said, because serializing MonoBehaviours is somewhat fraught with peril.

What you want to consider doing is separating all of the state that you want to save into something you CAN easily serialize. Unity provides a super useful tool for this right out of the box: ScriptableObject. ScriptableObjects can be trivially loaded and saved to JSON with a single method call, and they are basically designed precisely for your use case. Lemme sketch a clear example.

Let's say we have a component like so:

``` public class PenguinBrain : MonoBehaviour { // This is some data you want to save. private int _fishCount; private int _currentHp;

// here is other code, other fields, stuff you don't wanna save

} ```

This is tough to save, for all the reasons you've laid out in your post.

But what if instead, we create a serializable structure to hold the data that we need to save for each PenguinBrain? Then we stick one of those inside PenguinBrain, and let it keep using the data the same way, just now it's inside a little struct.

Now, when it's time to save, we stuff the contents of that struct into our ScriptableObject, and we save that whole object to JSON.

When we wanna load, we just load the JSON into our ScriptableObject, iterate over the saved penguin data, create instances of all our saved penguins, and set them up with the data we saved.

I'll sketch some more of this in code, because I think it will help to see what's happening.

``` // PenguinBrainSaveData.cs [Serializable] public struct PenguinBrainSaveData { public string Id; public int FishCount; public int CurrentHp; }

// PenguinBrain.cs public class PenguinBrain : MonoBehaviour { private PenguinBrainSaveData _saveData;

public PenguinBrainSaveData SaveData => _saveData;

public void Load(PenguinBrainSaveData saveData)
{
    _saveData = saveData;
}

// here is other code, other fields, stuff you don't wanna save

}

// PenguinSaveFile.cs public class PenguinSaveFile : ScriptableObject { [SerializeField] private List<PenguinBrainSaveData> _penguinBrains = ...

public List<PenguinBrainSaveData> PenguinBrains => _penguinBrains;

public void Save(PenguinBrainSaveData data)
{
    int index = _penguinBrains.Find(it => it.Id == data.Id);

    if (index == -1)
    {
        _penguinBrains.Add(data);
    }
    else
    {
        _penguinBrains[index] = data;
    }
}

private string DataFilePath => $"{Application.persistentDataPath}/penguins.json";

public void SaveToDisk()
{
    string serializedJson = JsonUtility.ToJson(this);
    File.WriteAllText(DataFilePath, serializedJson, Encoding.UTF8);
}

public static PenguinSaveFile LoadFromDisk()
{
    var instance = ScriptableObject.CreateInstance<PenguinSaveFile>();
    string serializedJson = File.ReadAllText(DataFilePath, Encoding.UTF8);
    JsonUtility.FromJsonOverwrite(serializedJson, instance);
    return instance;
}

}

// PenguinManager.cs public class PenguinManager : ... { [SerializeField] private GameObject _penguinPrefab;

private List<PenguinBrain> _penguinBrains = ...

public void Save()
{
    var saveFile = ScriptableObject.CreateInstance<PenguinSaveFile>();

    foreach (var penguinBrain in _penguinBrains)
    {
        saveFile.Save(penguinBrain.SaveData);
    }

    saveFile.SaveToDisk();
}

public void Load()
{
    var saveFile = PenguinSaveFile.LoadFromDisk();

    foreach (var saveData in saveFile.PenguinBrains)
    {
        var penguinBrain = Instantiate(_penguinPrefab)
            .GetComponent<PenguinBrain>();

        penguinBrain.Load(saveData);

        _penguinBrains.Add(penguinBrain);
    }
}

} ```

Hopefully you can see how you can apply this pattern generically to any kind of data that you want to save. I skipped a lot of error handling and abstraction to make it easier to see the pattern.

I hope that helps!

/r/gamedev Thread