cocos2d-x手游性能优化总结
发表于2016-07-13
近段时间在使用cocos2d-x开发2D手游,技术方案使用的是cocos2d-x+lua,因为游戏使用的是cocos2d-x 2.1.5版本,有些优化方案在最新版的cocos2d-x版本已经实现了。这篇文章主要是总结在使用cocos2d-x2.x版本+lua技术方案时遇到的问题和一些性能优化总结。
1、 渲染优化
a、合并渲染-自动批处理
因为我使用的cocos2d-x2.1.5版本,引擎并没有实现自动批渲染,底层也改动了不少代码,不太方便直接升级到3.0版本,这里参考了cocos2d-x3.0版本,修改了引擎代码加入自动批渲染。很大程度的降低了draw call批次,比使用CCSpriteBatchNode操控起来更加便捷,CCSpriteBatchNode需要把子节点在逻辑层面手动添加到父节点,逻辑控制比较复杂,特别当精灵节点分布不连续的时候,自动批渲染可以解决这个问题。通过改写精灵类的draw函数,把精灵信息添加到渲染队列,不进行绘制。等到一帧结束或者遇到非精灵节点绘制时才批量绘制渲染队列中的精灵。在绘制之前会对渲染队列中的精灵按逻辑层设置的z值进行排序,一般把相同材质ID(根据textureID+blendFunc+shaderProgram生成材质ID)的精灵设置相同或邻近的z值,根据精灵的模型视图矩阵信息得到世界坐标,在节点发生变换的时候更新模型视图矩阵信息( CCNode::transform()函数)。
1 | kmGLGetMatrix(KM_GL_MODELVIEW, &_modelViewTransform); |
这样就可以把非连续性分布但同材质ID的精灵批量进行绘制,一个批次可以绘制多个精灵,大大减少了draw call批次,降低cpu的消耗(帧率提高时也会导致cpu消耗变高)。整个流程如下图所示:
b、使用FBO减少渲染对象
使用FBO中的渲染到纹理技术,减少渲染对象,从而减少draw call。主要用于在一些尺寸不大但是包含多个元素的CCTableViewCell中,流程是将CCTableViewCell中所有子元素添加到一个精灵节点A中,然后创建CCRenderTexture对象。把精灵节点A绘制到在CCRenderTexture中绑定的纹理上,更新使用FBO的精灵的纹理,然后删除释放大精灵节点和所有子节点。下面是主要代码片段:
lua代码
1 2 3 4 5 6 7 | local item = CCBReaderLoad( "equip_item.ccbi" , proxy, true, itemOwnerName); item = tolua.cast(item, "CCSprite" ); --添加要绘制的元素到item中 ....... local nWidth, nHeight = 150 , 100 ; item:renderToTex(nWidth, nHeight); cell:addChild(item, 0 , i); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | void CCSprite::renderToTex( int nWidth, int nHeight) { CCRenderTexture * pRenderTex = CCRenderTexture::create(nWidth, nHeight, kCCTexture2DPixelFormat_RGBA8888, GL_DEPTH24_STENCIL8); if (pRenderTex) { pRenderTex->beginWithClear(0, 0, 0, 0, 0, 0); this ->visit(); pRenderTex->end(); this ->removeAllChildrenWithCleanup( true ); CCSprite * pRenderSprite = pRenderTex->getSprite(); if (pRenderSprite) { initWithTexture(pRenderSprite->getTexture()); } } } |
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 | GLenum _error_code = glGetError(); if ( _error_code != GL_NO_ERROR) { const char * extString = ( const char *)glGetString(GL_EXTENSIONS); if ( strstr (extString, "GL_OES_depth24" ) != 0) { glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24_OES, (GLsizei)powW, (GLsizei)powH); } else { glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, (GLsizei)powW, (GLsizei)powH); } glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_uDepthRenderBufffer); if (uDepthStencilFormat == GL_DEPTH24_STENCIL8) { glGenRenderbuffers(1, &m_uStencilRenderBufffer); glBindRenderbuffer(GL_RENDERBUFFER, m_uStencilRenderBufffer); glRenderbufferStorage(GL_RENDERBUFFER, GL_STENCIL_INDEX8, (GLsizei)powW, (GLsizei)powH); _error_code = glGetError(); if (_error_code == GL_NO_ERROR) { glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, m_uStencilRenderBufffer); } } #endif } else { // if depth format is the one with stencil part, bind same render buffer as stencil attachment glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_uDepthRenderBufffer); if (uDepthStencilFormat == GL_DEPTH24_STENCIL8) { glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, m_uDepthRenderBufffer); } } |
c、 动态设置帧率
UI界面帧率设置最高30帧
战斗等特效和动画比较多的界面帧率设置最高60帧
d、自动裁剪
在一些大地图中,显示在屏幕外的纹理元素不添加到自动批渲染队里中。
e、减少特效粒子数和帧数
把一些特效和帧动画改成骨骼动画,减少帧数量。
跟美术和策划沟通,在效果可接受的情况下减少特效粒子数。
2、内存优化
由于我们游戏前期这方面做的工作不多,后期随着游戏内容的丰富,内存消耗越来越大,导致在一些低端机出现卡顿,切换到后台,进程经常被系统kill。优化方案如下:
a、纹理压缩
压缩后的纹理直接使用gpu解码,不需要cpu解码展开再传输至gpu,极大减少内存,减少cpu计算,加快纹理加载速度。
格式:
android使用etc1,工具:Mali Texture Compression Tool
ios使用pvrtc4,工具:TexturePacker
内存占用:
ETC1占用内存跟图片尺寸有关,每个像素占0.5个字节,占用磁盘空间会比png8大。
PVRTC每个像素0.25~0.5个字节 (pvrtc2和pvrtc4)
其他常见纹理格式(2~4个字节)如图:
1 2 | CCTextureCache:sharedTextureCache():removeUnusedTextures(); CCTextureCache:sharedTextureCache():dumpCachedTextureInfo(); |
整理常驻内存UI资源
把主界面底部共用菜单等界面资源打包成一张纹理,常驻内存,省去加载和重新创建纹理的开销。
3、 遇到的坑
a、游戏使用luajit后,crash率上升了不少。
原因:因为luajit不支持c++方式编译。在lua的c代码中,如果遇到错误会使用luaL_error来报错。通常lua是以c的方式来编译的,luaL_error最终会调用longjump来实现函数远程跳转,而这种调转不遵循c++关于stack unwinding的规范,cocos2dx是使用c++语言编写的。这个会直接导致在调了luaL_error之后对象不会被析构(一般会在对象的析构函数中做一些处理,如重置状态和释放资源等)。在对这个没有被析构的对象上循环执行一些图像操作时可能会导致crash。分析游戏crash上报日志发现,多数crash发生在MTK芯片设备上的/system/vendor/lib/egl/libGLESv2_mtk.so模块中和一些opengl操作上。
解决方案:使用官方版的luac替代luajit,并使用c++的方式编译。