Unity自定义Inspector:为列表添加序列化类并显示子类属性
解决Unity多态Scriptable Object列表的Inspector属性编辑问题
1. 先搞定子类属性的序列化问题
Unity默认序列化对多态类型支持有限,直接用List<BehaviourBase>只会保存基类信息,子类属性不会被正确序列化。核心解决办法是用SerializeReference特性(Unity 2019.3+支持),它能完整序列化多态类型的所有字段。
修改你的ObjectData脚本:
using UnityEngine; using System.Collections.Generic; [CreateAssetMenu(fileName = "ObjectData", menuName = "ScriptableObjects/ObjectData")] public class ObjectData : ScriptableObject { // 用SerializeReference替代默认序列化,支持多态类型的完整字段保存 [SerializeReference] public List<BehaviourBase> behaviours = new List<BehaviourBase>(); }
同时确保所有BehaviourBase的子类都标记[Serializable]:
using UnityEngine; [Serializable] public abstract class BehaviourBase : ScriptableObject { public abstract void Execute(); } [Serializable] public class MoveBehaviour : BehaviourBase { public float moveSpeed = 5f; public Vector3 targetPosition; public override void Execute() { // 实现逻辑 } } [Serializable] public class AttackBehaviour : BehaviourBase { public int damage = 10; public float attackRange = 3f; public override void Execute() { // 实现逻辑 } }
2. 编写自定义Editor脚本绘制子类属性
自定义Editor时不能直接用默认的PropertyField处理整个列表,需要遍历每个元素,针对子类实例单独绘制其属性:
using UnityEditor; using UnityEngine; using System.Collections.Generic; using System.Linq; [CustomEditor(typeof(ObjectData))] public class ObjectDataEditor : Editor { private ObjectData _data; private void OnEnable() { _data = target as ObjectData; } public override void OnInspectorGUI() { base.OnInspectorGUI(); // 添加Behaviour的按钮 if (GUILayout.Button("Add Behaviour")) { GenericMenu menu = new GenericMenu(); // 获取所有BehaviourBase的非抽象子类 var behaviourTypes = System.AppDomain.CurrentDomain.GetAssemblies() .SelectMany(asm => asm.GetTypes()) .Where(t => t.IsSubclassOf(typeof(BehaviourBase)) && !t.IsAbstract); foreach (var type in behaviourTypes) { menu.AddItem(new GUIContent(type.Name), false, () => { Undo.RecordObject(_data, "Add Behaviour"); var newBehaviour = ScriptableObject.CreateInstance(type) as BehaviourBase; _data.behaviours.Add(newBehaviour); EditorUtility.SetDirty(_data); }); } menu.ShowAsContext(); } // 绘制每个Behaviour的属性 EditorGUILayout.Space(); EditorGUILayout.LabelField("Behaviours", EditorStyles.boldLabel); for (int i = 0; i < _data.behaviours.Count; i++) { var behaviour = _data.behaviours[i]; if (behaviour == null) { _data.behaviours.RemoveAt(i); i--; continue; } // 折叠框显示类名,展开后展示属性 using (var foldout = new EditorGUILayout.FoldoutScope(true, behaviour.GetType().Name)) { if (foldout.value) { EditorGUI.indentLevel++; // 序列化并绘制子类的所有可编辑属性 SerializedObject serializedBehaviour = new SerializedObject(behaviour); serializedBehaviour.Update(); SerializedProperty prop = serializedBehaviour.GetIterator(); // 跳过m_Script字段,避免误改脚本引用 if (prop.NextVisible(true)) { do { if (prop.name != "m_Script") { EditorGUILayout.PropertyField(prop, true); } } while (prop.NextVisible(false)); } serializedBehaviour.ApplyModifiedProperties(); EditorGUI.indentLevel--; } } // 删除按钮 if (GUILayout.Button($"Remove {behaviour.GetType().Name}", GUILayout.Width(120))) { Undo.RecordObject(_data, "Remove Behaviour"); _data.behaviours.RemoveAt(i); i--; EditorUtility.SetDirty(_data); } EditorGUILayout.Space(); } } }
3. 为什么之前的Property Drawer没生效?
大概率是这两个原因:
- 没给列表字段加
[SerializeReference],导致子类属性根本没被序列化,Property Drawer找不到可绘制的字段; - Property Drawer是针对单个固定类型字段的,而你需要处理的是列表中的多态元素,这种场景下自定义Editor脚本比Property Drawer更适配。
注意事项
SerializeReference要求Unity版本在2019.3及以上;- 如果子类是普通可序列化类(不继承
ScriptableObject),直接标记[Serializable]即可,创建实例用Activator.CreateInstance(type); - 用
Undo.RecordObject和EditorUtility.SetDirty确保操作可撤销,且修改能正确保存到资产中。
内容的提问来源于stack exchange,提问作者Chen Robin
相关产品推荐
相关产品推荐

