引言
在Unity开发过程中,编辑器的用户体验同样重要。Odin Inspector作为一个强大的编辑器扩展工具,允许开发者通过创建自定义Drawers来优化和个性化Inspector界面。自定义Drawers可以改变属性的显示方式,增加新的交互元素,甚至嵌入复杂的自定义编辑器逻辑。
什么是Drawers?
在Odin Inspector中,Drawers是用于绘制属性的组件。每个属性在Inspector中的表现都是由特定的Drawer负责。通过创建自定义Drawers,开发者可以完全控制属性的显示和编辑方式。
为什么需要自定义Drawers?
- 个性化编辑器界面:使编辑器界面更符合项目需求。
- 增强交互性:添加滑块、颜色选择器等交互元素。
- 优化工作流:通过定制化的编辑器界面提高开发效率。
如何制作 HealthBar Attribute
本指南的目标是创建一个简单的 HealthBarAttribute,该特性可应用于浮点字段或属性,以及一个 Drawer,该 Drawer 将在正常浮点字段下方绘制一个红色运行状况条。
我们需要的第一件事是 Attribute 本身。创建一个新类,将其命名为 HealthBarAttribute 并从 Attribute 类继承。 我们将添加一个 public max health 字段和一个构造函数,以使属性易于使用。
public class HealthBarAttribute : Attribute
{
public float MaxHealth;
public HealthBarAttribute(float maxHealth)
{
this.MaxHealth = maxHealth;
}
}
现在继续创建实际的 Drawer。创建一个新的脚本文件,并将其命名为 HealthBarAttributeDrawer。 例如,您应该确保此文件仅包含在编辑器中,方法是将其放置在编辑器文件夹下方。这将防止在以后构建项目时出现编译器错误。
我们来定义一下 Drawer。在文件中创建一个名为 HealthBarAttributeDrawer 的类,并从 OdinAttributeDrawer<TAttribute>
类继承。
您会发现 OdinAttributeDrawer 类有两个版本。一个具有单个泛型参数,另一个具有两个通用参数。 两个版本中的第一个参数是要为其创建抽屉的 Attribute 类型。在我们的例子中,这将是我们之前创建的 HealthBarAttribute 类。 第二个版本中的第二个参数是一种约束此属性绘制器可以处理的值类型的方法。这也将创建一个 强类型抽屉,让我们可以轻松访问抽屉被要求抽取的属性的值。 由于我们想根据 float 的值绘制一个健康条,因此我们将在本例中添加 float 作为第二个通用约束,确保 仅当 [HealthBar] 放在浮点值上时,此属性抽屉才适用。
public class HealthBarAttributeDrawer : OdinAttributeDrawer<HealthBarAttribute, float>
在这一点上,值得指出的是,这意味着我们的抽屉只能处理浮点类型。如果你想做抽屉 对于每一个基元类型,int、double 等,然后你必须制作一个具有唯一值的抽屉副本 您希望支持的每种基元类型的约束。单个抽屉还可以使用通用约束同时将自己限制为多种类型。
接下来,我们需要重写 DrawPropertyLayout 方法。这是实现自定义抽屉所需的主要方法。
protected override void DrawPropertyLayout(GUIContent label)
{
}
我们想要做的第一件事是绘制属性的法线浮点字段。我们可以手动调用类似 SirenixEditorFields 的东西。浮点字段 来绘制它,但相反,我们将调用 CallNextDrawer 方法。这进入了Odin的抽屉链,这允许不同属性的组合 以真正自定义检查器。例如,如果浮点值上还有一个 [Range] Attribute,则浮点数将绘制为范围而不是常规浮点字段,因为我们会 将调用传递给 [Range] Attribute的抽屉。
// In this case, we don't need the label for anything, so we will just pass it to the next drawer.
this.CallNextDrawer(label);
最后,我们可以画出我们的健康条。在本指南中,我不会详细介绍它是如何工作的。您可以在此处找到有关 IMGUI 系统的更深入指南:.
这应该是 DrawPropertyLayout 方法的最终实现。
protected override void DrawPropertyLayout(GUIContent label)
{
// Call the next drawer, which will draw the float field.
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);
}
这就是它的全部内容!您可以通过向运行状况栏添加控件来扩展此示例,例如,通过单击运行状况栏来更改值的功能。 您还可以通过在健康条前面绘制一个加 Editor Icon
来装饰健康条。或者完全转换健康条以绘制设定数量的生命,而不是条形图。
Attribute:
using System;
public class HealthBarAttribute : Attribute
{
public float MaxHealth;
public HealthBarAttribute(float maxHealth)
{
this.MaxHealth = maxHealth;
}
}
Drawer:
using Sirenix.OdinInspector.Editor;
using Sirenix.Utilities;
using Sirenix.Utilities.Editor;
using UnityEngine;
using UnityEditor;
public class HealthBarAttributeDrawer : OdinAttributeDrawer<HealthBarAttribute, float>
{
protected override void DrawPropertyLayout(GUIContent label)
{
// Call the next drawer, which will draw the float field.
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);
}
}
如何创建自定义值抽屉(Value Drawer)
本指南的目标是为自定义结构类型创建一个简单的自定义 Odin 值抽屉。
值抽屉是Odin中最基本的抽屉类型,通常是实际上最终在检查员中进行财产最终绘图的抽屉。出于这个原因,它们通常是抽屉链中的最后一个抽屉之一,通常不会继续链。
用于此目的的结构将包含两个公共浮点字段,一个 X 和一个 Y。我们的抽屉将绘制这两个字段,每个字段都有自己的滑块,并且位于一条线上。
[Serializable] // The Serializable attributes tells Unity to serialize fields of this type.
public struct MyStruct
{
public float X;
public float Y;
}
对于抽屉,我们将创建一个名为 MyStructDrawer 的新类,并继承自 OdinValueDrawer<T>
。由于我们正在为 MyStruct 制作一个抽屉,因此我们将为 OdinValueDrawer 的泛型参数指定 MyStruct。
public class MyStructDrawer : OdinValueDrawer<MyStruct>
{
}
接下来,让我们开始实现实际绘图。为此,我们需要重写 DrawPropertyLayout
方法。
protected override void DrawPropertyLayout(GUIContent label)
{
}
在这个抽屉里,我们首先需要的是一个可以吸引的区域。我们可以通过调用 Unity 的布局系统来获取它。这也让我们的抽屉可以与 Unity 的 IMGUI 系统的其余部分一起工作。
Rect rect = EditorGUILayout.GetControlRect();
我们还想为我们的属性画一个标签。请记住,在 Odin 中,标签是可选的,因此传递给此方法的标签可能是 null。
if (label != null)
{
rect = EditorGUI.PrefixLabel(rect, label);
}
最后,我们可以绘制结构体的字段。我们可以从 ValueEntry.SmartValue
中获取属性的当前值。我们还可以使用此属性来分配任何更改。
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 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;
}
}
制作自定义组
本指南的目标是创建一个 ColoredFoldoutGroupAttribute,该 Attribute 可用于任何字段、属性或方法,并且可以使用组路径与 Odin 的其他 Group Attributes 结合使用。
我们需要做的第一件事是定义组特性本身。我们将创建一个 ColoredFoldoutGroupAttribute 并从 PropertyGroupAttribute 类继承。 PropertyGroupAttribute 是一个特殊 Attribute,必须从中派生才能创建正确的组特性。
public class ColoredFoldoutGroupAttribute : PropertyGroupAttribute
遗憾的是,C# 不允许将自定义结构直接传递给 Attribute 构造函数,因此我们不能使用 Unity 的 Color 结构。取而代之的是,我们为红色、绿色、蓝色和 alpha 通道定义了 4 个字段。
我们还将定义两个构造函数。其中一个构造函数将只采用一条路径。这使用户只需在单个 Attribute 上指定一次颜色,然后再在所有其他 Attribute 上指定颜色 具有相同路径的 ColoredFoldoutGroupAttributes 将合并在一起以获取该颜色值。
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;
}
最后,我们需要实现 PropertyGroupAttribute 的 CombineValuesWith 方法。对于具有相同路径的所有 Attributes ,将调用此方法,这是我们将处理合并颜色值的地方,如前所述。
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);
}
这样,我们的新组特性就完成了。现在,让我们制作组抽屉。 创建一个新类,并将其命名为 ColoredFoldoutGroupAttributeDrawer。此类将使用编辑器代码,因此请记住确保它仅包含在 编辑汇编。例如,通过将文件放在 Editor 文件夹中或将其内容包装在 #if UNITY_EDITOR 预处理器指令中来执行此操作。
ColoredFoldoutGroupAttributeDrawer 应继承 OdinGroupDrawer<TGroupAttribute>
类。
public class ColoredFoldoutGroupAttributeDrawer : OdinGroupDrawer<ColoredFoldoutGroupAttribute>
由于这应该有一个折叠状态,所以我们需要一个折叠状态。我们可以添加一个布尔字段并完成它,但是每次都会重置它 抽屉已实例化。有一个持续的折叠状态不是更好吗? 我们可以通过从 Odin 的持久化系统中获取 LocalPersistentContext<T>
来实现这一点。 如果启用,这将在 Unity 重新加载甚至打开和关闭编辑器之间保存抽屉的状态。
private LocalPersistentContext<bool> isExpanded;
protected override void Initialize()
{
this.isExpanded = this.GetPersistentValue<bool>(
"ColoredFoldoutGroupAttributeDrawer.isExpanded",
GeneralDrawerConfig.Instance.ExpandFoldoutByDefault);
}
太好了!让我们继续实现实际的组抽屉。为此,我们需要重写 DrawPropertyLayout 方法。
protected override void DrawPropertyLayout(GUIContent label)
{
}
我们将从附加到抽屉的 Attribute 中获取颜色,我们可以使用 SirenixEditorGUI 中的方法绘制折叠盒。 我们还将使用 GUIHelper 将我们的颜色推送到 GUI.color。一旦我们对 BeginBox 和 BeginBoxHeader 都进行了绘制调用,我们将再次弹出颜色。这样,只有我们的折叠盒会被着色。
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();
这应该可以做到。这应该涵盖如何自己创建自定义组特性并在 Odin 的抽屉系统中实现它们。 你可以试着玩这个,看看你能做出什么。
Attribute:
using System;
using Sirenix.OdinInspector;
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);
}
}
Drawer:
using Sirenix.OdinInspector.Editor;
using Sirenix.Utilities.Editor;
using UnityEngine;
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();
}
}
如何使用 PropertyTree
通过使用 PropertyTree 类,您可以实现 Odin 绘图系统的全部功能:Attributes、属性解析器和其他所有内容,几乎可以在您需要的任何地方实现。
使用 PropertyTree 非常简单:使用 PropertyTree.Create 方法之一为目标对象创建一个实例,然后每帧调用 Draw() 方法。
MyClass myObject = new MyClass();
PropertyTree myObjectTree;
void DrawSingle()
{
if (this.myObjectTree == null)
{
this.myObjectTree = PropertyTree.Create(this.myObject);
}
this.myObjectTree.Draw(false);
}
请务必保留对原始创建的树的引用。不要调用 PropertyTree.Create 每个 GUI 调用!
如果需要,还可以为多个目标创建 PropertyTree,并一次编辑所有目标。
MyClass[] myObjectArray = new MyClass[]{ new MyClass(), new MyClass(), new MyClass() };
PropertyTree mutliTargetTree;
void DrawMultiple()
{
if (this.mutliTargetTree == null)
{
this.mutliTargetTree = PropertyTree.Create(this.myObjectArray);
}
this.mutliTargetTree.Draw(false);
}
最后,在创建 PropertyTree 时,您可以选择插入自己的自定义 OdinPropertyResolverLocator 和 OdinAttributeProcessorLocator 实例。这使您可以几乎完全自定义 PropertyTree 处理属性和特性的方式。
MyClass myObject = new MyClass();
PropertyTree customLocatorTree;
void DrawWithCustomLocator()
{
if(this.customLocatorTree == null)
{
this.customLocatorTree = PropertyTree.Create(this.myObject, new CustomAttributeProcessorLocator());
}
this.customLocatorTree.Draw(false);
}
public class CustomAttributeProcessorLocator : OdinAttributeProcessorLocator
{
private static readonly CustomMinionAttributeProcessor Processor = new CustomMinionAttributeProcessor();
public override List<OdinAttributeProcessor> GetChildProcessors(InspectorProperty parentProperty, MemberInfo member)
{
return new List<OdinAttributeProcessor>() { Processor };
}
public override List<OdinAttributeProcessor> GetSelfProcessors(InspectorProperty property)
{
return new List<OdinAttributeProcessor>() { Processor };
}
}
理解 Odin Drawers 上的泛型约束
为了充分利用 Odin Drawers 系统,有必要了解如何在自定义的 Odin Drawers 上使用泛型约束。
当从 Odin Drawers 类(如 OdinValueDrawer)继承时,您可以使用 C# 的泛型约束来指定您希望 Drawers 绘制的类型和值。
以下示例将为任何具有公共无参数构造函数的类的属性进行绘制。Odin 将为任何符合“class, new()”约束的类型为 Drawers 创建一个实例。
public class ClassDrawer<T> : OdinValueDrawer<T> where T : class, new()
{
...
}
您还可以指定具体类型:
public class Item { ... }
public class ItemDrawer : OdinValueDrawer<Item>
{
...
}
然而,Odin 只会将 ItemDrawer 用于类型为 Item 的属性;任何从 Item 继承的类都不会使用 ItemDrawer。您可以通过使 ItemDrawer 成为泛型来改变这一点:
public class GenericItemDrawer<T> : OdinValueDrawer<T> where T : Item
{
...
}
然后,Odin 将为任何 Item 或 Item 派生类型的属性创建一个具体类型的泛型实例。同样的规则也适用于抽象类和接口。如果您尝试这样做,Odin 将在控制台中引发错误:
public abstract class Weapon : Item { ... }
public class InvalidWeaponDrawer : OdinValueDrawer<Weapon>
{
...
}
您必须将 Weapon 指定为泛型约束。
public class CorrectWeaponDrawer<T> : OdinValueDrawer<T> where T : Weapon
{
...
}
如上所述,同样的规则也适用于接口。
public interface IMyInterface { ... }
public class InvalidMyInterfaceDrawer : OdinValueDrawer<IMyInterface> // 无效的 Drawers 声明
{
...
}
public class CorrectMyInterfaceDrawer<T> : OdinValueDrawer<T> // 有效的 Drawers 声明
where T : IMyInterface
{
...
}
对于列表和泛型类,您可以创建具有多个泛型约束的 Drawers。
public class ItemListDrawer<TList, TElement> : OdinValueDrawer<TList>
where TList : IList<TElement> // 使用 IList 使 CustomListDrawer 适用于 List 和数组。
where TElement : Item
最后,同样的规则也适用于 Drawer 类的泛型约束。
public abstract class Reference<TValue, TVariable> { ... }
public abstract class Variable<TValue> : ScriptableObject { ... }
public class InvalidReferenceDrawer<TReference, TValue> : OdinValueDrawer<TReference>
where TReference : Reference<TValue, Variable<TValue>>
{
...
}
public class CorrectReferenceDrawer<TReference, TVariable, TValue> : OdinValueDrawer<TReference>
where TReference: Reference<TValue, TVariable>
where TVariable : Variable<TValue>
{
...
}
有时泛型约束是不够的。因此,每个 Drawers 类都有可选的 CanDraw 方法,您可以重写该方法以插入额外的条件逻辑来确定应用 Drawers 的位置。
public class AllWeaponsExceptSwordDrawer<T> : OdinValueDrawer<T>
where T: Weapon
{
public override bool CanDrawTypeFilter(Type type)
{
return type != typeof(Sword);
}
}
最好尽可能指定更多的约束来缩小 CanDraw 被查询的类型范围,因为通过泛型约束系统约束 Drawers 比不断查询 CanDraw 所有类型要快得多。
提示、技巧和最佳实践
避免auto-magic
人们常常倾向于在 drawers 中自动更改或修复某个值。如果无论如何都需要这样做,那为什么还要麻烦用户呢?
问题在于,您最终会隐藏一个潜在的问题。对于 drawers 来说尤其如此,其中的 auto-magic 只会在实际检查对象时运行。这可能会导致奇怪和令人困惑的情况,即一段代码根据对象当前是否正在被检查而以两种不同的方式运行。
另一方面,如果您警告或向用户显示存在问题,那么他们会意识到它,可以修复它,并且现在该问题可能也存在于其他地方,需要在那里也进行修复。
小心转换(conversions)
从长整型转换为整型,然后再转换回长整型可能会导致数据丢失。因此,如果您发现自己需要进行这样的转换才能使用某个方法,那么最好只在用户实际在 drawers 中更改了值时才分配值的更改。
您可以使用 EditorGUI.BeginChangeCheck 和 EndChangeCheck 方法来实现此效果:
EditorGUI.BeginChangeCheck();
long newValue = SirenixEditorFields.IntField((int)this.longValue));
if (EditorGUI.EndChangeCheck())
{
this.longValue = newValue;
}
使用 Rect 代替 Layout
使用 IMGUI 的布局系统在许多用例中非常出色,但通常更简单的方法是获取单个 Rect,并使用数学手动操作和放置元素与 Rect。作为奖励,与使用布局组相比,这也将导致更快的绘制代码。
Odin 为 Rect 结构定义了许多有用的扩展方法,对于这种操作和定位非常有用。
Rect rect = EditorGUILayout.GetControlRect();
if (label != null)
{
// The PrefixLabel method eats the first part of rect that is used by the label.
rect = EditorGUI.PrefixLabel(rect, label);
}
// Take 80 pixels from the right side of the int field...
value = SirenixEditorFields.IntField(rect.SubXMax(80), value);
// ... and use them to make a button.
if (GUI.Button(rect.AlignRight(80), "Set Zero"))
{
value = 0;
}
标签:自定义,Attribute,float,class,Drawers,Inspector,抽屉,public,rect
From: https://blog.csdn.net/UnityBoy/article/details/141137771