NaughtyAttributes Drawer Attributes 完全指南:用声明式属性为 Unity Inspector 打造专业绘制界面

原创2026-10-12 22:01:281,664 阅读
文章标签:游戏开发开发工具

NaughtyAttributes Drawer Attributes 完全指南:用声明式属性为 Unity Inspector 打造专业绘制界面

本指南以 NaughtyAttributes 仓库的 Drawer Attributes 文档(docs/sphinx/src/attributes/drawer_attributes/index.rst)为主线,系统讲解这一组「绘制类属性」的用途、用法、底层原理与已知限制。读完本文,你将掌握 18 个绘制属性的完整 API 与示例代码,理解它们如何作用于序列化字段、非序列化字段、原生 C# 属性与方法,并能在自己的项目中直接落地使用。

什么是 Drawer Attributes

NaughtyAttributes 将扩展属性分为三大类:Meta Attributes(元数据,如 EnableIf、Label)、Validator Attributes(验证器,如 Required、MinValue)以及本文的主角 Drawer Attributes(绘制器)。

根据官方文档的定义:Drawer attributes provide special draw options for the Unity Inspector——它们为 Inspector 提供特殊的绘制选项。与只能作用于序列化字段的 Unity 原生 PropertyAttribute 不同,NaughtyAttributes 的绘制属性可以应用于:

  • 序列化字段(serialized fields);
  • 非序列化字段(non-serialized fields,如 ShowNonSerializedField);
  • 原生 C# 属性(native C# properties,如 ShowNativeProperty);
  • 函数 / 方法(如 Button)。

需要特别注意的是文档中给出的一条硬性规则:

一个 field、property 或 function 只能拥有一个 drawer attribute。

如果给同一目标叠加了多个绘制属性,按 README 的说明,只有最底下的那一个会生效(only the bottom one will be used)。这与 Meta Attributes(可以叠加多个)和 Validator Attributes(可以无限叠加)形成鲜明对比。

从源码结构看,所有绘制属性统一继承自 DrawerAttribute,而后者继承自 Unity 的 PropertyAttribute 并实现 INaughtyAttribute 接口:

public class DrawerAttribute : PropertyAttribute, INaughtyAttribute
{
}

DrawerAttribute 本身是空基类,它承担「标记」职责,让编辑器层可以统一识别并调度对应的绘制器。所有具体属性的 [AttributeUsage] 均声明为 AllowMultiple = false,这从编译层面就保证了「一个目标只能有一个绘制属性」的约束。

绘制属性的底层工作原理

了解绘制器如何被调用,有助于理解为什么某些属性在特定场景下会失效。

两条绘制管线

NaughtyAttributes 的编辑器代码位于 Assets/NaughtyAttributes/Scripts/Editor/,其核心调度逻辑在 NaughtyInspector.cs 中。绘制属性实际由两类绘制器基类驱动:

  1. PropertyDrawerBase(PropertyDrawers/PropertyDrawerBase.cs):继承自 Unity 的 PropertyDrawer,对应 AnimatorParam、Dropdown、CurveRange 等大多数属性;
  2. SpecialCasePropertyDrawerBase(PropertyDrawers_SpecialCase/SpecialCasePropertyDrawerBase.cs):对应 Button、ReorderableList、ShowNativeProperty、ShowNonSerializedField 这四个「特殊」绘制属性——它们无法作为标准的 CustomPropertyDrawer 工作,需要走 NaughtyInspector 的专用调度。

统一的绘制流程

无论是哪条管线,每次绘制都遵循同一个流程(见 PropertyDrawerBase.OnGUI 与 SpecialCasePropertyDrawerBase.OnGUI):

  1. 可见性检查:通过 PropertyUtility.IsVisible(property) 判断该字段是否被 ShowIf/HideIf 等元属性隐藏;
  2. 验证器执行:遍历字段上挂载的 ValidatorAttribute,调用各自的 ValidateProperty 执行校验;
  3. 禁用范围:通过 PropertyUtility.IsEnabled(property) 判断是否处于 EnableIf/DisableIf/ReadOnly 的禁用状态,并包裹在 EditorGUI.DisabledScope 中;
  4. 实际绘制:调用子类实现的 OnGUI_Internal 画出定制控件;
  5. 变更回调:通过 EditorGUI.EndChangeCheck() 检测值变化,触发 OnValueChanged 回调。

