ScriptableObject stored in MonoBehaviour lost on quit

This comes from a large complicated project, so I have tried to prepare a shorter code example. However, the provided example works correctly. I’ve been back and forward over it, and I can’t see how it differs, so I’m really just looking for any possible causes of the problem:

  • A field holding an array of ScriptableObject fails to retain data when you quit and reload Unity.

I have an array of ScriptableObject held on a MonoBehaviour. In certain circumstances these are also saved into the AssetDatabase, hence the use of ScriptableObject. For this problem, they are not saved to the database.

A custom editor allows you to create new instances, and edit the data on each item. This works fine, and when you run the game the data is not lost. The data is only lost when you quit and reload Unity.

There is a lot other data held on and under the MonoBehaviour which does not suffer from this problem. However this is the only data which extends from ScriptableObject. What’s confusing me mostly is the field holding the array itself seems to be lost rather than the individual items - on load the array is completely empty, rather than being full of null.

The following code I wrote in an attempt to isolate the problem - this should reflect what the main code is doing exactly, but frustratingly this works exactly as expected.

Test001.cs

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class Test001 : MonoBehaviour
{
    [SerializeField]
    private Test001Item[] items;

    public IEnumerable<Test001Item> Items
    {
        get
        {
            if (this.items == null) { this.items = new Test001Item[0]; }
            return this.items;
        }
    }

    public Test001Item AddItem()
    {
        Test001Item item = ScriptableObject.CreateInstance<Test001Item>();

        List<Test001Item> items = this.Items.ToList();
        items.Add(item);
        this.items = items.ToArray();

        return item;
    }
}

Test001Item.cs

using System;
using UnityEngine;

[Serializable]
public class Test001Item : ScriptableObject
{
    [SerializeField]
    private string data;

    public string Data { get { return this.data; } set { this.data = value; } }
}

Editor/Test001Editor.cs

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Test001))]
public class Test001Editor : Editor
{
    public static void DrawInspector(Test001 test)
    {
        GUI.changed = false;

        if (GUILayout.Button("Add")) { test.AddItem(); }

        foreach (Test001Item item in test.Items)
        {
            Test001ItemEditor.DrawInspector(item);
        }

        if (GUI.changed) { Test001Editor.SaveData(test); }
    }

    public override void OnInspectorGUI()
    {
        Test001 test = (Test001)this.target;
        Test001Editor.DrawInspector(test);
    }

    private static void SaveData(Test001 test)
    {
        EditorUtility.SetDirty(test);
        foreach (Test001Item item in test.Items)
        {
            EditorUtility.SetDirty(item);
        }
    }
}

Editor/Test001ItemEditor.cs

[CustomEditor(typeof(Test001Item))]
public class Test001ItemEditor : Editor
{
    public static void DrawInspector(Test001Item item)
    {
        item.Data = EditorGUILayout.TextField(item.Data);
    }
}

As I say, this does not recreate this issue, but I can’t see what the difference is between this and my main code. As such I’m just looking for any possible causes of the problem.

  • Does anyone have any ideas as to why an array of ScriptableObjects would be lost on quit only?

ScriptableObjects always have to be serialized on their own as asset if you want it to persist. There’s no way around using AssetDatabase and store it somewhere as asset. If you want the Item class to be serialized along with the MonoBehaviour you can’t derive it from ScriptableObject.

ScriptableObject represent assets and therefore if another serialized object (such as a MonoBehaviour) as a reference to a ScriptableObject, only the assetID will be saved as assetreference, just like one MonoBehaviour references another. Each standalone asset need to be stored somewhere in the project if you want it to persist.

If you use the CreateAssetMenu attribute the user can create an asset of that scriptable object via the create asset menu, If you want to create the instances manually in a custom inspector / EditorWindow you have to care about that yourself. So you have to use the AssetDatabase class to either save it as standalone asset or add it to an existing asset.

edit
Yes, you’re partly right ^^. It seems Unity does serialize ScriptableObject instances into the scene (actually as MonoBehaviour without a gameobject parent). However the way you try to save your changes is wrong. SetDirty doesn’t work for scene objects as you can read in the documentation. You should read it carefully.

If you implement a manual custom inspector like you did you might want to use EditorSceneManager.MarkSceneDirty or Undo.RecordObject which both would mark the scene as dirty.

It’s usually the best to use Undo.RecordObject like this:

if (GUILayout.Button("Add"))
{
    Undo.RecordObject(test, "Added new Item");
    test.AddItem();
}

This should work. “Undo.RecordObject” has to be called before you apply any changes. Unity does compare the state of the object at the end of the inspector call with the recorded state. It also adds the object to the undo stack.

You might also want to use Undo.RegisterCreatedObjectUndo for the newly instantiated ScriptableObject.