Unity開發筆記-Odin標簽實現原理探究


一些廢話

為避免不必要的篇幅,本文中指列出關鍵代碼。完整代碼工程地址:https://github.com/terrynoya/HowCustomEditorBindWork

Odin在Unity編輯器擴展中的地位不必多說。只需簡單的標簽,Odin就能自動為我們實現之前需要大量編碼才能實現的擴展。下面來探究下其背后的原理,在實踐中體會Odin基於標簽的設計思路的精妙和易於實用性。
我們知道,擴展Inspector需要用到CustomEditor標簽和實現Editor子類來完成。

下面是MyClass和MyClassInspector代碼,我們再熟悉不過了。

MyClass類

public class MyClass : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

MyClassInspector類

[CustomEditor(typeof(MyClass))]
public class MyClassInspector :UnityEditor.Editor
{
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();
        if (GUILayout.Button("btn"))
        {
            OnBtnClick();
        }
    }

    public void OnBtnClick()
    {
        Debug.Log("my class btn clicked");
    }
}

可以看到,Unity通過CustomEditor標簽把Myclass和MyClassInspector綁定起來。
但我們使用Odin的時候,並不需要聲明CustomEditor標簽和實現Editor的子類,其中的奧秘在哪里呢。

UnityEditor.CustomEditorAttributes是謎底

git源碼地址:https://github.com/Unity-Technologies/UnityCsReference/blob/e740821767d2290238ea7954457333f06e952bad/Editor/Mono/CustomEditorAttributes.cs
我們來看一下Rebuild函數,出現了關鍵的CustomEditor,大概可以理解為,遍歷之后,在kSCustomEditors或者kSCustomMultiEditors內,存放了關於Myclass和MyClassInspector之間的映射。

 internal static void Rebuild()
        {
            kSCustomEditors.Clear();
            kSCustomMultiEditors.Clear();
            var types = TypeCache.GetTypesWithAttribute<CustomEditor>();
            foreach (var type in types)
            {
                object[] attrs = type.GetCustomAttributes(typeof(CustomEditor), false);

                foreach (CustomEditor inspectAttr in  attrs)
                {
                    var t = new MonoEditorType();

下面我們動手做個試驗來進行驗證。
由於CustomEditorAttributes可見性是internal,我們需要利用反射調用內部的靜態函數。

CustomEditorAttributesType = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.CustomEditorAttributes");

然后獲得kSCustomEditors屬性

CustomEditorAttributesType_CustomEditors = CustomEditorAttributesType.GetField("kSCustomEditors", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);

我們打印一下ksCustomEditors的數據

var datas = (IDictionary)CustomEditorAttributesType_CustomEditors.GetValue(null);
foreach (var item in datas.Keys)
{
    Debug.Log(item);
}

在log中我們發現了MyClass

下面我們寫一個方法,來刪除kSCustomEditors內的所有數據,然后看看會發生什么

public static void ClearCustomEditors()
{
        ((IDictionary)CustomEditorAttributesType_CustomEditors.GetValue(null)).Clear();
}

調用之后我們看到之前寫的MyClass的Inspector已經沒有按鈕了。

事實上我們清除了所有CustomEditor的綁定關系,看一下RectTransform的Inspector也更原始了。

好在我們可以通過調用剛才的Rebuild方法重新建立綁定關系。

CustomEditorAttributesType_Rebuild = CustomEditorAttributesType.GetMethod("Rebuild",BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
if (UnityVersion.IsVersionOrGreater(2019, 1))
{
    CustomEditorAttributesType_Rebuild.Invoke(null, null);
    CustomEditorAttributesType_Initialized.SetValue(null, true);
    return;
}
CustomEditorAttributesType_Initialized.SetValue(null, false);

實現自己的標簽功能

很自然的,我們可以在CustomEditorAttributes的ksCustomEditor屬性中,加入我們想要的數據,實現綁定關系

MonoEditorType = CustomEditorAttributesType.GetNestedType("MonoEditorType", BindingFlags.Public | BindingFlags.NonPublic);
MonoEditorType_InspectedType = MonoEditorType.GetField("m_InspectedType", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
MonoEditorType_InspectorType = MonoEditorType.GetField("m_InspectorType", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

public static void SetCustomEditor(Type inspectedType, Type editorType, bool isFallbackEditor,
        bool isEditorForChildClasses, bool isMultiEditor)
    {
        object obj = Activator.CreateInstance(MonoEditorType);
        MonoEditorType_InspectedType.SetValue(obj, inspectedType);
        MonoEditorType_InspectorType.SetValue(obj, editorType);
        AddEntryToDictList((IDictionary) CustomEditorAttributesType_CustomEditors.GetValue(null), obj, inspectedType);
    }

下面寫一個Button標簽類

public class ButtonAttribute:Attribute
    {
        public string Text;

        public ButtonAttribute(string text)
        {
            Text = text;
        }
    }

新建一個MonoBehaviour類,模擬一個需要Button標簽的業務邏輯

public class NoCustomEditorAttributeClass : MonoBehaviour
{
    [Button("HowOdinAttributeWork")]
    public void MyBtnClick()
    {
        Debug.Log("my btn clicked!!");
    }
    
    [Button("btn2")]
    public void Btn2()
    {
        Debug.Log("btn2");
    }
}

下面實現NoClassInspector,在OnInspectorGUI中,我們通過反射調用GetMethods方法,查看target的函數中,是否有ButtonAttribute標簽,如果有,則繪制GUILayout.Button,然后method.Invoke實現標簽對應的函數調用

 public class NoClassInspector:UnityEditor.Editor
    {
        
        public override void OnInspectorGUI()
        {
            // Debug.Log($"target:{this.target}");
            base.OnInspectorGUI();

            var type = target.GetType();
            var methods =  type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance);
            foreach (var method in methods)
            {
                var attr = method.GetCustomAttribute<ButtonAttribute>();
                if (attr != null)
                {
                    if (GUILayout.Button(attr.Text))
                    {
                        method.Invoke(target,null);
                    }                    
                }
            }
        }
    }

最后通過之前寫好的SetCustomEditor建立NoCustomEditorAttributeClass和NoClassInspector之間的映射關系

CustomInspectorUtility.SetCustomEditor(typeof(NoCustomEditorAttributeClass),typeof(NoClassInspector),false,false,false);

大功告成


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM