近段时间在使用cocos2d-x开发2D手游,技术方案使用的是cocos2d-x+lua,因为游戏使用的是cocos2d-x 2.1.5版本,有些优化方案在最新版的cocos2d-x版本已经实现了。这篇文章主要是总结在使用cocos2d-x2.x版本+lua技术方案时遇到的问题和一些性能优化总结。
因为我使用的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消耗变高)。整个流程如下图所示:
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); |
c++代码
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()); } } } |
需要注意的坑:在小米3等手机使用CCRenderTexture出错,因为英伟达tegra4类型的gpu,glRenderbufferStorage分配渲染缓存时不支持GL_DEPTH24_STENCIL8,导致renderbuffer生成错误。解决方法: 通过glGetString(GL_EXTENSIONS)获取gpu信息。 如果不支持GL_DEPTH24_STENCIL8(24bits深度缓存+8bits的模板缓存,共享同一块renderbuffer) ,则分开创建depth和stencil缓存。 同时需要检测GL_OES_depth24,如果支持则使用GL_DEPTH_COMPONENT24_OES + GL_STENCIL_INDEX8,否则使用GL_DEPTH_COMPONENT16+GL_STENCIL_INDEX8。
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); } } |

(5) 切换场景时释放无用纹理
1
2
|
CCTextureCache:sharedTextureCache():removeUnusedTextures(); CCTextureCache:sharedTextureCache():dumpCachedTextureInfo(); |
(6)整理常驻内存UI资源
把主界面底部共用菜单等界面资源打包成一张纹理,常驻内存,省去加载和重新创建纹理的开销。
3. 遇到的坑
(1) 游戏使用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操作上。