找回密码
 立即注册
查看: 322|回复: 0

UnityEditor扩展,将IMGUI工作流转变为RMGUI,实现一个 ...

[复制链接]
发表于 2023-3-30 14:35 | 显示全部楼层 |阅读模式
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("aaa",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("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方法。
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,文本内容计算出文本需要显示的实际大小。
比如容器:容器可以根据自己内部的子组件计算出包含这些子组件的实际大小。
未完待续。。。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-16 21:44 , Processed in 0.098623 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表