这意味着你完全可以把绘制属性与元属性、验证器自由组合——例如「满足条件才可编辑的下拉框」「带进度条且带校验的数值字段」,绘制属性会无缝融入 NaughtyAttributes 的整套属性体系。

README 中还给出了一个重要的兼容性说明:大多数绘制属性基于 Unity 的 CustomPropertyDrawer 实现,因此在你自己编写的自定义 Editor 中也能正常工作;但 ReorderableList、Button、ShowNonSerializedField、ShowNativeProperty 这 4 个特殊属性无法在自定义 Editor 中直接生效。若希望所有属性在自定义 Editor 中完整可用,需要让你的 Inspector 继承 NaughtyInspector,并使用 NaughtyEditorGUI.PropertyField_Layout 代替 EditorGUILayout.PropertyField 来绘制字段。

下拉选择类属性

这一类属性将普通的文本输入框替换为下拉选择框,覆盖 Unity 中各种「命名资源」的选择场景。

AnimatorParam:选择 Animator 参数

AnimatorParam 让你通过下拉界面直接选择 Animator Controller 中的参数,无需手敲参数名或哈希值。它可以作用于 int(保存参数哈希值)或 string(保存参数名)字段:

public class NaughtyComponent : MonoBehaviour
{
    public Animator someAnimator;

    [AnimatorParam("someAnimator")]
    public int paramHash;

    [AnimatorParam("someAnimator")]
    public string paramName;
}

从 AnimatorParamAttribute 源码看,构造函数的第一个参数是 Animator 字段的名称字符串(即 "someAnimator"),而非 Animator 引用本身。其 AnimatorParamType 属性默认为 null(不过滤类型),也可以传入 AnimatorControllerParameterType 来限定只显示某种类型的参数(如 Float、Bool、Trigger)。

Dropdown:字段 / 属性 / 方法驱动的下拉框

Dropdown 是最灵活的下拉属性,它的选项值可以由三种来源提供:一个字段、一个属性或一个无参方法,只需在特性参数中传入对应的成员名称:

public class NaughtyComponent : MonoBehaviour
{
    [Dropdown("intValues")]
    public int intValue;

    [Dropdown("StringValues")]
    public string stringValue;

    [Dropdown("GetVectorValues")]
    public Vector3 vectorValue;

    private int[] intValues = new int[] { 1, 2, 3, 4, 5 };

    private List<string> StringValues { get { return new List<string>() { "A", "B", "C", "D", "E" }; } }

    private DropdownList<Vector3> GetVectorValues()
    {
        return new DropdownList<Vector3>()
        {
            { "Right",   Vector3.right },
            { "Left",    Vector3.left },
            { "Up",      Vector3.up },
            { "Down",    Vector3.down },
            { "Forward", Vector3.forward },
            { "Back",    Vector3.back }
        };
    }
}

这里展示了两种取值容器:

  • IList 实现(int[]、List<string>):选项的显示名直接用 value.ToString(),适用于值类型或 ToString 有意义的对象;
  • DropdownList<T>(定义在 DropdownAttribute.cs):显示名与值解耦,通过 Add(displayName, value) 或集合初始化器添加键值对,用于「显示名 ≠ 值的字符串表示」的场景,例如上面把 Vector3.right 显示为 "Right"。

从 DropdownPropertyDrawer 的 GetValues 方法可以看到其解析顺序:先按字段名查找字段(FieldInfo),再查属性(PropertyInfo),最后查找无参且返回值非 void 的方法(MethodInfo)。如果提供的名称不存在,或者目标字段类型与选项元素类型不匹配,绘制器会回退为默认绘制并显示一条 Invalid values with name '...' 的警告 HelpBox(源码 L117-L120)。

重要限制:文档明确警告——当 Dropdown 嵌套在 struct 内部时,下拉框的值不会更新。原因在于父 struct 的值需要通过反射更新,而反射更新要求 struct 先装箱(boxed)再拆箱(unboxed);虽然 struct 传入绘制器时已经是装箱状态,但拆箱需要知道 struct 的编译期类型,绘制器无法获得该信息。嵌套在 class 内部则完全正常。

