Odin-2.扩展Inspector窗口

Odin扩展Inspector窗口

最大的应用是和扩展 EditorWindow 结合,做一些编辑器工具
参考 https://odininspector.com/tutorials/using-attributes/simple-attribute-examples

扩展属性的选择和验证

  • 在这里插入图片描述

        public class OdinChecker : MonoBehaviour
        {
          
          
            [Range(0, 10)]
            public int Field = 2;
    
            [MinValue(0), MaxValue(100)]
            public int Health;
    
            [ChildGameObjectsOnly]
            public GameObject Child;
    
            //  验证字符串指向的是否是某类型对象
            [RequiredIn(PrefabKind.InstanceInPrefab)]
            public string InstanceInPrefab = "Instances of prefabs nested inside other prefabs";
    
            //  验证值是否存在,如果是 GameObject 也会验证对象是否存在,参数是不存在时的提示
            [Required("Missing DynamicExtensions")]
            public string DynamicExtensions = "cs, unity, jpg";
    
            //  可以把资源拖进来获取路径,或打开打开文件窗口定位文件路径
            //  默认转成相对Unity工程根路径(Assets/../)的路径,可以用 ParentFolder 指定相对哪个目录
            //  Extensions 指定扩展名,RequireExistingPath 验证文件是否存在,AbsolutePath 取绝对路径
            //  ParentFolder 和 Extensions 还可以使用变量名,比如 Extensions="$DynamicExtensions"
            [FilePath(ParentFolder = "Assets/Resources", Extensions = "cs,js", RequireExistingPath = true, AbsolutePath = true)]
            public string UnityProjectPath;
    
            //  同 FilePath
            [FolderPath(ParentFolder = "Assets/Resources", RequireExistingPath = true, AbsolutePath = true)]
            public string UnityProjectFolder;
    
            //  只能关联 Project 中的对象
            [AssetsOnly]
            public GameObject SomePrefab;
    
            //  只能关联 Hierarchy 中的对象
            [SceneObjectsOnly]
            public GameObject SomeSceneObject;
    
    
    
            //  验证值,指定一个验证函数,后面是验证不通过时的提示
            [ValidateInput("HasMeshRendererDefaultMessage", "Prefab must have a MeshRenderer component")]
            public GameObject DynamicMessage;
    
            private bool HasMeshRendererDefaultMessage(GameObject gameObject)
            {
          
          
                if (gameObject == null) return true;
    
                return gameObject.GetComponentInChildren<MeshRenderer>() != null;
            }
        }
    

扩展属性显示

  • 在这里插入图片描述

        public class OdinSimple : MonoBehaviour
        {
          
          
            //  HideLabel 不显示标题
            //  PropertyOrder 可调整属性显示顺序,越小越优先
            //  注意不只会提升该属性优先级,还会提升分组,父分组的优先级
            [HideLabel, PropertyOrder(-3)]
            public int Age;
    
            //  定义标题宽度
            [TableColumnWidth(50)]
            public string Name;
    
            //  定义按钮,显示标题为 RandomName,点击调用 RandomName
            [Button(ButtonSizes.Medium), GUIColor(0, 1, 0)]
            public void RandomName()
            {
          
          
            }
    
    
            //  Icon 在左边,  Properties 分组在右边
            //  HideLabel 不显示标题, PreviewField 用矩形窗口替代对象框,可用来预览
            [HorizontalGroup("Split", Width = 50), HideLabel, PreviewField(50)]
            public Texture2D Icon;
    
            [VerticalGroup("Split/Properties")]
            public string MinionName;
    
            //  LabelText 定制标题,可以使用其它变量
            [VerticalGroup("Split/Properties"),LabelText("$MinionName")]
            public float Health;
    
            
            //  ListDrawerSettings 可以定制列表项的创建和删除
            [ListDrawerSettings(
            CustomAddFunction = "CreateNewGUID",
            CustomRemoveIndexFunction = "RemoveGUID")]
            public List<string> GuidList;
            private string CreateNewGUID()
            {
          
          
                return Guid.NewGuid().ToString();
            }
            private void RemoveGUID(int index)
            {
          
          
                this.GuidList.RemoveAt(index);
            }
    
            //  必须添加 Serializable 特性
            [Serializable]
            public struct Item
            {
          
          
                public string name;
                public int num;
            }
    
            //  添加  TableList 可以像 excel 表格一样进行配置
            [TableList]
            public List<Item> ItemList;
    
        }
    

