【Unity】在Inspector上显示自定义的位掩码枚举(Flags)

【Unity】在Inspector上显示自定义的位掩码枚举(Flags)

前面啰嗦了踩坑过程,想看源码直接拉到最后。。。

IMGUI 实现,版本原因,没有查看 UIElements

Unity编辑器默认并不支持将 Flags 枚举以位掩码形式显示在Inspector上,像Layers这些控件,并不通用。

想让所有带有 Flags 特性的枚举都能支持多选,就要自定义编辑器扩展。然后,坑就来了!


坑1: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。


坑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 <enum_flags> defined on type <host_type> is not a field on the target object which is of type <scriptable_object>.

问题出在 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;

/// <summary>
/// 将枚举以位掩码的形式显示在Inspector上。
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public class ShowAsFlagsAttribute : PropertyAttribute
{
}
using System;
using System.Reflection;
using UnityEditor;
using UnityEngine;

/// <summary>
/// 自定义属性绘制器,将枚举以位掩码的形式显示在Inspector上。
/// </summary>
[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 <enum_flags> defined on type <host_type> is not a field on the target object which is of type <unity_object>.
    }
}

使用示例

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;
}

示例图

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章