前面啰嗦了踩坑过程,想看源码直接拉到最后。。。
以 IMGUI 实现,版本原因,没有查看 UIElements 。
Unity编辑器默认并不支持将 Flags
枚举以位掩码形式显示在Inspector上,像Layers这些控件,并不通用。
想让所有带有 Flags
特性的枚举都能支持多选,就要自定义编辑器扩展。然后,坑就来了!
EditorGUI.MaskField()
EditorGUI.MaskField()
,这个方法一看名字,就感觉是我需要的,然后我写出了这玩意:
// ...
property.intValue = EditorGUI.MaskField(position, label, property.intValue, property.enumDisplayNames);
// ...
Inspector成功地以位掩码形式显示出了 Flags
枚举,但是,用着用着感觉不对啊!它错位了!
问题在于,这个方法返回的不是所选的枚举的总值,而是所选的枚举项的索引,也就是说,他表示你选了哪几个枚举项。
这个方法的文档里,对返回值的说明是“The value modified by the user”,我觉得他应该改成“The indexes selected by the user”。
所以,如果要用这种方法正确地实现需求,要手动去获得 当前所选枚举值、每个枚举项的值、每个枚举项的名称,然后通过 位运算 手动去计算 当前选择了哪些枚举项 ,再调用 MaskField()
方法绘制控件。然后再拿到代表 当前所选枚举项索引 的返回值,通过 位运算 手动去计算 所选的索引对应的最终枚举值。
我这样做了下,确实是能正确显示了,但是不支持组合掩码(AB=A|B这种,见示例),不支持1<<31(见示例)。嫌烦没有去处理这个问题,所以查了下有没有其他方式实现,然后遇到了坑2。
EditorGUI.EnumFlagsField()
EnumFlagsField()
方法是这样用的:
Enum currentEnum = (Enum)fieldInfo.GetValue(property.serializedObject.targetObject);
Enum newEnum = EditorGUI.EnumFlagsField(position, label, currentEnum);
property.intValue = Convert.ToInt32(newEnum);
很简单,完美地解决了坑1中的所有问题。
但是,当把 Flags
枚举作为某个类型的字段,然后这个类型又在某个 ScriptableObject
中作为 数组成员 时,上述代码就会抛出异常:ArgumentException: Field
。
问题出在 property.serializedObject.targetObject
上,它有些乱七八糟的归属问题。
偷懒了,没解决,我把它绕过去了。
既然 property.serializedObject.targetObject
有归属问题,那么不通过这个对象获取枚举值就好了。但是我找了一圈,也没找到 通过枚举类型和枚举值来生成Enum对象 的方法。
这时我的想法是,看看 EnumFlagsField()
的内部是怎么实现的,然后照搬它的方式重新填一下坑1,于是反编译了 EnumFlagsField()
的代码。
反编译之后,在 EnumFlagsField()
的实现中有个意外的发现:EditorGUI
类型中含有个内部静态方法 IntToEnumFlags()
,可以通过枚举类型和枚举值来生成Enum对象,那么,有了这个方法,又可以回到坑2中来了!
用反射,拿到这个方法,然后生成枚举对象,代码变成了这样:
MethodInfo miIntToEnumFlags = typeof(EditorGUI).GetMethod("IntToEnumFlags", BindingFlags.Static | BindingFlags.NonPublic);
Enum currentEnum = miIntToEnumFlags.Invoke(null, new object[] { fieldInfo.FieldType, property.intValue });
Enum newEnum = EditorGUI.EnumFlagsField(position, label, currentEnum);
property.intValue = Convert.ToInt32(newEnum);
到此,坑填好了,所有问题已解决!(PS:其实我没特别全面地进行测试。。。)
using System;
using UnityEngine;
///
/// 将枚举以位掩码的形式显示在Inspector上。
///
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public class ShowAsFlagsAttribute : PropertyAttribute
{
}
using System;
using System.Reflection;
using UnityEditor;
using UnityEngine;
///
/// 自定义属性绘制器,将枚举以位掩码的形式显示在Inspector上。
///
[CustomPropertyDrawer(typeof(ShowAsFlagsAttribute))]
public class ShowAsFlagsDrawer : PropertyDrawer
{
private MethodInfo _miIntToEnumFlags;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// 如果不是枚举,则按默认显示
if (property.propertyType != SerializedPropertyType.Enum)
{
EditorGUI.PropertyField(position, property);
return;
}
if (_miIntToEnumFlags == null)
{
_miIntToEnumFlags = typeof(EditorGUI).GetMethod("IntToEnumFlags", BindingFlags.Static | BindingFlags.NonPublic);
}
// 复杂的转换问题,让Unity来解决(参考EditorGUI.EnumFlagsField()方法的反编译结果)
Enum currentEnum = (Enum)_miIntToEnumFlags.Invoke(null, new object[] { fieldInfo.FieldType, property.intValue });
EditorGUI.BeginProperty(position, label, property);
Enum newEnum = EditorGUI.EnumFlagsField(position, label, currentEnum);
property.intValue = Convert.ToInt32(newEnum);
EditorGUI.EndProperty();
// 备注:
// 不能使用以下方式获取枚举值:
// Enum currentEnum = (Enum)fieldInfo.GetValue(property.serializedObject.targetObject);
// 使用以下方式时,如果ScriptableObject中包含一个某类型的数组,该类型中包含了Flags枚举,将会导致Editor抛出ArgumentException:
// ArgumentException: Field defined on type is not a field on the target object which is of type .
}
}
using UnityEngine;
[System.Flags]
public enum MyFlags
{
A = 1 << 0,
B = 1 << 2,
Z = 1 << 31,
AB = A | B,
AZ = A | Z,
}
public class Test : MonoBehaviour
{
[ShowAsFlags] // 这样用
public MyFlags flags;
}