Skip to the final code#


I was making some small changes to how my Shoot behaviours work, and that involved making an abstract class, DirectionCalculation. This class defines an abstract method Get() that returns a float which is the direction to shoot the bullet in. Specific implementations must derive from this class and implement their own Get().

I ran into one problem though,

Unity doesn’t serialize abstract classes.#

Or atleast by default. But we can make a custom Attribute with a PropertyDrawer which we can use to pick any subclass or the class itself when not abstract.

public class SubclassPicker : PropertyAttribute { }

[CustomPropertyDrawer(typeof(SubclassPicker))]
public class SubclassPickerDrawer : PropertyDrawer
{
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return EditorGUI.GetPropertyHeight(property);
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
    }
}

This is just boilerplate that sets up everything we need to get a custom drawer when we use [SubclassPicker].

Getting derived classes#

Next we need a way to get all classes that inherit from the base class.

IEnumerable GetClasses(Type baseType)
{
    return Assembly.GetAssembly(baseType)
        .GetTypes()
        .Where(t => t.IsClass && !t.IsAbstract && baseType.IsAssignableFrom(t));
}

I’ll use Linq here for conciseness but you can easily do this with a for loop if you want.

We get all types in the base type’s assembly with Assembly.GetTypes(), and filter them to only get types that can be assigned to baseType and that aren’t abstract.

OnGUI#

Now it’s time to fill out the OnGUI() method.

Type t = fieldInfo.FieldType;
string typeName = property.managedReferenceValue?.GetType().Name ?? "Not set";

We get the type of the field, and the typeName of the actual instance assigned to the field. We use null propagation to set the string to “Not set” if the field is null.

Rect dropdownRect = position;
dropdownRect.x += EditorGUIUtility.labelWidth + 2;
dropdownRect.width -= EditorGUIUtility.labelWidth + 2;
dropdownRect.height = EditorGUIUtility.singleLineHeight;
if (EditorGUI.DropdownButton(dropdownRect, new(typeName), FocusType.Keyboard))
{
    GenericMenu menu = new GenericMenu();

    // null
    menu.AddItem(new GUIContent("None"), property.managedReferenceValue == null, () =>
    {
        property.managedReferenceValue = null;
        property.serializedObject.ApplyModifiedProperties();
    });

    // inherited types
    foreach (Type type in GetClasses(t))
    {
        menu.AddItem(new GUIContent(type.Name), typeName == type.Name, () =>
        {
            property.managedReferenceValue = type.GetConstructor(Type.EmptyTypes).Invoke(null);
            property.serializedObject.ApplyModifiedProperties();
        });
    }
    menu.ShowAsContext();
}

The first few lines just create a Rect that positions the dropdown nicely.

When the dropdown is clicked, we want to show a menu with null and all the possible types.

To do this, we iterate over the return of our GetClasses() function, and add a new item to the menu. First field is the label which we want as the type’s name. Second field is whether it shows up as selected, which we want to be true if it matches the name of the type currently assigned, and the third field is a function to run when clicked.

This function sets the managedReferenceValue of the field to a new instance of the type, which we create with GetConstructor().Invoke().

EditorGUI.PropertyField(position, property, label, true);

Lastly we just display a normal drawer for whatever is assigned. This would be the derived class’ fields.


Final code#

public class SubclassPicker : PropertyAttribute { }

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(SubclassPicker))]
public class SubclassPickerDrawer : PropertyDrawer
{
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return EditorGUI.GetPropertyHeight(property);
    }

    IEnumerable GetClasses(Type baseType)
    {
        return Assembly.GetAssembly(baseType).GetTypes().Where(t => t.IsClass && !t.IsAbstract && baseType.IsAssignableFrom(t));
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        Type t = fieldInfo.FieldType;
        string typeName = property.managedReferenceValue?.GetType().Name ?? "Not set";

        Rect dropdownRect = position;
        dropdownRect.x += EditorGUIUtility.labelWidth + 2;
        dropdownRect.width -= EditorGUIUtility.labelWidth + 2;
        dropdownRect.height = EditorGUIUtility.singleLineHeight;
        if (EditorGUI.DropdownButton(dropdownRect, new(typeName), FocusType.Keyboard))
        {
            GenericMenu menu = new GenericMenu();

            // null
            menu.AddItem(new GUIContent("None"), property.managedReferenceValue == null, () =>
            {
                property.managedReferenceValue = null;
                property.serializedObject.ApplyModifiedProperties();
            });

            // inherited types
            foreach (Type type in GetClasses(t))
            {
                menu.AddItem(new GUIContent(type.Name), typeName == type.Name, () =>
                {
                    property.managedReferenceValue = type.GetConstructor(Type.EmptyTypes).Invoke(null);
                    property.serializedObject.ApplyModifiedProperties();
                });
            }
            menu.ShowAsContext();
        }
        EditorGUI.PropertyField(position, property, label, true);
    }
}
#endif

Usage#

// class setup
public abstract class AbstractClass { }

public class DerivedClass1 : AbstractClass
{
    public float floatField;
}

public class DerivedClass2 : AbstractClass
{
    public string stringField;
}

// usage
[SerializeReference, SubclassPicker] AbstractClass myField;

Notes#

  • you need to use SerializeReference in combination with the attribute
  • the class needs to be in the same assembly as the base type
  • the class needs to have a valid parameterless constructor

Showcase1

Showcase2

Showcase3

My thoughts#

I find it strange that unity doesn’t already implement something like this by default. In my opinion it’s a really good programming pattern and now that I have this nice implementation I see ways to use it in so many places.