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

Question Put instance of Generic class in inspector?

Discussion in 'Scripting' started by ERS_Collins, Feb 28, 2023.

  1. ERS_Collins

    ERS_Collins

    Joined:
    Jan 11, 2022
    Posts:
    6
    Hi there, I have a generic base class and several children that derive from it, implementing the Generic type in order to make Unity happy. Here is an example:

    Class Synopsis:
    Code (CSharp):
    1. [System.Serializable]
    2. public abstract class SingleSelectSetSO<T> : ScriptableObject where T : ScriptableObject {...}
    3.  
    4. [System.Serializable]
    5. public class ModeSetSO : SingleSelectSetSO<ModeSO>
    6. { }
    7.  
    8. {System.Serializable]
    9. public ModeSO : ScriptableObject {...}
    (Exact implementation omitted since it's not necessary. Mode is a data container, and the Set has methods and events to alert when a selected T item has been changed. ModeSetSO is empty since all functionality is in the base Set class)


    I'm attempting to create a multi-use Script to attach to a dropdown. I want this script to be capable of presenting any Set as a dropdown. However, When I go to drag my instance of the ModeSet, it does not hold the reference. Unity knows that it is the correct type for this member variable, but it does not save the reference when I double-click or click and drag the ModeSet instance into the box. It also has this "`1" notation after the expected type which I assume is indicative of some issue.

    The reason I am trying to do this script via the base Set class is because I have several types of Sets, not just ModeSet. I currently have a "dropdown-converter" script per derived set-type, but with that comes a lot of repeated code. This, if possible, will be more elegant.

    I've attempted to search for info on Unity, Generics, this weird "`1" notation, and more, but I can't seem to find anything useful--at least not that I can understand, hence this post. Thanks in advance for your help
     

    Attached Files:

    Last edited: Mar 3, 2023
  2. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    6,016
    We'd need to see how this particular generic field is declared. Unity has some weird specifics when it comes to actually serialising generic fields correctly. In cases of generics within generics, I believe it will always have issues.

    Unity version is also helpful here. Older versions have more issues with generics.

    Also, fyi, there's no need for the
    [System.Serializable]
    attribute on your monobehaviours or scriptable objects. Unity will already try to serialise them.
     
    Bunny83 likes this.
  3. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,135
    You can get around this limitation by adding a non-generic base class from which your generic base class derives from, and serialize your references using that instead of the generic version.
    Code (CSharp):
    1. [System.Serializable]
    2. public abstract class SingleSelectSetSO : ScriptableObject {...}
    3.  
    4. [System.Serializable]
    5. public abstract class SingleSelectSetSO<T> : SingleSelectSetSO where T : ScriptableObject {...}
    Code (CSharp):
    1. public class SingleSelectSetAsDropdown
    2. {
    3.     [SerializeField] public SingleSelectSetSO set; // this should work
    4. }
     
    spiney199 likes this.
  4. ERS_Collins

    ERS_Collins

    Joined:
    Jan 11, 2022
    Posts:
    6
    Thank you both for your replies. I'm grateful for your insight.

    I'm using Unity 2021.3.15f. Thanks, I've now removed the unnecessary attributes


    You are right, I am able to pass my derived class into the inspector when the script variable is of the base class! I created the non-generic parent as you suggested.

    This creates a new problem: Since I am treating the member as an instance of the base class, what is the correct way to access methods of the generic derived class?


    Additional context may be necessary. Essentially, a RuntimeSet is a list of scriptable objects that act as data containers. This architecture is based off of the infamous talk by Ryan Hipple. Here are the classes that define the architecture, as well as a script that currently works but is based off of the child that implements the generic class explicitly. I have one of these dropdown scripts per implemented generic type, which is where the repeated code is coming from that I would like to consolidate
    upload_2023-3-1_11-26-17.png

    upload_2023-3-1_11-27-21.png

    If I want to be able to handle all the different derivatives of SingleSelectSetSOs in my dropdown script, I would think I would have to, somehow, be able to cast from the non-generic base type to the generic SingleSelectSetSO<> class. I'm not sure if this is possible, or the correct way to think about this. As you can see, I'm accessing the internal list (not part of the root non-generic class) and adding an explicityly-typed object to it in UpdateDropdown
     
    Last edited: Mar 1, 2023
  5. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,135
    Ah, Unity's serialization limitations... <3

    So what you could do to get past this second obstacle is to create a generic plain old C# class, and use that to hold your RuntimeSetSO reference, and cast it to RuntimeSetSO<T> as soon as possible during deserialization.

    This can work because Unity can serialize generic plain old C# classes, just not generic scriptable objects :D
    Code (CSharp):
    1. [Serializable]
    2. public sealed class RuntimeSet<T> : ISerializationCallbackReceiver where T : ScriptableObject
    3. {
    4.     [SerializeField]
    5.     private RuntimeSetSO setSerialized;
    6.  
    7.     private RuntimeSetSO<T> set;
    8.  
    9.     public int Count => set.Count;
    10.     public void Clear() => set.Clear();
    11.     public void Add(T item) => set.Add(item);
    12.     public void Remove(T item) => set.Remove(item);
    13.  
    14.     void ISerializationCallbackReceiver.OnBeforeSerialize() => setSerialized = setSerialized as RuntimeSetSO<T>;
    15.     void ISerializationCallbackReceiver.OnAfterDeserialize() => set = setSerialized as RuntimeSetSO<T>;
    16.  
    17.     public static implicit operator RuntimeSetSO<T>(RuntimeSet<T> runtimeSet) => runtimeSet.set;
    18. }
    Usage:
    Code (CSharp):
    1. [SerializeField]
    2. private RuntimeSet<ScriptableObject> set;
    3.  
    4. private void Awake()
    5. {
    6.     ScriptableObject item = null;
    7.  
    8.     set.Add(item);
    9.     set.Remove(item);
    10.     set.Clear();
    11. }
    One downside with this is that the field will look quite ugly in the inspector...

    inspector.png

    But it's possible to fix this by creating a custom property drawer for the RuntimeSet<> class that gets rid of the unnecessary foldout.
     
  6. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,572
    I've explained this already a couple of times in the past. A lot of people seem to confuse generic classes with type inheritance. It's not related and actually one is kind of the opposite of the other.

    Generic classes allows you to write code once and it can be reused on completely separate classes / types and the code works the same. Generic classes make types diverge. So a
    List<int>
    and a
    List<string>
    both use the exact same code, but the actual concrete types that are generated when the type parameter is bound to a type are completely incompatible with each other. There is something that's called generic covariance and contravariance which partly allows type compatibility when the generic type arguments actually are derived from each other. However you can only have either covariance or contravariance, never both. Which one you may have depends on the flow direction of the generic information. If it's only one way it would work. However a List stores and retrieves elements and therefore requires both directions (in and out). So in some sense generic classes allow the usage of the same code for different data / implementations. Those implementations however are separate from each other. They are individual types and not compatible to each other.

    Polymorphism on the other hand is kinda the opposite. Derived classes ARE actually instances of the parent class. However derived classes can override code / methods and change what the object actually does while still sharing the same base data. So essentially the opposite. Same data different code.

    When you actually pay attention in Ryan Hipple's talk about runtime sets, he actually has his MonoBehaviour "Thing". He also has a concrete RuntimeSet type called ThingRuntimeSet. You can see the class name when he selects his EnabledThings scriptable object asset. So that RuntimeSet is a concrete subclass of the generic class he has shown. Likewise he has a concrete MonoBehaviour, specialized for "Things" that is called "ThingDisabler". We don't see the code for those classes, but they may also have a generic base class and could otherwise be empty since all the code is implemented in the generic base class. So when you want to have a RuntimeSet for a particular type, you would create all the necessary types you may need for that isolated ecosystem.

    Creating such a wrapper as @SisusCo suggested does work, but could make things more complicated in the long run. You usually have concrete type safe collections / sets and concrete things they should work with. Even if the generic argument of two different instances are derived from each other, you can never treat one as the other.

    The issue here is, imagine this case. You have a "Base" class and a "Derived" class which is of course derived from Base. A
    List<Base>
    can of course store Base instances as well as Derived instances. However a
    List<Derived>
    can only store Derived instances (or further subclasses of Derived, of course). Though you can never treat a
    List<Derived>
    as a
    List<Base>
    . While reading elements would work, writing them would not work as the actual instance only allows Derived instances.

    Contravariance only works on generic arguments that are marked as "in". That means they can only be used as method arguments. So data only flows into the class. Contravariance allows you to assign a less derived instance (base class) to a more derived instance (subclass). The prime example of such a case is the
    System.Action<T>
    delegate type. This does work:

    Code (CSharp):
    1. Action<Base> del = someMethod;
    2. Action<Derived> del2 = del;
    Here del2 is more restricted. It only accepts Derived instances as arguments. Since "del" accepts Base instances, it also accepts Derived instances, so this does work. It would not work the other way round. If the actual method expects a Derived instance, you can not store it as an
    Action<Base>
    delegate as this would allow Base instances to be passed which doesn't make much sense.

    A covariance example would be the
    IEnumerator<T>
    interface. Here data only flows "out" of the class. Here the conditions are simply reversed. Here you can assign an
    IEnumerator<Derived>
    to an
    IEnumerator<Base>
    variable.

    Hopefully this clears up some confusion about generics. Yes, type-safety can be really annoying at times and that lets you appreciate dynamic scripting languages like javascript. Though proper type safety usually helps to speed things up and to avoid mistakes. That's generally the point of programming paradigms.
     
    SisusCo, Kurt-Dekker and spiney199 like this.
  7. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,135
    It depends. There could be valid reasons for wanting to create an ecosystem of multiple interchangeable generic base types.

    You could have a bunch of different generic archetypes for runtime sets with different behaviour.
    Code (CSharp):
    1. Set<TItem> : Set
    2. HashSet<TItem> : Set
    3. OrderedSet<TItem> : Set
    4. RandomizedSet<TItem> : Set
    5. ShuffledSet<TItem> : Set
    Then you could create multiple non-generic, concrete classes deriving from different base classes, but all sharing the exact same item type.
    Code (CSharp):
    1. OrderedVector3Set : OrderedSet<Vector3> // for Waypoints
    2. RandomizedVector3Set : RandomizedSet<Vector3> // for spawning at a random position
    With a class like RuntimeSet<T> you could retain in practice most of the benefits of type safety. Yes the Object reference is serialized behind the scenes without information about its generic type, but every time an object is serialized or deserialized, it discards any reference of the wrong type.

    Now ideally this should happen immediately when the user assigns an invalid value, and there should probably be a warning logged if an object of invalid type is detected during deserialization, but this was just a quick example I threw together to showcase how to get past the roadblock.

    That being said, I don't have enough information available to know if this would actually work or not for the use case of SingleSelectSetAsDropdown. I.e. is the item type always exactly the same or not.
     
  8. ERS_Collins

    ERS_Collins

    Joined:
    Jan 11, 2022
    Posts:
    6
    Thanks for your responses. I must admit that some of the information is beyond what I've encountered before so some of it is going a little over my head. But I'm learning and I'm thankful for the detailed explanations!

    @Bunny83 : I think I'm tracking. In fact, I already am implementing concrete types of my generics (Ryan's RuntimeSet<Thing> concept) except instead of Things I have, in one case, DisplayModes. The reason I am making a derivative of RuntimeSet<> called SingleSelectSet<> is because SingleSelectSet<>s have additional members, methods, and events to "activate" (per se) one of the items. I don't want all of my RuntimeSet<>s to be capable of this.
    upload_2023-3-3_10-17-32.png

    upload_2023-3-3_10-18-8.png

    @SisusCo I believe that aligns with what you mention in your last post.


    @SisusCo : I added the code you gave in this post:
    I'm extremely grateful to be able to just paste this into my project for testing. Thanks for using my existing types.

    Unfortunately, just pasting this in without changing anything, I'm still having issues with the inspector. Same as before, my applicable classes will appear, but when I double-click or drag the item into the inspector, it silently rejects the object and remains null
    upload_2023-3-3_10-25-39.png

    Now about your question
    I think I can answer "no". I have many different Types. Each of the sets above is a concrete implementation of SingleSelectSet<>. Below are two examples of what T is in DisplayModeSet and TrackerSet respectively. upload_2023-3-3_10-30-23.png

    upload_2023-3-3_10-30-28.png

    Each SingleSelectSet<> only contains a single Type, but not all SingleSelectSet<>s contain items of the same Type.


    When I change ScriptableObject here to one of my Types (lets say TrackerSO), then the inspector will accept the set that is of that type (so TrackerSet). I'm not sure why, since TrackerSO (.etc) inherit from ScriptableObject. This edit isn't a solution to my quest for reusability since sets with their <T> being other than TrackerSO are still rejected, but it may be useful information to you

    Thanks again for your help
     
    Last edited: Mar 3, 2023
  9. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,135
    That was actually the way I intended RuntimeSet<T> to be used; you would only be able to use RuntimeSet<SomeScriptableObject> with any sets that derive from RuntimeSetSO<SomeScriptableObject> specifically :)

    If you want type safety, then you do always need to specify the type of the items in the collection at some point. By using generics we have just been able to delay the inevitable. It's just like when you use List<T>: you can write some generic code that does things with any kind of List<T>, but eventually you'll want to do something more specific with the contents of lists, and this is when you do need to specify the type of the items.

    The only benefit that we have gained from using generics here, is that we have gained the flexibility of being able to assign any classes that derive from RuntimeSetSO<SomeType> to a RuntimeSet<SomeType> field. If you do not really need this flexibility, then it's simpler to just Create a single non-generic RuntimeSetSOSomeType type and use that directly in the client classes instead of RuntimeSet<SomeType>.

    If we dropped the requirement of type safety, then it would be really easy to create a non-generic collection that can be reused everywhere.
    Code (CSharp):
    1. [CreateAssetMenu]
    2. public sealed class RuntimeSet : ScriptableObject
    3. {
    4.     private readonly List<object> items = new List<object>();
    5.  
    6.     public int Count => items.Count;
    7.     public void Clear() => items.Clear();
    8.     public void Add(object item) => items.Add(item);
    9.     public void Remove(object item) => items.Remove(item);
    10.     public T GetItem<T>(int index) => (T)items[index];
    11. }
    But the big downside with this is that then the compiler can no longer warn us about assigning one type of item to a runtime set and then trying to get a different type of item out.
    Code (CSharp):
    1. [SerializeField]
    2. private RuntimeSet set;
    3.  
    4. private void Awake()
    5. {
    6.     set.Add("text");
    7.     Vector3 position = set.GetItem<Vector3>(0); // no warning from the compiler, but will fail at runtime
    8. }
     
    Last edited: Mar 3, 2023
  10. hdaniel_unity

    hdaniel_unity

    Joined:
    Dec 30, 2021
    Posts:
    8