|
UnityEditor扩展,将IMGUI工作流转变为RMGUI,实现一个树状层级结构模型简化版UIElement的思路 - jeoyao - 博客园
Unity内置Editor的IMGUI模式能够满足日常扩展,大多数情况下EditorGUILayout提供的控件,和布局方法BeginVertical,BeginHorizontal,配合大量的内置控件,可以满足快速开发需求。另外Untiy也提供了TreeView,ReorderableList这样的复杂组件。个人体会下来,大多数开发情况下,会倾向于这种选择:能使用自动布局体系的EditorGUILayout的就不会使用EditorGUI,而且通常不会混用。当这些已有的组件无法满足某些自定义需求的时候,我们便会感受到IMGUI的局限性。
先说说个人体会的优缺点:
IMGUI:即时模式,无状态#
优点:能快速实现逻辑,不需要写回调方法
缺点:逻辑和布局代码混在一起,复杂业务下,代码过长
布局复杂且需要更多交互和维持控件状态的时候实现起来复杂
无层级嵌套结构RMGUI: 保留模式,维护状态#
传统UI,比如UGUI,QT,WPF,安卓,还有已经成为过去的adobe Flex
优点:控件拥有状态,实现复杂交互控件更容易
UI具有层级结构,支持复杂的嵌套逻辑
缺点:控件通常需要实现回调例如:我希望实现一个滚动列表,列表超出部分需要显示滚动条,列表的单元格的高度不定,列表内的行单元格内可以任意布局元素。有点类似游戏中的排行榜,单元格可以选中,选中后,单元格的背景改为高亮色,如图所示。
EditorGUILayout或许能够实现上述功能会是如下代码:
private Vector2 _scrollPos;
private void OnGUI()
{
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos,GUILayout.MaxHeight(200f));
EditorGUILayout.BeginVertical();
for (int i = 0; i < 10; i++)
{
float h = Random.Range(10, 200);
EditorGUILayout.LabelField(&#34;aaa&#34;,GUILayout.MinHeight(h));
}
EditorGUILayout.EndVertical();
EditorGUILayout.EndScrollView();
}测试运行发现,并没有像预期的那样运作。
但上述这个不定高度的列表,其实最让人在意的是单元格选中状态,首先选中功能需要检测鼠标是否在单元格区域内,另外还需要让单元格保留选中状态,另外我们还需要考虑到单元格在Scroll中的偏移问题。
这就戳中了IMGUI的痛点,当我们需要一些布局和非布局的复杂嵌套,同时需要对非按钮控件进行鼠标交互并保留状态时,IMGUI显得有些难以实现了。
为了解决上述问题,或者其他更加复杂的结构,我希望在Editor中实现一个RIMGUI系统。
将IMGUI转化为RMGUI的思路#
我们可以将EditorGUI.DrawXXX方法视为一种渲染接口,并引入新的包装类对该方法进行封装,该封装类具有状态,这样就完成了IMGUI到RMGUI的转换。
例如一个色块组件,我们可以这么实现:
public class ColorRect:VisualElement
{
public Color Color = Color.white;
public ColorRect(float width, float height)
{
Width = width;
Height = height;
}
public override void Draw()
{
EditorGUI.DrawRect(renderArea,Color);
}
}外部调用时的代码大致如下,在外部调用时,之前无状态的写法被转换成了有状态的:
private ColorRect _colorRect;
private void Init()
{
_colorRect = new ColorRect(100, 100);
_colorRect.Color = Color.red;
}
private void OnGUI()
{
_colorRect.Draw();
}实现层级嵌套的树状结构#
RMGUI一般都具有嵌套功能。具体来说,所有组件都有parent属性,容器组件可以拥有子组件,并存在一个根组件(root/stage)用于管理所有组件。
层级结构如图所示:
下面是一些实现层级UI功能的不同功能类
VisualElement基类
所有组件都继承自VisualElement,这个基类定义了几个基本属性
parent,
localY,localY相对父容器的位置,
width,height,宽高,
通用方法比如
Draw,子类复写Draw实现具体的控件渲染,上面的ColorRect已实现了具体的Draw方法。
Measure,测量实际宽高。上面说过组件需要有能力知道自己的宽高。
LocalToGlobal,实现控件相对父类的坐标转换为stage坐标系中的世界坐标,在最终渲染控件的时候,统一使用世界坐标。
label类#
label具体实现如下,同样label实现了draw方法,外部通过在OnGUI中调用draw最终渲染出了label
另外label实现了Measure方法,通过Style.CalcSize(Content)方法计算出了label的真正宽高。
public class Label:VisualElement
{
public Label(string text)
{
Content = new GUIContent();
Style = new GUIStyle(&#34;label&#34;);
Text = text;
}
public override void Draw()
{
EditorGUI.LabelField(renderArea,Content,Style);
}
public override void Measure()
{
Vector2 size = Style.CalcSize(Content);
_measuredWidth = size.x;
_measuredHeight = size.y;
}
}Container类#
该类顾名思义,他是组件的容器,提供AddChlid方法,通过这个类,我们就能实现Tree结构了,如题代码大致如下,Container自身不可见,因此无需实现Draw方法。
public class Container:VisualElement
{
private List<VisualElement> _children = new List<VisualElement>();
public void AddChild(VisualElement value)
{
if (value == null)
{
return;
}
_children.Add(value);
value.Parent = this;
}
}Stage类#
Stage是所有组件的根,他也是一个容器,所以继承自Container。
OnGUI方法:该方法的实现是递归调用所有Stage的子类的Draw方法,外部只需要调用该方法,即可实现Stage内所有控件的渲染。
总的来说,继承结构大致可以用如下图所示:
测量和布局系统#
通常RMGUI中都必须包含布局逻辑,最常用的水平和垂直布局。通过组件,布局和容器,我们能组合出任意一种复杂的高级控件,比如List,Tree
所有控件都潜在支持鼠标交互。这样EditorGUI.DrawRect这样的最普通的控件也可以有交互功能。
所有控件都需要有能计算自己宽高的能力。
比如控件:Label可以根据自己内部的文字的fontsize,文本内容计算出文本需要显示的实际大小。
比如容器:容器可以根据自己内部的子组件计算出包含这些子组件的实际大小。
未完待续。。。 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|