做了这么多年Unity客户端,我最怕的不是做新界面,而是改老界面。尤其是那种已经迭代了半年以上的项目,UI脚本里全是一行行手写的Find/GetComponent,每个控件可能被三四个逻辑赋值,你根本不敢确定改一个Text会不会影响另一个模块。界面弹出的瞬间,几十个控件要挨个刷新,服务器又推过来几条新数据,整个刷新顺序乱成一锅粥。
第一次接触YIUI框架时,“数据驱动”这四个字确实吸引了我。它是在UGUI之上做了一层代码生成和数据绑定机制,同时与ET框架深度整合。这篇我打算从一个实际使用者的角度,把YIUI的核心机制、和ET的分工协作、以及我在项目里踩过的坑一次说清楚。不管你是正在评估UI框架选型,还是已经在用ET想提高UI开发效率,这篇应该都能给你一些参考。
1. 先搞清楚YIUI在解决什么问题
1.1 传统UGUI开发的三座大山
很多团队做UI,模式基本都是这样:策划需求来了,美术出图,程序在Canvas下摆好节点,然后打开脚本开始写代码。写的是什么?是大量的初始化逻辑。
第一座大山,是重复的控件获取。一个界面上有十几个Button、Text、Image,每个都要从节点树里找出来。transform.Find("Panel/Content/Title")这种代码写多了,手都会麻。更麻烦的是,一旦界面层级调整,Find路径就断了,运行时报空引用,你还得一个个去查是哪一行崩的。
第二座大山,是数据刷新靠手动。服务器数据过来了,你要自己写刷新函数,把数值填到对应的控件上。今天加一个属性,明天加一个条目,后天加一个红点,每次都要改动刷新代码。而且刷新顺序一旦出错,比如先刷新了列表再刷新了条目数据,界面就显示错乱。这种问题最难查,因为它不是崩溃,是显示不对。
第三座大山,是界面生命周期管理混乱。弹窗叠弹窗,打开顺序错了,后面的界面把前面的盖住;关闭时机不对,下层界面反而被销毁了;资源不释放,打开过十几个界面之后,内存里挂着一堆看不见的节点和贴图,帧率越来越低。
这三座大山在YIUI框架里,分别对应三套机制:代码生成代替手写、数据绑定代替手动刷新、框架统一管理界面生命周期。理解了这一点,你就知道YIUI不是要替代UGUI,而是在UGUI之上把开发模式重新规划了一遍。
1.2 YIUI给出的设计思路
YIUI的设计思路,我总结成三个词:约定优于配置、代码生成、数据驱动。
约定优于配置的意思是,UI预制体的制作有它的规范。不是说你随便拖几个控件就能生成,而是要在指定的节点层级下、挂指定的组件,工具才能识别。这看起来像约束,实际上省下了大量“这个按钮绑谁的点击事件”这种沟通成本。
代码生成是这套框架最直观的体验。你在Prefab里把控件放好、标记好绑定节点,运行YIUI的生成工具,它会自动扫描这个Prefab,生成一个C#脚本,里面包含了所有控件的引用和事件注册代码。你不需要再写Find,不需要再写GetComponent,生成出来的代码直接就能用。
数据驱动则是核心。YIUI把“数据”和“界面”解耦,你绑定的是数据对象,当数据对象变化时,界面自动刷新。开发者的注意力从“怎么刷新界面”转移到“怎么组织数据”,这个转变才是YIUI真正的价值所在。
我在实际使用中最直观的感受是:代码量明显少了。原来一个中等复杂度界面,初始化加刷新逻辑少说三百行,用YIUI之后,需要手写的业务代码大概只有原来的三分之一。剩下的都是模板和生成代码,出问题的概率小了很多。
2. 数据驱动的核心机制:绑定、刷新与更新的闭环
2.1 从预制体到代码:自动化生成是怎么设计的
YIUI的代码生成,是整个框架的入口。没有这套自动生成,后面的数据绑定都无从谈起。
先看它做了什么。你在Unity编辑器里创建一个UI面板Prefab,按规范挂好各节点组件,比如一个Button挂上YIUIButton标记,一个Text挂上YIUIText标记。然后点击YIUI的生成菜单,它会遍历这个Prefab的所有子节点,读取你挂的标记组件,然后生成一个partial类文件。
这个partial类包含两部分内容:控件的引用字段和初始化逻辑。控件引用字段就是类似public UnityEngine.UI.Button BtnLogin这样的成员,初始化逻辑则是把控件注册到事件系统或者数据绑定系统里。
关键点在于,生成代码和你写的业务代码在两个文件里,用partial关键字合成同一个类。生成的文件每次Prefab修改后都可以重新覆盖生成,你写的业务文件完全不受影响。这就解决了传统开发里“UI改一下,代码跟着改半天”的问题。
我只说一个大致的生成结果,不同版本细节会不一样,但思路是通用的:
// ===================================================== // 此文件由YIUI代码生成工具自动生成,请勿手动修改 // ===================================================== public partial class UILoginView { public UnityEngine.UI.Button BtnLogin; public UnityEngine.UI.InputField InputAccount; public UnityEngine.UI.InputField InputPassword; public TMPro.TextMeshProUGUI TxtTip; private void UILoginView_Init() { BtnLogin.onClick.AddListener(OnClickBtnLogin); } }你写的业务逻辑在另一个partial文件里,大概长这样:
public partial class UILoginView { protected void OnClickBtnLogin() { var account = InputAccount.text; var password = InputPassword.text; // 调用登录逻辑 } }这种设计的好处在于:生成代码是机械化的,它通过反射扫描Prefab得到结果,所以只要Prefab结构对了,生成代码一定是对的。而手写的Find路径,在Prefab调整后大概率是错的。这个差异,在界面频繁迭代的项目里有多重要,做过的人都明白。
2.2 数据绑定:界面怎么知道数据变了
代码生成解决的是“控件从哪来”,数据绑定解决的是“控件内容怎么更新”。
传统的做法是:你拿到服务器数据,写一个RefreshUI()之类的函数,里面把数据一个个赋给控件。比如TxtName.text = userInfo.Name; TxtLevel.text = userInfo.Level.ToString();这种。问题在于,这个Refresh函数要在多个时机被调用,一旦漏了某个时机,界面就不对。
YIUI的做法不同。它实现了类似MVVM的绑定机制:你定义一个数据对象,这个对象继承自框架的数据绑定基类,属性变化时能自动发出通知。然后你在界面初始化时,把控件和数据的属性绑定起来。之后,你只需要改数据,控件自动跟着变。
我拿一个最常见场景举例:角色信息面板。角色有等级、经验、金币三个数据。传统写法是每次登录、升级、吃道具、购买时都要调Refresh。而YIUI里,你只需要在初始化时绑定好:
// 类似于这样,在不同版本中API名称会有差异,核心思路是一对一的映射 BindProperty(m_PlayerData.Level, TxtLevel); BindProperty(m_PlayerData.Gold, TxtGold);之后不管升级还是购买,只要m_PlayerData.Level变了,TxtLevel的文本自动刷新。
这里的底层原理,其实就是一个观察者模式。数据对象内部维护一个属性变更事件,BindProperty时注册回调,给控件赋值;属性setter里检测到值变化,触发事件,回调里更新控件。
用人话说:传统方式是你隔三岔五去问对方“你变了没”,数据绑定是对方一变就主动打电话告诉你“我变了,请更新”。前者是轮询,后者是通知。通知的好处是,只要你保证数据源的准确性,界面就永远不会漏刷。
2.3 循环列表与动态内容的处理
列表是UI开发里最绕不开的组件。排行榜、背包、好友列表,全是列表。YIUI对列表的处理也走了数据驱动路线,它的虚拟列表(VirtualList)是基于数据源驱动的。
传统方式里,列表刷新大概是:清空子节点、重新实例化Item、给每个Item赋数据。遇到大量数据时还要考虑对象池,处理起来比较繁琐。
YIUI的列表,你只需要做三件事:设置Item模板、绑定数据源、告诉列表“数据变了”。内部的复用、回收、刷新,框架自己处理。
以背包列表为例,数据源是一个List<ItemData>,Item模板是一个绑定了ItemData字段的预制体。当你把新数据源丢给列表时,列表会自动计算出要显示多少个Item,已有的Item复用,不足的实例化,多余的回收,每一项自动绑定对应数据。
这一步省掉的代码量最惊人。我原来写一个带滑动、复用、点击事件的列表,少说两百行。YIUI里,模板做一次,绑定写一次,后面所有列表都一个套路。而且,因为Item的数据绑定是自动的,你不需要在Item的脚本里写Refresh(),只要数据源变了,Item界面跟着变。
这部分还有一个容易被忽略的价值:列表项和列表本身的数据隔离。Item只负责消费数据,不负责业务逻辑。这样改列表模板时,只要保持字段名一致,业务代码完全不用动。
3. 与ET框架的协作:UI在ECS架构中的位置
3.1 YIUI与ET的分工
ET框架在Unity圈子里有一定知名度,它采用了类似ECS的思想来组织服务器和客户端逻辑,特别适合多人实时对战这种有大量状态需要同步的项目。
YIUI和ET的关系,不是“YIUI依赖ET才能跑”,更准确的说法是:YIUI的调用环境放在ET的框架流程里,二者共用一个ECS实体体系。UI在ET里通常以组件的形式挂载在实体上,YIUI负责处理UI的显示、刷新、关闭,ET负责处理逻辑、网络、服务器数据。
分工很清楚:ET管数据对不对,YIUI管界面亮不亮。数据到达后,ET层更新数据实体,YIUI的数据绑定检测到变化,刷新界面。界面操作事件进来,YIUI把事件抛给ET的逻辑层,逻辑层处理后改数据,然后再回到界面。这是一个单向循环。
这种分工有一个很大的优点:UI层不会反向污染逻辑层。在传统项目里,经常出现逻辑代码里直接操作UI对象的情况,比如在战斗逻辑里写HpBar.value = hp。一开始觉得方便,等到界面需要重做时,逻辑代码里全是UI引用,拆都没法拆。YIUI把UI操作隔离在数据绑定层,逻辑层只操作数据,两边的耦合度降到了最低。
3.2 一次完整的界面打开流程
理解了分工,我们再走一遍真实流程,看看数据在ET和YIUI之间是怎么流动的。
比如玩家打开角色背包界面:
- 玩家点击主界面的“背包”按钮,YIUI捕获点击事件,通过ET的事件系统向逻辑层发出OpenBag消息。
- ET的背包逻辑收到消息后,向服务器请求背包数据,或者直接使用本地缓存的数据。
- ET层把背包物品数据更新到数据实体中,比如一个BagComponent组件上存了
List<ItemData>。 - 逻辑层通知YIUI可以打开背包界面了。
- YIUI打开UIBagView界面,初始化时从BagComponent读取数据源,绑定到背包列表和各项属性上。
- 数据源一旦绑定完成,列表立即刷新,界面展示出背包内容。
- 玩家在背包里点击一件装备,YIUI把点击事件抛给ET逻辑层,ET决定这个操作是穿戴还是出售,修改BagComponent中的数据。
- BagComponent的数据变化触发YIUI数据绑定的通知,界面自动更新,包括装备格子、角色属性面板、数量文本全部同步刷新。
整套流程中,YIUI不关心背包数据的来源和合法性,ET不关心背包界面怎么画。两边通过数据桥接,各干各的。
这种架构在多人同步场景里优势尤其明显。比如队友血量变化,服务器推送过来,ET更新血量数据,UI自动刷新,全程不需要写一行刷新界面的代码。而且因为数据更新和界面刷新是解耦的,服务器消息高频率推送时,界面不会因为频繁操作UI组件而卡顿。
3.3 多线程与Unity主线程的隔离
ET框架本身是支持多线程的,服务器逻辑可以在子线程里跑。但Unity的UI操作必须走主线程。YIUI天然承担了“线程隔离”这一层:逻辑层在子线程更新数据,YIUI通过主线程的同步机制把数据变化反映到界面上。
开发者不需要自己写线程切换代码,YIUI的绑定机制内部处理了主线程回调。这一点在我实际使用中很省心。原来用原生UGUI时,子线程一更改UI就会看到Unity的报错,提示只有主线程能调用UI API。有了YIUI之后,只要数据在子线程里改,主线程的UI绑定会自动更新,不需要额外分发。
不过有一个细节值得注意:虽然数据绑定是线程安全的,但数据源本身如果有复杂的集合操作,比如一边遍历一边修改,还是需要做好锁或者使用ET提供的同步机制。YIUI解决的是“谁能碰界面”,不是“数据线程安全”。
4. 实操:把第一个界面跑起来
4.1 环境准备与工程导入
纸上谈兵说了这么多,真正动手才是正经事。我用一个登录界面演示从建Prefab到界面弹出的完整流程。
环境上,YIUI的GitHub项目建议的Unity版本是2021.3或更新,ET部分也有对应的版本要求。导入YIUI的方式有两种:一种是直接把源码拷贝到Unity工程里,另一种是作为Package通过manifest引用。我推荐用Package方式,后续更新方便,也避免源码在Assets目录里误改。
导入后,检查Unity菜单栏,多了YIUI相关的工具菜单。菜单里主要有:UI代码生成、UI资源打包、界面层级配置等几个入口。
在开始做Prefab之前,需要一个ET运行起来的工程环境。直接用YIUI配套的Demo工程是最快的,里面已经配好了ET启动入口和YIUI初始化流程。我第一次接入时从空工程开始配,踩了不少坑,后来改用Demo工程再拆除多余部分,省了一半时间。
4.2 制作UI预制体
首先在Unity里创建一个Canvas,命名为UILoginView,保存到规定好的UI目录下。YIUI对界面预制体的路径有约定,一般放在Assets/GameScripts/Hotfix/UI/或其他指定目录,具体看版本。路径决定代码生成的命名空间和目录结构,这个要按工程约定来,不要乱放。
在Canvas上,YIUI会自动添加一个UIWindow管理组件。这个组件负责界面的显示层级、加载、销毁,不要手动删掉。
接下来制作内容节点:
- 创建一个Image作为背景底图,挂上YIUIImage标记。
- 创建两个InputField,分别输入账号和密码,挂上YIUIInputField标记。
- 创建一个Button作为登录按钮,挂上YIUIButton标记。
- 创建一个Text用于显示提示信息,挂上YIUIText标记。
每个挂载的标记组件都可以起一个别名,这个别名就是之后生成代码里的字段名。命名上一定要规范,我习惯用“前缀+含义”的格式,比如BtnLogin、InputAccount,生成出来的代码可读性会好很多。
4.3 生成代码与编写业务逻辑
Prefab制作完成,保存。打开YIUI菜单,点击代码生成。稍等一两秒,目录下多了几个文件,其中一个就是我们之前说的partial类。打开这个文件,你会看到控件字段已经全部生成好了,字段名和你起的别名一致。
这时候,不要在这个生成文件里写任何逻辑。你要做的是在同一个目录下手动创建一个新的partial类文件,比如UILoginView.Logic.cs,然后在这个文件里写业务方法。如果你用的是ET的Hotfix方式,这个文件的命名空间和类的继承方式可能有些特殊,照着Demo工程写就行。
启动入口上,打开界面用的是YIUI的WindowManager。大致用法是:
await YIUIMgr.Inst.OpenWindowAsync<UILoginView>();打开后,界面会自动调用你的初始化方法。在这个方法里绑定数据、注册点击事件、设置默认文本。对于一个登录界面,你只需要处理账号输入内容校验、登录按钮点击事件、以及登录成功后的跳转逻辑。
运行时,打开登录界面,你会发现所有控件的赋值、事件绑定都已经生效。全程没有写过一行Find,也没有手动给控件赋过初值。
4.4 界面打开与关闭的生命周期
YIUI界面生命周期里的几个方法,建议一开始就弄清楚:
- OnInit:界面初始化,只调用一次,适合创建数据对象和永久性绑定。
- OnOpen:每次打开时调用,适合刷新临时数据、注册监听。
- OnClose:每次关闭时调用,适合注销监听、清理临时资源。
- OnDestroy:界面销毁时调用,适合释放大资源。
这个生命周期和传统的UGUI面板脚本不一样的地方在于:OnOpen和OnClose是成对出现的,界面可能多次打开关闭,但OnInit和OnDestroy只出现一次。我在项目里见过不少新手把数据刷新写在OnInit里,结果第二次打开界面时数据还是旧的,就是没搞清这个区分。
5. 实战中躲不开的坑:从现象到根因的排查记录
5.1 改了Prefab生成代码没更新
有一次,我改了UI预制体,加了一个文本节点,也挂了标记组件,但运行起来后新节点死活不显示。检查生成代码,发现字段列表里没有这个新文本的引用。
排查链路是这样的:先确认标记组件确实挂上了,确认命名合法,确认保存了Prefab。到这里都没问题。重新运行生成工具,还是不生效。最后发现,预制体的文件路径变了,之前它在一个临时目录,我把它挪到了正式UI目录,但YIUI的代码生成缓存里还记着旧路径。删除生成缓存目录,重新生成,一切正常。
如果你也遇到生成结果和Prefab对不上,第一件事别急着改脚本,先看生成日志,里面会列出扫描到的节点列表。这个日志能直接告诉你,工具到底有没有识别到你这个节点,识别到了但字段名冲突,还是根本没扫到。
5.2 绑定不刷新的奇怪现象
另一个典型问题,是数据改了但界面不更新。我遇到的情况是,代码里直接给数据对象的字段赋值:
m_PlayerData.Level = 10;但界面上等级文本还是原来的值。第一反应以为是YIUI的绑定问题,查了一圈发现,数据对象虽然继承了绑定基类,但Level这个属性是用自动属性写的,没有在setter里调用通知方法。
在YIUI的绑定规则里,不是“数据变了就自动刷新”,而是“数据通过可绑定属性setter变更时才会发出通知”。如果你直接给一个普通字段赋值,框架根本感知不到。解决办法是把字段改成可绑定属性,或者通过框架提供的属性变更包装方法。
这个坑极易踩,因为它在代码里不报错,只是界面不刷新。排查时要先确认触发数据赋值的方式是不是走的可绑定入口,然后再看绑定关系本身。
5.3 界面关闭后资源释放与事件残留
YIUI框架自己管理界面生命周期,理论上关闭界面后资源会自动回收。但我发现,如果界面里注册了全局事件、或者开了协程、又或者用到了需要手动释放的对象,只关界面是不够的。
我在一个弹窗界面里用了ET的全局事件订阅,关闭弹窗后没有反订阅。结果每次弹窗打开,事件就多订阅一次,关闭再打开几次之后,同名事件被重复触发,弹窗逻辑跑了好几次。排查过程比较曲折,最后定位到是事件残留。
解决方案很清晰:在OnClose里把界面注册的所有全局事件、协程、引用全部解掉。YIUI虽然管理控件的生命周期,但它管不了你代码里额外注册的事件。这条建议,无论用不用YIUI,都值得遵守。
6. 持续迭代时,我的一些实际体会
YIUI这套框架给我的整体印象是,它把UI开发从“写代码去控制”变成了“做数据去驱动”。它没有魔法,该写的业务逻辑一句不少,但它把最冗余、最容易出错的部分交给了工具和机制,这就是它最大的价值。
如果你的项目正在用ET,或者你正打算转向数据驱动的UI开发方式,YIUI值得一试。初次接入会有一个适应期,主要是理解预制体制作规范和生命周期模型。一旦适应,你会发现界面开发的速度会有一个明显的提升。
最后分享一个我一直在用的习惯:给UI预制体组件命名时多花点心思,保持前后缀统一。生成的代码字段会直接反映你的命名,一个命名规范的UI类,读起来就像一份自文档化的数据结构。这比任何注释都管用,也是我在YIUI和ET这套组合上最想强调的实践。