InputAxis:选择输入轴

InputAxis 将字符串字段变成输入轴下拉框,选项来源于项目的 Input Manager(即 ProjectSettings/InputManager.asset 中配置的轴):

public class NaughtyComponent : MonoBehaviour
{
    [InputAxis]
    public string inputAxis;
}

对应的 InputAxisAttribute 是一个无参的空标记类,实际选项读取逻辑在 InputAxisPropertyDrawer.cs 中实现。

Layer:选择图层

Layer 让字符串或整数字段以下拉方式选择 Unity 图层(Layer):

public class NaughtyComponent : MonoBehaviour
{
    [Layer]
    public string layerName;

    [Layer]
    public int layerIndex;
}

string 字段保存图层名称,int 字段保存图层索引。定义见 LayerAttribute。

Scene:从 Build Settings 选择场景

Scene 从 Build Settings 中启用的场景列表读取选项,字符串字段保存场景名,整数字段保存场景索引:

public class NaughtyComponent : MonoBehaviour
{
    [Scene]
    public string bootScene; // scene name

    [Scene]
    public int tutorialScene; // scene index
}

ScenePropertyDrawer(ScenePropertyDrawer.cs)的实现细节值得注意:

  • 通过 EditorBuildSettings.scenes.Where(scene => scene.enabled) 只读取勾选启用的场景(L59-L62);
  • 用正则 .+/(.+)\.unity 从场景路径中提取场景名(L13);
  • 下拉选项的显示格式为 "{0} ({1})",即「场景名 (索引)」(L12、L65-L68);
  • 若 Build Settings 中没有启用任何场景,会显示 "No scenes in the build settings" 警告(L15、L33-L37);
  • 字段类型既不是 string 也不是 int 时,会显示类型警告(L14)。

SortingLayer:选择排序层

SortingLayer 以下拉方式选择 Unity 的 Sorting Layer,同样支持字符串与整数两种字段:

public class NaughtyComponent : MonoBehaviour
{
    [SortingLayer]
    public string layerName;

    [SortingLayer]
    public int layerId;
}

定义见 SortingLayerAttribute。

Tag:选择标签

Tag 以下拉方式选择 Unity 的 Tag:

public class NaughtyComponent : MonoBehaviour
{
    [Tag]
    public string tagField;
}

定义见 TagAttribute。这组「命名资源下拉」属性(Layer / Scene / SortingLayer / Tag / InputAxis)的共同价值在于:将容易打错的手工字符串录入,替换为受控的选择界面,从根源上消除命名错误。

数值与曲线类属性

CurveRange:限制曲线范围并修改曲线颜色

CurveRange 用于 AnimationCurve 字段,可以限制曲线的 x、y 值范围,并可选地改变曲线在 Inspector 中的显示颜色:

public class NaughtyComponent : MonoBehaviour
{
    [CurveRange(-1, -1, 1, 1)] // min.x = -1, min.y = -1, max.x = 1, max.y = 1
    public AnimationCurve curve;

    [CurveRange(EColor.Orange)]
    public AnimationCurve curve1;

    [CurveRange(0, 0, 5, 5, EColor.Red)]
    public AnimationCurve curve2;
}

从 CurveRangeAttribute 源码看,它提供三个构造函数重载:

构造函数 说明
CurveRange(Vector2 min, Vector2 max, EColor color = EColor.Green) 用两个 Vector2 指定最小/最大范围
CurveRange(EColor color) 只指定颜色,范围退化为 (0,0) 到 (1,1)
CurveRange(float minX, float minY, float maxX, float maxY, EColor color = EColor.Green) 直接给出四个坐标值,颜色默认为绿色

其中的 EColor 枚举定义在 Assets/NaughtyAttributes/Scripts/Core/Utility/EColor.cs,共 12 个取值:Clear、White、Black、Gray、Red、Pink、Orange、Yellow、Green、Blue、Indigo、Violet,并通过 GetColor() 扩展方法映射到具体的 Color32 值。

MinMaxSlider:双滑块范围选择

MinMaxSlider 绘制一个双滑块(double slider),用于选取范围区间。最小值保存到 Vector2(或 Vector2Int)字段的 x 属性,最大值保存到 y 属性:

