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/
convert input.png -resize 50% output.png
|
纹理压缩:
声音优化
| 格式 |
质量 |
体积 |
适用场景 |
| 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++; } } }
|
引擎代码裁剪
渲染优化
图集合批(DrawCall优化)
静态合图(Auto Atlas):
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;
|
注意:动态合图会增加内存占用,不适合频繁变化的UI,大尺寸图片不适合。
减少active切换
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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
|
合批原则:
- 相同材质的节点连续排列
- 相同图集的Sprite连续渲染
- 避免穿插不同图集的节点
内存优化
场景自动释放
1 2 3 4 5 6 7 8 9 10
|
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 (let i in arr) { }
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
| let arr = []; for (let i = 0; i < 1000; i++) { arr.push(i); }
let arr = []; for (let i = 0; i < 1000; i++) { arr[i] = i; }
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; }
function foo(...args: any[]) { }
function foo(a: number, b: number, c: number) { }
|
局部变量缓存
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++) { } }
|
帧率控制
渲染帧率:
1 2 3 4 5 6 7
| cc.game.setFrameRate(30);
|
物理帧率:
1 2 3 4 5 6 7 8
| const system = cc.PhysicsSystem2D.instance;
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
|
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(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;
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) { 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(); }); }); }
|
发布前检查清单
这些都是实际项目中用过的优化方法,效果比较明显。对象池和图集合批对帧率提升最明显,内存优化主要注意资源释放。
参考