0
\$\begingroup\$

I have an abstract class, call it FooClass. And there are two classes that inherit from it, call 'em BazClass and DazClass. I want to create instances of BazClass and DazClass at runtime.

I have scriptable object which I want to store a variable referring to the type of either BazClass or DazClass (Not to an instance of BazClass or DazClass), so I know which type to instantiate at runtime.

  1. Can I have a variable that takes types inherited from another class?

    For example:

    using System;
    Type<:T> variableName where T is FooClass; 
    //stores a type that inherits from FooClass.
    
  2. How can I visualise this variable in the Unity inspector?

\$\endgroup\$
1
  • \$\begingroup\$ Have you tried something like public class MyType<T> : System.Type where T : FooClass ? \$\endgroup\$
    – Mangata
    Commented Nov 20, 2022 at 11:16

1 Answer 1

1
\$\begingroup\$

Here's a demo of this working with a "Type Store" scriptable object storing two types: FooDerivedA which is a subclass of FooBase, and MeshCollider which is a subclass of Collider (so you can see it works with built-in types too)

Inspector screenshot with drop-down menu to select collider subtype

First, we can define a class to store our serialized type information. System.Type isn't itself serializable, so we'll need to store an identifier like its assembly-qualified name, and use that to look up the correct type when deserializing:

using UnityEngine;

[System.Serializable]
public class InspectableType<T> : ISerializationCallbackReceiver {

    [SerializeField] string qualifiedName;

    System.Type storedType;

    #if UNITY_EDITOR
    // HACK: I wasn't able to find the base type from the SerializedProperty,
    // so I'm smuggling it in via an extra string stored only in-editor.
    [SerializeField] string baseTypeName;
    #endif

    public InspectableType(System.Type typeToStore) {
        storedType = typeToStore;
    }

    public override string ToString() {
        if (storedType == null) return string.Empty;
        return storedType.Name;
    }

    public void OnBeforeSerialize() {
        qualifiedName = storedType.AssemblyQualifiedName;

        #if UNITY_EDITOR
        baseTypeName = typeof(T).AssemblyQualifiedName;
        #endif
    }

    public void OnAfterDeserialize() {
        if (string.IsNullOrEmpty(qualifiedName) || qualifiedName == "null") {
            storedType = null;
            return;
        }
        storedType = System.Type.GetType(qualifiedName);
    }

    public static implicit operator System.Type(InspectableType<T> t) => t.storedType;

    // TODO: Validate that t is a subtype of T?
    public static implicit operator InspectableType<T>(System.Type t) => new InspectableType<T>(t);
}

This lets you add a field in your scriptable object something like this:

[SerializeField]
InspectableType<BaseTypeOrInterface> myType = typeof(DerivedType);

I've implemented implicit conversions to and from System.Type so you can mostly ignore this wrapper elsewhere.

Next, create a folder called "Editor" and add the following property drawer:

using UnityEngine;
using UnityEditor;
using System.Linq;

using Type = System.Type;

[CustomPropertyDrawer(typeof(InspectableType<>), true)]
public class InspectableTypeDrawer : PropertyDrawer {
    System.Type[] _derivedTypes;
    GUIContent[] _optionLabels;
    int _selectedIndex;

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        var storedProperty = property.FindPropertyRelative("qualifiedName");
        string qualifiedName = storedProperty.stringValue;

        if (_optionLabels == null) {
            Initialize(property, storedProperty);
        } else if (_selectedIndex == _derivedTypes.Length) {
            if (qualifiedName != "null") UpdateIndex(storedProperty);
        } else {
            if (qualifiedName != _derivedTypes[_selectedIndex].AssemblyQualifiedName) UpdateIndex(storedProperty);
        }

        var propLabel = EditorGUI.BeginProperty(position, label, property);
        EditorGUI.BeginChangeCheck();

        _selectedIndex = EditorGUI.Popup(position, propLabel, _selectedIndex, _optionLabels);

        if (EditorGUI.EndChangeCheck()) {            
            storedProperty.stringValue = _selectedIndex < _derivedTypes.Length ? _derivedTypes[_selectedIndex].AssemblyQualifiedName : "null";
        }
        EditorGUI.EndProperty();
    }

    static Type[] FindAllDerivedTypes(Type baseType)
    {
        return baseType.Assembly
            .GetTypes()
            .Where(t =>
                t != baseType &&
                baseType.IsAssignableFrom(t)
                ).ToArray<Type>();
    }

    void Initialize(SerializedProperty property, SerializedProperty stored) {       

        var baseTypeProperty = property.FindPropertyRelative("baseTypeName");
        var baseType = Type.GetType(baseTypeProperty.stringValue);

        _derivedTypes = FindAllDerivedTypes(baseType);

        if (_derivedTypes.Length == 0) {
            _optionLabels = new[] { new GUIContent($"No types derived from {baseType.Name} found.")};
            return;
        }
        
        _optionLabels = new GUIContent[_derivedTypes.Length+1];
        for (int i = 0; i < _derivedTypes.Length; i++) {
            _optionLabels[i] = new GUIContent(_derivedTypes[i].Name);
        }
        _optionLabels[_derivedTypes.Length] = new GUIContent("null");

        UpdateIndex(stored);
    }

    void UpdateIndex(SerializedProperty stored) {
        string qualifiedName = stored.stringValue;
        
        for (int i = 0; i < _derivedTypes.Length; i++)
        {
            if (_derivedTypes[i].AssemblyQualifiedName == qualifiedName)
            {
                _selectedIndex = i;
                return;
            }
        }
        _selectedIndex = _derivedTypes.Length;
        stored.stringValue = "null";
    }
}

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .