Unity编辑器针对字段的拓展一般使用特性(Attribute)。这些特性需要直接写在字段声明位置前面来修饰这个字段,影响其在Inspector的显示。这些特性可能与其修饰的字段相关,规范其取值或显示形式;也可能与这些字段没有任何关系,仅仅是依附于它。
Unity内置了很多这类常用特性来供我们使用

  • [Range(num1,num2)]:限定数值取值范围为[num1,num2]
  • [Multiline(num)]:提供num行的输入框
  • [TextArea(num1,num2)]:提供一个最小num1行,最大num2行的文本框
  • [SerializeField]:序列化字段
  • [NonSerialized]:反序列化字段,并将其在Inspector隐藏
  • [HideInInspector]:将一个字段在Inspector隐藏
  • [FormerlySerializedAs(“str”)]:当变量名发生改变时,可以保存原来str的值
  • [Header(“str”)]:添加名为str的标题
  • [Space(num)]:添加大小为num的间隔
  • [Tooltip(“str”)]:添加信息为str的提示(鼠标悬浮在字段名上显示)
  • [ColorUsage(true)]:设定取色面板类型(是否可修改alpha、是否是HDR以及取值范围)

下面的内容将讲解如何编写自己的特性。

构成

这类拓展至少需要两个脚本,分别是下面两者的子类:

  • PropertyAttribute
  • GUIDrawer
    前者的功能是定义特性的数据结构,获取绘制GUI所需的所有参数。它只储存参数,不包含任何函数。例如[Tooltip(“str”)]实际就调用了TooltipAttribute的构造函数,传入了”str”这一个参数。
    后者的功能是利用特性的参数(以及特性修饰的字段)来绘制GUI

PropertyAttribute

PropertyAttribute在UnityEngine命名空间下,是Atrribute的直接子类。对于继承它的特性,一般来说其中所有字段都用public和readonly修饰,通过构造函数赋值。
注意,它的子类不能放在Editor文件夹中
另外按照规范,特性的名称应该以大写字母开头,并以 “Attribute” 结尾,例如 “SeperatorAttribute”。C#编译器也会自动识别这一命名方式,允许我们直接用“Seperator”这个名字来使用特性。
下面我们实现一个叫做Seperator的特性,用来在Inspector中绘制一条分割线。

1
2
3
4
5
6
7
8
9
10
11
12
13
using UnityEngine;
[System.AttributeUsage(System.AttributeTargets.Field, AllowMultiple = true)] //用于修饰字段,允许对同一字段重复添加
public class SeperatorAttribute : PropertyAttribute
{
public readonly float Height;
public readonly float Spacing;

public SeperatorAttribute(float height = 1, float spacing = 10)
{
Height = height;
Spacing = spacing;
}
}

可见未来我们要使用Seperator特性时,应该写[Seperator(height,spacing)]或者[Seperator](height和spacing为构造函数默认的1和10)

GUIDrawer

GUIDrawer有两个子类,需要根据特性功能是否需要用到被修饰的字段来决定继承哪一个
继承GUIDrawer的类可以且最好放进Editor文件夹
pic.png

DecoratorDrawer

不影响字段的绘制,而是纯粹基于从其对应的PropertyAttribute中获取的数据来绘制装饰元素。如Header特性只是依附于一个字段来绘制标题,至于其所依附的字段如何是完全不用关心的;前面我们准备编写的的Seperator特性也属于这一类。
(Attribute纯粹储存数据,DecoratorDrawer应用数据绘制。通过atrribute字段获取数据)
pic.png
变量attribute即为Drawer对应的特性,使用时我们需要将其强转为相应的类型。
下面就是实现Seperator特性需要的第二个脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using UnityEngine;
using UnityEditor;
[CustomPropertyDrawer(typeof(SeperatorAttribute))] //将Drawer与Attribute绑定
public class SeperatorDrawer : DecoratorDrawer
{
//GUI的绘制逻辑在OnGUI编写;
public override void OnGUI(Rect position)
{
SeperatorAttribute sa = attribute as SeperatorAttribute;
//position为常规该行元素应该占据的位置,我们在这里相对于position绘制一条高度为sa.Height,与上一行间距为sa.Spacing的横线
Rect r = new Rect(position.xMin, position.yMin + sa.Spacing, position.width, sa.Height);
EditorGUI.DrawRect(r, new Color(88f / 255, 88f / 255, 88f / 255));

}
//GetHeight决定编辑器应当为特性的绘图预留多高的空间
public override float GetHeight()
{
SeperatorAttribute sa = attribute as SeperatorAttribute;
return sa.Height + sa.Spacing * 2;
}
}

实现效果如下:
QQ-20230813185414.png

OnGUI的position参数其实还是值得讲一下的,这里没看懂的话可能会折磨很久…补一张图
QQ-20230813185243.png
这里的坐标系使用的是第四象限,(position.xMin,position.yMin)为元素左上角的坐标,position.width和position.height则描述的是常规一行的尺寸。

PropertyDrawer

这类特性影响或需要使用字段。比如Range特性用一个滑动条来限制数值类型字段的取值范围,那我们就需要让这个特性能够覆盖原字段的输入框,在相应位置绘制滑动条,且允许使用者通过与滑动条交互来给字段赋值。DecoratorDrawer无法获得其修饰字段的引用,显然做不到这件事。

下面是一个应用在Vector2类字段的特性,能够用Slider规范字段x和y的取值范围,且保证x<=y

1
2
3
4
5
6
7
8
9
10
11
12
13
using UnityEngine;
[System.AttributeUsage(System.AttributeTargets.Field, AllowMultiple = false)]
public class MinMaxRangeAttribute : PropertyAttribute
{
public readonly float min;
public readonly float max;

public MinMaxRangeAttribute(float min, float max)
{
this.min = min;
this.max = max;
}
}

根据我们使用Seperator的经验,我们应该用[MinMaxRange(min,max)]来使用这个特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using UnityEngine;
using UnityEditor;
[CustomPropertyDrawer(typeof(MinMaxRangeAttribute))] //将Drawer与Attribute绑定
public class MinMaxRangeDrawer : PropertyDrawer
{
//PropertyDrawer的OnGUI多了两个参数。property即为属性所修饰的字段,而label则是字段标签的UI元素(GUI上展示的字段名)
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
MinMaxRangeAttribute at = (MinMaxRangeAttribute)attribute;

if (property.propertyType == SerializedPropertyType.Vector2)
{
Vector2 v = property.vector2Value;
EditorGUI.MinMaxSlider(position, label, ref v.x, ref v.y, at.min, at.max);//绘制MinMax滑动条
property.vector2Value = v;
//下面多占一行,用来显示当前两端的取值
Rect left = new Rect(position.xMin, position.yMin + EditorGUIUtility.standardVerticalSpacing, (position.width / 2), position.height);
Rect right = new Rect(position.xMin + (position.width / 2), position.yMin + EditorGUIUtility.standardVerticalSpacing, position.width / 2, position.height);
EditorGUI.LabelField(left, "Min: " + v.x, EditorStyles.miniLabel);
EditorGUI.LabelField(right, "Max: " + v.y, EditorStyles.miniLabel);
}
else
{
EditorGUI.HelpBox(position, "MinMaxRangeDrawer必须应用在Vector2类型字段上", MessageType.Warning);
}
}
//与DecoratorDrawer的GetHeight作用类似,只是确定高度的对象不同(DecoratorDrawer是独立与字段之外的装饰,而PropertyDrawer重写的是字段本身的绘制)
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return 2 * base.GetPropertyHeight(property, label);
}
}

注意!:PropertyDrawer不可以使用EditorGUILayout,必须用EditorGUI。使用EditorGUILayout将会报错
效果展示:
QQ-20230813191521.png

延伸

Unity还有很多应用在类或方法上的特性,可以给我们的开发带来很大的便利。
Unity手册—Attribute汇总说明