近段时间在使用cocos2d-x开发2D手游,技术方案使用的是cocos2d-x+lua,因为游戏使用的是cocos2d-x 2.1.5版本,有些优化方案在最新版的cocos2d-x版本已经实现了。这篇文章主要是总结在使用cocos2d-x2.x版本+lua技术方案时遇到的问题和一些性能优化总结。

1. 渲染优化
(1). 合并渲染-自动批处理

因为我使用的cocos2d-x2.1.5版本,引擎并没有实现自动批渲染,底层也改动了不少代码,不太方便直接升级到3.0版本,这里参考了cocos2d-x3.0版本,修改了引擎代码加入自动批渲染。很大程度的降低了draw call批次,比使用CCSpriteBatchNode操控起来更加便捷,CCSpriteBatchNode需要把子节点在逻辑层面手动添加到父节点,逻辑控制比较复杂,特别当精灵节点分布不连续的时候,自动批渲染可以解决这个问题。通过改写精灵类的draw函数,把精灵信息添加到渲染队列,不进行绘制。等到一帧结束或者遇到非精灵节点绘制时才批量绘制渲染队列中的精灵。在绘制之前会对渲染队列中的精灵按逻辑层设置的z值进行排序,一般把相同材质ID(根据textureID+blendFunc+shaderProgram生成材质ID)的精灵设置相同或邻近的z值,根据精灵的模型视图矩阵信息得到世界坐标,在节点发生变换的时候更新模型视图矩阵信息( CCNode::transform()函数)。

kmGLGetMatrix(KM_GL_MODELVIEW, &_modelViewTransform);

这样就可以把非连续性分布但同材质ID的精灵批量进行绘制,一个批次可以绘制多个精灵,大大减少了draw call批次,降低cpu的消耗(帧率提高时也会导致cpu消耗变高)。整个流程如下图所示:

clipboard

(2). 使用FBO减少渲染对象 
使用FBO中的渲染到纹理技术,减少渲染对象,从而减少draw call。主要用于在一些尺寸不大但是包含多个元素的CCTableViewCell中,流程是将CCTableViewCell中所有子元素添加到一个精灵节点A中,然后创建CCRenderTexture对象。把精灵节点A绘制到在CCRenderTexture中绑定的纹理上,更新使用FBO的精灵的纹理,然后删除释放大精灵节点和所有子节点。下面是主要代码片段:
lua代码
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++代码

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。

   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);
    }
   }

 

(3). 动态设置帧率 
UI界面帧率设置最高30帧
战斗等特效和动画比较多的界面帧率设置最高60帧
(4). 自动裁剪 
在一些大地图中,显示在屏幕外的纹理元素不添加到自动批渲染队里中。
(5). 减少特效粒子数和帧数
把一些特效和帧动画改成骨骼动画,减少帧数量。
跟美术和策划沟通,在效果可接受的情况下减少特效粒子数。
2. 内存优化
由于我们游戏前期这方面做的工作不多,后期随着游戏内容的丰富,内存消耗越来越大,导致在一些低端机出现卡顿,切换到后台,进程经常被系统kill。优化方案如下:
(1)纹理压缩
压缩后的纹理直接使用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个字节)如图:
clipboard
下面是在Android平台上纹理压缩实现步骤:
1. 使用TexturePacker把要压缩的散图,打包生成png和plist文件。
2. 使用pkm批处理工具,遍历和解析资源目录下的plist文件,根据plist中的png大图(textureFileName的值)生成同名pkm文件,这里需要注意的是生成pkm文件的时候根据情况选择是否创建带alpha部分,因为etc1本身不支持透明图片,使用mali工具生成一张拼接在一起的纹理,这张纹理上半部分是原始图片(无alpha信息),下半部分是alpha信息图片。带有alpha信息的pkm
图片在渲染的时候使用特殊的shader进行渲染,后缀名使用.pkm,不带alpha信息的图片后缀使用.pkm0。
带alpha信息
etcpack.exe %1 %2 -v -c etc1 -aa -s fast
不带alpha信息
etcpack.exe %1 %2 -v -c etc1 -s fast
3. 编写顶点shader和片段shader,在顶点着色器中得到原图纹理坐标和带有alpha信息的纹理坐标,然后在片段着色器中对纹理进行采样,预乘alpha。
4. 使用shader,因为游戏中有些图片没有进行纹理压缩,所以这里要在精灵类的initWithTexture函数中根据纹理名后缀进行区分使用哪个shader,在CCNodeLoader::parsePropTypeSpriteFrame)ccb加载的时候也需要根据是否有plist文件判断使用哪个shader。
5. 把plist中key为textureFileName的值的后缀由png替换成pkm或pkm0。
(2)使用texturepacker拼图
用texturepacker把一些小散图打包到一张大图,减少纹理IO和draw call。
(3)使用对象池
在需要频繁创建对象的场景中,使用对象池。
(4)UI(button,非渐变背景)使用九宫格

(5) 切换场景时释放无用纹理

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操作上。

解决方案:使用官方版的luac替代luajit,并使用c++的方式编译。

cocos2dx-html5 实现网页版flappy bird游戏

我也是第一次使用cocos2d_html5,对js和html5也不熟,看引擎自带的例子和引擎源码,边学边做,如果使用过cocos2d-x的话,完成这个游戏还是十分简单的。游戏体...

阅读全文

【cocos2d-x开发实战 特训99-终结篇】移植到android平台和添加admob广告

上一篇已经完成特性99在win32平台下的开发,现在把它移植到android上,首先修改Android.mk文件,内容如下: LOCAL_PATH := $(call my-dir) include $(CLEAR_...

阅读全文

【cocos2d-x开发实战 特训99-part6】完成游戏首页

现在特性99游戏的功能基本完成了,但是游戏没有首页也挺奇怪,所以这篇博客为游戏添加首页,通过首页进入游戏。 首页的元素比较简单,就是一张背景图,一个l...

阅读全文

4 条评论

  1. 合并渲染那块能不能更具体一点?C++不是很熟悉,看3.0的感觉涉及的类库比较多。想看看有没有具体一点的思路

  2. 自动裁剪 ,OpenGL在对图元处理的时候,已经包含了裁剪,这一步你们还做了其他的处理吗?

    1. 这里的auto-culling跟opengl对图元的裁剪不一样,只是逻辑上控制屏幕外面的精灵或其他节点不加入绘制队列而已,适合于那种地图比较大,地图上小节点比较多的情况。

欢迎留言