写在前面:UI 代码常常比玩法代码更早变乱
个人游戏早期 UI 很少。
一个主菜单。
一个暂停面板。
一个背包。
几个按钮。
开发者通常直接在面板脚本里写逻辑:
点击按钮就扣钱。
打开面板就读库存。
滑条变化就改设置。
这样做很快。
但当游戏进入中期,UI 会迅速增长:
- 背包
- 商店
- 任务
- 设置
- 弹窗
- 提示
- 角色状态
- 建造菜单
- 结算面板
- 教程遮罩
每个面板都和游戏状态互相读写,很快会变成一团。
唐以宁做过一款小型茶馆经营游戏。
玩家安排茶叶、茶具、客人座位和每日菜单,还要处理熟客故事。
项目早期 UI 直接写在面板脚本里。
到第三个月,最常见的 bug 不是经营系统,而是 UI 刷新错、按钮状态错、弹窗叠错。
她后来把 UI 从“面板直连玩法系统”改成“状态模型加事件驱动刷新”。
一、旧 UI 方案的问题
旧方案里,每个面板都自己查数据。
库存面板查 InventoryManager。
商店面板查 ShopManager。
任务面板查 QuestManager。
每日结算面板又查库存、金币、客人满意度。
按钮点击时,面板直接调用业务方法。
问题慢慢出现:
- 库存变化后,有些面板刷新,有些不刷新
- 商店购买后金币显示延迟
- 弹窗关闭时恢复到错误面板
- 教程遮罩挡住了不能点的按钮
- 同一份格式化逻辑复制在多个面板
- 面板打开顺序不同,状态结果不同
最麻烦的是依赖方向。
UI 知道太多业务系统。
业务系统有时又反过来调用 UI 显示提示。
这让测试和修改都很困难。
二、为什么没有直接上 MVVM 框架
唐以宁考虑过完整 MVVM。
数据绑定、ViewModel、命令、响应式属性,看起来很适合 UI。
但她没有选择完整框架。
原因是:
- 项目使用的引擎 UI 不完全适合通用 MVVM
- 学习和接入成本高
- 当前 UI 复杂度中等
- 她不想让所有面板都重写
- 个人项目不需要大型企业式绑定系统
她也不想继续面板脚本直连。
最后选择折中:
业务状态集中,UI 通过事件订阅刷新,面板只发命令,不直接改底层数据。
这比完整 MVVM 轻,比旧方案清楚。
三、状态模型先收拢
她先定义几个 UI 需要读取的状态模型:
PlayerWalletInventoryStateShopStateQuestLogStateCustomerStateDaySummaryState
这些不是新的业务系统。
它们是业务系统对 UI 暴露的只读视图。
例如库存内部可能有批次、品质、过期时间。
但 UI 列表只需要:
- 物品 ID
- 显示数量
- 品质标签
- 是否可出售
- 是否有新物品标记
UI 不再直接读库存内部结构。
这让 UI 不会因为业务内部重构而到处修改。
四、命令代替直接调用
按钮点击也改了。
旧代码:
ShopManager.Buy(itemId)
新代码:
GameCommands.RequestBuyItem(itemId)
命令层会处理:
- 钱是否足够
- 库存是否满
- 当前是否允许购买
- 成功后更新状态
- 失败后发出提示事件
UI 只表达玩家意图。
它不负责判断买不买得起。
也不负责自己扣钱。
这样避免了多个面板重复业务判断。
五、事件驱动刷新
唐以宁建立了几个事件:
InventoryChangedWalletChangedQuestUpdatedShopStockChangedDayAdvancedSettingsChanged
UI 面板订阅自己关心的事件。
库存变化后:
- 背包刷新
- 商店按钮刷新
- 制茶面板刷新
- 顶部资源条刷新
但它们都从状态模型读取数据,不互相调用。
这解决了“一个面板改完忘记通知另一个面板”的问题。
事件不是越多越好。
唐以宁避免为每个小字段建事件,而是按业务区域发事件。
六、弹窗和面板栈单独管理
旧方案里,每个面板自己打开其他面板。
商店打开确认弹窗。
确认弹窗又可能打开错误提示。
教程也会临时挡住按钮。
层级很乱。
她做了一个 UIScreenManager:
- 管理主面板
- 管理弹窗栈
- 管理遮罩
- 管理返回键
- 管理输入焦点
面板不再直接创建弹窗。
它发送请求:
ShowConfirmPurchase(itemId)
由 UI 管理器决定显示哪个弹窗、是否压栈、关闭后回到哪里。
这让返回键和手柄操作稳定很多。
七、格式化逻辑集中
茶馆游戏里很多文本需要格式化:
- 金币
- 茶叶品质
- 客人满意度
- 时间
- 折扣
- 任务进度
旧方案里,每个面板自己拼字符串。
后来她集中到 DisplayFormatters。
例如:
FormatMoney(value)FormatTeaQuality(quality)FormatQuestProgress(done, total)FormatCustomerMood(mood)
这样本地化也更容易。
UI 不再到处拼“3/5 已完成”这种文本。
八、测试变得更可行
新方案还有一个好处:状态模型可以单独测试。
例如:
- 金币变化后
PlayerWallet是否更新 - 库存满时购买命令是否失败
- 任务完成后
QuestLogState是否显示完成 - 日期推进后商店库存是否刷新
她没有给每个 UI 动画写测试。
但核心状态和命令可以测试。
这已经能拦住很多 bug。
九、最终取舍
唐以宁的最终方案是:
- 不引入完整 MVVM 框架
- 建立 UI 只读状态模型
- UI 通过命令表达玩家意图
- 业务变化发事件
- 面板订阅事件刷新
- 弹窗和面板栈集中管理
- 格式化逻辑集中
它不是最纯粹的架构。
但适合一个人维护。
最重要的是,UI 不再到处直接改业务状态。
结语:UI 架构的目标是状态一致
唐以宁后来发现,UI bug 少了很多。
不是因为她写代码更小心。
而是因为数据流更清楚:
玩家点击 UI。
UI 发命令。
业务系统修改状态。
状态发事件。
UI 重新读取显示。
个人游戏技术选型里,UI 架构很容易被忽略。
但玩家每天看到的都是 UI。
如果 UI 状态经常错,游戏会显得不可靠。
不一定要上大型框架。
但至少要让 UI 不直接散乱读写业务系统,让状态变化有统一路径。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。