Cocos Creator性能优化踩坑记录:包体、渲染、内存、CPU实战经验

Cocos Creator性能优化踩坑记录

做游戏时性能问题踩了不少坑,从包体、渲染、内存到CPU都有涉及,记录一下实际用过并验证有效的优化方法。

包体优化

资源占比

实际项目里资源占的内存远大于代码:

资源类型 内存占比 优化优先级
图片资源 60-80%
声音资源 15-25%
字体资源 5-10%
代码资源 5-15%

图片优化

重要警告:单张图片超过2048x2048会导致微信小程序平台无法进入界面。

图片格式选择

格式 特点 使用场景
JPG 有损压缩,体积小 照片、复杂图像
PNG 无损压缩,支持透明 UI、图标、需要透明的图像
WebP 体积比JPG/PNG小30% 现代浏览器和小游戏平台

体积对比:JPG < PNG < 未压缩

图片压缩工具

1
2
3
4
5
# 在线压缩
https://tinify.cn/ # TinyPNG中文版

# ImageMagick批量压缩
convert input.png -resize 50% output.png

纹理压缩

1
2
3
4
// 在资源管理器中设置
// 1. 选择图片资源
// 2. 属性检查器中勾选 useCompressTexture
// 3. 选择合适的压缩格式(ETC、ASTC等)

声音优化

格式 质量 体积 适用场景
MP3 背景音乐、音效
OGG 循环音乐
WAV 最高 短音效

推荐:背景音乐用OGG,短音效用MP3。

字体优化

Cocos支持三种字体:

字体类型 渲染方式 DrawCall影响 推荐度
系统字体 系统渲染 打断合批
动态字体(TTF) 运行时生成 打断合批
位图字体(BMFont) 预渲染图集 可合批

实际做法

  • 用BMFont预渲染常用字符
  • 把BMFont和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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
export class PoolManager {
private static instance: PoolManager;
private pools: Map<string, cc.NodePool> = new Map();

static getInstance(): PoolManager {
if (!this.instance) {
this.instance = new PoolManager();
}
return this.instance;
}

// 创建对象池
createPool(name: string, prefab: cc.Prefab, size: number = 10): void {
if (this.pools.has(name)) return;

const pool = new cc.NodePool();
for (let i = 0; i < size; i++) {
const node = cc.instantiate(prefab);
pool.put(node);
}
this.pools.set(name, pool);
}

// 获取对象
get(name: string, prefab?: cc.Prefab): cc.Node {
const pool = this.pools.get(name);
if (pool && pool.size() > 0) {
return pool.get();
}
if (prefab) {
return cc.instantiate(prefab);
}
return null;
}

// 回收对象
put(name: string, node: cc.Node): void {
const pool = this.pools.get(name);
if (pool) {
pool.put(node);
} else {
node.destroy();
}
}

// 清空对象池
clear(name: string): void {
const pool = this.pools.get(name);
if (pool) {
pool.clear();
this.pools.delete(name);
}
}
}

// 使用示例
const poolMgr = PoolManager.getInstance();

// 初始化对象池
poolMgr.createPool('enemy', this.enemyPrefab, 20);

// 获取敌人
const enemy = poolMgr.get('enemy', this.enemyPrefab);
if (enemy) {
enemy.parent = this.node;
enemy.setPosition(pos);
}

// 回收敌人
poolMgr.put('enemy', enemy);

对象池优势

  • 避免频繁的创建和销毁操作
  • 减少垃圾回收压力
  • 提高游戏帧率稳定性

预制体拆分与异步创建

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
// 不推荐:巨型预制体
// 推荐:拆分为多个子预制体

// 优化前:同步创建导致卡顿
for (let i = 0; i <= 100; i++) {
let node = cc.instantiate(this.prefab);
node.parent = this.node;
}

// 优化后:异步创建
private async createNodesAsync() {
for (let i = 0; i <= 100; i++) {
await new Promise<void>(resolve => {
setTimeout(() => {
let node = cc.instantiate(this.prefab);
node.parent = this.node;
resolve();
}, i * 100);
});
}
}

// 或用帧间创建
private createIndex: number = 0;
private createPerFrame: number = 5;

update(dt: number) {
if (this.createIndex < 100) {
for (let i = 0; i < this.createPerFrame; i++) {
if (this.createIndex >= 100) break;
let node = cc.instantiate(this.prefab);
node.parent = this.node;
this.createIndex++;
}
}
}

引擎代码裁剪

1
2
3
4
5
6
// 项目设置 -> 模块设置
// 可裁剪模块:
// - 物理引擎(如不使用)
// - 3D模块(如2D游戏)
// - 粒子系统(如不使用)
// - WebGL渲染器(如仅Canvas)

渲染优化

图集合批(DrawCall优化)

静态合图(Auto Atlas)

