Cocos Creator 屏幕适配踩坑记:winSize、visibleSize 到底该用哪个?

背景

做移动端游戏最头疼的就是屏幕适配。不同设备尺寸千奇百怪,iPhone SE 是 640×1136,iPhone X 是 1125×2436,还有各种安卓奇葩比例。Cocos Creator 提供了好几个分辨率相关的 API,但到底该用哪个?我刚开始也是一头雾水,写这篇文章记录下我的理解和踩过的坑。

核心概念

坐标系统对比

1
2
3
4
5
6
7
8
9
10
11
12
┌─────────────────────────────────────────┐
│ 屏幕坐标系 │
│ (0,0) ───────────────→ X+ │
│ │ │
│ │ ┌─────────────┐ │
│ │ │ │ │
│ │ │ 游戏内容 │ │
│ │ │ │ │
│ │ └─────────────┘ │
│ ↓ │
│ Y+ │
└─────────────────────────────────────────┘

关键术语

术语 说明 对应 API
设计分辨率 开发时设定的参考分辨率 getDesignResolutionSize
屏幕分辨率 设备的实际像素分辨率 getFrameSize
可见区域 实际能看到的游戏内容区域 getVisibleSize
视图大小 以设计像素为单位的画布大小 getWinSize

核心 API 对比

1. getDesignResolutionSize - 获取设计分辨率

1
2
3
4
getDesignResolutionSize: function () {
// 返回设计分辨率
return cc.view.getDesignResolutionSize();
}

说明:

  • 这是在 Cocos Creator 编辑器 Canvas 中设置的尺寸
  • 开发时所有 UI 布局都基于此分辨率
  • 无论实际设备分辨率如何,逻辑坐标都基于此

使用场景:

1
2
3
4
5
6
7
8
// 获取设计分辨率进行比例计算
let designSize = cc.view.getDesignResolutionSize();
let scaleX = designSize.width / 720; // 假设基准是 720
let scaleY = designSize.height / 1280; // 假设基准是 1280

// 根据设计分辨率调整 UI
this.node.scaleX = scaleX;
this.node.scaleY = scaleY;

2. getFrameSize - 获取屏幕分辨率

1
2
3
4
getFrameSize: function () {
// 返回硬件分辨率
return cc.view.getFrameSize();
}

说明:

  • 这是设备屏幕的实际像素尺寸
  • 手机屏幕或浏览器窗口的实际大小
  • 包含状态栏、导航栏等系统 UI 占据的空间

使用场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 根据设备分辨率调整画质
let frameSize = cc.view.getFrameSize();
let totalPixels = frameSize.width * frameSize.height;

if (totalPixels > 2073600) { // 1080p
// 使用高清资源
cc.loader.loadResDir("hd/textures", (err, assets) => {
// 加载高清纹理
});
} else {
// 使用标准资源
cc.loader.loadResDir("textures", (err, assets) => {
// 加载标准纹理
});
}

3. getVisibleSize - 获取可见区域

1
2
3
4
getVisibleSize: function () {
// 获取运行场景的可见大小
return cc.view.getVisibleSize();
}

说明:

  • 返回实际可见的游戏区域大小
  • 考虑到安全区域(刘海屏、圆角屏)
  • 这是玩家实际能看到的游戏内容范围

使用场景:

1
2
3
4
5
6
7
// 将 UI 元素放置在安全区域内
let visibleSize = cc.view.getVisibleSize();
let visibleOrigin = cc.view.getVisibleOrigin();

// 放置顶部按钮在安全区域内
this.topButton.x = visibleOrigin.x + visibleSize.width / 2;
this.topButton.y = visibleOrigin.y + visibleSize.height - 50;

4. getWinSizeInPixels - 获取像素级视图大小

1
2
3
4
getWinSizeInPixels: function () {
// 获取运行场景的可见大小(像素)
return cc.director.getWinSizeInPixels();
}

说明:

  • 返回以像素为单位的可见区域大小
  • 与 getVisibleSize 类似,但以像素为单位

5. getWinSize - 获取视图大小

1
2
3
4
getWinSize: function () {
// 获取视图的大小,以设计像素为单位
return cc.winSize;
}

说明:

  • 这是 Cocos Creator 中最常用的属性
  • 返回以设计像素为单位的画布大小
  • 等同于 cc.director.getWinSize()

分辨率适配策略

固定高度适配

1
2
3
4
5
6
// 在 main.js 或项目设置中
cc.view.setDesignResolutionSize(
720, // 设计宽度
1280, // 设计高度
cc.ResolutionPolicy.FIXED_HEIGHT // 固定高度
);

效果:

  • 高度始终填满屏幕
  • 宽度可能裁剪或留白
  • 适合竖屏游戏

固定宽度适配

1
2
3
4
5
cc.view.setDesignResolutionSize(
720,
1280,
cc.ResolutionPolicy.FIXED_WIDTH // 固定宽度
);

效果:

  • 宽度始终填满屏幕
  • 高度可能裁剪或留白
  • 适合横屏游戏

适配策略对比

策略 特点 适用场景
FIXED_WIDTH 宽度固定,高度自适应 横屏游戏
FIXED_HEIGHT 高度固定,宽度自适应 竖屏游戏
SHOW_ALL 显示全部,可能有黑边 需要完整显示内容
NO_BORDER 无黑边,可能裁剪 允许裁剪边缘内容
EXACT_FIT 拉伸填充,可能变形 不推荐

实际应用示例

1. 背景图片全屏适配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const { ccclass, property } = cc._decorator;

