NaughtyAttributes Drawer Attributes 完全指南:用声明式属性为 Unity Inspector 打造专业绘制界面
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 中。绘制属性实际由两类绘制器基类驱动:
PropertyDrawerBase(PropertyDrawers/PropertyDrawerBase.cs):继承自 Unity 的PropertyDrawer,对应AnimatorParam、Dropdown、CurveRange等大多数属性;SpecialCasePropertyDrawerBase(PropertyDrawers_SpecialCase/SpecialCasePropertyDrawerBase.cs):对应Button、ReorderableList、ShowNativeProperty、ShowNonSerializedField这四个「特殊」绘制属性——它们无法作为标准的CustomPropertyDrawer工作,需要走 NaughtyInspector 的专用调度。
统一的绘制流程
无论是哪条管线,每次绘制都遵循同一个流程(见 PropertyDrawerBase.OnGUI 与 SpecialCasePropertyDrawerBase.OnGUI):
- 可见性检查:通过
PropertyUtility.IsVisible(property)判断该字段是否被ShowIf/HideIf等元属性隐藏; - 验证器执行:遍历字段上挂载的
ValidatorAttribute,调用各自的ValidateProperty执行校验; - 禁用范围:通过
PropertyUtility.IsEnabled(property)判断是否处于EnableIf/DisableIf/ReadOnly的禁用状态,并包裹在EditorGUI.DisabledScope中; - 实际绘制:调用子类实现的
OnGUI_Internal画出定制控件; - 变更回调:通过
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;
}
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 内部的非序列化字段。
常见限制与注意事项汇总
综合文档与源码,使用绘制属性时有以下几点需要特别留意:
- 唯一性约束:同一字段/属性/方法只能挂一个绘制属性,叠加多个时只有最底部的一个生效;
- struct 嵌套问题:
Dropdown、ReorderableList、ShowNativeProperty、ShowNonSerializedField、Button在嵌套于序列化 struct 或 class 内部时均不可用或不更新,原因是反射无法可靠地对装箱后的 struct 进行拆箱回写;如需在嵌套结构中使用元属性(如EnableIf),需要配合AllowNesting属性(见 AllowNesting 文档); - 类型白名单:
ShowNativeProperty与ShowNonSerializedField只支持文档列出的 20 种类型,超出范围无法绘制; - Unity 版本:
ReorderableList在 Unity 2020.2.x 起因原生支持而不再必需;本仓库要求 Unity 2022.3 或更高版本(见 README.md); - 自定义 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,适合快速预览效果。
结语
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 嵌套等边界限制,是正确选型与避坑的关键。

