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.