How to draw custom inspector for List<base_class>

Hello all,

I’m trying to make a custom inspector editor for quests which would have different types of tasks that the user can add dynamically, reorder, and so on. However there is no easy way for me to accomplish this, I’ve tried property drawers, reflection,etc.

Below I’ve pasted my quest class and the editor Ive gotten closest to creating so far. I tried brute forcing it and looping through all the tasks, checking the type, and drawing based on type, but even this has an issue: when reloading scripts, the inspector stops drawing them for some reason. My only assumption is that the reload causes the List to revert all its elements to the base class instead of the derived classes added by the user, thus not being considered as their derived classes in the foreach loop in
OnInspectorGUI() anymore.

Quest class

public class Quest : ScriptableObject
{

    public List<QuestTaskBase> Tasks = new List<QuestTaskBase>();




    [System.Serializable]
    public class QuestTaskBase
    {
    }

    [System.Serializable]
    public class KillEnemyTask : QuestTaskBase
    {
        public EnemyData Enemy;
        public int Quantity;
    }

    [System.Serializable]
    public class CollectItemTask : QuestTaskBase
    {
        public ItemBase Item;
        public int Quantity;
    }

}

The editor I got so far

#if UNITY_EDITOR
[CustomEditor(typeof(Quest))]
public class QuestEditor : Editor
{

    private Quest _quest;
    private GenericMenu _menu;

    private void OnEnable()
    {
        _quest = (Quest)target;

        _menu = new GenericMenu();
        _menu.AddItem(new GUIContent("Kill Enemy Task"), true, () => AddTask(typeof(Quest.KillEnemyTask)));
        _menu.AddItem(new GUIContent("Collect Item Task"), true, () => AddTask(typeof(Quest.CollectItemTask)));
    }

    private void AddTask(Type type)
    {
        _quest.Tasks.Add((Quest.QuestTaskBase)Activator.CreateInstance(type));
        EditorUtility.SetDirty(_quest);
        AssetDatabase.SaveAssets();

    }

    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        if (GUILayout.Button("Add task..."))
        {
            _menu.ShowAsContext();
        }

        if (!_quest.Tasks.HasElements())
        {
            GUILayout.Label("No tasks assigned.");
            return;
        }

        foreach (var task in _quest.Tasks)
        {
            EditorGUILayout.Space();

            GUILayout.Label($"TASK {_quest.Tasks.IndexOf(task)}:", EditorStyles.boldLabel);

            if (task is Quest.KillEnemyTask killEnemyTask)
                DrawKillEnemyTask(killEnemyTask);
            else if (task is Quest.CollectItemTask collectItemTask)
                DrawCollectItemTask(collectItemTask);
        }

    }
    void DrawCollectItemTask(Quest.CollectItemTask collectItemTask)
    {
        GUILayout.Label($"Collect {collectItemTask.Quantity} {collectItemTask.Item?.name ?? "[Item]"}s");
        collectItemTask.Item = EditorGUILayout.ObjectField("Item", collectItemTask.Item, typeof(ItemBase), true) as ItemBase;
        collectItemTask.Quantity = EditorGUILayout.IntField("Quantity", collectItemTask.Quantity);
    }

    void DrawKillEnemyTask(Quest.KillEnemyTask killEnemyTask)
    {
        GUILayout.Label($"Kill {killEnemyTask.Quantity} {killEnemyTask.Enemy?.name ?? "[Enemy]"}s");
        killEnemyTask.Enemy = EditorGUILayout.ObjectField("Enemy", killEnemyTask.Enemy, typeof(EnemyData), true) as EnemyData;
        killEnemyTask.Quantity = EditorGUILayout.IntField("Quantity", killEnemyTask.Quantity);
    }

}
#endif

Kinda amusing that this thread was posted about the same time as yours: Sub class property drawer

1 Like

You’re correct, Unity inline serializes all derived classes as their base class. They have, however, added a new [SerializeReference] attribute somewhat recently that may help you here.

I personally have no experience using it, but hopefully it helps.

2 Likes

I need to add it’s broken a bit sometimes, but it’s more stable from 2021.
By broken I mean losing references/serialized data or just errors in console.

that is so funny! thank you and thanks @GroZZleR for the suggestions, ill try them out

UPDATE:
spineys suggestion works best, but only works on 2021 LTS which i thankfully use