扩展属性布局

  • 在这里插入图片描述

        public class OdinGroup : MonoBehaviour
        {
          
          
            //  创建Tab页,Group名称省略的话默认为 _DefaultTabGroup
            [TabGroup("Tab1")]
            public int dataInTab1;
            [TabGroup("Tab1")]
            public int data2InTab1;
            [TabGroup("Tab2")]
            public int dataInTab2;
    
            //  有多个 Group 的时候,Group 名称就不能省略
            [TabGroup("MyGroup", "Tab1")]
            public int dataInMyGroupTab1;
            [TabGroup("MyGroup", "Tab2")]
            public int dataInMyGroupTab2;
    
            //  水平布局,不要超过2个,会有问题,
            [HorizontalGroup("HGroup")]
            public bool data1InHGroup;
            [HorizontalGroup("HGroup")]
            public bool data2InHGroup;
    
            //  嵌套垂直布局,垂直布局的分组必须是水平布局的子分组
            [HorizontalGroup("HGroup1")]
            public bool data1InHGroup1;
            [VerticalGroup("HGroup1/VGroup")]
            public bool data1InHGroup1VGroup;
            [VerticalGroup("HGroup1/VGroup")]
            public bool data2InHGroup1VGroup;
    
            //  BoxGroup    多一个标题
            //  FoldoutGroup    可以折叠
            [HorizontalGroup("HGroup2")]
            [BoxGroup("HGroup2/BoxGroup")]
            public bool data1InHGroup2BGroup;
            [FoldoutGroup("HGroup2/FoldoutGroup")]
            public bool data1InHGroup2FGroup;
    
    
            //  TabGroup 标签即是分组又会创建按钮
            #region SplitGroup1
            [TitleGroup("Tabs")]
            //  指定 Tabs/Split 的子分组水平排列,宽度各占 50%
            //          Group1          |        Group2  
            //  类似的还有 
            [HorizontalGroup("Tabs/Split")]
            //  如果有多个分组或有子分组,则不要省略 Group名称
            //  Tab 和 Group 的区别是 Tab 创建Group同时额外创建Tab按钮
            //  下面这句创建了 Group1Tab1 分组,还创建了 Group1Tab1 按钮
            [TabGroup("Tabs/Split/Group1", "Group1Tab1")]
            public int dataInGroup1Tab1;
            [TabGroup("Tabs/Split/Group1/Group1Tab2", "Sub1")]
            public int dataInGroup1Tab2Sub1;
            [TabGroup("Tabs/Split/Group1/Group1Tab2", "Sub2")]
            public int dataInGroup1Tab2Sub2;
            #endregion
    
    
            #region SplitGroup2
            //  Group2 在 Group1 右侧
            [TabGroup("Tabs/Split/Group2", "Tab1")]
            public int dataInGroup2Tab1;
            //  把多个按钮归成一组 BtnGroup ,放置在 Group2 的 Tab1 页下
            //  之前必须先用 TabGroup 定义过 Tab1 分组,或者取消下一行的注释
            //  [TabGroup("Tabs/Split/Group2", "Tab1")]
            [ResponsiveButtonGroup("Tabs/Split/Group2/Tab1/BtnGroup")]
            public void Hello()
            {
          
          
                Debug.Log($"Hello Click");
            }
            //  World 做为按钮的名称,点击按钮后调用 World 函数
            [ResponsiveButtonGroup("Tabs/Split/Group2/Tab1/BtnGroup")]
            public void World() {
          
           }
    
            //  可以有多个 TabGroup,最后一个应用于指明 Button 所属分组
            //  前面的只是用来定义分组
            [Button]
            [TabGroup("Tabs/Split/Group2", "Tab2")]
            [TabGroup("Tabs/Split/Group2/Tab2/SubBtnGroup", "A")]
            public void SubButtonA()
            {
          
          
                Debug.Log($"SubButtonA Click");
            }
    
            [Button]
            [TabGroup("Tabs/Split/Group2/Tab2/SubBtnGroup", "A")]
            public void SubButtonB() {
          
           }
    
            [Button(ButtonSizes.Gigantic)]
            [TabGroup("Tabs/Split/Group2/Tab2/SubBtnGroup", "B")]
            public void SubButtonC() {
          
           }
    
            [TabGroup("Tabs/Split/Group2", "Tab2")]
            public float subValue;
            #endregion
        }
    