@ccclass
export default class BackgroundAdapter extends cc.Component {

@property(cc.Sprite)
bgSprite: cc.Sprite = null;

onLoad() {
this.adaptBackground();
}

adaptBackground() {
// 获取设计分辨率
let designSize = cc.view.getDesignResolutionSize();

// 获取实际屏幕大小
let frameSize = cc.view.getFrameSize();

// 计算宽高比
let designRatio = designSize.width / designSize.height;
let frameRatio = frameSize.width / frameSize.height;

// 根据比例决定缩放方式
if (frameRatio > designRatio) {
// 屏幕更宽,按宽度缩放
let scale = frameSize.width / designSize.width;
this.bgSprite.node.scale = scale * (designSize.height / this.bgSprite.node.height);
} else {
// 屏幕更高,按高度缩放
let scale = frameSize.height / designSize.height;
this.bgSprite.node.scale = scale * (designSize.width / this.bgSprite.node.width);
}
}
}

2. UI 安全区域适配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const { ccclass, property } = cc._decorator;

@ccclass
export default class SafeAreaAdapter extends cc.Component {

@property(cc.Node)
topBar: cc.Node = null;

@property(cc.Node)
bottomBar: cc.Node = null;

@property(cc.Node)
content: cc.Node = null;

onLoad() {
this.adaptToSafeArea();
}

adaptToSafeArea() {
// 获取可见区域
let visibleSize = cc.view.getVisibleSize();
let visibleOrigin = cc.view.getVisibleOrigin();

// 获取设计分辨率
let designSize = cc.view.getDesignResolutionSize();

// 计算顶部安全区域
if (this.topBar) {
let topMargin = designSize.height - visibleSize.height - visibleOrigin.y;
this.topBar.y = designSize.height / 2 - topMargin - this.topBar.height / 2;
}

// 计算底部安全区域
if (this.bottomBar) {
let bottomMargin = visibleOrigin.y;
this.bottomBar.y = -designSize.height / 2 + bottomMargin + this.bottomBar.height / 2;
}

// 调整内容区域
if (this.content) {
let safeHeight = visibleSize.height - this.topBar.height - this.bottomBar.height;
this.content.height = safeHeight;
}
}
}

3. 横竖屏适配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const { ccclass, property } = cc._decorator;

@ccclass
export default class OrientationAdapter extends cc.Component {

@property(cc.Node)
portraitLayout: cc.Node = null;

@property(cc.Node)
landscapeLayout: cc.Node = null;

onLoad() {
this.checkOrientation();
cc.view.setResizeCallback(() => {
this.checkOrientation();
});
}

checkOrientation() {
let frameSize = cc.view.getFrameSize();
let isLandscape = frameSize.width > frameSize.height;

if (this.portraitLayout) {
this.portraitLayout.active = !isLandscape;
}

if (this.landscapeLayout) {
this.landscapeLayout.active = isLandscape;
}

// 调整 Canvas 适配策略
if (isLandscape) {
cc.view.setDesignResolutionSize(
1280, 720,
cc.ResolutionPolicy.FIXED_HEIGHT
);
} else {
cc.view.setDesignResolutionSize(
720, 1280,
cc.ResolutionPolicy.FIXED_WIDTH
);
}
}
}

4. 字体大小适配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const { ccclass, property } = cc._decorator;

@ccclass
export default class FontSizeAdapter extends cc.Component {

@property
baseFontSize: number = 24;

@property(cc.Label)
labels: cc.Label[] = [];

onLoad() {
this.adaptFontSize();
}

adaptFontSize() {
// 获取设计分辨率和实际分辨率
let designSize = cc.view.getDesignResolutionSize();
let frameSize = cc.view.getFrameSize();

// 计算缩放比例(取宽高中较小的比例)
let scaleW = frameSize.width / designSize.width;
let scaleH = frameSize.height / designSize.height;
let scale = Math.min(scaleW, scaleH);

// 限制缩放范围
scale = Math.max(0.8, Math.min(scale, 1.5));

// 应用字体大小
let adaptedSize = Math.floor(this.baseFontSize * scale);

this.labels.forEach(label => {
label.fontSize = adaptedSize;
});
}
}

常见问题

1. 节点坐标与设计分辨率不匹配

1
2
3
4
5
6
7
// 问题:直接使用屏幕坐标设置节点位置
this.node.x = 100; // 这 100 是设计像素

// 解决:理解坐标系统
// 所有节点操作都基于设计分辨率
let designSize = cc.view.getDesignResolutionSize();
this.node.x = designSize.width / 2; // 屏幕中心

2. 刘海屏适配

1
2
3
4
5
6
7
8
9
// 在 iPhone X 等刘海屏设备上
let visibleSize = cc.view.getVisibleSize();
let visibleOrigin = cc.view.getVisibleOrigin();

// 检查是否有刘海(顶部安全区域大于 0)
if (visibleOrigin.y > 0) {
// 有刘海,调整 UI
this.topBar.y -= visibleOrigin.y;
}

3. Canvas 尺寸与 WinSize 不一致

1
2
3
4
5
6
7
8
9
10
// Canvas 组件的 designResolution
let canvas = this.getComponent(cc.Canvas);
let designRes = canvas.designResolution;

// 实际 winSize
let winSize = cc.winSize;

// 两者可能不同,取决于适配策略
cc.log("Design:", designRes.width, designRes.height);
cc.log("WinSize:", winSize.width, winSize.height);

小结

API 返回值含义 使用建议
getDesignResolutionSize 设计分辨率 UI 布局基准
getFrameSize 实际屏幕像素 设备判断、资源选择
getVisibleSize 可见区域大小 安全区域计算
getWinSize 设计像素画布 最常用的参考尺寸

核心原则:只关注设计分辨率和视图大小,让 Cocos Creator 的适配系统处理与设备像素的转换。

这些是我项目中常用的方法,基本能覆盖各种适配场景。