游戏 UI 体系
Category
client / Web 前端。节点定位:游戏 UI 与应用 UI 共享底座但目标函数不同——HUD 读状态渲染、菜单流走界面栈、列表控内存、适配留安全区。
Definition
游戏 UI 与应用 UI 共享一套技术底座(布局、文本、输入),但目标函数完全不同:应用 UI 追求信息效率与一致性,游戏 UI 追求沉浸与手感——它要么隐形(HUD 只在需要时被读到),要么本身就是玩法(背包整理、装备对比)。本页按 HUD、菜单流、列表、适配四块展开 Web 与小游戏侧的游戏 UI 体系。
Problem
应用 UI 的直觉直接搬到游戏 UI 会出事:HUD 被游戏逻辑直接操纵(该读状态渲染)、列表一次全建(内存与卡顿)、返回键没接界面栈(直接退出游戏)、异形屏没留安全区(按键被刘海挡住)。游戏 UI 要么隐形、要么本身就是玩法,实现路径因此不同。
HUD:分层的仪表盘
HUD(Heads-Up Display)是常驻在玩法画面上的信息层:血条、小地图、技能冷却、伤害飘字。组织 HUD 的第一原则是分层:
- 常驻层:血条、资源条、核心操作按钮——位置固定、密度极低,玩家应能在半秒内读到
- 情境层:目标提示、交互提示、任务追踪——只在相关时出现,消失要干脆
- 反馈层:伤害数字、状态图标、成就提示——短生命周期,注意堆叠上限与合批(同类飘字合并是一条成熟经验)
HUD 的实现要点是「读状态渲染」:每帧根据游戏状态刷新显示,而不是由游戏逻辑直接操纵 UI 控件——这正是客户端分层在 UI 上的投影。血条每帧从 100 缩到 0 没问题,但「谁扣了血、为什么扣」不该出现在 UI 代码里。
菜单流与界面栈
菜单(主界面、设置、背包、商店)的组织方式几乎总是一个界面栈:打开设置入栈,返回出栈,底层界面保持但不刷新。栈式管理要处理四件事:加载与释放(入栈时加载资源、出栈时按引用计数释放,对照第 8 章 03 页)、输入焦点(弹窗期间底层按钮不可点,遮罩不只是视觉)、返回键(Web 的浏览器返回键与小游戏平台的返回手势要接入界面栈而不是退出游戏)、以及状态恢复(从战斗切回主界面,界面要反映最新状态而不是缓存快照)。
网格列表与大量条目
背包、好友列表、排行榜是性能事故高发区:条目几百上千个,每个都是带图标、文字、按钮的复合节点。方案是虚拟滚动——只为可视区域(外加少量缓冲行)创建真实节点,滚动时复用池化节点更新内容。配套三件事:固定行高(或可预估高度,动态高度要做测量缓存)、异步图标加载(滚得快时只加载最终停留处的图)、以及滚动位置恢复(退出重进回到原位)。判断一个列表实现是否合格:滚一万条不卡、不闪烁、不重复。
多分辨率与安全区
Web 端多分辨率、小游戏端多机型,UI 适配靠同一套工具:锚点定位(九宫格四角加中心)、九宫格切图(圆角与描边不随拉伸变形)、安全区(刘海与手势条区域内不放可交互元素,平台提供安全区查询,两侧各留够边距)、字号层级(标题、正文、辅助信息三级字号,跨分辨率只缩布局不缩字号下限)。世界坐标转屏幕坐标是 UI 跟随 3D/2D 对象(血条浮点、飘字)的基础运算,2D 正交相机的最小模型:
// 假设:相机居中,zoom 为「屏幕像素 / 世界单位」,屏幕 y 轴向下而世界 y 轴向上
function worldToScreen(wx, wy, camera, viewport) {
return {
sx: (wx - camera.x) * camera.zoom + viewport.width / 2,
sy: viewport.height / 2 - (wy - camera.y) * camera.zoom,
}
}实际引擎里用引擎的相机换算接口,但自己写一次最能理解「相机一变、全部要重算」——相机移动的同帧,跟随型 UI 必须同步刷新。
常见误区
- 逻辑直改 UI:游戏代码里到处
label.string = ...,一旦要做回放、重连、多语言,全 UI 得手工追状态——UI 应从状态推导 - 每帧全量刷新:无变化的界面也在重建布局,CPU 白烧——有变化才标记脏、才重排
- 安全区只处理一边:只留了刘海侧,手势条侧按钮被吃
- 列表不做池化:背包一开五百个节点,低端机直接卡死
- 字体当普通图片用:多语言一加、字号一改,字体图集重打——字体方案要在立项时定(静态图集还是运行时渲染)
- 忽略输入延迟:按钮响应超过 100 毫秒就有「不跟手」的体感,UI 交互比画面帧率更敏感
实战案例:一次「背包一开就卡死,多语言要 9 人天」的 UI 体系排查
案例为教学示例(非摘自真实项目),数字经过取整简化。它把本页「网格列表与大量条目」「HUD:分层的仪表盘」「菜单流与界面栈」用到深水区:单个界面对应四类失守——这是列表池化与读状态渲染的归因排查,也是「UI 从状态推导」的完整展开。
背景与现象:背包 640 条目一开就卡:低端机开包 2.8 秒,中端机掉到 18 帧;接入多语言时全 UI 手工追状态 9 人天;玩家反馈「误点返回直接退出游戏」占会话 11%,刘海与手势条区域误触投诉 86 条。
第一步:现象与口径——把四类失守各自入账
列表口径:条目 640 个全量建节点,每个是带图标、文字、按钮的复合节点;图标同步解码,低端机内存直接打满。
状态口径:label.string = ... 这类逻辑直改 UI 散布 214 处——多语言、回放、重连每个新场景都要手工追一遍状态。
刷新口径:主界面每帧全量重建布局,无变化也在重排,CPU 占比 31%。
栈口径:浏览器返回键与平台返回手势没接界面栈,误退游戏占会话 11%;安全区只留了刘海侧,手势条侧误触 86 条。
第二步:分层归因——按本页常见误区清单比对
UI 事故归因 占比
列表不做池化,640 个节点一次全建 46%
逻辑直改 UI 与每帧全量刷新 31%
返回键没接界面栈、安全区只留一边 23%逐项比对:640 个节点全建正中「列表不做池化:背包一开五百个节点,低端机直接卡死」;214 处直改与 31% CPU 正中「逻辑直改 UI」与「每帧全量刷新」两条;11% 误退与 86 条误触正中「安全区只处理一边:只留了刘海侧,手势条侧按钮被吃」。
第三步:根因——UI 被当成应用 UI 做,状态与表现的单向数据流没建立
游戏 UI 要么隐形、要么本身就是玩法,实现路径与应用 UI 不同。项目的 UI 由逻辑直接操纵控件,而不是每帧读状态渲染:一旦要做回放、重连、多语言,全 UI 得手工追状态——本页一句话收尾:UI 应从状态推导。214 处直改就是「读状态渲染」没落地的连锁。列表侧则是把「几百上千条目」当成应用侧的小列表处理,虚拟滚动与池化从未进设计。
第四步:处置与回填——四层一起收口
- 列表池化:虚拟滚动只为可视区域(外加少量缓冲行)创建真实节点,图标异步加载、固定行高;640 个节点 → 14 个池化节点,开包 2.8 → 0.2 秒。
- 读状态渲染:HUD 与菜单改为每帧从状态推导,214 处直改 → 0;多语言接入 9 人天 → 1 人天。
- 脏标记刷新:无变化的界面不重建布局,主界面 CPU 31% → 9%。
- 栈与安全区:返回键接入界面栈而非退出游戏,安全区两侧各留够边距;误退 11% → 1.2%,误触投诉 86 → 0 条。
复核账:背包节点 640 → 14 个,开包 2.8 → 0.2 秒;直改 UI 214 → 0 处;主界面 CPU 31% → 9%;误退 11% → 1.2%。
回填清单:
| 回填项 | 动作 | 回填处 |
|---|---|---|
| 虚拟滚动 | 可视区外加少量缓冲行建节点,图标异步、行高可预估 | 网格列表与大量条目 |
| 读状态渲染 | HUD 每帧从状态推导,禁止逻辑直改控件 | HUD:分层的仪表盘 |
| 脏标记刷新 | 有变化才标记脏才重排,无变化不重建 | 常见误区 |
| 栈与安全区 | 返回键接入界面栈,安全区两侧各留边距 | 菜单流与界面栈 |
案例的三个教训
- 列表不做池化就是事故——虚拟滚动只为可视区域建节点,滚一万条不卡、不闪烁、不重复。
- UI 从状态推导——逻辑直改控件省一时,多语言、回放、重连每次都回来找你。
- 安全区两侧都要留——只留刘海侧,手势条侧的按钮照样被吃。
Used By
HUD 与战斗界面实现(读状态渲染、分层刷新);菜单/背包/商店的界面栈设计;大量条目列表(虚拟列表、对象池);多分辨率与安全区适配。
Related
关系链:HUD → 界面栈 → 列表与内存 → 分辨率与安全区。上游:客户端内部分层(web/06,UI 是表现层)。下游:client 树交互与输入。
Reference
本页为工程经验归纳;各平台安全区与返回手势 API 以官方文档为准;页内实战案例为教学示例(数字取整简化)。