Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Serialized Objects, Generic Arrays and Inheritance

Discussion in 'Scripting' started by CDF, Mar 3, 2014.

  1. CDF

    CDF

    Joined:
    Sep 14, 2013
    Posts:
    1,283
    I've been struggling with trying to create a proper serialized editor for my objects. Specifically, generic arrays/lists which contain different sub classes of a base abstract class.

    If it's not possible at all to have a serialized generic array which contains different classes, then that's fine. I'll move on.
    If it is, I'd love to hear how ;)

    I would love to be able to:
    • Add custom instances to my serialized array *Not InsertNewElement()
    • Have a abstract base class with child classes properly serialized

    Thanks
     
  2. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,716
    Base class that support polymorphism and that you have access;

    - ScriptableObject
    - MonoBehaviour

    Anything else will revert to its base class when deserialized.

    Be careful with ScriptableObject, as they do not follow when you create a prefab or duplicate a GameObject. ScriptableObject should be use mostly when saving Asset on the disk.

    MonoBehaviour should be used for the rest, and yes, it means adding component to your GameObject. Here, we solved that by rewriting the Inspector so that "components" added in a list or other polymorphic fields are not displayed as script, but as "child" of its owner.
     
  3. CDF

    CDF

    Joined:
    Sep 14, 2013
    Posts:
    1,283
    Still doesn't work for me. Here's some test code, all values are always "Null"

    Code (csharp):
    1.  
    2. using System;
    3. using UnityEditor;
    4. using UnityEngine;
    5. using System.Collections.Generic;
    6.  
    7. [Serializable]
    8. class TestBaseClass : ScriptableObject {
    9.  
    10.     public enum Type {
    11.         None, Type1, Type2
    12.     }
    13.  
    14.     public string name = "my name";
    15.    
    16.     public virtual Type type {
    17.    
    18.         get { return Type.None; }
    19.     }
    20. }
    21.  
    22. [Serializable]
    23. class TestSubClass1 : TestBaseClass {
    24.  
    25.     public bool myBool = true;
    26.    
    27.     public override Type type {
    28.    
    29.         get { return Type.Type1; }
    30.     }
    31. }
    32.  
    33. [Serializable]
    34. class TestSubClass2 : TestBaseClass {
    35.  
    36.     public int myInt = 1;
    37.    
    38.     public override Type type {
    39.    
    40.         get { return Type.Type2; }
    41.     }
    42. }
    43.  
    44. class Container : MonoBehaviour {
    45.  
    46.     public List<TestBaseClass> list;
    47. }
    48.  
    49. [CustomEditor(typeof(Container))]
    50. class ContainerEditor : Editor {
    51.  
    52.     void OnEnable() {
    53.  
    54.         if ((target as Container).list == null) {
    55.  
    56.             (target as Container).list = new List<TestBaseClass>();
    57.         }
    58.     }
    59.  
    60.     public override void OnInspectorGUI() {
    61.  
    62.         serializedObject.Update(); 
    63.        
    64.         if (GUILayout.Button("Add Class 1")) {
    65.        
    66.             (target as Container).list.Add(TestSubClass1.CreateInstance<TestSubClass1>());
    67.         }
    68.        
    69.         if (GUILayout.Button("Add Class 2")) {
    70.        
    71.             (target as Container).list.Add(TestSubClass2.CreateInstance<TestSubClass2>());
    72.         }
    73.        
    74.         SerializedProperty list = serializedObject.FindProperty("list");
    75.        
    76.         for (int i = 0; i < list.arraySize; i++) {
    77.        
    78.             SerializedProperty item = list.GetArrayElementAtIndex(i);          
    79.            
    80.             Debug.Log(item.FindPropertyRelative("name")); //Null
    81.            
    82.             SerializedProperty type = item.FindPropertyRelative("type"); //Null
    83.            
    84.             if (type != null) {
    85.            
    86.                 TestBaseClass.Type typeEnum = (TestBaseClass.Type)item.FindPropertyRelative("type").enumValueIndex;
    87.  
    88.                 switch (typeEnum) {
    89.  
    90.                     case TestBaseClass.Type.Type1:
    91.  
    92.                         Debug.Log(item.FindPropertyRelative("myBool")); //Null
    93.                         break;
    94.  
    95.                     case TestBaseClass.Type.Type2:
    96.  
    97.                         Debug.Log(item.FindPropertyRelative("myInt")); //Null
    98.                         break;
    99.                 }
    100.             }
    101.         }
    102.        
    103.         serializedObject.ApplyModifiedProperties();
    104.     }
    105. }
    106.  
     
    Last edited: Mar 3, 2014
  4. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
  5. CDF

    CDF

    Joined:
    Sep 14, 2013
    Posts:
    1,283
    Still no luck, added the hideFlags, removed and re-attached test component. Values are still Null.

    The only thing that works is creating a new SerializedObject for the SerializedProperty:

    Code (csharp):
    1.  
    2. SerializedProperty item = list.GetArrayElementAtIndex(i);
    3. SerializedObject itemObject = new SerializedObject(item.objectReferenceValue);
    4.  
    5. itemObject .FindPropertyRelative("myBool"); //Returns true. BUT, can never be changed because the above line creates a new SerializedObject
    6.  
    It looks like Unity is just putting that class into an object reference rather than in the serialized array.
    I believe the problem is Adding a new object to the array:

    Code (csharp):
    1.  
    2. (target as Container).list.Add(TestSubClass2.CreateInstance<TestSubClass2>()); //this seems wrong
    3. list.InsertArrayElementAtIndex(list.arraySize); //proper way, but you can't set the type, will always be the BaseClass
    4.  
     
  6. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,716
    Works perfectly with me.



    The above screenshot was taken after a save/reload of a map with a GameObject having the Container script.

    However, I think I spotted your error...

    EACH class deriving from ScriptableObject or MonoBehaviour MUST exist in its own file, and that file MUST be named with the same name as the class.

    Ex.: Container must be inside Container.cs, TestSubClass2 must be in TestSubClass2.cs, and so on.

    When Unity serialize an object, it cannot serialize its type (it could save its AssemblyQualifiedName, but it doesn't because it would only work for C#). Instead, it serialize only its class name, and retrieve the proper type by finding the code file with the same name. Yes, it's horribly dumb, but it's how it works.
     
  7. CDF

    CDF

    Joined:
    Sep 14, 2013
    Posts:
    1,283
    yeah, my files are in separate classes named correctly. My running example looks like yours, but it's public fields are all Null.

    If "myBool" and "myInt" are public shouldn't they show up when calling this function?:

    Code (csharp):
    1.  
    2. EditorGUILayout.PropertyField(item, true); //This only shows the script name
    3. Debug.Log(item.FindPropertyRelative("myBool")); //should say true in the example above if the item is a TestSubClass1 instance
    4.  
     
  8. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,716
    Frankly, no idea... Since I don't really use SerializedProperty/SerializedObject.

    Just to be sure that serialization was occuring correctly, I plugged in my own Inspector;



    This was after a save/reload... And the Int and Bool stayed the way they were.

    Maybe you could stick with the default inspector, save your ScriptableObject on the disk as .Asset, or replace them with MonoBehaviour, which would show up as component you could still add to a list.
     
  9. CDF

    CDF

    Joined:
    Sep 14, 2013
    Posts:
    1,283
  10. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,716
    I think you shouldn't give up, because it does work. Hell, I use it daily!

    Have you tried to cast your type and directly access the variable?
     
  11. CDF

    CDF

    Joined:
    Sep 14, 2013
    Posts:
    1,283
    http://www.codingjargames.com/blog/2012/11/30/advanced-unity-serialization/

    This guy explains a fix, much like what you suggested with adding MonoBehaviours.
    I'm only wanting to use SerializedObjects for the undo/redo stuff. But I can live without it.

    I can't cast the type because item is a SerializedProperty, unless there's some way to find the object it references.
    Accessing the item directly from the list doesn't help as modifying that doesn't add undo/redo commands Or even notify the prefab of a change.

    I'm not prepared to hack my code to support this feature just yet. Hopefully the unity team will take enough notice and fix it.
     
  12. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,716
    You can add Undo to just about everything...
     
  13. CDF

    CDF

    Joined:
    Sep 14, 2013
    Posts:
    1,283
    yeah, serialized properties just seemed like a nice simple way to do so.
     
  14. Zero_Xue

    Zero_Xue

    Joined:
    Apr 18, 2012
    Posts:
    126
    i had this in the past, unfortunately unity doesn't like serializing classes that have a custom class or enum inside.
     
  15. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,716
    Seriously, what are you talking about? I have dozen of them at work and all of them work fine.
     
  16. fenderrio

    fenderrio

    Joined:
    Mar 2, 2013
    Posts:
    147
    I had this same issue @CDF, and found a solution.

    Basically, you need to create a new SerializedObject instance for the referenced generic instances, before you can then access their serialised properties.

    From your example above;

    Code (CSharp):
    1. // instead of doing this...
    2. SerializedProperty item = list.GetArrayElementAtIndex(i);
    3. Debug.Log(item.FindPropertyRelative("name")); // returns Null
    4.  
    5. // try this...
    6. SerializedObject genericObjectInstance = new SerializedObject (list.GetArrayElementAtIndex(idx).objectReferenceValue);
    7. Debug.Log(genericObjectInstance.FindProperty("name")); // returns SerializedProperty
    Hope that helps someone :)
     
  17. technostrife

    technostrife

    Joined:
    Apr 5, 2022
    Posts:
    3