Unity开发笔记-Odin标签实现原理探究

一些废话#

为避免不必要的篇幅,本文中指列出关键代码。完整代码工程地址:https://github.com/terrynoya/HowCustomEditorBindWork

Odin在Unity编辑器扩展中的地位不必多说。只需简单的标签,Odin就能自动为我们实现之前需要大量编码才能实现的扩展。下面来探究下其背后的原理,在实践中体会Odin基于标签的设计思路的精妙和易于实用性。
我们知道,扩展Inspector需要用到CustomEditor标签和实现Editor子类来完成。

下面是MyClass和MyClassInspector代码,我们再熟悉不过了。

MyClass类

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

MyClassInspector类

Copy
[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之间的映射。

Copy
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,我们需要利用反射调用内部的静态函数。

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

然后获得kSCustomEditors属性

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

我们打印一下ksCustomEditors的数据

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

在log中我们发现了MyClass

下面我们写一个方法,来删除kSCustomEditors内的所有数据,然后看看会发生什么

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

调用之后我们看到之前写的MyClass的Inspector已经没有按钮了。

事实上我们清除了所有CustomEditor的绑定关系,看一下RectTransform的Inspector也更原始了。

好在我们可以通过调用刚才的Rebuild方法重新建立绑定关系。

Copy
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属性中,加入我们想要的数据,实现绑定关系

Copy
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标签类

Copy
public class ButtonAttribute:Attribute { public string Text; public ButtonAttribute(string text) { Text = text; } }

新建一个MonoBehaviour类,模拟一个需要Button标签的业务逻辑

Copy
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实现标签对应的函数调用

Copy
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之间的映射关系

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

大功告成

posted @   jeoyao  阅读(1625)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示
目录