Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Draw a Property as a child type

Discussion in 'Scripting' started by McGregor777, Feb 11, 2021.

  1. McGregor777

    McGregor777

    Joined:
    Sep 6, 2013
    Posts:
    8
    Hello !
    In my project, Heroes and Creatures are both a type of Character. To represent this, I use a Character class from which the Hero and Creature classes derive :
    Code (CSharp):
    1. public class Character {}
    2. public class Hero : Character {}
    3. public class Creature : Character {}
    In the same pattern, I use Controller classes, one by Character type :
    Code (CSharp):
    1. public abstract class ActorController<T> : MonoBehaviour
    2. {
    3.    [SerializeField] protected T actor;
    4.    // Some more members
    5. }
    6. public class CharacterController : ActorController<Character>
    7. {
    8.    // Even more members
    9. }
    10. public class HeroController : CharacterController
    11. {
    12.    // A bit more members
    13. }
    14. public class CreatureController : CharacterController
    15. {
    16.    // A few more members
    17. }
    My issue is that, this way, the Inspector always considers actor as a Character object in all the current cases. I am seeking to make so actor is of Hero type in the HeroController and of Creature type in the CreatureController. The idea I had in mind was to use a CustomEditor to draw actor as a specific type on CreateInspectorGUI, but I do not know at what point I should do it, if it is a way to go, or if there is a better way to do that.

    I already see an easiest way to achieve that, but it is at the cost of polymorphism, and before sacrificing it, I would like to know if there is way to save the day, by tickling with the CustomEditor world or something like that.

    Thank you by advance for your help fellow Unity developpers !
     
    Last edited: Feb 11, 2021
  2. Adrian

    Adrian

    Joined:
    Apr 5, 2008
    Posts:
    1,061
    Unity serialization (by default*) does not support polymorphism. Meaning that whatever type your field is, only this exact type will be serialized. If you assign a derived type to the field, Unity will (de-)serialize it as the base type.

    So you need the
    actor
    field to be of the exact Character sub-type you need. In your example, the field is always of the base Character type. You could just push the type parameter up, so the field ends up as the specific sub-type you need and use a generic constraint instead:
    Code (CSharp):
    1. public abstract class ActorController<T> : MonoBehaviour
    2. {
    3.    [SerializeField] protected T actor;
    4.    // Some more members
    5. }
    6. public class CharacterController<T> : ActorController<T>
    7.     where T : Character
    8. {
    9.    // Even more members
    10. }
    11. public class HeroController : CharacterController<Hero>
    12. {
    13.    // A bit more members
    14. }
    15. public class CreatureController : CharacterController<Creature>
    16. {
    17.    // A few more members
    18. }
    * A custom inspector won't help here, as the limitation is in Unity's serialization system. You can use the SerializeReference attribute to support polymorphism but that comes with its own limitations.
     
    McGregor777 likes this.
  3. McGregor777

    McGregor777

    Joined:
    Sep 6, 2013
    Posts:
    8
    Initially, I wanted to use a generic class to have that :
    Code (CSharp):
    1.  public class CharacterController<T> where T : Character
    2. {
    3.     [SerializeField] protected T actor;
    4. }
    5. public class HeroController : CharacterController<Hero> {}
    6. public class CreatureController : CharacterController<Creature> {}
    Alas MonoBehaviour is not supported as a generic class and has to be a concrete class. So my workaround is to use an ActorController<T> abstract generic class from which a CharacterController concrete class inherits, and so on.

    For example, one of the things that Unity would not let me do the way I just shown above was assigning a CreatureController object in a List<CharacterController<T>> contained in a Zone Component (I just realized I did not tried to assign a CharacterController<T> object to this List to see if would accept it or not).

    EDIT : I just read the link you have posted about SerializeReference, and it mentions ScriptableObjects. I use them as Databases for my Characters' sub-types:
    Code (CSharp):
    1. public class HeroDatabase : ScriptableObject
    2. {
    3.     public List<Hero> heroes = new List<Hero>();
    4. }
    Could you tell me if making my Character class inheriting from ScriptableObject would help, and eventually if I am using ScriptableObjects the wrong way ?
     
    Last edited: Feb 11, 2021
  4. Adrian

    Adrian

    Joined:
    Apr 5, 2008
    Posts:
    1,061
    Not sure I understand your issues correctly.

    Code (CSharp):
    1. public class CharacterController<T> : MonoBehaviour
    2.     where T : Character
    3. {
    4.     [SerializeField] protected T actor;
    5. }
    6. public class HeroController : CharacterController<Hero> {}
    7. public class CreatureController : CharacterController<Creature> {}
    This works fine, though? Don't see how Unity forces you to introduce the ActorController<T> here. A script can't have open generic type arguments but there are no restrictions on generics in the type hierarchy. (Note that I needed to add
    [Serializable]
    to the Character sub-classes for them to serialized, not sure if you just omitted that in the examples).

    As for having a List<CharacterController<T>>, this isn't really a limitation of Unity but of C#. CharacterController<T> is an open generic type and those only exist as a compile-time construct. You cannot use open generic types interchangeably with regular types in C#. One common way to resolve this is to have a non-generic base class, which can then be used in places where the type argument is not known:
    Code (CSharp):
    1. public class CharacterController : MonoBehaviour {}
    2. public class CharacterController<T> : CharacterController
    3.     where T : Character
    4. {
    5.     [SerializeField] protected T actor;
    6. }
    7. public class HeroController : CharacterController<Hero> {}
    8. public class CreatureController : CharacterController<Creature> {}
    9.  
    10. public class Zone : MonoBehaviour
    11. {
    12.     public List<CharacterController> controllers;
    13. }
    Note that List<CharacterController> only works because CharacterController derives from UnityEngine.Object. This would not work with List<Character>, as the serialization rules are different.
     
    McGregor777 likes this.
  5. Adrian

    Adrian

    Joined:
    Apr 5, 2008
    Posts:
    1,061
    ScriptableObjects are assets. Depends on wether you want to create an asset for each of your characters or if they are defined somewhere in the scene or as part of a prefab. Functionally, everything you can do with ScriptableObject you can also do with MonoBehaviour/prefabs.
    https://docs.unity3d.com/Manual/class-ScriptableObject.html

    You should familiarize yourself with Unity's serialization system. It's important you understand the rules and the differences between Unity objects and regular C# objects:
    https://docs.unity3d.com/2020.2/Documentation/Manual/script-Serialization.html
     
    McGregor777 likes this.
  6. McGregor777

    McGregor777

    Joined:
    Sep 6, 2013
    Posts:
    8
    Okay, you had the right trick there : using a concrete class as a base, use a generic class inheriting from it, then use concrete classes deriving from the generic class. It did the trick of allowing me to put ActorControllers and sub-types in Lists and yet using the inherited functions of CharacterControllers<T>.

    Thank you for the help, and I will take a closer look to Unity's Serialization indeed, I was definitely missing something there.