【Unity3D框架】关于手游的UI框架的一些思考

一、概述

        不知不觉入行Unity3D也四五年了,期间经历了几个手游项目,正好想写点什么总结性的文章来记录一下自己对过往经验的一些思考。

        这篇文章主要记录一下正在使用的UI框架在演进过程中遇到的问题的一些小的思考。

二、关于组件和可复用

        在实践过程中,UI界面元素其实大致分为两类,一类是重复的结构在UI界面以列表、网格等形式重复出现,一类是非重复或者容器性质的UI界面元素。所以在设计UI管理类的时候,思路上就可以定义两个概念:面板和组件。基本关系可以是:所有完整的界面都是面板,而面板同时可以持有多个组件,并作为一个父级单位管理好子级的组件的生命周期。

        组件存在的意义一方面是可以拼一个预设体,让面板负责实例化,达到复用。另一方面是,对于复杂结构的面板,可以做功能拆分,减少超重型面板。

        有了这个划分,UI脚本的模板即可以围绕此设计出来面板和组件两类。

        由于Unity的UGUI只提供了ScrollRect,在此基础上,UI框架本身可以封装类似于iOS的ListView/CollectionView或者Android的RecycleView等风格的自动管理组件复用的UI,这样,类似于列表和网格类的UI就可以通用,而无需每次都去造轮子。

三、关于MVC

        数据、逻辑、界面分离的话题,并不是Unity开发独有。

        一般来说,支持热更新的项目,用的比较多的应该是C#底层 + Lua脚本的形式(不讨论ILRuntime和Huatuo等C#热更新框架)。大多数的UI界面写在Lua层比较方便进行热更新。

        Lua本身不支持面向对象,但是使用metatable是可以实现面向对象的基本功能的。

        比如有大佬封装好的middleclass仓库,实现起Lua的面向对象就比较方便。
GitHub - kikito/middleclass: Object-orientation for LuaObject-orientation for Lua. Contribute to kikito/middleclass development by creating an account on GitHub.icon-default.png?t=N7T8https://github.com/kikito/middleclass.git        在使用Lua开发的过程中,没有严格使用MVC模式,但是大致上通过一下途径去遵循这个思想:

        Model层是通过建立一个个Data类去实现。尽可能以合理使用面向对象的形式。

        Controller层通过建立一个个Manager/Controller管理类去实现。

        View层通过Prefab拼UI预设,生成UI脚本模板,UI脚本内控制UI表现去实现。

       

        之前新版Unity的DOTS刚推出时,时兴过一阵ECS模式,数据和行为完全分开,实际上已经变成异步形式。此外还有见过MVVM的形式。

        但是由于有一定的学习成本,在团队间推广起来有一定的困难。目前还是使用上述简单粗暴的方式。要是有特别好用的其他形式,欢迎评论赐教。

四、层级划分和管理

        UI框架本身有了基本的生命周期管理之后,理论上就可以满足大多数的UI的使用了。但是复杂度在开始引入一些复杂的特效,以及开始出现数量较多的UI界面之后就开始陡然上升了。

        为了预防后续开发过程中,由于同时弹出多个界面,或者有多级弹窗等形式,导致的各个面板之间的界面层级穿插,最好是在立项之初,开发UI框架时,就实现一套自动化程度较高的层级划分和管理机制。否则当UI界面制作到一定数量级之后,修改起来就非常费劲了。

        关于这个问题,我的一个思考的方案是:

        1、首先,UI层级使用Canvas的sortingOrder管理,划分几个基础的层级段,比如划分了6个层级(可以是-300~-100、-100~100,100~300,300~500, 500~700, 700~900)。根据游戏的UI特点,对不同类型的界面应用不同的层,层的枚举可以配在UI脚本模板里。

        2、特效的层级使用相对层级管理,可以提供C#脚本,挂在具体需要调控层级的特效或UI元素上,填入偏差值offset,比如填1最终的效果是比面板层级高1,并为该脚本实现常用Component(如ParticleSystem/Canvas/Spine等)刷新层级的接口。

        3、UI管理类按层级段和打开面板的先后顺序,自动维护一个当前段顶端的层级值,并且能够轻松地被各个界面获取到自身的层级值。自动维护的核心逻辑在于通过获取上述脚本或其他方式自动获取到该界面所占用掉的层级段的宽度,并累加到顶端值,下一个面板打开的时候面板本身的层级由该值决定。

        或者说由每个UI面板添加一个自己需要跨多少层级的字段也可以。

        再或者约定一个固定间隔,每个UI之间间隔固定的值。

        总之,实现时自动维护的程度完全可以由项目复杂度、性能等方面作取舍,但是需要能获取到打开的UI占用了多少个层级。

        4、必要的话,在关闭UI时可以回收并刷新一下改层上方的UI,回收UI段。或者一开始段就留好。

        通过以上操作,应该在展示界面时,就完全不会出现多个界面同时出现时,层级之间相互交叉导致各种各样的问题。

