CocosCreator性能优化整理
谈到性能优化,就是优化CPU这部分,因为GPU的性能是过剩的。
CPU这部分做什么呢,分为2个部分,1个是开发者自身的游戏逻辑,另1个是游戏的渲染逻辑。游戏逻辑这部分就因项目而异了,比如合理的数据结构、查询函数、碰撞检测等等。渲染逻辑这一块就是要合理的使用引擎的渲染功能,避免多余的操作,尽量性能使用最大化。学习别人的一些经验整理在这里供以后翻阅。
降低DrawCall
DC是一个不得不谈的话题,至于DC是什么,为什么要降低它,这里就不做赘述了。
降低DC就是合并渲染批次,在CCC的渲染流中有一个_batch变量,它负责处理节点的渲染信息。它会检测当前节点和当前渲染流的材质以及相机剔除掩码是否完全一致,如果一致则传入当前信息模型,如果不一致则传入新的信息模型中。
1、静态合图
这是一个很常用的办法,就是将多个散图拼成一个大图。这样连续多个渲染节点都是使用的这个纹理且其它参数一致的情况下,就会合并渲染批次。同时还能带来的好处就是降低IO,本地则是读取本地文件,网上的话则是减少网络请求次数。
获取合图的途径可通过CCC的自动图集来产生,或者通过TexturePacker手动生成。
2、动态合图
这是CCC提供的一种机制,就是它会在内存中动态生成一张纹理,数量限制是5,大小是2048*2048。满足条件的散图就可以参与动态合图。
开启动态合图。
参与条件可通过cc.dynamicAtlasManager.maxFrameSize设置,默认是512。
参与动态合图会改变原纹理的uv坐标,所以自定义shader的中如果有涉及uv的部分可能会无效。
3、Label
使用Label通常会打断合批。因为默认label是根据输入内容动态生成的一个纹理去显示的。
一个方案是使用位图字体BMFont。它就相当于是静态合图。
如果使用系统字或者TTF字体,则可以使用CCC提供的Label的缓存模式,BITMAP模式是将整个Label生成一个散图,并尝试并入动态合图中。CHAR模式则是将每个字符生成散图,并入一个专属label的动态合图中。
4、列表优化
列表优化最常见的就是虚拟列表,这个就略过不提了。
但是当Item是由各种Sprite、Label穿插的时候,合批会被打断,导致DC是单个ItemDC * 显示的Item数量。
在论坛搜索看到了2篇文章,我觉得挺好的,他们的原理都是希望在content这个列表下遍历Item的时候可以按照指定的顺序去遍历,尽量不打断合批。
方案一:
https://forum.cocos.org/t/ui/80026
Item预制体对应一个config文件描述每个node的优先级。
在render-flow.js中添加一个渲染函数针对列表中的content。
先遍历得到content下的所有子节点,再按照config中设置的优先级进行排序。再执行渲染。
ps:个人觉得不好,每次content渲染都要进行一次排序,config这种硬编码容易出错,导致比较严重的问题。
方案二:
https://forum.cocos.org/t/topic/133526
在list节点上去遍历content的子节点,遍历每个Item的时候,每个node都有对应的层级,根据层级存入对应的数组,最后将二维数组按顺序合并为一维数组。
在render-flow中渲染的时候,让渲染流去遍历这个一维数组。这样就可以保证总DC = 单个Item的DC。
ps:我觉得这个方案比方案一的config要好。还是不错的。
5、图集数组
当一个页面使用多张图集的时候,你希望它们能够进行合批,就可以使用此方案。
自定义材质允许传入多张纹理,建议不超过8。以及参数texture_idx,根据下标在片段着色器中使用对应的纹理。
自定义Assembler和顶点格式,添加一个浮点数用于存储texture_idx。
自定义Sprite,更新assembler 和 传入texture_idx。
写了个简单Demo如下,
参考自:https://forum.cocos.org/t/topic/136349
https://forum.cocos.org/t/topic/95087
// CustomAssembler.ts //@ts-ignore let gfx = cc.gfx; let vfmtCustom = new gfx.VertexFormat([ { name: gfx.ATTR_POSITION, type: gfx.ATTR_TYPE_FLOAT32, num: 2 }, { name: gfx.ATTR_UV0, type: gfx.ATTR_TYPE_FLOAT32, num: 2 }, { name: gfx.ATTR_COLOR, type: gfx.ATTR_TYPE_UINT8, num: 4, normalize: true }, { name: "a_texture_idx", type: gfx.ATTR_TYPE_FLOAT32, num: 1 } ]); @ccclass export default class CustomAssembler extends GTSimpleSpriteAssembler2D { verticesCount = 4; indicesCount = 6; uvOffset = 2; colorOffset: 4; idxOffset = 5; floatsPerVert = 6; // 此部分略去 都是参考自 MovingBGAssembler.ts ... updateTextureIdx(textureIdx: number) { let verts = this._renderData.vDatas[0]; let idx: number = this.idxOffset; for(let i = 0; i < this.verticesCount; ++i) { verts[idx + i * this.floatsPerVert] = textureIdx; } } }
@ccclass export default class CustomSprite extends cc.Sprite { _resetAssembler() { this.setVertsDirty(); let assembler = this._assembler = new CustomAssembler(); assembler.init(this); // this.spriteFrame = cc.find("Canvas").getComponent(Main).spf; assembler.updateTextureIdx(this.getTextureIdx()); //@ts-ignore this._updateColor(); // may be no need } getTextureIdx() { let texture = this.spriteFrame.getTexture(); let textures = cc.find("Canvas").getComponent(Main).atlas; //@ts-ignore this.getMaterial(0).updateHash(99999); let textureidx = textures.indexOf(texture); return textureidx; } }
6、共享节点
大致看了下,比如一个节点挂载了Sprite组件,显示了一张图片。
这个时候将该节点作为共享节点,生成很多子节点,这些子节点并不挂载Sprite组件。
修改渲染流,当渲染到子节点时,就使用共享节点的渲染数据,对其中的顶点坐标进行一个偏移,使其移动到目标位置。
这样在某种适合使用大量共享节点的场景下,就可以省掉很多的渲染组件和渲染流程。
内存优化
我认为内存的优化主要就是2个方向。
1.资源有合理的管理,需要的资源保留,不需要的资源释放。
2.尽量减少资源的体积,比如没透明像素的图片使用jpg格式,能九宫格的图片使用九宫格。采用一些压缩方式压缩资源的体积。
1.资源的管理
在理想的情况下,内存中应该只有需要的资源。所以对资源的加载和释放要有进行合理的控制。
在CCC中,加载的资源会再cc.assetManager.assets中缓存。每个资源都会有缓存计数。
如果是静态引用的资源,如场景、预制体引用的资源,引用计数会加1,并在销毁之后减1。这由引擎那边进行管理。
如果是通过Bundle动态加载的资源,则需程序员自己进行管理。最初加载进来,引用计数为0。如果节点销毁会进行引用计数检查,为0,引擎会帮你释放掉。但如果你从节点树上移除了,不进行销毁。它就会一直在内存中。比较合理的方案是动态加载的资源,自己手动进行引用计数的管理,当为0的时候,调用assetManger的相关方法进行释放。
2.资源的压缩
优先使用jpg,和九宫格。
采用压缩纹理,安卓端使用ETC,IOS端使用PVR。
这一块还是不甚了解,这是看一些文章大致说的意思是压缩纹理读入内存就可以给GPU使用,且占用内存更小,是推荐使用的。