最近因为写编辑器拓展写烦了打算研究UIToolkit版本的类Odin拓展。
采用UIToolkit写编辑器拓展是看中了其性能,号称OneDrawCall绘制所有UI并且兼顾Editor和Runtime。
不过其实用UIToolkit写编辑器拓展意外的繁琐,而且都是写繁琐无脑的业务活儿,于是想用类似Odin的方式优化一下。
用UIToolkit实现类Odin标签的优势
- 性能好 (RMGUI带状态,不需要像IMGUI每帧创建并绘制)
- 更换和设置样式方便 (有专门的样式文件)
- 所见即所得 (带可视化编辑器)
- 节省代码量 (相比与IMGUI)
参考资料
正常重写Inspector流程
- 创建一个继承Unity.Editor的类。
!
- 重写OnInspector方法。
!
- 用CostomEditor标注要重写那个类的Inspector页面。
效果
可以看出
- 在OnInspector流程中我重写了OnInspectorGUI()和CreateInspectorGUI()只有CreateInspectorGUI()的log触发,也就是说用uitoolkit重写的方法优先度高于用GUI重写的方法,且有可能俩方法互斥。
- 用uitoolkit重写的方法只会调用一次 。
如果用gui绘制会怎样呢?
- 注意看鼠标和Log情况,当鼠标在Inspector窗口移动时,才会更新调用。
- IMGUI虽然是立即更新的模式,但是Untiy还是做了一些优化,没有每帧调用。
CostomEditor特性重定向原理
Unity开发笔记-Odin标签实现原理探究 - jeoyao - 博客园 (cnblogs.com)
- 这个特性记录了类和Inspector的联系。
- 也就是需要注册的意思。
Odin标签重绘原理
dbrizov/NaughtyAttributes
- 创建一个继承Unity.Editor的基类。
- 在OnInspector窗口里重写对应的特性检测方法。
- 用CostomEditor标注要重写的范围类为MonoBehaviour或者Object,并且设置重写其子类为true。
结果
触发了empty的log, 这说明其父类绘制的过程没有用到VisualElement方法绘制,有可能使用了GUI绘制,这样导致我们无法继承前面的绘制内容,这相当于覆盖性重绘,这不是我想要的。
问题原因猜想 :
- 项目里有odin插件,有可能是odin先劫持了Mono并且用GUI绘制了一遍导致VisualElement丢失。
- Unity的Inspector的页面还是使用了GUI的绘制方式。
(仅猜想未验证)
总之用UIElement绘制Button的尝试暂时失败了
字段属性的绘制过程
- 观察CostomInspector和NaughtyAttributes这俩个库,发现了其很多特性使用了PropertyDrawer这个类
- F12反编译查看其接口
namespace UnityEditor;
// 一个用于自定义属性绘制的抽象基类, GUIDrawer是一个空的抽象类
public abstract class PropertyDrawer : GUIDrawer
{
internal PropertyAttribute m_Attribute;
internal FieldInfo m_FieldInfo;
internal string m_PreferredLabel;
// 相对应的属性
public PropertyAttribute attribute => m_Attribute;
// 对应的字段反射信息
public FieldInfo fieldInfo => m_FieldInfo;
// IMGUI需要用的标签信息
public string preferredLabel => m_PreferredLabel;
// 作用不明,但是从Label判断这是给IMGUI用的
internal void OnGUISafe(Rect position, SerializedProperty property, GUIContent label)
{
ScriptAttributeUtility.s_DrawerStack.Push(this);
OnGUI(position, property, label);
ScriptAttributeUtility.s_DrawerStack.Pop();
}
// 用IMGUI绘制属性的接口
public virtual void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
GUIContent label2 = new GUIContent(label);
EditorGUI.LabelField(position, label2, EditorGUIUtility.TempContent("No GUI Implemented"));
}
// 用UIElement绘制属性的接口
public virtual VisualElement CreatePropertyGUI(SerializedProperty property)
{
return null;
}
// 带Lebel 应该是给GUI用的
internal float GetPropertyHeightSafe(SerializedProperty property, GUIContent label)
{
ScriptAttributeUtility.s_DrawerStack.Push(this);
float propertyHeight = GetPropertyHeight(property, label);
ScriptAttributeUtility.s_DrawerStack.Pop();
return propertyHeight;
}
// 这个应该也是绘制GUI样式需要用的
public virtual float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return 18f;
}
// 缓存面板的内部方法, 作用不明,但是没有Lebel标签,也许是通用方法
internal bool CanCacheInspectorGUISafe(SerializedProperty property)
{
ScriptAttributeUtility.s_DrawerStack.Push(this);
bool result = CanCacheInspectorGUI(property);
ScriptAttributeUtility.s_DrawerStack.Pop();
return result;
}
// 判断是否能对Inspector面板进行缓存的虚方法
public virtual bool CanCacheInspectorGUI(SerializedProperty property)
{
return true;
}
}
有用的信息
- UIElement绘制的接口 : CreatePropertyGUI(SerializedProperty property)
- 自带特性和字段的反射信息,还有方法提供的SerializedProperty参数
流程
定义特性
特性还是使用之前的Button特性
继承PropertyDrawer使用其接口进行绘制
public class ButtonAttributeDrawer : PropertyDrawer
{
MethodInfo method;
Object target;
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
VisualElement visualElement = new VisualElement();
target = property.serializedObject.targetObject;
GameFramework.Attribute.ButtonAttribute buttonAttribute = (GameFramework.Attribute.ButtonAttribute)attribute;
if (buttonAttribute.methodType != null)
{
method = target.GetType().GetMethod(buttonAttribute.methodType);
if (method != null)
{
var button = new Button() { text = method.Name };
if (buttonAttribute.useFieldAsParameters)
{
ParameterInfo[] parameters = method.GetParameters();
if (parameters.Length == 1)
{
if (parameters[0].ParameterType.IsInstanceOfType(fieldInfo.GetValue(target)))
{
button.RegisterCallback<MouseUpEvent>((evt) =>
{
method.Invoke(target, new object[] { fieldInfo.GetValue(target) });
});
visualElement.Add(button);
}
else visualElement.Add(new HelpBox("parameter is invalid", HelpBoxMessageType.Warning));
}
else visualElement.Add(new HelpBox("parameter is invalid", HelpBoxMessageType.Warning));
}
else
{
button.RegisterCallback<MouseUpEvent>((evt) =>
{
method.Invoke(target, null);
});
visualElement.Add(button);
}
}
else visualElement.Add(new HelpBox("methodType is invalid", HelpBoxMessageType.Warning));
}
else visualElement.Add(new HelpBox("attributeTarget is field case, methodType cant be null", HelpBoxMessageType.Warning));
CheckMethod(visualElement);
return visualElement;
}
public void CheckMethod(VisualElement visualElement)
{
var methods = target.GetType().GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance);
foreach (var method in methods)
{
var attribute = method.GetCustomAttribute<GameFramework.Attribute.ButtonAttribute>();
if (attribute != null)
{
var button = new Button() { text = method.Name };
button.RegisterCallback<MouseUpEvent>((evt) =>
{
method.Invoke(target, null);
});
visualElement.Add(button);
}
}
}
}
使用CustomPropertyDrawer特性注册重绘什么特性标注过的字段
[CustomPropertyDrawer(typeof(GameFramework.Attribute.ButtonAttribute))]
public class ButtonAttributeDrawer : PropertyDrawer
结果
从测试用例中可以看到, 只有标注在字段上的方法能够生效,而标注在方法无法触发绘制
但是通过CheckMethod方法证明可以用字段额外绘制方法上的button标签,只是在方法上标签无法触发CustomPropertyDrawer的绘制
Bug与解决
遇到了一个奇怪的bug,也就是重聚焦后Inspector面板的Button会失效
重新从别的页面的Inspector窗口切换回来会触发Inspector窗口的绘制会重新起效
原因
原因从烟雨大佬的帖子中得到了答案
https://www.lfzxb.top/unity-ui-element-total/
聚焦丢失会导致Selection丢失, 在UIElement中Selection丢失我们就无法选取到那个元素,自然button就会失效
总结
- UIElement可以用来制作Odin标签,但Unity对其支持并不完善,所以暂时不推荐
- 其实用什么方式绘制没有什么太大的差别, Odin最舒服的地方其实在序列化方面, 便携的特性 + 优秀的序列化发挥了1+1>2的效果
- 但是我还是想骂Unity,还主推UIToolkit,但是都多少年了,还是不堪大用,重聚焦会丢失selection这种级别的bug拖了俩年了还不修!