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

示例圖

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