阔别两年,最近几个月又重新将大学时候写的UI框架UniVue重构了一遍,工作近一年后对UI工作流有了一些新的认识,觉得以前的设计不合适团队协作,于是不再强绑定MVVM,而是转向响应式渲染,由数据和事件驱动UI的更新。今天已经发布了全新的版本,一个是正式发布版,一个是开发版。Avalon712/UniVue: High-performance, lightweight reactive UI framework for Unity.
https://github.com/Avalon712/UniVuehttps://github.com/Avalon712/UniVue3-Develop
https://github.com/Avalon712/UniVue3-Develop
UniVue是一个很轻量的UI框架,最核心的功能就是响应式,解决实际项目中UI代码多而且很冗余的问题(很多时候为了赶时间根本没时间关心一个函数被多次冗余调用,只要能运行没有Bug,前期没问题(到了后期排除bug,这种冗余调用会让调试抓狂......),后期游戏开始出现卡顿之后,此时优化根本无从下手优化,大量资源、大量代码,错综复杂......),响应式开发让我们只需要关系架构上的设计而非写这些毫无卵用的低级代码,固定的工作流交给AI可以获得高效同时犯错率极低的输出。
多语言、资源管理等这些UniVue都没有,因为这些功能每个功能都能单独拿出来做一个框架,市面上已经有很多优秀的这些框架,因此不再重复造轮子,UniVue只暴露了接口,需要你自己根据自己的项目使用的框架进行定制化实现。而且实际项目中这两个部分往往还会根据项目有定制化的需求。
目前由于精力有限,只针对了UGUI进行了实现。如果你想扩展其他UI框架,如FariyGUI、UI Toolkit,完全可以在现在代码的基础上剥离UGUI部分(只有极少部分,主要部分就是UGUI是基于GameObject的,因此内部BaseView、BaseComponet的设计也是基于此的)。你只需要单独实现BaseView、BaseComponet就能无缝实现基于其他框架的UniVue。
响应式渲染设计
UniVue内部响应式本质是数据驱动+事件驱动,数据层主要是通过继承BaseModel,在数据发生变化时通知渲染层,事件层则是UI事件和自定义事件被渲染层监听时触发渲染。
渲染层的结构是一个多索引有向无环图,任意路径的末尾节点都是渲染函数,当数据变化或事件触发时,抵达渲染节点最多不会超过5层,即时间复杂度为O(1)。
**带延迟性的响应式渲染。**考虑到UI渲染大多数不需要很高的渲染频率,因此内部默认的渲染频率为0.1秒,即在这0.1秒内被触发的所有渲染只会被当作一次渲染调用,这种带延迟性的响应式渲染可以大幅降低重复渲染带来的问题。比如当前帧内会对模型的属性A、B、C修改多次,如果每次都是立即式渲染,渲染函数会被多次重复调用,然而事实上当前帧的所有变化都可以收敛为一次渲染调用。当然也可以修改延迟调用的频率,最低的渲染频率为延迟一帧。

图中M代表数据Model,E代表事件Event,P代表数据Model的属性,R代表渲染函数,G代表RGraph。
一次完整的渲染流程举例:当数据M1的P1属性发生了变化时,从Entry进入索引得到M1绑定了两个RGraph:G1和G4,从路径G1->M1->P1->R3得到渲染函数R3,从路径G4->M1->R5得到渲染函数R5,从路径G4->M1->R6得到渲染函数R6,得到所有要触发渲染函数为R3、R5、R6,这些函数不会立即执行,而是被放到一个HashSet中,等待渲染延迟结束后才被执行。如果在等待期间事件E2触发,从路径G2->E2->R2,G2->E2->R3,G4->32->R6得到渲染函数R2、R3、R6,放入HashSet,去重得到R2、R3、R5、R6,避免了重复渲染。特别是一些渲染函数要加载资源时,这种情况下会极大减少因为UI刷新导致的性能卡顿。
绑定的本质就是一个watch函数,数据和事件被监听。