abstract SkillList that can be edited in Inspector

Right here’s a decent generic/reusable example using UI Toolkit (because I’m buggered if I’m going to do this with tired old IMGUI).

The Attribute

using System;
using UnityEngine;

[System.Diagnostics.Conditional("UNITY_EDITOR")]
[AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
public class SubclassSelectorAttribute : PropertyAttribute { }

The Drawer

#if UNITY_EDITOR
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor;
using UnityEditor.UIElements;

[CustomPropertyDrawer(typeof(SubclassSelectorAttribute))]
public sealed class SubclassSelectorPropertyDrawer : PropertyDrawer
{
   #region Overrides

   public override VisualElement CreatePropertyGUI(SerializedProperty property)
   {
       var visualElement = new VisualElement();

       var propertyField = new PropertyField();
       propertyField.BindProperty(property);
       propertyField.label = " ";

       if (property.propertyType != SerializedPropertyType.ManagedReference)
       {
           visualElement.Add(propertyField);
           return visualElement;
       }

       var types = GetTypes(fieldInfo, property);

       var dropdownField = new TypePopupField(property, types);
       visualElement.Add(dropdownField);
       
       visualElement.Add(propertyField);

       return visualElement;
   }

   #endregion

   #region Internal Methods

   private static bool IsCollection(Type fieldType)
   {
       if (fieldType.IsArray == true)
       {
           return true;
       }

       if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(List<>))
       {
           return true;
       }

       return false;
   }

   private static List<Type> GetTypes(FieldInfo fieldInfo, SerializedProperty property)
   {
       var value = property.managedReferenceValue;
       Type currentType = value?.GetType();

       Type fieldType = fieldInfo.FieldType;
       Type baseType;

       bool isCollection = IsCollection(fieldType);

       var types = new List<Type>()
       {
           currentType,
           null
       };

       if (fieldType.IsAbstract == false && isCollection == false)
       {
           types.Add(fieldType);
       }
       
       if (isCollection == true)
       {
           baseType = fieldType.GetGenericArguments()[0];
           if (baseType.IsAbstract == false)
           {
               types.Add(baseType);
           }
       }
       else
       {
           baseType = fieldType;
       }

       var derivedTypes = TypeCache.GetTypesDerivedFrom(baseType);
       foreach (var derivedType in derivedTypes)
       {
           types.Add(derivedType);
       }

       return types;
   }

   #endregion

   #region Internal Types

   public sealed class TypePopupField : PopupField<Type>
   {
       #region Internal Members

       private readonly SerializedProperty _property;

       #endregion

       public TypePopupField(SerializedProperty property, List<Type> types) : base(property.displayName, types, 0, GetTypeName, GetTypeName)
       {
           _property = property;
           this.RegisterValueChangedCallback(OnValueSelected);
       }

       #region Internal Methods

       private void OnValueSelected(ChangeEvent<Type> changeEvent)
       {
           Type selectedType = changeEvent.newValue;
           if (selectedType == null)
           {
               _property.managedReferenceValue = null;
               _property.serializedObject.ApplyModifiedProperties();
           }
           else
           {
               var constructor = selectedType.GetConstructor(Type.EmptyTypes);
               if (constructor != null)
               {
                   var value = constructor.Invoke(null);
                   _property.managedReferenceValue = value;
                   _property.serializedObject.ApplyModifiedProperties();
               }
               else
               {
                   Debug.LogWarning($"Selected Type {selectedType.Name} does not have a parameterless constructor. Cannot assign instance of type.");
               }
           }
       }

       private static string GetTypeName(Type type)
       {
           if (type == null)
           {
               return "Null";
           }
           else
           {
               return ObjectNames.NicifyVariableName(type.Name);
           }
       }

       #endregion
   }

   #endregion
}
#endif

An Example

using System.Collections.Generic;
using UnityEngine;

public class SubclassSelectorExample : MonoBehaviour
{
    #region Inspector Fields

    [SerializeReference, SubclassSelector]
    private IBaseInterface _baseInterface = null;

    [SerializeReference, SubclassSelector]
    private BaseClass _classBase = null;

    [SerializeReference, SubclassSelector]
    private List<BaseClass> _classes = new();

    #endregion

    #region Properties

    public IBaseInterface BaseInterface => _baseInterface;

    public BaseClass ClassBase => _classBase;

    public List<BaseClass> Classes => _classes;

    #endregion

    #region Types

    public interface IBaseInterface { }

    [System.Serializable]
    public class BaseClass : IBaseInterface { }

    [System.Serializable]
    public class ClassA : BaseClass
    {
        public string ClassAString;
    }

    [System.Serializable]
    public class ClassB : BaseClass
    {
        public float ClassBFLoat;
    }

    [System.Serializable]
    public class ClassC : ClassA
    {
        public int ClassCInt;
    }

    #endregion
}

And looks pretty meh given the nature of using property fields:

This was not without frustrations and I was regularly missing Odin Inspector’s superior API the entire time. Property Drawers are pain, and DropdownField/PopupField are stupidly designed visual elements. But we still got there in the end and it’s better than whatever ChatGPT farted out (no offence Ryiah).

Note: We can’t do custom drawers for collections so it always adds null as the value of new element.

6 Likes