- Home /
Can a ScriptableObject contain a List of ScriptableObjects?
I have a ScriptableObject ("monster defn list") which contains a list of objects ("monster defns") - when those contained objects are [Serializable], everything works fine; but if I instead make them ScriptableObjects, then the Inspector complains with "mismatched type" when viewing them. Should this work? Note that I'm on the 5.4 beta at the moment.
To see the issue; the following code creates a new Editor Window that can be opened with Control+Shift+m - click the "create list" button and then click the "create defn" button a few times. Select the created asset (in /Assets/MonsterDefnList) in the inspector, and notice that the defns look fine (as they are currently [serializable]). now delete that asset, and uncomment the first line of the code below and repeat; now the inspector will instead tell you "Type Mismatch" for those defns.
// #define UseScriptableObject
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
#if UseScriptableObject
public class MonsterDefn : ScriptableObject
#else
[System.Serializable]
public class MonsterDefn
#endif
{
public int MaxHP;
}
public class MonsterDefnList : ScriptableObject
{
public List<MonsterDefn> MonsterDefns;
}
public class MonsterDefnEditor : EditorWindow
{
public MonsterDefnList monsterDefnList;
[MenuItem("Window/Monster Defn Editor %#m")]
static void Init()
{
EditorWindow.GetWindow(typeof(MonsterDefnEditor));
}
void OnEnable()
{
monsterDefnList = AssetDatabase.LoadAssetAtPath("Assets/MonsterDefnList.asset",
typeof(MonsterDefnList)) as MonsterDefnList;
}
void OnGUI()
{
if (GUILayout.Button("Create New MonsterDefn List")) {
monsterDefnList = ScriptableObject.CreateInstance<MonsterDefnList>();
monsterDefnList.MonsterDefns = new List<MonsterDefn>();
AssetDatabase.CreateAsset(monsterDefnList, "Assets/MonsterDefnList.asset");
AssetDatabase.SaveAssets();
}
if (GUILayout.Button ("Add MonsterDefn")) {
#if UseScriptableObject
MonsterDefn newMonsterDefn = ScriptableObject.CreateInstance<MonsterDefn>();
#else
MonsterDefn newMonsterDefn = new MonsterDefn ();
#endif
monsterDefnList.MonsterDefns.Add (newMonsterDefn);
}
if (GUI.changed) EditorUtility.SetDirty(monsterDefnList);
}
}
Answer by Bunny83 · Apr 02, 2016 at 05:42 AM
Yes, it is supported, however it won't work the way you use it. ScriptableObjects need to be explicitly saved as asset. They can't be serialized along with another ScriptableObject like "normal" serializable classes can. References to ScriptableObjects are actually serialized as asset references. That's why each ScriptableObject need to be an asset in your project.
Keep in mind that you can use AssetDatabase.AddObjectToAsset to add multiple assets into the same asset file. However due to the way Unity stores multiple assets in the same file it can happen that one of your "child" assets become the "main asset".
Thank you for your reply! I changed the code to use AddObjectToAsset, and sure enough ran into the child asset beco$$anonymous$$g the main asset; i.e. create the monsterdefnlist and add a few monsterdefns to it; then in the Project window expand the monsterdefnlist asset and (1) all of the child assets are of type monsterdefnlist (ins$$anonymous$$d of monsterdefn), and (2) the actual monsterdefnlist is the last child.
Am I misusing AddObjectToAsset? Here is the new approach:
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
public class $$anonymous$$onsterDefn : ScriptableObject {
public int $$anonymous$$axHP;
}
public class $$anonymous$$onsterDefnList : ScriptableObject {
public List<$$anonymous$$onsterDefn> $$anonymous$$onsterDefns;
}
public class $$anonymous$$onsterDefnEditor : EditorWindow {
$$anonymous$$onsterDefnList monsterDefnList;
[$$anonymous$$enuItem("Window/$$anonymous$$onster Defn Editor %#m")]
static void Init() {
EditorWindow.GetWindow(typeof($$anonymous$$onsterDefnEditor));
}
void OnEnable() {
monsterDefnList = AssetDatabase.LoadAssetAtPath("Assets/$$anonymous$$onsterDefnList.asset",
typeof($$anonymous$$onsterDefnList)) as $$anonymous$$onsterDefnList;
}
void OnGUI() {
if (GUILayout.Button("Create New $$anonymous$$onsterDefn List")) {
monsterDefnList = ScriptableObject.CreateInstance<$$anonymous$$onsterDefnList>();
monsterDefnList.$$anonymous$$onsterDefns = new List<$$anonymous$$onsterDefn> ();
AssetDatabase.CreateAsset(monsterDefnList, "Assets/$$anonymous$$onsterDefnList.asset");
AssetDatabase.SaveAssets();
}
if (GUILayout.Button ("Add $$anonymous$$onsterDefn")) {
$$anonymous$$onsterDefn new$$anonymous$$onsterDefn = ScriptableObject.CreateInstance<$$anonymous$$onsterDefn>();
monsterDefnList.$$anonymous$$onsterDefns.Add (new$$anonymous$$onsterDefn);
AssetDatabase.AddObjectToAsset (new$$anonymous$$onsterDefn, monsterDefnList);
AssetDatabase.SaveAssets();
}
if (GUI.changed) EditorUtility.SetDirty(monsterDefnList);
}
}
nb: If anyone else tries does the above and gets stuck on the inspector giving a "The associated script cannot be loaded..." error when you view the asset after exiting/restarting; it looks like you need to break the above classes out into their own cs files.
$$anonymous$$eep in $$anonymous$$d that ScriptableObjects have the same limitation as $$anonymous$$onoBehaviours. They need to be defined in their own file with the filename matching the class name. Otherwise Unity can't associate the class with a script asset.
In your code it seems you have all classes in the same file?!
Since the Editor and EditorWindow class are also derived from ScriptableObject it's the same for those.
@bunny83 yeah, figured that out and was editing the Q as you responded; I had just included them in one file for the purposes of this question and didn't know about the limitation :). Thanks for pointing it out though! Do you have any insight into how to avoid the child/main asset issue (or why it's happening)? Thanks again!
As far as i know there's no way to avoid it. The problem is that there is no parent / child relationship between multiple assets inside a single assetfile. If you switch the asset serialization mode to "force text" the asset file will be serialized as yaml text. That means you can open the asset in a normal text editor. If you do you will see that all assets you've added are simply listed one after the other. Unity simply treats the first asset in the file as main asset.
I'm not sure why Unity reorders the assets in the file, but it seems to depend on certain hardcoded sorting rules. It's a bit annoying. It would be nice if there was a way to specify the order of the assets or at least if it would preserve the initial order. I haven't really looked into the problem and if the order can be changed. You could try using different classnames and see if that affects the order.
Edit: Don't $$anonymous$$d this comment, I did not realized you already cracked the case on your answer bellow.
Original Comment: I don't know if it's still an issue for you. But I've found the parent/child reordering problem to be directly linked to the alphabetical order of the name of the assets. If your child's name comes before your parent's then the order will be bugged.
As a simple workaround the thing I do is adding the letter 'z' in front of all my children's name so that they always come last in the alphabetical order.
Answer by jeffsim · Apr 03, 2016 at 05:08 PM
On a related note, I believe I figured out what's causing the parent/child relationship issue as well as a workaround. It actually has to do with the naming of the asset file vs. the name of the subassets (!). I've logged a bug with Unity, but here's a simple test case that (a) repros the issue and (b) shows how to work around it:
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
// note to viewer: Unity needs this to be in a separate file, otherwise it'll
// fail to load; included in this file just for repro purposes.
public class MyItemDefn : ScriptableObject
{
}
public class MyItemDefnList : ScriptableObject
{
public List<MyItemDefn> Defns;
}
public class MyItemDefnEditor : EditorWindow
{
MyItemDefnList itemDefnList1;
MyItemDefnList itemDefnList2;
[MenuItem("Window/MyItemDefn Editor %#m")]
static void Init()
{
EditorWindow.GetWindow<MyItemDefnEditor>();
}
void OnGUI()
{
if (GUILayout.Button ("Run Test")) {
// The bug: When adding a ScriptableObject as a child of a ScriptableObject,
// the inspector can get confused about which is the parent and which
// is the child, and display the containing object as one of the children
//
// Repro'ed in: Unity 5.4.0b12 (beta), but it sounds like this has been
// around for a while.
//
// The reason this sometimes happens: If the filename of the main asset is
// alphabetically after the name of a child asset, then the inspector
// misidentifies which is which and the child asset appears as the main
// asset (+vice versa).
//
// To workaround: Name your main/containing asset's file something
// alphabetically before all child assets. e.g.: append with "aaa"
//
// This one will fail; the main asset appears as the last asset in the container
itemDefnList1 = ScriptableObject.CreateInstance<MyItemDefnList>();
itemDefnList1.Defns = new List<MyItemDefn> ();
createTestAsset ("zzzTestAsset", itemDefnList1);
// This one will succeed; the main asset will appear as the parent asset
itemDefnList2 = ScriptableObject.CreateInstance<MyItemDefnList>();
itemDefnList2.Defns = new List<MyItemDefn> ();
createTestAsset("aaaTestAsset", itemDefnList2);
AssetDatabase.SaveAssets();
}
if (GUI.changed) {
EditorUtility.SetDirty (itemDefnList1);
EditorUtility.SetDirty (itemDefnList2);
}
}
void createTestAsset(string assetName, MyItemDefnList itemDefnList)
{
// Save the container asset with the specified name
string path = AssetDatabase.GenerateUniqueAssetPath ("Assets/"+assetName+".asset");
AssetDatabase.CreateAsset(itemDefnList, path);
// Create some test items and add them to the container asset.
for (int i = 0; i < 5; i++)
{
MyItemDefn newMyItemDefn = ScriptableObject.CreateInstance<MyItemDefn> ();
newMyItemDefn.name = "jjj " + i;
itemDefnList.Defns.Add (newMyItemDefn);
AssetDatabase.AddObjectToAsset (newMyItemDefn, itemDefnList);
}
}
}
Pretty crazy, and a fun one to track down =). Thanks again to @bunny83!
Some people might find this useful: a StackOverflow question about characters that would sort last alphabetically and look cleaner than adding "z" at the end.
I've opted for using the character "一" at the front.
Your answer
Follow this Question
Related Questions
Unity inspector field overlap with eachother 1 Answer
Why doesn't my ScriptableObject save using a custom EditorWindow? 3 Answers
Custom Inspector for ScriptableObject 1 Answer
Show multiple attached scripts variables in one script in the inspector 0 Answers
Referencing / linking a .asset / .prefab file in another .asset / .prefab file programmatically. 2 Answers