1
2
3
4
5
6
7
// 在资源管理器中创建Auto Atlas
// 1. 右键 -> 新建 -> Auto Atlas
// 2. 配置最大尺寸(建议2048x2048)
// 3. 添加需要合图的图片资源

// Auto Atlas会自动将碎图打包到一张大图
// 减少DrawCall数量

Auto Atlas参数配置

参数 说明 推荐值
Max Width 最大宽度 2048
Max Height 最大高度 2048
Padding 图片间距 2
Allow Rotation 允许旋转 勾选
Force Squared 强制正方形 不勾选

动态合图(Dynamic Atlas)

1
2
3
4
5
6
7
// 默认在小游戏和原生平台关闭
// 手动开启
cc.dynamicAtlasManager.enabled = true;

// 在精灵属性中开启
// 1. 选择Sprite组件
// 2. 勾选 Packable 属性

注意:动态合图会增加内存占用,不适合频繁变化的UI,大尺寸图片不适合。

减少active切换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 不推荐:频繁设置active
this.node.active = false;
this.node.active = true;

// 推荐:用位置或透明度替代
// 隐藏
this.node.setPosition(10000, 10000); // 移出屏幕
// 或
this.node.opacity = 0; // 透明

// 显示
this.node.setPosition(originalPos);
// 或
this.node.opacity = 255;

active切换代价

  • 触发节点树遍历
  • 重新计算渲染批次
  • 打断DrawCall合批

节点层级优化

1
2
3
4
5
6
7
8
9
10
11
12
推荐层级结构:
Canvas
├── Background (Layer 0)
├── GameLayer (Layer 1)
│ ├── Sprites (同图集,可合批)
│ ├── Characters (同图集,可合批)
│ └── Effects (单独层级)
├── UILayer (Layer 2)
│ ├── UI_Panel1
│ └── UI_Panel2
└── TopLayer (Layer 3)
└── Popups

合批原则

  1. 相同材质的节点连续排列
  2. 相同图集的Sprite连续渲染
  3. 避免穿插不同图集的节点

内存优化

场景自动释放

1
2
3
4
5
6
7
8
9
10
// 在资源管理器中设置
// 1. 选择场景文件
// 2. 属性检查器中勾选"自动释放资源"

// 代码中设置
cc.director.loadScene('SceneName', () => {
// 场景加载完成
}, () => {
// 场景切换完成,旧场景资源已释放
});

建议

  • 二级场景勾选自动释放
  • 常驻场景(如MainScene)不勾选
  • Loading场景不勾选

动态资源管理

resources目录规范

1
2
3
4
5
6
7
8
assets/
├── resources/ # 动态加载资源
│ ├── textures/
│ ├── audios/
│ └── prefabs/
├── scripts/ # 脚本(非动态加载)
├── scenes/ # 场景(非动态加载)
└── settings/ # 配置(自动打包)

只有resources文件夹中的资源可以动态加载。

手动资源释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 加载资源
cc.resources.load('textures/enemy', cc.SpriteFrame, (err, spriteFrame) => {
if (err) {
console.error(err);
return;
}
this.sprite.spriteFrame = spriteFrame;
});

// 释放单个资源
cc.resources.release('textures/enemy', cc.SpriteFrame);

// 释放所有引用
cc.assetManager.releaseAsset(spriteFrame);

// 批量释放
cc.assetManager.releaseAll();

TileMap资源特殊处理

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
export class TileMapManager {
private _assetMap = new Map<string, AssetInfo>();

loadTiledMap(tiledMap: cc.TiledMap, path: string, callback?: Function) {
if (null !== tiledMap.tmxAsset) {
tiledMap.tmxAsset = null;
}

cc.resources.load(path, cc.TiledMapAsset, (err, assets: cc.TiledMapAsset) => {
if (!!err) {
this.releaseTiledMap(assets);
return;
}
this.addAssetRef(assets, 'system_name');
tiledMap.tmxAsset = assets;
if (!!callback) callback();
});
}

private releaseTiledMap(tiledMap: any) {
if (!tiledMap) return;

const uuid: string = tiledMap._uuid;
const deps = cc.assetManager.dependUtil.getDepsRecursively(uuid);

for (let i = 0; i < deps.length; i++) {
const uuid = deps[i];
const asset = cc.assetManager.assets.get(uuid);

// 跳过动态图集中的纹理
if (asset instanceof cc.Texture2D) {
if (asset.packable) continue;
}

if (!asset || !asset.decRef) continue;
asset.decRef();
}
tiledMap.decRef();
}
}

纹理内存计算

纹理格式 每像素占用 1024x1024内存占用
RGBA8888 4 bytes 4 MB
RGBA4444 2 bytes 2 MB
RGB888 3 bytes 3 MB
RGB565 2 bytes 2 MB
ETC2 压缩 ~0.5-1 MB
ASTC 压缩 ~0.25-0.5 MB

CPU优化

循环优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 不推荐:for...in遍历数组
for (let i in arr) {
// 操作
}

