- Home /
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
ScriptableObject
s would be lost on quit only?
Answer by Bunny83 · Jul 09, 2016 at 11:59 AM
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.
Hm, to be honest I really thought that wasn't the case - it was under the impression that any ScriptableObject referenced by a $$anonymous$$onoBehaviour that wasn't saved into the database was saved to the scene ins$$anonymous$$d.
The example code I posted for example allows me to add non-database ScriptableObjects to the $$anonymous$$onoBehaviour, save the scene, quit and reload, and they all persist. This blog post also suggests that's the case.
I can probably refactor it so it requires all data to be saved to assets, but I'd much prefer to work out why this is working in one place and not another - Unities serialization is not good for my blood pressure :P
Just found this thread where one of the Play$$anonymous$$aker developers was suffering from a similar problem - in his case everything saved correctly as in the example here, but when the GameObject
holding the $$anonymous$$onoBehaviour
was added to a prefab the array of ScriptableObject
was lost.
It certainly looks like whatever support there is for saving them into the scene is sketchy at best. I think I'll just refactor my system to require all inner assets to be saved to the database.
Thanks for your help
I've edited my answer.
The thread of the Play$$anonymous$$aker developers is way too old now. A lot has changed in Unity. Unity's way how prefabs and prefab instances work has changed completely.
In the past a prefab instance was a normal gameobject but has a reference to the prefab it was created from. Now a prefab instance in the scene doesn't contain the actual gameobjects / components / ... but only a reference to the prefab asset as well as a list of modifications.
That's amazing, thank you. That makes perfect sense to me, the objects themselves can serialize and deserialize hence nothing is lost when switching to play mode - that was the bit that was throwing me, normally when I get these problems it's play mode that causes the data to be lost. Then the editor doesn't know it needs to update the scene unless you flag it as such hence nothing retained on quit.
In my particular case most instances are only used once. Saving all the one offs as named files was getting really messy. This way I can keep the one offs in the scene, and provide a button to export them and make them reusable.
Woah, it must have been a while since I read the SetDirty documentation. Planned to be deprecated I see!
$$anonymous$$any thanks again for your help. :)