Fivee

导航

游戏UI架构

引言

一个健壮、好用的架构,能让写业务的人专心写业务,不用把精力耗在那些零碎细节上。要解决的无非是解耦、复用、层级、性能、多端适配这几件事。

一、UI 层的三个特点

UI 层有三个绕不开的特点:

  1. 代码量最大(除引擎外)。
  2. 变更最频繁。策划改数值、UX 调布局、美术换资源,项目里任何一处变动,最后都会传导到 UI。
  3. 状态组合最多。加载、显示、隐藏、被遮挡、动画中、关闭、销毁,再加上各项目自己加的状态。

二、三个特点,三类故障

特点 典型故障 根因
代码量最大 一个 ListView 的 initialize、intoView、绑定/解绑事件,代码里几十份,区别只在里面的函数名 缺少可复用的基类,没有统一生命周期约定
变更最频繁 改 A 界面,把 B 界面改坏了 隐式引用,没做解耦
状态组合最多 界面二次打开数据不对(没刷新)、UI 显示不对(动画残留),而且偶发 生命周期职责没分清

前两个只是代码难看,肉眼能看出来;第三条是偶发 bug,复现不了也定位不到,头秃 +1。

三、架构要回答的唯一问题

UI 层架构其实只回答一件事:怎么把界面从业务逻辑里干净地剥出来,让两边各自演进而互不干扰。

四、边界:做啥,不做啥

有个简单的检验办法:把整个 UI 层删了,游戏还能不能跑。能跑,说明边界划对了;跑不了,说明同志还须努力了。

4.1 职责划分

职责 归属 说明
渲染 UI 把数据画成像素
交互响应 UI 玩家点了,做出反应
布局与适配 UI 位置、尺寸、安全区、分辨率
进出场过渡 UI 淡入、缩放、滑动
输入转译 UI 物理输入 → 语义动作
层级与栈管理 UI 谁盖着谁,谁能回退
资源引用持有 UI 界面用到的资源,用完要还
网络请求 NetService 服务器交互
数值计算 Model 伤害、得分、概率,数据变化
存档读写 Storage UI 只管什么时候触发保存
配置解析 Config → Model UI 只读解析好的配置对象
平台能力(登录/支付/分享/震动) Platform UI 只调接口,不碰 SDK

4.2 两条推论

UI 层可以有数据,但只能存副本。在 UI 层改数据既不允许也没意义,数据本身归 Model 层管。

UI 层往外给的是意图,不是结果。玩家点了 Login,UI 表达的就是 Intent_Player_Login,至于账号密码校验、SDK 调用、网络请求,都跟 UI 没关系。

框架的所有设计,都是为了这两条推论服务的。

五、分层模型与依赖方向

┌───────────────────────────────────────────────
│  业务层 Business                              
│  玩法规则 / 数值 / 网络                       
│  ── 通过框架路由打开界面                      
│  ── 通过事件接收 UI 意图                      
└───────────────────┬───────────────────────────
                    │ 依赖 ↓
┌───────────────────▼───────────────────────────
│  UI 层                                        
│  渲染 / 交互 / 布局 / 过渡 / 埋点 / 资源引用  
└───────────────────┬───────────────────────────
                    │ 依赖 ↓
┌───────────────────▼───────────────────────────
│  底座层 Infrastructure                        
│  EventMgr / TimerMgr / ObjectPool             
│  Log / Input / AssetMgr                       
└───────────────────────────────────────────────

5.1 依赖必须单向

跨边界只能走接口或者事件。底座层不能反过来依赖任何上层。

5.2 底座层能做到的三件事

  • 能提前加载、不依赖任何界面存在;
  • 内部怎么改,上层都无感,换实现不换接口;
  • 既不管上层怎么迭代,也不管是谁在用自己。

六、框架层的数据模型

框架和业务之间唯一的契约,是一份元数据,可能是配置来的,可能是自己组织的。

6.1 框架层看到的

UIMetadata {
    name        唯一标识
    layer       属于哪一层
    mode        同层策略:互斥 / 栈 / 单例
    bundle      资源包名
    assetPath   资产路径
    resPaths    依赖的额外资产
    cache       关闭后是否保留实例
}

框架不知道界面长什么样,也不管上面有几个按钮。它只干一件事:把这份描述变成一个能正确加载、挂载、显示、回收的界面。

框架不认识业务,认的就是这一份 meta。

6.2 业务层写的

SomePanel {
    name      : "SomeOne",
    layer     : eLayer.some,
    mode      : eStackMode.stack,
    bundle    : "main",
    assetPath : "xxx/xx/x",
    resPaths  : {"x", "y"},
    cache     : true
}

6.3 框架层接口

UIFacade.Open(panelKey)      // 业务只传一个标识,不传任何界面细节
UIFacade.Close(panelKey)

界面类是哪个、长什么样,框架一概不管。

七、核心抽象

整套架构真正要设计的就三个类型。

类型 A:策略容器 —— UIMeta、UIMode、DeviceTier

只是一些「配置项的集合」,本身不含行为。作用是把变化点从代码里抽出来变成配置。

类型 B:运行时管理者 —— UIManager、ResourceTracker、InputRouter、CoroutineRunner、UIPool、Store、ScreenAdapter

它们是有状态的常驻服务,全生命周期存在,负责管一批东西的生命周期、分配、回收。

类型 C:契约基类 —— UIPanel

它是业务代码的扩展点。业务继承它、覆写钩子。框架通过它把生命周期「注入」给业务。

八、一次完整流程

8.1 打开

1.  UIFacade.Open(panelKey)
        └─ 业务层只传标识,不传界面细节
2.  查配置表,取出该界面的 meta
3.  查重:是否已经打开?
        └─ 是 → show() + refresh(),结束
4.  检查同层排他策略
        └─ Exclusive → 先关闭同层所有旧界面
5.  插入过渡(黑屏)
6.  加载资产(prefab + resPaths)
        └─ 引用计数 +1
7.  挂载到对应的 Layer 节点
8.  show() → 触发 onShow(data)
9.  推入 stack / 登记缓存
10.  广播开启事件 → 埋点、日志、其他订阅者

第 4 步的位置很关键。排他检查必须在加载资产之前,先关旧的、释放旧资源,再加载新的。放到挂载之后,会出现新旧界面同时在内存里的瞬时峰值。

8.2 关闭(反向)

1.  UIFacade.Close(panelKey)
2.  从 stack 中弹出,移出缓存登记
3.  hide() → 触发 onHide()
4.  分支:
        ├─ cache 模式 → 保留实例,仅隐藏
        └─ 非 cache   → destroy()
                          ├─ 触发 onDestroy()
                          ├─ 自动解绑所有 bind 注册的订阅
                          └─ 释放 resPaths 声明的资源(引用计数 -1)
5.  广播关闭事件 → 埋点、日志、其他订阅者

所以架构到底是什么

我的答案是一句话:边界清晰、依赖单向、生命周期管理、通信、资源管理,这五样齐了,就是一个合格的脚手架。写业务的人只用管这个界面长什么样、玩家点了会怎样,剩下的交给其他人吧(Agent!)。

细节展开 游戏UI架构(二)。

posted on 2026-11-04 13:22  Fivee  阅读(0)  评论(0)    收藏  举报