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