扩展属性状态控制

  • 在这里插入图片描述

        public class OdinState : MonoBehaviour
        {
          
          
            //  监听值改变,并设置 exampleList 是否展开
            //  所有属性默认都有3种状态: Visible Enabled Expanded
            [OnStateUpdate("@#(exampleList).State.Expanded = $value.HasFlag(ExampleEnum.UseStringList)")]
            public ExampleEnum exampleEnum;
    
            public List<string> exampleList;
    
            [Flags]
            public enum ExampleEnum
            {
          
          
                None,
                UseStringList = 1 << 0,
                // ...
            }
    
    
            // 可以获取Tab分组数,并在selectedTab值改变时修改激活的tab页
            //  $value-1 减号前后要有空格
            // All groups silently have "#" prepended to their path identifier to avoid naming conflicts with members.
            // Thus, the "Tabs" group is accessed via the "#(#Tabs)" syntax.
            [OnStateUpdate("@#(#Tabs1).State.Set<int>(\"CurrentTabIndex\", $value - 1)")]
            [PropertyRange(1, "@#(#Tabs1).State.Get<int>(\"TabCount\")")]
            public int selectedTab = 1;
    
            [TabGroup("Tabs1", "Tab 1")]
            public string exampleString1;
    
            [TabGroup("Tabs1", "Tab 2")]
            public string exampleString2;
    
            [TabGroup("Tabs1", "Tab 3")]
            public string exampleString3;
        }
    

扩展属性渲染

  • 扩展内置数据类型的属性渲染
    参考 https://odininspector.com/tutorials/how-to-create-custom-drawers-using-odin/how-to-make-a-healthbar-attribute
    这里以扩展血条显示为例,在血量的文本下方增加进度条表示血条

        //  先定义血条特性
        public class HealthBarAttribute : Attribute
        {
          
          
            public float MaxHealth;
    
            public HealthBarAttribute(float maxHealth)
            {
          
          
                this.MaxHealth = maxHealth;
            }
        }
    
        //  定义使用血条特性渲染的类,这个类只能包含在 Editor 的程序集中
        public class HealthBarAttributeDrawer : OdinAttributeDrawer<HealthBarAttribute, float>
        {
          
          
            protected override void DrawPropertyLayout(GUIContent label)
            {
          
          
                // 调用原来的渲染,如果把这句放到最后面,那么血条将画在变量上面
                this.CallNextDrawer(label);
    
                // Get a rect to draw the health-bar on.
                Rect rect = EditorGUILayout.GetControlRect();
    
                // Draw the health bar using the rect.
                float width = Mathf.Clamp01(this.ValueEntry.SmartValue / this.Attribute.MaxHealth);
                SirenixEditorGUI.DrawSolidRect(rect, new Color(0f, 0f, 0f, 0.3f), false);
                SirenixEditorGUI.DrawSolidRect(rect.SetWidth(rect.width * width), Color.red, false);
                SirenixEditorGUI.DrawBorders(rect, 1);
            }
        }
    
        //  使用
        public class Test : MonoBehaviour
        {
          
          
            //  1000 表示血量上限
            [HealthBar(1000)]
            public float Health;
        }
    
  • 扩展自定义类型的属性渲染
    参考 https://odininspector.com/tutorials/how-to-create-custom-drawers-using-odin/how-to-create-a-custom-value-drawer

        [Serializable] // The Serializable attributes tells Unity to serialize fields of this type.
        public struct MyStruct
        {
          
          
            public float X;
            public float Y;
        }
    
        //  定义渲染类
        public class MyStructDrawer : OdinValueDrawer<MyStruct>
        {
          
          
            protected override void DrawPropertyLayout(GUIContent label)
            {
          
          
                Rect rect = EditorGUILayout.GetControlRect();
    
                if (label != null)
                {
          
          
                    rect = EditorGUI.PrefixLabel(rect, label);
                }
    
                MyStruct value = this.ValueEntry.SmartValue;
                GUIHelper.PushLabelWidth(20);
                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);
                GUIHelper.PopLabelWidth();
    
                this.ValueEntry.SmartValue = value;
            }
        }
    
        //  使用
        public class Test : MonoBehaviour
        {
          
          
            public MyStruct data;
        }
    
  • 扩展自定义接口或象类的属性渲染
    参考 https://odininspector.com/tutorials/how-to-create-custom-drawers-using-odin/understanding-generic-constraints-on-odin-drawers
    OdinValueDrawer 只会对 MyStruct 生效,但可以使用范型来扩大适用范围

        //  适用于所有 new 的 class 对象,对于特例仍使用其自己的类型
        //  比如 MyStruct 仍会使用  OdinValueDrawer<MyStruct>  而不是 ClassDrawer<MyStruct>
        public class ClassDrawer<T> : OdinValueDrawer<T> where T : class, new()
        {
          
          
           
        }
    
        //  对于接口和抽象类,需要这样声明,使其适用于派生类
        public class CorrectWeaponDrawer<T> : OdinValueDrawer<T> where T : Weapon
        {
          
          
            //  可以通过重载该函数来缩小适用范围
            public override bool CanDrawTypeFilter(Type type)
            {
          
          
                return type != typeof(Sword);
            }
        }
    
  • 扩展自定义分组布局
    参考 https://odininspector.com/tutorials/how-to-create-custom-drawers-using-odin/how-to-make-a-custom-group

        //  定义自己的分组特性
        public class ColoredFoldoutGroupAttribute : PropertyGroupAttribute
        {
          
          
            public float R, G, B, A;
    
            public ColoredFoldoutGroupAttribute(string path)
                : base(path)
            {
          
          
            }
    
            public ColoredFoldoutGroupAttribute(string path, float r, float g, float b, float a = 1f)
                : base(path)
            {
          
          
                this.R = r;
                this.G = g;
                this.B = b;
                this.A = a;
            }
    
            //  后来的颜色跟前面的颜色怎么合并 
            protected override void CombineValuesWith(PropertyGroupAttribute other)
            {
          
          
                var otherAttr = (ColoredFoldoutGroupAttribute)other;
    
                this.R = Math.Max(otherAttr.R, this.R);
                this.G = Math.Max(otherAttr.G, this.G);
                this.B = Math.Max(otherAttr.B, this.B);
                this.A = Math.Max(otherAttr.A, this.A);
            }
        }
    
        //  定义分组渲染器
        public class ColoredFoldoutGroupAttributeDrawer : OdinGroupDrawer<ColoredFoldoutGroupAttribute>
        {
          
          
            private LocalPersistentContext<bool> isExpanded;
    
            protected override void Initialize()
            {
          
          
                this.isExpanded = this.GetPersistentValue<bool>(
                    "ColoredFoldoutGroupAttributeDrawer.isExpanded",
                    GeneralDrawerConfig.Instance.ExpandFoldoutByDefault);
            }
    
            protected override void DrawPropertyLayout(GUIContent label)
            {
          
          
                GUIHelper.PushColor(new Color(this.Attribute.R, this.Attribute.G, this.Attribute.B, this.Attribute.A));
                SirenixEditorGUI.BeginBox();
                SirenixEditorGUI.BeginBoxHeader();
                GUIHelper.PopColor(); 
                this.isExpanded.Value = SirenixEditorGUI.Foldout(this.isExpanded.Value, label);
                SirenixEditorGUI.EndBoxHeader();
    
                if (SirenixEditorGUI.BeginFadeGroup(this, this.isExpanded.Value))
                {
          
          
                    for (int i = 0; i < this.Property.Children.Count; i++)
                    {
          
          
                        this.Property.Children[i].Draw();
                    }
                }
    
                SirenixEditorGUI.EndFadeGroup();
                SirenixEditorGUI.EndBox();
            }
        }
    
        //  使用
        public class Test : MonoBehaviour
        {
          
          
            [ColoredFoldoutGroup("Green", 0, 1, 0)]
            public MyStruct data;
            [ColoredFoldoutGroup("Green")]
            public int a;
        }
    

使用解析器进一步定制特性的行为

  • Action Resolvers
    把字符串解析为函数调用

        //  定义一个特性,参数为字符串,字符串为函数名或可解析代码
        public class ActionButtonAttribute : Attribute
        {
          
          
            public string Action;
    
            public ActionButtonAttribute(string action)
            {
          
          
                this.Action = action;
            }
        }
        //  定义特性渲染器,会在变量上面渲染一个按钮,点击后执行 ActionButton 特性传入的函数
        public class ActionButtonAttributeDrawer : OdinAttributeDrawer<ActionButtonAttribute>
        {
          
          
            private ActionResolver actionResolver;
            
            protected override void Initialize()
            {
          
          
                //  在初始化的时候,把 Action 解析成可执行函数
                this.actionResolver = ActionResolver.Get(this.Property, this.Attribute.Action);
            }
            
            protected override void DrawPropertyLayout(GUIContent label)
            {
          
          
                //  这句可以没有,加这个是当Action字符串无法解析成可执行函数时,渲染一个错误提示
                this.actionResolver.DrawError();
                
                if (GUILayout.Button("Perform Action"))
                {
          
          
                    //  点击按钮时,执行 Action 字符串代表的函数
                    this.actionResolver.DoActionForAllSelectionIndices();
                }
                
                this.CallNextDrawer(label);
            }
        }
        //  使用
        public class Example : MonoBehaviour
        {
          
          
            [ActionButton("@UnityEngine.Debug.Log(\"Action invoked with attribute expressions!\")")]
            public string expressionActions;
    
            //  执行  DoAction 函数
            [ActionButton("DoAction")]
            public string methodReferenceActions;
    
            private void DoAction()
            {
          
          
                Debug.Log("Action invoked with method reference!");
            }
    
            //  字符串无法转成可执行代码,所以会出现一个提示
            [ActionButton("Bad String!")]
            public string methodReferenceActions;
        }
    
  • 把字符串解析为值
    把字符串解析为值,然后使用它,好处是字符串中可以包含其它变量

        //  定义特性
        public class ColorIfAttribute : Attribute
        {
          
          
            public string Title;
            public string Color;
            public string Condition;
    
            public ColorIfAttribute(string Title, string color, string condition)
            {
          
          
                this.Title = Title;
                this.Color = color;
                this.Condition = condition;
            }
        }
    
        //  定义特性渲染器
        public class ColorIfAttributeDrawer : OdinAttributeDrawer<ColorIfAttribute>
        {
          
          
            private ValueResolver<Color> colorResolver;
            private ValueResolver<bool> conditionResolver;
    
            protected override void Initialize()
            {
          
          
                //  初始化时将字符串解析为对应类型的值
                //  如果解析出错,返回的将是默认值,bool 默认返回 false, Color 默认返回 (0,0,0,0)
                this.colorResolver = ValueResolver.Get<Color>(this.Property, this.Attribute.Color);
                this.conditionResolver = ValueResolver.Get<bool>(this.Property, this.Attribute.Condition);
                //  对于字符串类型,特殊提供了一个函数 GetForString,解析出错时原样返回,因为时候就需要原来的数据
                //  其实就是 ValueResolver.Get<string>(this.Property, this.Attribute.Title, this.Attribute.Title);
                this.GetForString(this.Property, this.Attribute.Title);
            }
    
            protected override void DrawPropertyLayout(GUIContent label)
            {
          
          
                //  如果解析出错,这里会提示错误信息,不加就没有
                ValueResolver.DrawErrors(this.colorResolver, this.conditionResolver);
                
               
                bool condition = this.conditionResolver.GetValue();
                if (this.colorResolver.HasError)
                {
          
          
                    condition = false;
                }
    
                if (condition)
                {
          
          
                    GUIHelper.PushColor(this.colorResolver.GetValue());
                }
                
                this.CallNextDrawer(label);
    
                if (condition)
                {
          
          
                    GUIHelper.PopColor();
                }
            }
        }
    
        //  使用
        public class Example : MonoBehaviour
        {
          
          
            [ColorIf("$Title", "@Color.green", "ColorCondition")]
            public string coloredString;
    
            public string Title;
            private bool ColorCondition()
            {
          
          
                // Color the property if the string has an even number of characters
                return coloredString?.Length % 2 == 0;
            }
        }
    
  • 增加额外的变量更复杂的把字符串转换成值

        public class DisplayFormattedDateAttribute : Attribute
        {
          
          
            public string FormattedDate;
    
            public DisplayFormattedDateAttribute(string formattedDate)
            {
          
          
                this.FormattedDate = formattedDate;
            }
        }
    
        public class DisplayFormattedDateAttributeDrawer : OdinAttributeDrawer<DisplayFormattedDateAttribute>
        {
          
          
            private ValueResolver<string> formattedDateResolver;
            
            protected override void Initialize()
            {
          
          
                this.formattedDateResolver = ValueResolver.GetForString(this.Property, this.Attribute.FormattedDate, new NamedValue[]
                {
          
          
                    new NamedValue("hour", typeof(int)),
                    new NamedValue("minute", typeof(int)),
                    new NamedValue("second", typeof(int)),
                });
            }
            
            protected override void DrawPropertyLayout(GUIContent label)
            {
          
          
                if (this.formattedDateResolver.HasError)
                {
          
          
                    this.formattedDateResolver.DrawError();
                }
                else
                {
          
          
                    var time = DateTime.Now;
                    this.formattedDateResolver.Context.NamedValues.Set("hour", time.Hour);
                    this.formattedDateResolver.Context.NamedValues.Set("minute", time.Minute);
                    this.formattedDateResolver.Context.NamedValues.Set("second", time.Second);
                    
                    GUILayout.Label(this.formattedDateResolver.GetValue());
                }
                
                this.CallNextDrawer(label);
            }
        }
    
        public class Example : MonoBehaviour
        {
          
          
            //  使用的变量是在 DisplayFormattedDateAttributeDrawer 中通过 NameValue 提供的
            [DisplayFormattedDate("@$hour + \":\" + $minute + \":\" + $second")]
            public string datedString;
        }
    

自动添加特性

  • 可以自动为某种类型定义的字段,或某种类型的成员变量添加特性
    注意只能应用于 Inspector 特性,无法应用于 Serializer 特性
        //  用到自动添加特性的类
        public class MyMonoBehaviour : MonoBehaviour
        {
          
          
            public MyProcessedClass Processed;
        }
    
        //  要自动添加特性的类
        [Serializable]
        public class MyProcessedClass
        {
          
          
            public ScaleMode Mode;
            public float Size;
        }
    
        //  定义该类,自动为 MyProcessedClass 和其成员添加特性
        public class MyProcessedClassAttributeProcessor : OdinAttributeProcessor<MyProcessedClass>
        {
          
          
            //  重载该函数,为 MyProcessedClass 定义的字段添加特性,每个用 MyProcessedClass 定义的字段都会调用一次
            //  比如 这里表现为 MyMonoBehaviour.Processed 将拥有特性 [InfoBox]  和 [InlineProperty]
            public override void ProcessSelfAttributes(InspectorProperty property, List<Attribute> attributes)
            {
          
          
                attributes.Add(new InfoBoxAttribute("Dynamically added attributes!"));
                attributes.Add(new InlinePropertyAttribute());
            }
    
            //  重载该函数,为 MyProcessedClass 的成员变量增加特性,每个成员都会调用一次
            //  
            public override void ProcessChildMemberAttributes(
                InspectorProperty parentProperty,
                MemberInfo member,
                List<Attribute> attributes)
            {
          
          
                // 这2个特性将应用到所有成员上
                attributes.Add(new HideLabelAttribute());
                attributes.Add(new BoxGroupAttribute("Box", showLabel: false));
    
                if (member.Name == "Mode")
                {
          
          
                    //  这个特性只应用到 Mode 成员上
                    attributes.Add(new EnumToggleButtonsAttribute());
                }
                else if (member.Name == "Size")
                {
          
          
                    //  这个特性只应用到 Size 成员上
                    attributes.Add(new RangeAttribute(0, 5));
                }
            }
        }
    

猜你喜欢

转载自blog.csdn.net/qmladm/article/details/130040958