6.1 UI 层级与多分辨率适配
第 5 章把场景流程搭好了,但 Canvas 下还是空的。UI 系统的第一件事不是画按钮,而是确定坐标系:设计稿画在多大的画布上、这块画布怎么映射到从 960×540 到 2532×1170 的各种真实屏幕、以及界面元素被摆到屏幕四角时靠什么保证不越界。
这三个问题在 Cocos Creator 3.8 里分别由 Canvas(渲染根与设计分辨率)、Widget(相对父容器定位)和 SafeArea(刘海与手势条)三个组件承担。这一节把《方块冲刺》的 UI 骨架定下来,后续所有界面都长在这套骨架上。
与站内
game/client/那批文章的分工:那些文章把「多分辨率适配」当作概念讲(拉伸、黑边、安全区),本节给的是 Cocos Creator 3.8 里逐项可照做的编辑器操作与运行时代码。
6.1.1 UI 渲染根:Canvas 与 UI 相机
在 3.8 里,所有 UI 节点必须挂在 Canvas 下,Canvas 是 UI 的渲染根。它背后自动带一个正交相机,负责把 UI 层渲染出来。节点树长这样(L3 工程文件,需 Cocos Creator 3.8 编辑器打开,本节不可命令行复现):
{
"__type__": "cc.Node",
"_name": "Canvas",
"_components": [ { "__id__": 3 }, { "__id__": 4 }, { "__id__": 5 } ],
"_children": [
{ "__id__": 6 },
{ "__id__": 10 },
{ "__id__": 14 }
]
}
需 Cocos Creator 3.8 编辑器打开,本节不可命令行复现。 打开 assets/scenes/main.scene,Canvas 的 _components 依次是 cc.UITransform、cc.Canvas、cc.Widget;子节点依次是 Camera(UI 相机)、HudLayer、PopupLayer。节点在 _children 里的顺序就是绘制顺序,越靠后越晚画、越盖在上面。所以层级约定是:
| 层 | 节点 | 内容 | 绘制顺序 |
|---|---|---|---|
| 背景层 | BgLayer | 滚动背景、远景 | 最先(最底) |
| 玩法层 | GameLayer | 角色、障碍、金币 | 中 |
| HUD 层 | HudLayer | 金币数、距离、暂停键 | 靠上 |
| 弹窗层 | PopupLayer | 暂停面板、结算、设置 | 最后(最顶) |
HUD 与弹窗分两层,是为了让弹窗盖住 HUD 时不用去改 HUD 的节点顺序;PopupLayer 天然在 HudLayer 之后绘制。
6.1.2 设计分辨率:960 × 540 与 SHOW_ALL
设计分辨率是「美术出图时假定的画布尺寸」,block-dash 锁定 960 × 540(16:9)。真实设备几乎没有正好 16:9 的屏幕,中间那层映射策略决定了「画面是留黑边还是被裁」。
编辑器里设置(需 Cocos Creator 3.8 编辑器):菜单 项目 → 项目设置 → 项目数据 → 设计分辨率,填 宽度 960、高度 540,同时勾选 Fit Width 与 Fit Height。同时勾选时引擎采用 ResolutionPolicy.SHOW_ALL——整张设计稿始终完整可见,多余空间留黑边,任何内容都不会被裁掉。
这一步的配置会落到工程文件里(L3 工程文件,需 Cocos Creator 3.8 编辑器打开,本节不可命令行复现):
{
"__type__": "cc.Settings",
"general": {
"designResolution": {
"width": 960,
"height": 540,
"fitWidth": true,
"fitHeight": true
}
}
}
需 Cocos Creator 3.8 编辑器打开,本节不可命令行复现。 在编辑器里,这一步对应的是项目设置面板上的两个勾选框,改完保存即写入上面的文件,无需手改 JSON。
为什么横版跑酷也选 SHOW_ALL 而不是更常见的「高度铺满」?因为《方块冲刺》的障碍是按设计坐标摆放的,SHOW_ALL 能保证设计坐标到屏幕坐标的映射是一个统一的正等比缩放,障碍永远不会因为屏幕更窄而被挤到可视区外。代价是非 16:9 屏幕上的黑边——这部分靠背景层用 NO_BORDER 铺满来遮。
| 策略 | 缩放 | 结果 | 适用 |
|---|---|---|---|
SHOW_ALL | 取较小缩放 | 设计稿完整可见,留黑边 | 本工程全局锁定 |
FIXED_HEIGHT | 高度对齐 | 高度铺满,宽度按比例 | 只想保证纵向构图时 |
FIXED_WIDTH | 宽度对齐 | 宽度铺满,高度按比例 | 只想保证横向构图时 |
NO_BORDER | 取较大缩放 | 铺满,边缘被裁 | 全屏背景图 |
运行时也应当显式再设一次,避免编辑器预览与真机不一致:
import { _decorator, Component, view, ResolutionPolicy } from 'cc';
const { ccclass } = _decorator;
@ccclass('ResolutionBootstrap')
export class ResolutionBootstrap extends Component {
start(): void {
view.setDesignResolutionSize(960, 540, ResolutionPolicy.SHOW_ALL);
const visible = view.getVisibleSize();
console.log('[ResolutionBootstrap] 可见尺寸', visible.width, visible.height);
console.log('[ResolutionBootstrap] 缩放', view.getScaleX(), view.getScaleY());
}
}
view.getVisibleSize() 返回的是设计单位下的可见尺寸:SHOW_ALL 下它至少有一边等于 960 或 540,另一边可能更大(黑边区)。做全屏遮罩、渐变背景时用它取尺寸,比硬编码 960×540 更稳。
6.1.3 用 Widget 钉住 HUD 四角
HUD 元素要「永远贴在屏幕某个角」,这正是 Widget 组件做的事。编辑器步骤(需 Cocos Creator 3.8 编辑器):
- 层级管理器右键
Canvas→ 创建 → UI → Node,重命名为HudLayer。 - 选中
HudLayer,属性面板 → 添加组件 → UI → Widget。 - 在
Widget组件里勾选 Top 与 Left,Top填16、Left填24,Align Mode选ON_WINDOW_RESIZE。 - 在
HudLayer下新建CoinLabel,加Label组件,String填0,Font Size填28。
操作后属性面板应显示:HudLayer 的 UITransform 里 Content Size 是 (100, 100)(占位值,由 Widget 在运行时重算),Widget 的 Top = 16、Left = 24、Align Mode = ON_WINDOW_RESIZE。
同样的效果也可以用代码设置,方便批量初始化:
import { _decorator, Component, Node, Widget } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('HudAnchors')
export class HudAnchors extends Component {
@property(Node) coinLabel: Node | null = null;
@property(Node) pauseButton: Node | null = null;
start(): void {
this.anchor(this.coinLabel, { top: 16, left: 24 });
this.anchor(this.pauseButton, { top: 16, right: 24 });
}
private anchor(
node: Node | null,
edge: { top?: number; left?: number; right?: number; bottom?: number },
): void {
if (node === null) return;
const w = node.getComponent(Widget) ?? node.addComponent(Widget);
if (edge.top !== undefined) { w.isAlignTop = true; w.top = edge.top; }
if (edge.bottom !== undefined) { w.isAlignBottom = true; w.bottom = edge.bottom; }
if (edge.left !== undefined) { w.isAlignLeft = true; w.left = edge.left; }
if (edge.right !== undefined) { w.isAlignRight = true; w.right = edge.right; }
w.alignMode = Widget.AlignMode.ON_WINDOW_RESIZE;
w.updateAlignment();
}
}
Widget.AlignMode 有三档,含义差别很大:ONCE 只在首次激活时对齐一次(脚本或动画之后可以接管位置),ON_WINDOW_RESIZE 在窗口尺寸变化时重算(推荐值),ALWAYS 每帧重算。不要给几百个 HUD 元素都用 ALWAYS——它会让每帧的布局计算变成帧预算里的固定开销。updateAlignment() 是手动触发一次对齐,在代码里改完 top/left 后立即生效。
6.1.4 安全区:刘海、圆角与手势条
现代手机的有效显示区不是矩形。刘海、灵动岛、圆角、底部手势条都会盖住内容。SafeArea 组件挂在 UI 根节点上后,运行时会自动把该节点收缩进安全区。
编辑器步骤(需 Cocos Creator 3.8 编辑器):
- 选中
Canvas下的HudLayer。 - 属性面板 → 添加组件 → UI → SafeArea。
- 确认
SafeArea的Symmetric不勾选(四边独立收缩)。
SafeArea 会把节点的 UITransform 尺寸与位置改到安全区范围内,因此它必须和 Widget 配合:SafeArea 负责「往内缩」,Widget 负责「缩完之后贴到哪条边」。顺序上,SafeArea 应在 Widget 之后生效,实际做法是把 SafeArea 挂在外层容器、Widget 挂在里层的具体元素上。运行时也可以动态检查:
import { _decorator, Component, view, screen, sys } from 'cc';
const { ccclass } = _decorator;
@ccclass('SafeAreaProbe')
export class SafeAreaProbe extends Component {
start(): void {
const frame = view.getFrameSize();
const pixel = view.getVisibleSizeInPixel();
console.log('[SafeAreaProbe] 帧尺寸', frame.width, frame.height);
console.log('[SafeAreaProbe] 像素可见', pixel.width, pixel.height);
console.log('[SafeAreaProbe] 设备像素比', screen.devicePixelRatio);
if (sys.isMobile && sys.platform === sys.Platform.WECHAT_GAME) {
// 小游戏端的安全区由平台接口给出,需在 6.1.5 的换算里折算进设计空间
}
}
}
安全区上报的是物理像素,而布局用的是设计像素,两者相差一个缩放系数。view.getScaleX() / view.getScaleY() 就是这个系数;把它们除掉,才能得到设计空间里的内边距。
6.1.5 分辨率无关的换算:纯逻辑,可以单测
「把物理像素的安全区内边距换算到设计空间」这件事与引擎无关,抽出来就能单测。这一段是本节唯一能在命令行复现的部分:
// src/core/ui-scale.ts —— 与引擎无关,可直接单测
export interface Insets {
readonly top: number;
readonly bottom: number;
readonly left: number;
readonly right: number;
}
export interface Rect {
x: number;
y: number;
width: number;
height: number;
}
/**
* 把物理像素的四边内边距除以缩放系数,回到设计空间。
* scale 为 0 或负数时按 1 处理,避免除零把整个布局算成 Infinity。
*/
export function insetsToDesign(insets: Insets, scale: number): Insets {
const s = scale > 0 ? scale : 1;
return {
top: insets.top / s,
bottom: insets.bottom / s,
left: insets.left / s,
right: insets.right / s,
};
}
/** 在设计空间里扣掉四边安全区,得到可用矩形 */
export function safeRect(design: Rect, insets: Insets, scale: number): Rect {
const d = insetsToDesign(insets, scale);
return {
x: design.x + d.left,
y: design.y + d.top,
width: design.width - d.left - d.right,
height: design.height - d.top - d.bottom,
};
}
配一个单测,把「除零要兜底」和「四边独立收缩」两条钉住:
// tests/ui-scale.test.ts
import { describe, it, expect } from 'vitest';
import { insetsToDesign, safeRect } from '../src/core/ui-scale';
describe('ui-scale', () => {
it('物理像素除以缩放回到设计空间', () => {
const d = insetsToDesign({ top: 88, bottom: 68, left: 0, right: 0 }, 2);
expect(d.top).toBe(44);
expect(d.bottom).toBe(34);
});
it('scale 为 0 时不产生 Infinity', () => {
const d = insetsToDesign({ top: 10, bottom: 10, left: 10, right: 10 }, 0);
expect(d.top).toBe(10);
});
it('安全区从设计矩形四边扣掉', () => {
const r = safeRect(
{ x: 0, y: 0, width: 960, height: 540 },
{ top: 40, bottom: 20, left: 0, right: 0 },
1,
);
expect(r.y).toBe(40);
expect(r.height).toBe(480);
});
});
6.1.6 工程收口
这一节做完,工程里多了这些确定的东西:
settings/v2/packages/project.json:设计分辨率960 × 540,fitWidth与fitHeight均为true(SHOW_ALL)。assets/scenes/main.scene:Canvas下按Camera / BgLayer / GameLayer / HudLayer / PopupLayer顺序组织。assets/scripts/ui/resolution-bootstrap.ts:启动时显式设定分辨率策略。assets/scripts/ui/hud-anchors.ts:HUD 元素四角定位。assets/scripts/core/ui-scale.ts:安全区换算,纯逻辑可单测。
多分辨率回归清单(每次改 UI 都要过一遍):在编辑器预览里依次切 960×540、1334×750、2532×1170、1170×2532 四档,检查 HUD 四角元素是否贴边不越界、弹窗是否居中、安全区在竖屏下是否生效。下一节把 PopupLayer 与 HudLayer 用起来,先做最耗性能的那类界面:长列表。
阅读导航:上一节:目录 · 下一节:6.2 列表与滚动容器 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。