个人游戏技术选型案例:UI 架构为什么从面板脚本改成事件驱动

一个个人经营游戏在面板脚本直连、MVC、MVVM 和事件驱动 UI 之间做技术选型的案例,详细讨论库存、状态刷新、弹窗、测试和维护成本。

写在前面:UI 代码常常比玩法代码更早变乱

个人游戏早期 UI 很少。

一个主菜单。
一个暂停面板。
一个背包。
几个按钮。

开发者通常直接在面板脚本里写逻辑:

点击按钮就扣钱。
打开面板就读库存。
滑条变化就改设置。

这样做很快。

但当游戏进入中期,UI 会迅速增长:

  • 背包
  • 商店
  • 任务
  • 设置
  • 弹窗
  • 提示
  • 角色状态
  • 建造菜单
  • 结算面板
  • 教程遮罩

每个面板都和游戏状态互相读写,很快会变成一团。

唐以宁做过一款小型茶馆经营游戏。
玩家安排茶叶、茶具、客人座位和每日菜单,还要处理熟客故事。

项目早期 UI 直接写在面板脚本里。
到第三个月,最常见的 bug 不是经营系统,而是 UI 刷新错、按钮状态错、弹窗叠错。

她后来把 UI 从“面板直连玩法系统”改成“状态模型加事件驱动刷新”。

一、旧 UI 方案的问题

旧方案里,每个面板都自己查数据。

库存面板查 InventoryManager
商店面板查 ShopManager
任务面板查 QuestManager
每日结算面板又查库存、金币、客人满意度。

按钮点击时,面板直接调用业务方法。

问题慢慢出现:

  • 库存变化后,有些面板刷新,有些不刷新
  • 商店购买后金币显示延迟
  • 弹窗关闭时恢复到错误面板
  • 教程遮罩挡住了不能点的按钮
  • 同一份格式化逻辑复制在多个面板
  • 面板打开顺序不同,状态结果不同

最麻烦的是依赖方向。

UI 知道太多业务系统。
业务系统有时又反过来调用 UI 显示提示。

这让测试和修改都很困难。

二、为什么没有直接上 MVVM 框架

唐以宁考虑过完整 MVVM。

数据绑定、ViewModel、命令、响应式属性,看起来很适合 UI。

但她没有选择完整框架。

原因是:

  • 项目使用的引擎 UI 不完全适合通用 MVVM
  • 学习和接入成本高
  • 当前 UI 复杂度中等
  • 她不想让所有面板都重写
  • 个人项目不需要大型企业式绑定系统

她也不想继续面板脚本直连。

最后选择折中:
业务状态集中,UI 通过事件订阅刷新,面板只发命令,不直接改底层数据。

这比完整 MVVM 轻,比旧方案清楚。

三、状态模型先收拢

她先定义几个 UI 需要读取的状态模型:

  • PlayerWallet
  • InventoryState
  • ShopState
  • QuestLogState
  • CustomerState
  • DaySummaryState

这些不是新的业务系统。
它们是业务系统对 UI 暴露的只读视图。

例如库存内部可能有批次、品质、过期时间。
但 UI 列表只需要:

  • 物品 ID
  • 显示数量
  • 品质标签
  • 是否可出售
  • 是否有新物品标记

UI 不再直接读库存内部结构。

这让 UI 不会因为业务内部重构而到处修改。

四、命令代替直接调用

按钮点击也改了。

旧代码:

ShopManager.Buy(itemId)

新代码:

GameCommands.RequestBuyItem(itemId)

命令层会处理:

  • 钱是否足够
  • 库存是否满
  • 当前是否允许购买
  • 成功后更新状态
  • 失败后发出提示事件

UI 只表达玩家意图。

它不负责判断买不买得起。
也不负责自己扣钱。

这样避免了多个面板重复业务判断。

五、事件驱动刷新

唐以宁建立了几个事件:

  • InventoryChanged
  • WalletChanged
  • QuestUpdated
  • ShopStockChanged
  • DayAdvanced
  • SettingsChanged

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 不直接散乱读写业务系统,让状态变化有统一路径。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页