五、栈

        在UI框架中使用栈,主要是为了实现一个常用的功能:Android设备返回键或者其他返回手势返回时,关闭当前打开的面板。

        在UI管理类中维护一个栈,并且自动在提供的管理接口,如打开界面、关闭界面等地方维护这个栈。在UI管理类监听Key事件,当监听到返回键按下时,就可以通过栈顶来做对应的响应。不想要响应的界面,可以通过给UI脚本添加统一字段来设置。这样当栈顶是该界面的时候可以不返回。最后栈为空的时候,可以弹出是否退出游戏的弹窗。

六、队列

        在UI框架中引入队列,主要是为了实现一些复杂的调度。

        具体的场景可能有:

        a、如果游戏的新手引导比较多,可能希望新手引导的出现优先于一切其他UI界面的自动弹出。

        b、当游戏的活动、礼包等元素非常多,而又有自动触发弹出等情况,则打开一个时其他的界面需要等待上一个界面关闭之后再弹出。

        c、有一些复杂的动画,动画过程中不希望被界面打断

        除此以外,还有可能有很多别的情况。

        这些复杂的调度,目前我思考的方案是:

        1、实现多队列或者优先级队列,使界面的弹出能够被排序、管理、插队等。

        2、界面的出队,由UI管理类自动管理。而除了界面,提供插入动画或者接口等方式,管理特殊情况的排队。

        3、由于排队的目的是不希望有不该同时出现的界面同时弹出,因此理论上最好是所有的界面都应该通过队列调度。

        4、通过属性配置,兼容多级弹窗的逻辑。

        5、预留完整的日志系统,万一有队列逻辑不对卡住了,需要能快速定位问题。

        这个问题实际上由于需求的复杂程度,实际上并不能完完全全解决所有问题。如果有大佬有更加完善的实现思路,求赐教。

        此外,这个机制和层级管理机制一样,越早加入框架越好,否则等界面太多了再改,就非常的困难。

七、关于性能

        关于UGUI的性能,合批、图集、动静分离等诸多话题,可以说是非常常见,有不少优秀的文章也进行了讨论。

        此处,我想记录的是维护UI框架过程中遇到的另一个问题:当打开界面的时候去加载预设体的话,容易引发一个CPU占用峰值。

        为此,UI框架其实可以维护一个GameObject的对象池。这个对象池提供以下的功能:

        1、能够支持配置UI的预设体名和数量和情景,在切换场景过程中有加载界面遮挡的情况下提前加载,或者说在游戏过程中后台静默分帧加载。

        2、能直接提供GameObject回收,如界面关闭之后,并不直接销毁,而是挂在到一个看不见的节点,并由对象池持有引用。

        3、提供整体销毁的逻辑,在合适的时机回收。

        这个做法能够降低打开界面时的峰值掉帧,但是另一个方面则有内存引用风险,因此是一个需要平衡的机制。此外,需要预加载的物品是哪些,以及加载数量最佳为多少,其实也需要通过统计科学设定。

        

        如果时间精力允许的话,计划多写几篇对手游框架的一些简单思考做成一个系列。

        下面是这个系列的另一篇文章,欢迎点评:

【Unity3D框架】Unity Package Manager自定义包管理实践-CSDN博客

你可能感兴趣的:(unity,游戏引擎,游戏,3d,ui)