Home > Enterprise >  How to Draw a list of non-MonoBehaviour Objects, that Inherits same Class in Unity Editor?
How to Draw a list of non-MonoBehaviour Objects, that Inherits same Class in Unity Editor?

Time:01-19

I have a List<AbilityEffect> effects and a lot of sub-classes of AbilityEffect, such as DamageEffect, HealEffect e.t.c. that HAVE [System.Serializable] property on it. If I create class with field such as DamageEffect - Default editor will draw it perfectly! (And other effects too!)

I've added a ContextMenu Attribute to this function in AbilityData.cs

[ContextMenu(Add/DamageEffect)]

public static void AddDamageEffect()
{
    effects.Add(new DamageEffect());
}

BUT default Unity Editor draws it if was an AbilityEffect, NOT a DamageEffect!

I've write some Custom Editor for class, that contains List<AbilityEffect> effects = new List<AbilitiEffect>(), write code that draws a custom list! But how do I tell a Editor to draw a DamageEffect specifically, NOT AbilityEffect?

I'll put some code below:

Ability Data Class

using UnityEngine;
using System.Collections.Generic;

[CreateAssetMenu(fileName = "New Ability", menuName = "ScriptableObject/Ability")]
public class AbilityData : ScriptableObject
{
    public int cooldown = 0;
    public int range = 1;
    public List<AbilityEffect> effects = new List<AbilityEffect>();
    public bool showEffects = false;

    [ContextMenu("Add/DamageEffect")]
    public void AddDamageEffect()
    {
        effects.Add(new DamageEffect());
    }
}

Ability Data Editor Class

using UnityEditor;
using UnityEngine;
using System.Collections.Generic;

[CustomEditor(typeof(AbilityData))]
public class AbilityEditor : Editor
{
    public override void OnInspectorGUI()
    {
        var ability = (AbilityData)target;
        DrawDetails(ability);
        DrawEffects(ability);
    }

    private static void DrawEffects(AbilityData ability)
    {
        EditorGUILayout.Space();
        ability.showEffects = EditorGUILayout.Foldout(ability.showEffects, "Effects", true);

        if (ability.showEffects)
        {
            EditorGUI.indentLevel  ;
            List<AbilityEffect> effects = ability.effects;
            int size = Mathf.Max(0, EditorGUILayout.IntField("Size", effects.Count));

            while (size > effects.Count)
            {
                effects.Add(null);
            }

            while (size < effects.Count)
            {
                effects.RemoveAt(effects.Count - 1);
            }

            for (int i = 0; i < effects.Count; i  )
            {
                DrawEffect(effects[i], i);
            }
            EditorGUI.indentLevel--;
        }
    }

    private static void DrawDetails(AbilityData ability)
    {
        EditorGUILayout.LabelField("Details");
        EditorGUILayout.Space();

        EditorGUILayout.BeginHorizontal();
        EditorGUILayout.LabelField("Cooldown", GUILayout.MaxWidth(60));
        ability.cooldown = EditorGUILayout.IntField(ability.cooldown);
        EditorGUILayout.LabelField("Range", GUILayout.MaxWidth(40));
        ability.range = EditorGUILayout.IntField(ability.range);
        EditorGUILayout.EndHorizontal();
    }

    private static void DrawEffect(AbilityEffect effect, int index)
    {
        //if (effect is DamageEffect)
        //    effect = EditorGUILayout
        // HOW??
    }
}

Ability Effect class (NOT ABSTRACT)

[System.Serializable]
public class AbilityEffect
{
    public virtual void Affect() { }
}

Damage Effect Class

[System.Serializable]
public class DamageEffect : AbilityEffect
{
    public int damageAmout = 1;
    public override void Affect() { ... }
}

CodePudding user response:

First of all: Note that since Unity 2021 the foldout is built-in default for all lists and arrays so actually I see absolutely no need for a custom editor at all really (at least for the list part) ;)


There is a couple of problems with that approach.

BUT default Unity Editor draws it if was an AbilityEffect, NOT a DamageEffect.

Yes, because it is serialized only as a AbilityEffect!

So even if you can manage to add subclass items it will only be temporary! After e.g. saving, closing Unity and reopening all the subtypes should be converted to AbilityEffect because that's tue only type the Serializer actually sees for those.


My recommendation would be to rather make your AbilityEffect also of type ScriptableObject. This way you don't even have to bother with a custom drawer for them at all and could have as many instances with different types and configurations as you want, reuse them etc.


This said now a general thing: Don't directly go through the target in editors! (except you know exactly what you are doing)

This doesn't mark this object as "dirty", doesn't work with Undo/Redo and worst of all - it won't save these changes persistently!

Always rather go through the serializedObject and the SerializedPropertys.

[CustomEditor(typeof(AbilityData))]
public class AbilityEditor : Editor
{
    SerializedProperty cooldown;
    SerializedProperty range;
    SerializedProperty effects;
    SerializedProperty showEffects;

    private void OnEnable ()
    {
        // Link up the serialized fields you will access
        cooldown = serializedObject.FindProperty(nameof(AbilityData.cooldown)); 
        range = serializedObject.FindProperty(nameof(AbilityData.range)); 
        effects = serializedObject.FindProperty(nameof(AbilityData.effects));
        showEffects = serializedObject.FindProperty(nameof(AbilityData.showEffects));
    }

    public override void OnInspectorGUI()
    {
        // refresh current actual values into the editor
        serializedObject.Update();

        DrawDetails();
        DrawEffects();

        // write back any changed values from the editor back to the actual object
        // This handles all marking dirty, saving and handles Undo/Redo
        serializedObject.ApplyModifiedProperties();
    }

    private void DrawEffects()
    {
        // Now always ever only read and set values via the SerializedPropertys

        EditorGUILayout.Space();
        showEffects.boolValue = EditorGUILayout.Foldout(showEffects.boolValue, effects.displayName, true);

        if (showEffects.boolValue)
        {
            EditorGUI.indentLevel  ;

            // This already handles all the list drawing by default
            EditorGUILayout.PropertyField(effects, GUIContent.none, true);
            EditorGUI.indentLevel--;
        }
    }
        
    private void DrawDetails()
    {
        EditorGUILayout.LabelField("Details");
        EditorGUILayout.Space();

        EditorGUILayout.BeginHorizontal();
        EditorGUILayout.LabelField(cooldown.displayName, GUILayout.MaxWidth(60));
        cooldown.intValue = EditorGUILayout.IntField(cooldown.intValue);
        EditorGUILayout.LabelField(range.displayName, GUILayout.MaxWidth(40));
        range.intValue = EditorGUILayout.IntField(range.intValue);
        EditorGUILayout.EndHorizontal();
    }
}

Now if you really really want to customize the behavior of the list drawing you could use a ReorderableList and can then implement a drawer for each element and there you could indeed perform a type check.

But as said I wouldn't go this way at all since the Serializer doesn't support it anyway.

CodePudding user response:

Because of how Serialization works, once you deserialize some data Unity will try to populate an object instance based on the type specified in the class definition. If you have a List<AbilityEffect> Unity won't be able to differentiate which specific AbilityEffect you previously serialized. There is really one solution, change AbilityEffect to be a ScriptableObject, so that Unity doesn't actually serialize them as raw data but as GUID references, so that the referenced assets know by themselves what subtype of AbilityEffect they are. The downside is that this way all your effects will have to be assets in your Assets folder.

  •  Tags:  
  • Related