Unity编辑器拓展-Odin

1.相比于原生Unity的优势

  • Unity不支持泛型类型序列化,例如字典原生Unity不支持序列化,而Odin可以继承序列化的Mono实现
  • 功能强大且使用简单,原生Unity想实现一些常见的功能需要额外自己编写Unity扩展的编码,实现功能只需要加一个特性即可Odin帮忙写好了内部管理和实现
  • 编辑器的窗口实现简单且美观

2.常用功能代码总结

通过Tools-Odin Inspector-Attribute Overview即可打开一个预览各个特性的效果的窗口,可供参考

OdinValueDrawer

类继承自OdinValueDrawer,其中T为自定义数据类型,之后重写DrawPropertyLayout方法,实现自定义绘制,实现了对于我们自定义数据类型的自定义绘制,重写Initialize来做对其下序列化部分的初始化,例如Selector的生成,选择变化的事件监听,初始值的定义。同时在DrawPropertyLayout中定义对应的按钮,在按下的时候用selector.ShowInPopup();打开对应的selector

#if UNITY_EDITOR
namespace Sirenix.OdinInspector.Demos
{
    using UnityEngine;
    using System;
 
#if UNITY_EDITOR
 
    using Sirenix.OdinInspector.Editor;
    using UnityEditor;
    using Sirenix.Utilities;
 
#endif
 
    // 演示如何为自定义类型生成自定义drawer的示例。
    [TypeInfoBox("此示例演示如何为自定义结构或类实现自定义drawer")]
    public class CustomDrawerExample : MonoBehaviour
    {
        public MyStruct MyStruct;
        [ShowInInspector]
        public static float labelWidth = 10;
    }
 
    // 自定义数据结构,用于演示。
    [Serializable]
    public struct MyStruct
    {
        public float X;
        public float Y;
    }
 
#if UNITY_EDITOR
 
    public class CustomStructDrawer : OdinValueDrawer<MyStruct>
    {
        protected override void DrawPropertyLayout(GUIContent label)
        {
            //获取我们绘制类的值
            MyStruct value = this.ValueEntry.SmartValue;
 
            //获取要绘制的区域(rect)
            var rect = EditorGUILayout.GetControlRect();
            //在Odin中,标签是可选项,可以为空,所以我们必须考虑到这一点。
            if (label != null)
            {
                rect = EditorGUI.PrefixLabel(rect, label);
            }
 
            //保存原始labelWidth的宽度,此label为struct中对应的X,Y
            var prev = EditorGUIUtility.labelWidth;
 
            //设定新的label宽度
            EditorGUIUtility.labelWidth = CustomDrawerExample.labelWidth;
 
            //根据slider对应的值进行赋值
            value.X = EditorGUI.Slider(rect.AlignLeft(rect.width * 0.5f), "X", value.X, 0, 1);
            value.Y = EditorGUI.Slider(rect.AlignRight(rect.width * 0.5f), "Y", value.Y, 0, 1);
 
            //恢复设定原始label宽度
            EditorGUIUtility.labelWidth = prev;
 
            //将新的Struct赋值给我们定义的MyStruct
            this.ValueEntry.SmartValue = value;
        }
    }
#endif
}
#endif
 

OdinSelector

一个选择框,可以选择提供的列表,同时选择后触发对应的事件

BuildSelectionTree

BuildSelectionTree的作用是在OdinSelector中构建一个选择树,也就是显示在弹出窗口中的树形结构的列表。你可以通过重写这个方法,来决定选择树中包含哪些值,以及它们的层级关系和排序方式。

protected override void BuildSelectionTree(OdinMenuTree tree)  
{  
//搜索框
    tree.Config.AutoFocusSearchBar = true;  
    tree.Config.DrawSearchToolbar = true;  
    tree.Selection.SupportsMultiSelect = false;  
  
    tree.MenuItems.Clear();  
    foreach (结合excel枚举对应的pb)  
    {      tree.Add(path, instance);
    }}

在外部进行的初始化

