Unity的CustomPropertyDrawer和PropertyDrawer详解

一、什么是 PropertyDrawer

PropertyDrawer 是 Unity 编辑器扩展中的一个基类,继承自 GUIDrawer,用于自定义属性在 Inspector 面板中的绘制方式。它让你能够完全控制某个字段在编辑器中的视觉表现,而不需要编写完整的 CustomEditor。

PropertyDrawer 有两种核心用途:

  1. ‌自定义 Serializable 类的每个实例的 GUI‌
  2. ‌自定义带有 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;
}

七、重要注意事项

  1. ‌PropertyDrawer 仅对可序列化的字段有效‌ — 非 public 且没有 [SerializeField] 标记的字段不会被绘制。

  2. ‌OnGUI 中必须使用 EditorGUI 而非 GUI 类‌ — EditorGUI 会自动处理缩进,GUI 类则不会。

  3. ‌OnGUI 内不要使用 Layout 方法‌ — 在 EditorGUI.BeginProperty / EndProperty 包裹范围内使用 Layout 方法会导致异常:ArgumentException: Getting control 1's position in a group with only 1 controls when doing repaint

  4. ‌正确使用 BeginProperty / EndProperty‌ — 这一对方法确保 prefab 的覆盖逻辑能正确处理整个属性块。

  5. ‌UIElements 与 IMGUI 互斥‌ — 如果在基于 UIElements 的检查器中使用 PropertyDrawer,则优先使用 CreatePropertyGUI 实现;如果在 IMGUI 环境中,则只使用 OnGUI

  6. ‌PropertyDrawer 不能用于继承 MonoBehaviour 的类‌ — 只能用于 [Serializable] 类或带有 PropertyAttribute 的字段。

八、常用技巧

  • ‌获取所属对象‌:property.serializedObject.targetObject
  • ‌查找子属性‌:property.FindPropertyRelative("字段名")
  • ‌控制缩进‌:EditorGUI.indentLevel++ 和 EditorGUI.indentLevel--
  • ‌计算实际高度‌:绘制结束后用 rect.y - position.y 即可得出实际占用高度
  • ‌展开/折叠‌:使用 EditorGUI.Foldout 实现可折叠的自定义绘制

PropertyDrawer 是 Unity 编辑器扩展中最灵活、最常用的组件之一,掌握了它,你就能随心所欲地定制 Inspector 面板的外观,显著提升开发效率和团队协作体验。

posted on 2026-05-19 11:02  -冷夜-  阅读(61)  评论(0)    收藏  举报

导航