Search Unity

Custom inspector with array of inherited types?

Discussion in 'Immediate Mode GUI (IMGUI)' started by Yandalf, Aug 9, 2018.

  1. Yandalf

    Yandalf

    Joined:
    Feb 11, 2014
    Posts:
    491
    Hey everyone!

    I've been trying out some slightly trickier things with Editor scripting and have run into a snag.
    I've made a ScriptableObject called Species that contains an array of class EvoRequirement, and two inherited classes of EvoRequirement:
    Code (CSharp):
    1. public class Species : ScriptableObject
    2. {
    3.     public EvoRequirement[] Requirements = new EvoRequirement[0];
    4. }
    5.  
    6. [Serializable]
    7. public class EvoRequirement { }
    8. public class EmptyRequirement : EvoRequirement { }
    9. public class StatRequirement: EvoRequirement { }
    Now I'm writing a custom inspector for Species, where I copy Requirements into a List for easier editing, and during OnDisable I give Requirements the content of the copied and edited list, after removing all null and EmptyRequirement elements:
    Code (CSharp):
    1. [CustomEditor(typeof(Species))]
    2. public class SpeciesEditor : Editor
    3. {
    4.     private Species m_myTarget;
    5.     private List<EvoRequirement> m_requirements;
    6.  
    7.     private void OnEnable()
    8.     {
    9.         m_myTarget = target as Species;
    10.         m_requirements = new List<Requirement(m_myTarget.Requirements);
    11.     }
    12.  
    13.     private void OnDisable()
    14.     {
    15.         if(m_myTarget != null && m_requirements != null)
    16.         {
    17.             for(int i = m_requirements.Count - 1; i >= 0; i--)
    18.             {
    19.                 if (m_requirements[i] == null ||
    20.                        m_requirements[i].GetType() == typeof(EmptyRequirement))
    21.                     m_requirements.RemoveAt(i);
    22.             }
    23.             m_myTarget.Requirements = m_requirements.ToArray();
    24.             m_requirements.Clear();
    25.         }
    26.     }
    27. }
    This all seems to work fine as intended, but when I recompile any part of my code (including classes that have nothing to do with this) and then inspect the SO again, the content of the array seems to get all set back to the base type.

    This is what shows in the inspector, I made it so base EvoRequirements would write "Unimplemented Requirement type!", while EmptyRequirements give a dropdown that when changed, changes that element to a new EvoRequirement of the specified type. StatRequirement draws the element with 3 enum popups.
    Null elements write a "Null element!" message.
    Code (CSharp):
    1.     private EvoRequirement DrawRequirement(EvoRequirement requirement)
    2.     {
    3.         if (requirement as EmptyRequirement != null)
    4.         {
    5.             EmptyRequirement req = (EmptyRequirement)requirement;
    6.             req.Type = (EmptyRequirement.RequirementType)EditorGUILayout.EnumPopup(req.Type);
    7.             if (req.Type == EmptyRequirement.RequirementType.Stat)
    8.             {
    9.                 return new StatRequirement();
    10.             }
    11.             return req;
    12.         }
    13.         else if (requirement as StatRequirement != null)
    14.         {
    15.             StatRequirement req = (StatRequirement)requirement;
    16.             EditorGUILayout.BeginHorizontal();
    17.             req.stat = (StatRequirement.Stat)EditorGUILayout.EnumPopup(req.stat);
    18.             req.comparison = (EvoRequirement.Comparison)EditorGUILayout.EnumPopup(req.comparison);
    19.             req.amount = EditorGUILayout.IntField(req.amount);
    20.             EditorGUILayout.EndHorizontal();
    21.             return req;
    22.         }
    23.         else if(requirement != null)
    24.         {
    25.             EditorGUILayout.LabelField("Unimplemented Requirement type!");
    26.             return requirement;
    27.         }
    28.         else
    29.         {
    30.             EditorGUILayout.LabelField("Null element!");
    31.             return null;
    32.         }
    33.     }
    After a recompile all elements will however turn into Unimplemented messages. It does remember the length of the array, but the individual data is lost.
    Is the Unity Serializer incapable of dealing with inherited types? How should I go about this instead?
    Thanks in advance!
     
  2. Yandalf

    Yandalf

    Joined:
    Feb 11, 2014
    Posts:
    491
    Disregard the question if Unity doesn't support inherited serialization. No, no it doesn't.

    I'm currently workin on a workaround by having Species have an array of each subtype of EvoRequirement, and only adding all these together in the inspector and splitting it back up on save.
    This works, but it does make the class slightly more bloated to use since each of those arrays needs to be public so I can edit it in the inspector.
    Is there actually a way to write an inspector for elements that are private with [SerializeField] attributes? As far as I'm aware you can't reach those, but I'd be very happy to hear I'm wrong there.
     
  3. brownboot67

    brownboot67

    Joined:
    Jan 5, 2013
    Posts:
    375
    I solve this by making the base class inherit ScriptableObject, for you this is EvoRequirement. Then, as you add new instances of the child classes (empty, stat, whatever) to your array create their assets and add them inside your main ScriptableObject class's asset (inside the Species asset).
     
  4. Yandalf

    Yandalf

    Joined:
    Feb 11, 2014
    Posts:
    491
    Hi brownboot67!

    Thanks for the suggestion. it's a solution I'm considering but it is gonna cause a wild growth of ScriptableObjects that I'd rather avoid.
    I read it's possible to access private serialized fields by using SerializedObjects and finding the properties instead, but since that doesn't give me access to types or allow me to change it as easily (for the EmptyRequirements to change to Stat and later Requirements) I'm not sure that'll fix it either.
     
  5. Suduckgames

    Suduckgames

    Joined:
    Nov 28, 2016
    Posts:
    218
    To serialize lists of non-ScriptableObject subclasses, you'll need to implement ISerializationCallbackReceiver to handle your own serialization. But as brownboot suggested, you the best way is to inherit from ScriptableObject on EvoRequirement

    I think that what are you looking for is hideFlags = HideFlags.HideAndDontSave on the OnEnable() part of the object; This is due because of Unity reload assembly sytem as marked in


    to make it work just add on the ScriptableObject EvoRequieremnt extend from scriptableObject and add

    Code (CSharp):
    1.  
    2.     public void OnEnable()
    3.     {
    4.        
    5.         hideFlags = HideFlags.HideAndDontSave;
    6.     }
    You will see that when you enter/exit plays mode the data persist, however take care because in my case, I lost the data when I restart unity
     
  6. technostrife

    technostrife

    Joined:
    Apr 5, 2022
    Posts:
    3
    AustinDrozin and mickeyband like this.