例如在上述的Drawer的Init中进行初始化,需要new出这个类,并且增加监听事件,且设置初始值。同时监听外部按钮,打开这个selector

m_WeaponIDSelector = new WeaponIDSelector();  
m_WeaponIDSelector.SelectionTree.UpdateMenuTree();  
m_WeaponIDSelector.SetSelection(WeaponData.weaponKey.thingID); //初始值 
m_WeaponIDSelector.SelectionChanged += OnWeaponThingIDChanged;//改变时候监听
// 重写Initialize方法
protected override void Initialize()
{
// 调用基类的Initialize方法
base.Initialize();

// 实例化OdinMenuTree对象
tree = new OdinMenuTree();

// 添加一些菜单项到选择树中,例如一些GameObject或者其他自定义对象
tree.Add("Cube", GameObject.CreatePrimitive(PrimitiveType.Cube));
tree.Add("Sphere", GameObject.CreatePrimitive(PrimitiveType.Sphere));
tree.Add("MyObject", new MyObject());

// 设置选择树的配置,例如是否支持多选,是否显示搜索栏等
tree.Config.DrawSearchToolbar = true;
tree.Config.MultiSelect = true;

// 设置选择树的事件,例如当菜单项被选中或者双击时执行一些操作
tree.Selection.SelectionChanged += OnSelectionChanged;
tree.MenuItems.OnDoubleClick += OnDoubleClick;

// 在Initialize的时候调用UpdateMenuTree方法,以便初始化选择树中的菜单项,以及处理搜索和排序等功能
tree.UpdateMenuTree();
}

在外部按钮按下进行的显示这个selector

//FocusType是一个枚举类型,用于表示按钮是否可以通过键盘选择。FocusType有三个可能的值:Passive、Keyboard和Native.Passive表示按钮不会接收键盘焦点,只能通过鼠标点击选择;Keyboard表示按钮可以通过Tab键或方向键在其他控件之间切换焦点;Native表示按钮使用平台本地的焦点行为,例如在Mac上使用Option+Tab键切换焦点.在你的代码中,FocusType.Passive表示你的下拉按钮不需要通过键盘选择,只能通过鼠标点击打开下拉菜单。
if (EditorGUILayout.DropdownButton(new GUIContent("武器选择: " + m_WeaponIDSelector.CurSelectName), FocusType.Passive))  
{  
    m_WeaponIDSelector.RebuildSelectionTree();  
    m_WeaponIDSelector.EnsureSingleClickToSelect();  
    m_WeaponIDSelector.ShowInPopup();  
}
public void RebuildSelectionTree()  
{  
    List<uint> curSelections = GetCurrentSelection().ToList();  
    BuildSelectionTree(SelectionTree);  
    EnableIDChangeEvent = false;  
    SetSelection(curSelections);  
    EnableIDChangeEvent = true;  
    SelectionTree.Config.AutoFocusSearchBar = true;  
}
public void EnsureSingleClickToSelect()  
{  
//设置选择树的Config.SelectionConfirmNavigationKey属性为KeyCode.None,这样就可以取消双击确认选择的功能
    EnableSingleClickToSelect();  
    //设置了SelectionTree.Config.ConfirmSelectionOnDoubleClick属性为false,这个属性也是用于控制是否需要双击确认选择的功能虽然这个属性和Config.SelectionConfirmNavigationKey属性有重复的作用,但是为了保险起见,最好都设置一下。
    SelectionTree.Config.ConfirmSelectionOnDoubleClick = false;  
    //设置了SelectionTree.Config.AutoFocusSearchBar属性为true,这个属性可以让选择树在打开时自动聚焦搜索栏,方便用户输入搜索关键词
    SelectionTree.Config.AutoFocusSearchBar = true;  
}

处理Selector的改变

这里需要注意的是要用var first = col.FirstOrDefault(); 来取,并且需要做判空,一般是取完keyID之后再去读表刷上其他的数据,或者是根据