// 推荐:用for循环
for (let i = 0, len = arr.length; i < len; i++) {
// 操作
}

// 更优:缓存数组长度
let len = arr.length;
while (len--) {
// 操作
}

// 最优:用数组方法
arr.forEach((item) => {
// 操作
});

数组操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 不推荐:频繁push
let arr = [];
for (let i = 0; i < 1000; i++) {
arr.push(i);
}

// 推荐:直接赋值
let arr = [];
for (let i = 0; i < 1000; i++) {
arr[i] = i; // 比push更快
}

// 预分配长度
let arr = new Array(1000);
for (let i = 0; i < 1000; i++) {
arr[i] = i;
}

避免使用arguments

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 不推荐
function foo() {
const args = arguments;
// 使用args
}

// 推荐:用rest参数
function foo(...args: any[]) {
// 使用args
}

// 或明确参数
function foo(a: number, b: number, c: number) {
// 使用a, b, c
}

局部变量缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 不推荐:重复访问全局变量
function update() {
for (let i = 0; i < 100; i++) {
cc.director.getScene().getChildByName('Player');
}
}

// 推荐:缓存到局部变量
function update() {
const player = cc.director.getScene().getChildByName('Player');
for (let i = 0; i < 100; i++) {
// 使用player
}
}

帧率控制

渲染帧率

1
2
3
4
5
6
7
// 设置游戏帧率(只能设置15, 30, 60)
cc.game.setFrameRate(30); // 降低到30fps

// 适合:
// - 不需要高帧率的UI界面
// - 后台运行时的降级处理
// - 低端设备适配

物理帧率

1
2
3
4
5
6
7
8
const system = cc.PhysicsSystem2D.instance;

// 设置物理步长(默认1/60)
system.fixedTimeStep = 1 / 30;

// 设置迭代次数
system.velocityIterations = 8;
system.positionIterations = 8;

注意:降低物理帧率可能增加刚体穿透概率。

Update管理优化

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
46
47
48
49
50
51
52
// 不推荐:所有节点都有update
// 推荐:集中管理更新

export class UpdateManager {
private static instance: UpdateManager;
private updaters: Set<IUpdatable> = new Set();

static getInstance(): UpdateManager {
if (!this.instance) {
this.instance = new UpdateManager();
}
return this.instance;
}

register(updater: IUpdatable): void {
this.updaters.add(updater);
}

unregister(updater: IUpdatable): void {
this.updaters.delete(updater);
}

update(dt: number): void {
this.updaters.forEach(updater => {
if (updater.enabled) {
updater.onUpdate(dt);
}
});
}
}

interface IUpdatable {
enabled: boolean;
onUpdate(dt: number): void;
}

// 使用示例
export class Enemy extends cc.Component implements IUpdatable {
enabled: boolean = true;

onEnable() {
UpdateManager.getInstance().register(this);
}

onDisable() {
UpdateManager.getInstance().unregister(this);
}

onUpdate(dt: number) {
// 更新逻辑
}
}

事件监听优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 不推荐:在update中检测输入
update(dt: number) {
if (cc.eventManager.isListenerEnabled) {
// 处理输入
}
}

// 推荐:用事件监听
onEnable() {
cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this);
cc.systemEvent.on(cc.SystemEvent.EventType.TOUCH_START, this.onTouchStart, this);
}

onDisable() {
cc.systemEvent.off(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this);
cc.systemEvent.off(cc.SystemEvent.EventType.TOUCH_START, this.onTouchStart, this);
}

加载优化

并发下载配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 设置最大并发连接数
cc.assetManager.downloader.maxConcurrency = 10;

// 设置每帧最大请求数
cc.assetManager.downloader.maxRequestsPerFrame = 6;

// 原生平台配置(在main.js中)
cc.assetManager.init({
bundleVers: settings.bundleVers,
remoteBundles: settings.remoteBundles,
server: settings.server,
jsbDownloaderMaxTasks: 32,
jsbDownloaderTimeout: 60
});

分帧加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
async loadSceneAsync(sceneName: string) {
// 显示Loading界面
this.showLoading();

// 预加载资源
await this.preloadResources();

// 分批实例化
await this.instantiateInBatches();

// 切换场景
cc.director.loadScene(sceneName);
}

private async preloadResources(): Promise<void> {
return new Promise((resolve) => {
cc.resources.preloadDir('scenes/level1', (err) => {
if (err) console.error(err);
resolve();
});
});
}

发布前检查清单

  • 图片是否已压缩和纹理压缩
  • 是否使用对象池管理预制体
  • Auto Atlas是否已配置
  • 场景自动释放是否已设置
  • update是否已优化
  • 帧率设置是否合理
  • 资源释放是否完整

这些都是实际项目中用过的优化方法,效果比较明显。对象池和图集合批对帧率提升最明显,内存优化主要注意资源释放。

参考