如何在 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,最糟糕的是 - 它不会永久保存这些更改!
总是宁愿通过 serializedObject
和 SerializedProperty
s。
[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 无论如何都不支持它。
我有一个 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,最糟糕的是 - 它不会永久保存这些更改!
总是宁愿通过 serializedObject
和 SerializedProperty
s。
[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 无论如何都不支持它。