解决了一个困扰很久的问题,虽然没啥技术含量,不如说现在才想到这个解决办法的我是真的蠢
但是还是记录(水)一下,避免像我这样的傻蛋有俩个
首先前置条件是先了解一下UIToolkit
关于 UItoolkit 的几种更新方式
UIToolkit 的更新方法主要以下几种。
注册变化更新事件
XXX.RegisterCallback<ChangeEvent<bool>>(OnXXXChange);
大概的传播链条是 : 用户对控件输入交互 => 触发更新事件 => 对脚本进行数据更新。
所以说这种注册方法只能做单向更新。
绑定序列化对象字段
XXX.bindingPath = "要绑定目标字段名";
XXX.Bind(SerializedObject); // 目标对象的序列化对象
这种绑定方法可以把控件的值和目标对象的序列化字段进行绑定。
也就是二者的值可以同步变化 => 可以做双向更新。
缺点是 : 目标字段必须添加序列化特性(要确保目标字段可以被序列化)
特殊的属性控件
UIToolkit 有个 Inspector 相关的控件名叫 PropertyField。
XXX.bindingPath = "要绑定目标字段名";
XXX.BindProperty(SerializedObject); // 目标对象的序列化对象
这个和 IMGUI 中的 SerializedProperty 的用法类似。
可以用来绘制 Unity 本身便可自动序列化显示在 Inspector 的字段,例如 List int string …
不如说这个就是专门用来做 Insepctor 的控件。
比对用 UIToolkit 绘制 和 Unity 自动绘制的区别
比如 : 你想用 UIToolkit 绘制一个 List 如下图。
你需要对图中每个元素都创建一个 UIToolkit 的控件作为“外壳”。
甚至还需要专门 new 些控件用于交互,比如控制 list 元素数量的 + -Button,左上角显示元素数据的 Field ,分割数据的 Foldout。
然后处理元素的对应关系,处理元素的更新, 处理控件的样式…
这坨复杂的逻辑想想就头大…
这其实 Unity 已经帮你实现好了,上图就是 Unity 自动可以在 Inspector 窗口中绘制的样式。
想要用 UIToolkit 实现只需要用 PropertyField 进行字段绑定即可(可序列化字段)
当然你可以自己处理这一套绘制关系和序列化关系并且封装成类。
然后搞一套特性用于标注什么时候用这个类进行绘制。
仅仅是字段的话用PropertyDrawer 特性怎样?
UIToolkit版本类Odin标签研究记录 一
这样就用 UIToolkit 完成了简单的类 Odin 标签化字段绘制。
比如 : 绘制一个字典的 Inspector 窗口。
UIToolkit 相对 IMGUI 的缺陷和解决办法
IMGUI 是即时绘制模式,几乎每帧都重新绘制。
UIToolkit 是只绘制一次然后靠数据绑定或者事件监听的方式进行更新。
举例和 UIToolkit 的缺陷
这样UIToolkit 做非数据绑定类的数据更新时就显得额外吃力。
比如 GF 的 ObjectPool 的自定义 Inspector 页面是用 IMGUI 做的, 如下图。
这是通过每帧获取 ObjectPoolComponent 的方法接口获取所有的对象池信息进行绘制的。
数据来源并不是来自序列化本身的字段。
并且 ObjectPoolComponent 本身也没有其他字段可开放用于序列化,因为真正数据存在 ObjectPoolManager 中。
用 UIToolkit 复刻这样一个 Inspector样式其实很简单,无非就是控件的排列组合。
问题是数据更新
使用事件监听的方式只能做到单向传递 。
数据绑定的方式,数据源做了隔离只能通过方法接口每帧获取并更新。
每帧获取数据源 和 生命周期问题
每帧获取数据源进行更新的话, Editor 有专门的 Update 方法可以订阅。
EditorApplication.update();
但是生命周期是 EditorApplication 和 Inspector 的生命周期不一致。
而且 ObjectPool 的数据是 Runtime 数据不是 Editor. 这需要的是 Runtime 的帧更新…
那么用 mono 的 update 驱动怎样?
那如何保持其生命周期和 这个Insepctor 的生命周期一致?
OnEnable绘制时创建一个 mono 对象丢场景中 当作 Driver, OnDisable 时销毁?这太蠢了。
其实公共 mono 能做到这个, OnEnable 时订阅,OnDisable 时取消订阅 (我之前的解决方案其实就是这个)
不过这样多了一个依赖不说其实还有一个焦点问题…
关于焦点的问题
对 IMGUI 绘制过程中打 Log 可以得知,IMGUI 并不是严格的每帧更新。
- IMGUI 其实在页面失焦是不进行更新的,只有页面聚焦的时候才会进行更新。
- 更新是跟随鼠标的,鼠标不动不更新。
- 鼠标在 hierarchy project inspector 窗口之间穿过时会更新。
- 主要更新集中在鼠标在 inspector 内移动时,这时候应该是每帧绘制。
核心问题总结
如何让 UIToolkit 像 IMGUI 一样聚焦的时候每帧更新数据。
解决方法
如何让 UIToolkit 像 IMGUI 一样聚焦的时候每帧更新数据?
谜题就在谜面上。
UIToolkit 的有个特殊容器叫 IMGUIContainer。
找到 IMGUIContainer 的源代码发现里面有个更新事件。
写个简单的用例测试一下
[CustomEditor(typeof(Test))]
public class TestInspector : Editor
{
VisualElement Root { get; set; }
public override VisualElement CreateInspectorGUI()
{
Root = new VisualElement();
TextField text = new TextField();
IMGUIContainer container = new IMGUIContainer();
Button button = new Button();
container.onGUIHandler += () =>
{
Debug.Log("It time to render imgui");
};
Root.Add(text);
Root.Add(container);
Root.Add(button);
return Root;
}
}
那么我们就可以使用 IMGUIContainer 作为更新事件的分发者来驱动这个 Inspector 页面的其他控件进行数据更新
好了,这样我就可以对GF剩下的几个Inspector窗口和Debugger页面也完成UIToolkit化了