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. Dismiss Notice

Inspector with inheritance?

Discussion in 'Scripting' started by makeshiftwings, Apr 27, 2014.

  1. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    I've got a base class with subclasses, like this for example:

    Code (csharp):
    1.  
    2. class Animal;
    3. class Dog : Animal;
    4. class Cat : Animal;
    5.  
    And a class that contains an instance of the base class that can be set to a subclass, like this:
    Code (csharp):
    1.  
    2. public class AnimalBox
    3. {
    4.      public AnimalTypeEnum animalType;
    5.      public Animal animal;
    6. }
    7.  
    What I'd like is to get an inspector to show a dropdown for animalType, and then depending on what you select, set the variable animal to either a Dog or Cat and show the variables for that object as normal. Is there any way to do this? I've been messing with ScriptableObjects and custom PropertyDrawers but can't figure out a way to get it to instantiate a new object of the correct type because of Unity's weird serialization.
     
  2. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,716
    It's not impossible... But it is one major pain in the ass to implement.

    First, your class that uses polymorphism must derive from ScriptableObject or MonoBehaviour, otherwise anytime the serialization occur, you will lose your type and its serialized data.
     
  3. Suddan

    Suddan

    Joined:
    Jan 12, 2013
    Posts:
    21
    Not quite sure if that's want you wanted to have, but give it a try and maybe play around a bit to achieve your goal.

    Three classes, i just made the animal class abstract for no special reason, just undo if you want to..

    Code (csharp):
    1.  
    2. public abstract class Animal
    3. {
    4.     public enum AnimalType {cat, dog}
    5.     public abstract void DoSomething();
    6. }
    7.  
    8. public class Dog : Animal
    9. {
    10.     const AnimalType type = AnimalType.dog; // or whatever attributes you wanna have in here
    11.     public override void DoSomething()
    12.     {
    13.         Debug.Log("I am a " + type.ToString() + "! Wooof...");
    14.     }
    15. }
    16.  
    17. public class Cat : Animal
    18. {
    19.     const AnimalType type = AnimalType.cat;
    20.  
    21.     public override void DoSomething()
    22.     {
    23.         Debug.Log("I am a " + type.ToString() + "! Miauw...");
    24.     }
    25. }
    26.  
    27.  
    28.  
    The AnimalBox class, when Awake() is called, the value set in the inspector will either make it a cat or a dog...
    But i don't know how you could show the animals variables in the inspector now.


    Code (csharp):
    1.  
    2. public class AnimalBox : MonoBehaviour
    3. {
    4.     public Animal animal;
    5.     public Animal.AnimalType animalType;
    6.  
    7.    
    8.     // use Awake if you want to assign a type in inspector
    9.     // or create conctructors
    10.  
    11.     void Awake()
    12.     {
    13.         if (animalType == Animal.AnimalType.cat)
    14.             animal = new Cat();
    15.         else if (animalType == Animal.AnimalType.dog)
    16.             animal = new Dog();
    17.  
    18.         animal.DoSomething();
    19.     }
    20. }
    21.  
     
    Last edited: Apr 28, 2014
  4. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,716
    While this work, it also has no data serialized per type... Which kind be a deal breaker in many case.
     
  5. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    Yeah as LightStriker said, I need actual data in the subclasses (otherwise there's no point to having an Inspector). If you give Dog a "public string bark" and Cat a "public int livesRemaining", it won't show either in the Inspector the way you have it above, because it doesn't know the subtype. I've had some luck using ScriptableObject and PropertyDrawers, but running into all kinds of weird errors if I need to put things into a List, or include the class as a member in a different object, because Unity tries to run default constructors and then throws an error because it's not allowed to run a default constructor on a ScriptableObject. I think for now it's just more trouble than it's worth, and I'm going to make an ugly mega-class that just has a different member for each subclass, and I'll ignore the ones that aren't used. It works, but it hurts my soul. :~(
     
  6. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,500
    How are you creating them? This shouldn't be an issue. As long as you create them using the appropriate methods this should be handled for you. That's how I do it, and I'm pretty sure it's one of the intended use cases.
     
  7. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,716
    Right now, there's one solution that exist on the AssetStore. I think it's called "Full Inspector", which serialized the inner data of a MonoBehaviour in a different way - Json.NET, protobuf-net, Binary, etc. ( https://www.assetstore.unity3d.com/#/content/14913 )

    Maybe in time I will release my own solution, which uses MonoBehaviour instead of ScriptableObject. By the way, using ScriptableObject prevent your object from being used properly in Prefabs, or from being duplicated. ( http://forum.unity3d.com/threads/210478-Editor-driven-dependency-injection )
     
  8. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,500
    I thought that a ScriptableObject could be used fine for either of those things as long as it's created as an asset?

    But yeah, I wouldn't mind a more robust system for managing arbitrary, cross-platform, persistent data. Right now all of the approaches I'm aware of are a) fiddly and b) have significant drawbacks for common use cases. I've always managed to get where I wanted to go, but sometimes it's been harder than I've felt it could have been.
     
  9. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    There are a couple places where it's failed:
    1) Adding to a list in the inspector - The normal way to add elements to a list in the inspector is to use InsertArrayElementAtIndex in the SerializedProperty class, but this attempts to insert a blank object using the default constructor, which throws an exception if you try it with anything containing a ScriptableObject.
    2) Any custom serializing/deserializing - I'm using JSON to save/load objects, and there's no easy way to "load" any object containing a ScriptableObject from a serialized format, because deserialization again tries to create a blank object with a default constructor, which throws an exception if there's a ScriptableObject anywhere in the hierarchy.
    3) Prefabs - Adding a class with a ScriptableObject to a prefab seems to cause all kinds of weird behavior if there are multiple copies of the prefab; I've received random exceptions or the values disappear.
     
  10. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,716
    Sure, but then you must assume that everything point towards that asset. On top, creating that asset get a bit more complicated and has to be done outside the inspector. Duplicating or creating prefabs will not make copy or prefab-instanciated version of a ScriptableObject. Overriding data in a prefab instance is quite useful too.

    If ScriptableObject could be placed inside a prefab, most of those issue would vanish.

    I cannot speak for the "Full Inspector", but those are all issues I had to solve with my own inspector. I dropped ScriptableObject in favor of hidden MonoBehaviour. I'm using standard Unity serialization, which the only downside is the lack of interface support... Which I can survive without.

    As for the inspector itself, I totally rewrote it and dumped all SerializedProperty and SerializedObject. Those can only support one serialization context at a time, and it's a limitation I'm not happy with. PropertyDrawer are also, in my opinion, too limited. For example, GUILayout/EditorGUILayout are just too useful to work without. Also, I like to support fields, properties or even parameter-less methods.

    The downside of dropping SerializedProperty/SerializedObject is that I had to write a LOT of code (10k+ lines). However, the upside is that I was able to add a huge amount of feature that Unity doesn't support out of the box.

    Everything here is done only with attributes:
    http://i.imgur.com/gAGlolQ.png

    Here's a list of the attributes I made, which most can be combined;
    - AdvancedInspector: Switch old inspector with my own
    - AllowSceneObject: Object field allow scene picking
    - Angle: Turn float/int into an agle spinning wheel
    - Bypass: Forces Unity's type to show up using my inspector
    - CreateDerived: Make this field able to create a polymorphic derived instance based on the field type.
    - Descriptor: Change the name, tooltip, color, icon, and even a help URL for this field.
    - DisplayAsParent: Force sub-object's fields to be display as being part of the parent.
    - Expandable: Make a class or any field expandable (arrow to browse their inner data)
    - FieldEditor: Override the default editor for a field.
    - Group: Group a collection of field.
    - Help: Show a help box under this field.
    - Inspect: For this field, property or method to show up in the inspector.
    - MaskedEnum: Turn an enum into a bitfield (masked enum)
    - Range: Turn a float or int into a slider.
    - ReadOnly: Prevent a field from being editable. Useful for debug.
    - Restrist: Turn any field type into a drop down list of restricted choices.
    - Size: Force an array/list to be of a specific size.
    - Space: Add space under this field.
    - Style: Change the style of this field. Can also prevent the label from being drawn.
    - TextField: Change how a string field is draw; text field, text area, password, file picker, folder picker, etc.
    - Toolbar: Group item horizontally in a toolbar.

    Most of those also accept delegate or method name to be able to retrieve data at runtime. For example, "Help" can be turned on and off, or even change the message depending on the condition of your choice.

    Also had to rewrite support for undo, prefab, and all the stuff Unity supports. But it also allowed me to add Copy/Paste per field, apply/revert per field, field sorting, field hiding (ex.: debug field) or contextual menu per field type.

    http://i.imgur.com/Ndu326c.png

    Now, I just can't understand how I used to deal with the normal inspector. :p
     
    Last edited: Apr 28, 2014
  11. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    Wow, LightStriker, that looks really useful! I'm sure you could get some interest on the Asset Store. Personally I'm going to try to stick with the default inspector for now, at least until Unity 5 comes out and I can see if there are any changes. But maybe I'll switch it up in my next project.
     
  12. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,500
    Ugh. Fair enough, I certainly see the issue!

    I find 1 in particular to be a bit bizarre, since Unity is so anal about not letting you call constructors. Then it goes and does it itself? I was lucky enough to not run into that myself, though.

    2 I kind of expected, and is one of the main reasons I'd like a more robust system. I did read that one of the recent updates includes a partial implementation of DataContractSerializer. It's not perfect, but a full implementation would go a long way to meeting my needs. I've used the Mono one before, but that requires including a DLL (so no web builds) and there are some things that I couldn't make work with it despite MSDN being pretty adamant that DCS should support it (suggesting it's not quite exactly the same).

    3 is something we just ended up not doing, because yeah, it just sucked.