public class NaughtyComponent : MonoBehaviour
{
    [MinMaxSlider(0.0f, 100.0f)]
    public Vector2 minMaxSlider;
}

构造参数即滑块的硬边界(MinValue、MaxValue),见 MinMaxSliderAttribute。相比让美术/策划手填两个数值,双滑块能直观地保证 min ≤ max。仓库中的 MinMaxSliderTest.cs 提供了 Vector2 与 Vector2Int 两种字段的用例。

ProgressBar:可点击的进度条

ProgressBar 将 int 或 float 字段绘制为带颜色和标签的进度条,并且支持直接点击或拖拽进度条修改数值:

public class NaughtyComponent : MonoBehaviour
{
    [ProgressBar("Health", 300, EColor.Red)]
    public int health = 250;

    [ProgressBar("Mana", 100, EColor.Blue)]
    public int mana = 25;

    [ProgressBar("Stamina", "maxStamina", EColor.Green)] // Dynamic max value constructor
    public int stamina = 150;

    public int maxStamina = 200;
}

从 ProgressBarAttribute 源码看,它提供四个构造函数:

构造函数 说明
ProgressBar(string name, float maxValue, EColor color = EColor.Blue) 指定标签与常量最大值
ProgressBar(string name, string maxValueName, EColor color = EColor.Blue) 动态最大值:maxValueName 指向另一个字段/属性/无参方法
ProgressBar(float maxValue, EColor color = EColor.Blue) 省略标签,只用常量最大值
ProgressBar(string maxValueName, EColor color = EColor.Blue) 省略标签,只用动态最大值

ProgressBarPropertyDrawer(ProgressBarPropertyDrawer.cs)的实现要点:

  • 动态最大值通过反射按「字段 → 属性 → 无参方法」顺序解析,其中方法要求返回类型为 float 或 int(L119-L140);
  • 进度条支持鼠标交互:在条上按下鼠标左键或按住拖拽即可按比例改写数值(HandleInput,L67-L107),并自动钳制在 0 到最大值之间;
  • 数值显示格式:int 显示整数,float 显示两位小数(如 250/300、25.50/100);
  • 若字段不是数字类型,或动态最大值解析失败,会回退为默认绘制并显示对应警告(L24-L28、L56-L62)。

展示增强类属性

EnumFlags:枚举标志位下拉

EnumFlags 将位掩码枚举([Flags] 风格,即值为 2 的幂)绘制为带勾选框的下拉界面,可以直观地组合多个标志:

public enum Direction
{
    None = 0,
    Right = 1 << 0,
    Left = 1 << 1,
    Up = 1 << 2,
    Down = 1 << 3
}

public class NaughtyComponent : MonoBehaviour
{
    [EnumFlags]
    public Direction flags;
}

