如何在 Unity Editor 中绘制继承相同 Class 的非 MonoBehaviour 对象列表?

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

我有一个 List<AbilityEffect> effects 和很多子 class 的 AbilityEffect,比如 DamageEffect,HealEffect e.t.c。上面有 [System.Serializable] 属性。 如果我使用 DamageEffect 等字段创建 class - 默认编辑器将完美地绘制它! (还有其他效果!)

我在 AbilityData.cs

中为这个函数添加了一个 ContextMenu 属性
[ContextMenu(Add/DamageEffect)]

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

但是如果是 AbilityEffect,默认的 Unity 编辑器会绘制它,而不是 DamageEffect

我为 class 编写了一些自定义编辑器,其中包含 List<AbilityEffect> effects = new List<AbilitiEffect>(),编写绘制自定义列表的代码!但是我如何告诉编辑器专门绘制 DamageEffect,而不是 AbilityEffect

我将在下面放一些代码:

能力数据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());
    }
}

能力数据编辑器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??
    }
}

技能效果class(非抽象)

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

伤害效果Class

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

由于序列化的工作方式,一旦您反序列化某些数据,Unity 将尝试根据 class 定义中指定的类型填充对象实例。如果您有 List<AbilityEffect>,Unity 将无法区分您之前序列化的具体 AbilityEffect。确实有一个解决方案,将 AbilityEffect 更改为 ScriptableObject,这样 Unity 实际上不会将它们序列化为原始数据,而是作为 GUID 引用,以便引用的资产自己知道 AbilityEffect 的子类型他们是。缺点是这样你所有的效果都必须是你的资产文件夹中的资产。

首先:请注意,自从 Unity 2021 以来,折页是所有列表和数组的内置默认设置,所以实际上我认为完全不需要自定义编辑器(至少对于列表部分);)


你的方法有几个问题。

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

是的,因为它仅被序列化为 AbilityEffect! 对于序列化程序,该列表中的所有项目都是 AbilityEffect 类型并且它不会'不要再往前走了。

因此,即使您可以设法添加子类物品,也只是暂时的!例如之后保存、关闭 Unity 并重新打开所有子类型应该转换为 AbilityEffect 因为那是星期二序列化程序实际看到的类型。


我的建议是让您的 AbilityEffect 也成为 ScriptableObject 类型。这样,您甚至根本不必为它们设置自定义抽屉,并且可以根据需要拥有尽可能多的具有不同类型和配置的实例,重用它们等。


这说的是一般性的事情:不要直接在编辑器中通过target(除非你确切地知道你在做什么)

这不会将此对象标记为“脏”,不适用于 Undo/Redo,最糟糕的是 - 它不会永久保存这些更改!

总是宁愿通过 serializedObjectSerializedPropertys。

[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();
    }
}

现在,如果您真的想自定义列表绘图的行为,您可以使用 ReorderableList 然后可以为每个元素实现一个抽屉,在那里您确实可以执行类型检查。

但如前所述,我根本不会这样做,因为 Serializer 无论如何都不支持它。