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