UnityEditor扩展,将IMGUI工作流转变为RMGUI,实现一个树状层级结构模型简化版UIElement的思路

Unity内置Editor的IMGUI模式能够满足日常扩展,大多数情况下EditorGUILayout提供的控件,和布局方法BeginVertical,BeginHorizontal,配合大量的内置控件,可以满足快速开发需求。另外Untiy也提供了TreeView,ReorderableList这样的复杂组件。个人体会下来,大多数开发情况下,会倾向于这种选择:能使用自动布局体系的EditorGUILayout的就不会使用EditorGUI,而且通常不会混用。当这些已有的组件无法满足某些自定义需求的时候,我们便会感受到IMGUI的局限性。

先说说个人体会的优缺点:

IMGUI:即时模式,无状态#

Copy
优点:能快速实现逻辑,不需要写回调方法 缺点:逻辑和布局代码混在一起,复杂业务下,代码过长 布局复杂且需要更多交互和维持控件状态的时候实现起来复杂 无层级嵌套结构

RMGUI: 保留模式,维护状态#

Copy
传统UI,比如UGUI,QT,WPF,安卓,还有已经成为过去的adobe Flex 优点:控件拥有状态,实现复杂交互控件更容易 UI具有层级结构,支持复杂的嵌套逻辑 缺点:控件通常需要实现回调

例如:我希望实现一个滚动列表,列表超出部分需要显示滚动条,列表的单元格的高度不定,列表内的行单元格内可以任意布局元素。有点类似游戏中的排行榜,单元格可以选中,选中后,单元格的背景改为高亮色,如图所示。

EditorGUILayout或许能够实现上述功能会是如下代码:

Copy
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("aaa",GUILayout.MinHeight(h)); } EditorGUILayout.EndVertical(); EditorGUILayout.EndScrollView(); }

测试运行发现,并没有像预期的那样运作。

但上述这个不定高度的列表,其实最让人在意的是单元格选中状态,首先选中功能需要检测鼠标是否在单元格区域内,另外还需要让单元格保留选中状态,另外我们还需要考虑到单元格在Scroll中的偏移问题。
这就戳中了IMGUI的痛点,当我们需要一些布局和非布局的复杂嵌套,同时需要对非按钮控件进行鼠标交互并保留状态时,IMGUI显得有些难以实现了。

为了解决上述问题,或者其他更加复杂的结构,我希望在Editor中实现一个RIMGUI系统。

将IMGUI转化为RMGUI的思路#

我们可以将EditorGUI.DrawXXX方法视为一种渲染接口,并引入新的包装类对该方法进行封装,该封装类具有状态,这样就完成了IMGUI到RMGUI的转换。

例如一个色块组件,我们可以这么实现:

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

外部调用时的代码大致如下,在外部调用时,之前无状态的写法被转换成了有状态的:

Copy
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的真正宽高。

Copy
public class Label:VisualElement { public Label(string text) { Content = new GUIContent(); Style = new GUIStyle("label"); 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方法。

Copy
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,文本内容计算出文本需要显示的实际大小。
比如容器:容器可以根据自己内部的子组件计算出包含这些子组件的实际大小。

未完待续。。。

posted @   jeoyao  阅读(464)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示
目录