2
\$\begingroup\$

I'm making an effort to use ScriptableObjects more in my game architecture, after watching a fantastic Unity Unite talk on the subject. The central idea is to use ScriptableObjects, during runtime, as a kind of pluggable, mutable global data container. It's intended to replace things like the singleton pattern in a more testable, less tightly-coupled way.

I'm not going to go into detail on the arguments in favor of this, but I do definitely recommend watching the talk.

When using this pattern, you'll probably run into data-container-style ScriptableObjects that you want to persist between play sessions. The problem I'm running into is that, when Unity's serializer is run on a ScriptableObject that references other ScriptableObjects, and those referenced ScriptableObjects have changed during runtime, the serializer ignores the changes on those sub-SOs.

Is there a good way to serialize nested SOs containing data that changes during runtime?


For some examples/more in-depth background:

In the game I'm working on, I'm emulating a file system that multiple otherwise-independent game objects are interested in. It looks somewhat like this:

public abstract class FilesystemObject : ScriptableObject
{
    public string FileName;
}

[CreateAssetMenu(fileName = "NewDirectory.asset", menuName = "Directory")]
public class FilesystemDirectory : FilesystemObject
{
    public List<FilesystemObject> Contents;
}

[CreateAssetMenu(fileName = "NewTextFile.asset", menuName = "Directory")]
public class FilesystemTextFile : FilesystemObject
{
    public string Contents;
}

In my project directory, I have a FilesystemDirectory asset file named FSRoot.asset that represents the root directory of the filesystem. It's populated with all the files that need to exist when the player first launches the game.

If a MonoBehaviour wants to read or write to this filesystem (say, an in-game text editor), all I have to do is expose a FilesystemDirectory field to Unity's serializer, and then hook FSRoot up to that field using the inspector. Then, during runtime, if the player creates, edits, or deletes any filesystem data, those manipulations are done directly on the FSRoot object. Any other MonoBehaviour with the same reference to FSRoot sees the same mutated data, since those changes persist in memory.

However, unless you're in the editor, those changes do not persist between sessions. You have to serialize and deserialize the data manually for that. However, when I serialize FSRoot using Unity's JsonUtility, it only captures changes that have happened to FSRoot directly, and not any changes in subdirectories/files inside FSRoot. Any files that were authored in edit time are completely unchanged.

\$\endgroup\$
4
  • \$\begingroup\$ I appreciate the goal of eliminating singletons, but these quirks are exactly why I recommend only using immutable ScriptableObjects. A first step would be to print the data you're trying to serialize to the console to make sure you have the right references. If it prints correctly but nested objects don't serialize, it may be that the engine doesn't recognize that the SO is dirty. \$\endgroup\$
    – Kevin
    Commented Jul 7, 2020 at 22:51
  • 1
    \$\begingroup\$ You should probably share the code that serializes and deserializes the SOs, in case there's a problem with your serialization/deserialization code. \$\endgroup\$
    – Kevin
    Commented Jul 7, 2020 at 22:51
  • \$\begingroup\$ Did my answer solve your issue? \$\endgroup\$
    – Kevin
    Commented Jul 10, 2020 at 17:22
  • \$\begingroup\$ Having now worked on a project (originally built by another developer) that uses ScriptableObjects as a kind of pluggable, mutable data container, my feeling is that the downsides outweigh the benefits. This approach has most of the downsides of static global variables, but also introduces new downsides (e.g. it can be a big headache to determine which scripts are accessing and making changes to which ScriptableObjects). \$\endgroup\$
    – Kevin
    Commented Nov 12, 2020 at 0:03

1 Answer 1

2
\$\begingroup\$

IIRC Unity won't serialize a List where the data type is abstract.

Your FilesystemDirectory class has one field:

public List<FilesystemObject> Contents;

FilesystemObject is an abstract class, which Unity can't serialize as-is. Unfortunately Unity's built-in serialization does not work well with abstraction.

You could try adding the [SerializeReference] attribute in front of the field:

[SerializeReference] public List<FilesystemObject> Contents;

That can get Unity to serialize interfaces, but I'm not sure if it supports abstract base classes. If it doesn't work, you may have to either stop using an abstract base, or write a little custom code to copy the data into a structure that can be serialized. Or I believe you can serialize and deserialize using .NET instead of Unity, e.g.

public bool SaveFile(object obj, string path) {
    FileStream stream = null;
    try {
        stream = new FileStream(path, FileMode.Create);
        BinaryFormatter formatter = new BinaryFormatter();
        formatter.Serialize(stream, obj);
    } catch (Exception ex) {
        Debug.LogException(ex);
    } finally {
        if (stream != null) stream .Close();
    }
}
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .