Unity的CustomPropertyDrawer和PropertyDrawer详解
一、什么是 PropertyDrawer
PropertyDrawer 是 Unity 编辑器扩展中的一个基类,继承自 GUIDrawer,用于自定义属性在 Inspector 面板中的绘制方式。它让你能够完全控制某个字段在编辑器中的视觉表现,而不需要编写完整的 CustomEditor。
PropertyDrawer 有两种核心用途:
- 自定义 Serializable 类的每个实例的 GUI
- 自定义带有 PropertyAttribute 的脚本成员的 GUI
二、[CustomPropertyDrawer] 特性
[CustomPropertyDrawer] 是一个标记特性,用于告诉 Unity 该绘制器针对的是哪个类型。它需要放置在 PropertyDrawer 子类的上方。
针对 Serializable 类时:
[CustomPropertyDrawer(typeof(Ingredient))] public class IngredientDrawer : PropertyDrawer { // ... }
针对自定义 PropertyAttribute 时:
[CustomPropertyDrawer(typeof(MyCustomAttribute))] public class MyCustomDrawer : PropertyDrawer { // ... }
三、自定义 Serializable 类的绘制
假设你有这样一个可序列化的类:
using System; using UnityEngine; public enum IngredientUnit { Spoon, Cup, Bowl, Piece } [Serializable] public class Ingredient { public string name; public int amount = 1; public IngredientUnit unit; } public class Recipe : MonoBehaviour { public Ingredient potionResult; public Ingredient[] potionIngredients; }
现在编写一个 PropertyDrawer 来自定义它在 Inspector 中的外观。
IMGUI 方式
using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(Ingredient))] public class IngredientDrawer : PropertyDrawer { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { EditorGUI.BeginProperty(position, label, property); position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label); var indent = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; var amountRect = new Rect(position.x, position.y, 30, position.height); var unitRect = new Rect(position.x + 35, position.y, 50, position.height); var nameRect = new Rect(position.x + 90, position.y, position.width - 90, position.height); EditorGUI.PropertyField(amountRect, property.FindPropertyRelative("amount"), GUIContent.none); EditorGUI.PropertyField(unitRect, property.FindPropertyRelative("unit"), GUIContent.none); EditorGUI.PropertyField(nameRect, property.FindPropertyRelative("name"), GUIContent.none); EditorGUI.indentLevel = indent; EditorGUI.EndProperty(); } }
UIElements 方式(推荐新项目使用)
using UnityEditor; using UnityEditor.UIElements; using UnityEngine.UIElements; [CustomPropertyDrawer(typeof(Ingredient))] public class IngredientDrawerUIE : PropertyDrawer { public override VisualElement CreatePropertyGUI(SerializedProperty property) { var container = new VisualElement(); var nameField = new PropertyField(property.FindPropertyRelative("name"), "Fancy Name"); var amountField = new PropertyField(property.FindPropertyRelative("amount")); var unitField = new PropertyField(property.FindPropertyRelative("unit")); container.Add(nameField); container.Add(amountField); container.Add(unitField); return container; } }
四、自定义 PropertyAttribute 的绘制
这是另一种常见用法:先创建一个自定义 Attribute,再为其编写 PropertyDrawer。
步骤 1:创建自定义 Attribute
using UnityEngine; public class RangeAttribute : PropertyAttribute { public float min; public float max; public RangeAttribute(float min, float max) { this.min = min; this.max = max; } }
步骤 2:创建对应的 PropertyDrawer
using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(RangeAttribute))] public class RangeDrawer : PropertyDrawer { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { RangeAttribute range = attribute as RangeAttribute; if (property.propertyType == SerializedPropertyType.Float) EditorGUI.Slider(position, property, range.min, range.max, label); else if (property.propertyType == SerializedPropertyType.Integer) EditorGUI.IntSlider(position, property, (int)range.min, (int)range.max, label); else EditorGUI.LabelField(position, label.text, "Use Range with float or int."); } }
步骤 3:使用自定义 Attribute
public class MyComponent : MonoBehaviour { [Range(0f, 100f)] public float health; }
只需加上 [Range(0f, 100f)],Inspector 中就会自动显示为滑块,无需额外配置。
五、关键方法与属性
| 方法 / 属性 | 说明 |
|---|---|
OnGUI(Rect, SerializedProperty, GUIContent) |
IMGUI 方式下的核心绘制方法 |
CreatePropertyGUI(SerializedProperty) |
UIElements 方式下的核心绘制方法 |
GetPropertyHeight(SerializedProperty, GUIContent) |
重写以自定义属性所占的高度 |
attribute |
获取当前 PropertyDrawer 对应的 Attribute 实例 |
fieldInfo |
获取当前字段的反射信息 |
六、重写 GetPropertyHeight
当自定义绘制的内容高度超过默认单行时,必须重写此方法:
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { // 例如:标题一行 + 三行内容 + 间距 return EditorGUIUtility.singleLineHeight * 4 + EditorGUIUtility.standardVerticalSpacing * 3; }
七、重要注意事项
-
PropertyDrawer 仅对可序列化的字段有效 — 非 public 且没有
[SerializeField]标记的字段不会被绘制。 -
OnGUI 中必须使用 EditorGUI 而非 GUI 类 — EditorGUI 会自动处理缩进,GUI 类则不会。
-
OnGUI 内不要使用 Layout 方法 — 在
EditorGUI.BeginProperty/EndProperty包裹范围内使用 Layout 方法会导致异常:ArgumentException: Getting control 1's position in a group with only 1 controls when doing repaint。 -
正确使用 BeginProperty / EndProperty — 这一对方法确保 prefab 的覆盖逻辑能正确处理整个属性块。
-
UIElements 与 IMGUI 互斥 — 如果在基于 UIElements 的检查器中使用 PropertyDrawer,则优先使用
CreatePropertyGUI实现;如果在 IMGUI 环境中,则只使用OnGUI。 -
PropertyDrawer 不能用于继承 MonoBehaviour 的类 — 只能用于
[Serializable]类或带有 PropertyAttribute 的字段。
八、常用技巧
- 获取所属对象:
property.serializedObject.targetObject - 查找子属性:
property.FindPropertyRelative("字段名") - 控制缩进:
EditorGUI.indentLevel++和EditorGUI.indentLevel-- - 计算实际高度:绘制结束后用
rect.y - position.y即可得出实际占用高度 - 展开/折叠:使用
EditorGUI.Foldout实现可折叠的自定义绘制
PropertyDrawer 是 Unity 编辑器扩展中最灵活、最常用的组件之一,掌握了它,你就能随心所欲地定制 Inspector 面板的外观,显著提升开发效率和团队协作体验。
浙公网安备 33010602011771号