selector.SelectionChanged += col => //添加SelectionChanged事件的处理方法
{
var first = col.FirstOrDefault(); //获取第一个选择的对象
if (first != null) //如果不为空
{
selectedName = first.name; //更新selectedName为对象的名称
}

缩进级别

  • GUIHelper.PushIndentLevel(1);
  • GUIHelper.PopIndentLevel();
    结合使用,增加一个缩进级别,可以让GUI的元素更加有层次感和结构化。例如,你可以用这个方法来创建一个树形的菜单或列表。

结构化API

  • EditorGUILayout.BeginHorizontal();这两个老朋友了就不解释了
  • EditorGUILayout.BeginVertical();
  • GUIHelper.PushGUIEnabled(bool);//表示接下来的一个范围内我要检查一下这个bool值来觉得这些UI是否可以被选
  • GUIHelper.PopGUIEnabled();
//GUIHelper.PushGUIEnabled和GUIHelper.PopGUIEnabled是两个用于控制GUI元素是否可用的方法。它们的作用是在一段代码中临时改变GUI元素的可用状态,而不影响其他代码中的GUI元素。具体来说,GUIHelper.PushGUIEnabled方法可以接受一个布尔值作为参数,表示要设置的GUI元素的可用状态。这个方法会将当前的GUI元素的可用状态压入一个栈中,然后设置新的可用状态。GUIHelper.PopGUIEnabled方法则会从栈中弹出之前保存的GUI元素的可用状态,并恢复它。这样就可以在一段代码中临时禁用或启用一些GUI元素,而不影响其他代码中的GUI元素。
GUIHelper.PushGUIEnabled(toggle); //根据复选框的状态设置按钮的可用状态
if (GUILayout.Button("Click Me")) //绘制按钮会受到是否可选的影响
{
Debug.Log("You clicked the button!"); //打印日志
}
GUIHelper.PopGUIEnabled(); //恢复之前的可用状态

补充

可以通过UnityToolbarExtender来打开目标窗口,使用起来也非常简单

static ToolbarGUIUtility()  
{  
    uxToolPrefabOpen = EditorPrefs.GetBool("UXToolPrefabOpen", false);  
    uxToolPowerOpen = EditorPrefs.GetBool("UXToolPowerOpen", false);  
    ToolbarExtender.LeftToolbarGUI.Add(OnLeftToolbarGUI);  
    ToolbarExtender.RightToolbarGUI.Add(OnRightToolbarGUI);  
}
private static GUIStyle ToolbarButtonLeftStyle  
{  
    get  
    {  
        if (toolbarButtonLeftStyle == null) toolbarButtonLeftStyle = new GUIStyle("toolbarbuttonLeft");  
        return toolbarButtonLeftStyle;  
    }}
static void OnLeftToolbarGUI()  
{  
    if (TestCommon.TestHelper.ColorButton(new GUIContent("XXX配置", "XXXToolTip"), new Color(0.55f, 0.37f, 1f), GUILayoutOptions.Width(90), ToolbarButtonLeftStyle))  
    {        TestWindow.OpenWindow();  
    }

快速创建Button的管理者类

#if UNITY_EDITOR  
using System;  
using System.Collections.Generic;  
using System.Reflection;  
using Sirenix.Utilities;  
using Sirenix.Utilities.Editor;  
using UnityEditor;  
using UnityEngine;  
  
namespace TestCommon
{  
    public static class TestHelper
    {  
        private static GUIStyle ColoredButtonStyle = new GUIStyle(GUI.skin.button);  
  
        public static bool ColorButton(string text, Color color, int width, int height, GUIStyle guiStyle = null, GUIStyle labelGUIStyle = null)  
        {            ColoredButtonStyle.normal.textColor = Color.white;  
            GUIHelper.PushColor(color);  
            Rect buttonRect = GUILayoutUtility.GetRect(width, height);  
            bool res = guiStyle != null ? GUI.Button(buttonRect, "", guiStyle) : GUI.Button(buttonRect, "");  
            GUIHelper.PopColor();  
            GUI.Label(buttonRect, text, labelGUIStyle ?? SirenixGUIStyles.LabelCentered);  
            return res;  
        }  
        public static bool ColorButton(string text, Color color, GUILayoutOption[] guiLayoutOptions, GUIStyle guiStyle = null, GUIStyle labelGUIStyle = null)  
        {            ColoredButtonStyle.normal.textColor = Color.white;  
            GUIHelper.PushColor(color);  
            GUIHelper.PushContentColor(Color.clear);  
            bool res = guiStyle != null ? GUILayout.Button(text, guiStyle, guiLayoutOptions) : GUILayout.Button(text, guiLayoutOptions);  
            GUIHelper.PopContentColor();  
            GUIHelper.PopColor();  
            Rect buttonRect = GUILayoutUtility.GetLastRect();  
            GUI.Label(buttonRect, text, labelGUIStyle ?? SirenixGUIStyles.LabelCentered);  
            return res;  
        }  
        public static bool ColorButton(GUIContent guiContent, Color color, GUILayoutOption[] guiLayoutOptions, GUIStyle guiStyle = null, GUIStyle labelGUIStyle = null)  
        {            ColoredButtonStyle.normal.textColor = Color.white;  
            GUIHelper.PushColor(color);  
            GUIHelper.PushContentColor(Color.clear);  
            bool res = guiStyle != null ? GUILayout.Button(guiContent, guiStyle, guiLayoutOptions) : GUILayout.Button(guiContent, guiLayoutOptions);  
            GUIHelper.PopContentColor();  
            GUIHelper.PopColor();  
            Rect buttonRect = GUILayoutUtility.GetLastRect();  
            GUI.Label(buttonRect, guiContent.text, labelGUIStyle ?? SirenixGUIStyles.LabelCentered);  
            return res;  
        }  
        public static bool ColorButton(Rect rect, string text, Color color, GUIStyle guiStyle = null, GUIStyle labelGUIStyle = null)  
        {            GUIHelper.PushColor(color);  
            bool res = guiStyle != null ? GUI.Button(rect, "", guiStyle) : GUI.Button(rect, "");  
            GUIHelper.PopColor();  
            GUI.Label(rect, text, labelGUIStyle ?? SirenixGUIStyles.LabelCentered);  
            return res;  
        }  
        public static bool ColorButton(Rect rect, Texture2D texture2D, Color color, GUIStyle guiStyle = null)  
        {            GUIHelper.PushColor(color);  
            bool res = guiStyle != null ? GUI.Button(rect, texture2D, guiStyle) : GUI.Button(rect, texture2D);  
            GUIHelper.PopColor();  
            return res;  
        }  
        public static void RichLabel(Rect rect, string text, Color color, int fontSize = 12, bool italic = false, bool bold = false, GUIStyle guiStyle = null)  
        {            string richText = text;  
            richText = $"<size={fontSize}><color=#{ColorUtility.ToHtmlStringRGB(color)}>{richText}</color></size>";  
            if (italic) richText = $"<i>{richText}</i>";  
            if (bold) richText = $"<b>{richText}</b>";  
            if (guiStyle != null)  
                GUI.Label(rect, richText, guiStyle);  
            else  
                GUI.Label(rect, richText);  
        }  
        public static void RichLabel(Rect rect, GUIContent guiContent, Color color, int fontSize = 12, bool italic = false, bool bold = false, GUIStyle guiStyle = null)  
        {            string richText = guiContent.text;  
            richText = $"<size={fontSize}><color=#{ColorUtility.ToHtmlStringRGB(color)}>{richText}</color></size>";  
            if (italic) richText = $"<i>{richText}</i>";  
            if (bold) richText = $"<b>{richText}</b>";  
            guiContent.text = richText;  
            if (guiStyle != null)  
                GUI.Label(rect, guiContent, guiStyle);  
            else  
                GUI.Label(rect, guiContent);  
        }  
        public static void RichLabel(string text, Color color, int fontSize = 12, bool italic = false, bool bold = false, GUIStyle guiStyle = null, GUILayoutOption[] guiLayoutOptions = null)  
        {            string richText = text;  
            richText = $"<size={fontSize}><color=#{ColorUtility.ToHtmlStringRGB(color)}>{richText}</color></size>";  
            if (italic) richText = $"<i>{richText}</i>";  
            if (bold) richText = $"<b>{richText}</b>";  
            if (guiStyle != null)  
            {                if (guiLayoutOptions != null)  
                    GUILayout.Label(richText, guiStyle, guiLayoutOptions);  
                else  
                    GUILayout.Label(richText, guiStyle);  
            }            else  
            {  
                if (guiLayoutOptions != null)  
                    GUILayout.Label(richText, guiLayoutOptions);  
                else  
                    GUILayout.Label(richText);  
            }        }  
        public static void RichLabel(GUIContent guiContent, Color color, int fontSize = 12, bool italic = false, bool bold = false, GUIStyle guiStyle = null, GUILayoutOption[] guiLayoutOptions = null)  
        {            string richText = guiContent.text;  
            richText = $"<size={fontSize}><color=#{ColorUtility.ToHtmlStringRGB(color)}>{richText}</color></size>";  
            if (italic) richText = $"<i>{richText}</i>";  
            if (bold) richText = $"<b>{richText}</b>";  
            guiContent.text = richText;  
            if (guiStyle != null)  
            {                if (guiLayoutOptions != null)  
                    GUILayout.Label(guiContent, guiStyle, guiLayoutOptions);  
                else  
                    GUILayout.Label(guiContent, guiStyle);  
            }            else  
            {  
                if (guiLayoutOptions != null)  
                    GUILayout.Label(guiContent, guiLayoutOptions);  
                else  
                    GUILayout.Label(guiContent);  
            }        }  
        public enum Alignment  
        {  
            UpperLeft,  
            UpperCenter,  
            UpperRight,  
            MiddleLeft,  
            MiddleCenter,  
            MiddleRight,  
            BottomLeft,  
            BottomCenter,  
            BottomRight,  
        }  
  
        public static bool IsUpper(this Alignment alignment)  
        {            return alignment == Alignment.UpperRight || alignment == Alignment.MiddleRight || alignment == Alignment.BottomRight;  
        }  
        public static bool IsMiddle(this Alignment alignment)  
        {            return alignment == Alignment.MiddleLeft || alignment == Alignment.MiddleCenter || alignment == Alignment.MiddleRight;  
        }  
        public static bool IsBottom(this Alignment alignment)  
        {            return alignment == Alignment.BottomLeft || alignment == Alignment.BottomCenter || alignment == Alignment.BottomRight;  
        }  
        public static bool IsLeft(this Alignment alignment)  
        {            return alignment == Alignment.UpperLeft || alignment == Alignment.MiddleLeft || alignment == Alignment.BottomLeft;  
        }  
        public static bool IsRight(this Alignment alignment)  
        {            return alignment == Alignment.UpperRight || alignment == Alignment.MiddleRight || alignment == Alignment.BottomRight;  
        }  
        public static bool IsCenter(this Alignment alignment)  
        {            return alignment == Alignment.UpperCenter || alignment == Alignment.MiddleCenter || alignment == Alignment.BottomCenter;  
        }  
        public static void DrawResponsiveLayout(int rowWidth, List<int> elementWidths, Action<int> drawActions, Alignment alignment)  
        {            int accumulatedWidth = 0;  
            GUILayout.BeginVertical(GUILayoutOptions.ExpandWidth(false).ExpandHeight(false));  
            {                if (alignment.IsMiddle() || alignment.IsBottom()) GUILayout.FlexibleSpace();  
                GUILayout.BeginHorizontal();  
                if (alignment.IsRight() || alignment.IsCenter()) GUILayout.FlexibleSpace();  
                for (int index = 0; index < elementWidths.Count; index++)  
                {                    int elementWidth = elementWidths[index] + 4;  
                    if (accumulatedWidth + elementWidth > rowWidth)  
                    {                        accumulatedWidth = 0;  
                        if (alignment.IsLeft() || alignment.IsCenter()) GUILayout.FlexibleSpace();  
                        GUILayout.EndHorizontal();  
                        GUILayout.BeginHorizontal();  
                        if (alignment.IsRight() || alignment.IsCenter()) GUILayout.FlexibleSpace();  
                    }  
                    accumulatedWidth += elementWidth;                    drawActions.Invoke(index);  
                }  
                if (alignment.IsLeft() || alignment.IsCenter()) GUILayout.FlexibleSpace();  
                GUILayout.EndHorizontal();  
                if (alignment.IsMiddle() || alignment.IsUpper()) GUILayout.FlexibleSpace();  
            }            GUILayout.EndVertical();  
        }  
        [InitializeOnLoad]  
        public class ScrollableTextArea  
        {  
            private delegate string ScrollableTextAreaInternalDelegate(  
                Rect position,  
                string text,  
                ref Vector2 scrollPosition,  
                GUIStyle style);  
  
            private static readonly ScrollableTextAreaInternalDelegate EditorGUI_ScrollableTextAreaInternal;  
  
            static ScrollableTextArea()  
            {                MethodInfo method = typeof(EditorGUI).GetMethod("ScrollableTextAreaInternal", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);  
                if (method == null)  
                    return;  
                EditorGUI_ScrollableTextAreaInternal = (ScrollableTextAreaInternalDelegate)Delegate.CreateDelegate(typeof(ScrollableTextAreaInternalDelegate), method);  
            }  
            public static string EditorGUITextArea(Rect rect, string text, ref Vector2 scrollPos, int minLines = 0, int maxLines = 10)  
            {                return EditorGUI_ScrollableTextAreaInternal(rect, text, ref scrollPos, EditorStyles.textArea);  
            }  
            public static string EditorGUILayoutTextArea(string text, ref Vector2 scrollPos, int minLines = 0, int maxLines = 10)  
            {                float height = 32f + (float)((Mathf.Clamp(Mathf.CeilToInt(EditorStyles.textArea.CalcHeight(GUIHelper.TempContent(text), GUIHelper.ContextWidth) / 13f), minLines, maxLines) - 1) * 13);  
                Rect controlRect = EditorGUILayout.GetControlRect(false, height);  
                return EditorGUI_ScrollableTextAreaInternal(controlRect, text, ref scrollPos, EditorStyles.textArea);  
            }        }  
        public static Rect BeginColorBox(string label, Color color, params GUILayoutOption[] options)  
        {            Rect rect = SirenixEditorGUI.BeginBox(options);  
            SirenixEditorGUI.DrawSolidRect(rect, color);  
            SirenixEditorGUI.DrawBorders(rect, 1, new Color(0.24f, 0.24f, 0.24f));  
            SirenixEditorGUI.BeginBoxHeader();  
            float fieldWidth = EditorGUIUtility.fieldWidth;  
            EditorGUIUtility.fieldWidth = 10f;  
            Rect controlRect = EditorGUILayout.GetControlRect(false);  
            EditorGUIUtility.fieldWidth = fieldWidth;  
            GUI.Label(controlRect, label);  
            SirenixEditorGUI.EndBoxHeader();  
            return rect;  
        }  
        public static void EndColorBox()  
        {            SirenixEditorGUI.EndBox();  
        }  
        public static Color Grey(float value)  
        {            return new Color(value, value, value);  
        }  
        public static Color WithGrey(this Color color, float value)  
        {            return new Color(color.r * value, color.g * value, color.b * value);  
        }  
        public static Color WithAlpha(this Color color, float alpha)  
        {            color.a = alpha;  
            return color;  
        }    }}  
#endif

参考

十分钟入门Unity革命性编辑器拓展插件——Odin_哔哩哔哩_bilibili
Odin常用功能整理 | 登峰造极者,殊途亦同归。 (lfzxb.top)
marijnz/unity-toolbar-extender at dbd76ed996483d219c39d240f785b1790aa039ed (github.com)