注意示例中的枚举虽然没有显式标注 <a href="https://link.gitcode.com/i/5b3790f8677392cb43f82f5af0732f21" target="_blank">Flags],但通过 1 << n 定义了独立的二进制位,因此可以直接用于位运算组合。定义见 [EnumFlagsAttribute。

Expandable:展开 ScriptableObject

Expandable 让 Inspector 中的 ScriptableObject 引用直接内联展开,显示其内部序列化字段,免去点击「打开资产」的额外操作:

public class NaughtyComponent : MonoBehaviour
{
    [Expandable]
    public ScriptableObject scriptableObject;
}

从 ExpandableAttribute 源码看,它还有一个可选的 isReadonly 构造参数(默认 false),传入 true 时展开内容将处于只读状态,适合展示运行时数据或配置快照。仓库的测试资产(_TestScriptableObjectA.cs 等)与 Sample 场景中的 NaughtyScriptableObjecA.asset 可用于体验该效果。

ShowAssetPreview:显示资源缩略图

ShowAssetPreview 在字段旁显示资源(Sprite、Prefab 等)的纹理预览图:

public class NaughtyComponent : MonoBehaviour
{
    [ShowAssetPreview]
    public Sprite sprite;

    [ShowAssetPreview(128, 128)]
    public GameObject prefab;
}

从 ShowAssetPreviewAttribute 源码看,其默认预览尺寸常量为 DefaultWidth = 64、DefaultHeight = 64,可通过构造参数 (width, height) 自定义。

ResizableTextArea:可缩放的多行文本

ResizableTextArea 将字符串字段绘制为可拖拽调整大小的多行文本框,且默认展示全部文本。相比 Unity 原生的 Multiline 与 TextArea(固定只显示 3 行,查看或编辑长文本时必须手动滚动到目标行),它更适合编辑长文本内容:

public class NaughtyComponent : MonoBehaviour
{
    [ResizableTextArea]
    public string resizableTextArea;
}

定义见 ResizableTextAreaAttribute。典型场景包括关卡描述、剧情对话、多行日志模板等需要完整预览的文本数据。

特殊绘制属性(SpecialCase)

这组属性不依赖 Unity 的 CustomPropertyDrawer 机制,而是由 NaughtyInspector 单独调度(见 SpecialCasePropertyDrawerBase.cs 中的 SpecialCaseDrawerAttributeExtensions)。

Button:在 Inspector 中直接调用方法

Button 将任意方法渲染为 Inspector 中的一个按钮,点击即执行。支持实例方法与静态方法:

public class NaughtyComponent : MonoBehaviour
{
    [Button]
    private void MethodOne(int parameter = 0) { } // Works with default parameters

    [Button("Button Text")] // Specify button text
    private void MethodTwo() { }

    [Button(enabledMode: EButtonEnableMode.Editor)]
    private void EnabledInEditorOnly() { }

    [Button(enabledMode: EButtonEnableMode.Playmode)]
    private void EnabledInPlaymodeOnly() { }

    [Button] // When pressed and the target object is a MonoBehaviour, it will start the coroutine
    private IEnumerator SomeCoroutine() { }
}

从 ButtonAttribute 源码看,其完整签名是 ButtonAttribute(string text = null, EButtonEnableMode enabledMode = EButtonEnableMode.Always),配套的 EButtonEnableMode 枚举有三种取值:

取值 行为
Always 按钮始终可用(默认)
Editor 仅在编辑器模式下可用
Playmode 仅在播放模式下可用

值得注意的进阶用法:

  • 默认参数:方法可以带默认参数,点击时使用默认值调用;
  • 协程支持:当目标对象是 MonoBehaviour 且方法返回 IEnumerator 时,点击按钮会自动 StartCoroutine 启动协程。

仓库的 ButtonTest.cs 给出了完整用例:EButtonEnableMode.Always 的 IncrementMyInt、EButtonEnableMode.Editor 的 DecrementMyInt、EButtonEnableMode.Playmode 且带默认参数的 LogMyInt,以及一个每 1 秒递增一次共 5 秒的协程 IncrementMyIntCoroutine——可直接对照阅读。

警告:Button 不适用于嵌套在序列化 struct 或 class 内部的方法。

ReorderableList:可拖拽排序的数组 / 列表

ReorderableList 为数组类型字段提供拖拽排序、增删元素的界面:

public class NaughtyComponent : MonoBehaviour
{
    [ReorderableList]
    public int[] intArray;

    [ReorderableList]
    public List<float> floatArray;
}

文档特别提示:从 Unity 2020.2.x 起,所有列表默认就是可重排序的,因此新版本中通常不再需要这个属性。实现上,ReorderableListPropertyDrawer 基于 UnityEditorInternal.ReorderableList,并维护一个按目标对象实例 ID 与字段名组合的缓存字典(源码中兼容了 Unity 6 的 EntityId API 与旧版 GetInstanceID 两种键值方案)。

警告:与 Dropdown 类似,它不适用于嵌套在序列化 struct 或 class 内部的列表。

ShowNativeProperty:显示原生 C# 属性

ShowNativeProperty 将原生 C# 只读属性(没有 [SerializeField] 概念,也无法被 Unity 序列化)显示在 Inspector 中。它只支持特定类型:bool、short、ushort、int、uint、long、ulong、float、double、string、Vector2、Vector3、Vector4、Vector2Int、Vector3Int、Color、Bounds、Rect、RectInt、UnityEngine.Object。

public class NaughtyComponent : MonoBehaviour
{
    public List<Transform> transforms;

    [ShowNativeProperty]
    public int TransformsCount => transforms.Count;
}

典型用途是把「由运行时数据推导出的统计值」展示出来,例如上例中的 transforms.Count。属性定义见 ShowNativePropertyAttribute,其 AttributeTargets 为 Property。

警告:不适用于嵌套在序列化 struct 或 class 内部的属性。

ShowNonSerializedField:显示非序列化字段

ShowNonSerializedField 将 private、const、static readonly 等非序列化字段显示在 Inspector 中,便于调试与查看运行时状态:

public class NaughtyComponent : MonoBehaviour
{
    [ShowNonSerializedField]
    private int myInt = 10;

    [ShowNonSerializedField]
    private const float PI = 3.14159f;

    [ShowNonSerializedField]
    private static readonly Vector3 CONST_VECTOR = Vector3.one;
}

文档给出了两个重要的行为说明:

  • 非静态非序列化字段:如果在代码中修改了它的值,Inspector 中的显示值要到编辑器按下 Play 之后才会刷新;
  • 静态非序列化字段:不存在上述问题,因为静态字段的值在编译期就已更新。

它与 ShowNativeProperty 支持的类型集合完全相同(上文列出的 20 种)。属性定义见 ShowNonSerializedFieldAttribute。

警告:不适用于嵌套在序列化 struct 或 class 内部的非序列化字段。

常见限制与注意事项汇总

综合文档与源码,使用绘制属性时有以下几点需要特别留意:

  1. 唯一性约束:同一字段/属性/方法只能挂一个绘制属性,叠加多个时只有最底部的一个生效;
  2. struct 嵌套问题:Dropdown、ReorderableList、ShowNativeProperty、ShowNonSerializedField、Button 在嵌套于序列化 struct 或 class 内部时均不可用或不更新,原因是反射无法可靠地对装箱后的 struct 进行拆箱回写;如需在嵌套结构中使用元属性(如 EnableIf),需要配合 AllowNesting 属性(见 AllowNesting 文档);
  3. 类型白名单:ShowNativeProperty 与 ShowNonSerializedField 只支持文档列出的 20 种类型,超出范围无法绘制;
  4. Unity 版本:ReorderableList 在 Unity 2020.2.x 起因原生支持而不再必需;本仓库要求 Unity 2022.3 或更高版本(见 README.md);
  5. 自定义 Editor 兼容性:4 个 SpecialCase 属性无法在自定义 Editor 中直接生效,需要继承 NaughtyInspector 并使用 NaughtyEditorGUI.PropertyField_Layout(见 README.md)。

示例与验证资源

仓库提供了丰富的验证材料,可直接对照学习:

  • 单元测试脚本:Assets/NaughtyAttributes/Scripts/Test/ 目录下几乎为每个绘制属性都准备了同名测试类,例如 DropdownTest.cs、ProgressBarTest.cs、SceneTest.cs 等,展示了文档示例的完整可运行版本;
  • 演示场景:Assets/NaughtyAttributes/Samples/DemoScene/DemoScene.unity 聚合了全部属性的实际效果,配合 _NaughtyComponent.cs、_NaughtyScriptableObject.cs 等测试对象即可直观验证;
  • 官方截图:各属性的 Inspector 效果图位于 docs/sphinx/images/ 目录,例如 ProgressBar_Inspector.png、ReorderableList_Inspector.gif,适合快速预览效果。

ProgressBar 在 Inspector 中绘制的红、蓝、绿三条带标签进度条

ReorderableList 为数组字段提供的可拖拽重排序界面

结语

NaughtyAttributes 的 18 个 Drawer Attributes 覆盖了「选择类」(AnimatorParam、Dropdown、InputAxis、Layer、Scene、SortingLayer、Tag)、「数值与曲线类」(CurveRange、MinMaxSlider、ProgressBar)、「展示增强类」(EnumFlags、Expandable、ShowAssetPreview、ResizableTextArea)以及「特殊绘制类」(Button、ReorderableList、ShowNativeProperty、ShowNonSerializedField)四大场景。它们以声明式特性取代手写 OnInspectorGUI 与自定义 PropertyDrawer,让团队可以用极少的代码获得专业、一致且不易出错的 Inspector 界面。理解它们背后统一的绘制管线(可见性检查 → 验证 → 禁用 → 绘制 → 变更回调)与 struct 嵌套等边界限制,是正确选型与避坑的关键。

登录后查看全文
NaughtyAttributes