感谢

2、运营阶段

统计数据:统计移动端版本情况、Android/IOS版本、CPU/GPU版本。
统计工具:友盟、openinstall、matomo、bugly(异常监控)

三、优化手段

程序优化的根本是为达到用户满意的体验,而在(计算)时间与(内存)空间上进行的权衡,这两者是矛盾关系,更多的计算可以换取空间上的减少,更多的空间也可以换取计算量的减少,具体如何权衡就看哪种做法可以更有效地改善体验。

1、内存优化(CPU)

缓存

根据2/8原则,我们要找出游戏中占据内存较多的模块。包含但不限于以下:

  • 引擎缓存
    • AudioCache
    • SpriteFrameCache
    • GLProgramStateCache
    • TextureCache
    • ActionTimelineCache
    • AnimationCache
    • FontAtlasCache
    • GLProgramCache
    • GLProgramStateCache
  • 游戏(业务)缓存
    • 各种文件配置数据、视频数据等等。

如果不加以管理,很可能导致内存浪费,我们可以在此基础上再做一套针对应用层的资源管理模块,参考TextureCache模式,采用引用计数的方式跟踪全部类型的缓存资源,至于在合适何时进行加载和清理,可以再建立一套UI栈(Android Activity),建立一个界面基类,制定好生命周期函数,规范好有序的创建/退出界面,自然地我们也就可以做到有序地加载/卸载资源。

Lua缓存

这部分可以好好研究大做文章,日后再仔细研究。

  1. collectgarbage("setpause", 100 )
  2. collectgarbage("setstepmul", 5000)

对象池

有些对象可能会被频繁的create/delete,add/remove,可以利用池机制,避免频繁的内存分配。
示意代码如下:

  1. #include "Singleton.h"
  2. // 多多使用C++的模板技术。
  3. template<class T>
  4. class ObjectPool : public Singleton<ObjectPool<T>>
  5. {
  6. public:
  7. ObjectPool();
  8. ~ObjectPool();
  9. //get a bullet
  10. T* getObject();
  11. void freeObject(T *obj);
  12. private:
  13. std::list<T*> objects;
  14. const int INITALCAPACITY = 256;
  15. };

等等(待添加)

tableView列表缓存,类似Android的ListView,缓存那个出到列表之外的Item,留给即将进入的那个Item使用,防止卡顿。
……

2、显存优化(GPU)

OpenGL对象创建之后一般都常驻于显存之中,需要及时的手动删除。

  1. GLuint objHandle = glCreateCreate(......); // 创建对象。
  2. ......; // 可能会为这个对象开辟由它管理的内存,并填充数据。
  3. glDeleteObject(objHandle); // 删除对象。
  4. // ************************************
  5. // 常用到的OpenGL对象类型。
  6. // ************************************
  7. glGenBuffers(...); // Buffer缓冲类型,如VBO、EBO
  8. glGenSamplers(...); // Sampler采样器
  9. glGenTextures(...); // Texture纹理对象
  10. glGenFrameBuffers(...); // 帧缓冲对象
  11. glGenVertexArrays(...); // VAO
  12. glGenRenderBuffers(...); // 渲染缓冲对象类型
  13. glGenProgramPipelines(...); // 管线对象
  14. glGenTransformFeedback(...); // TransformFeedback

如果我们自己编写了GL代码,比如CustomCommand自定义绘制,别忘记glDelete*()。

上面我们讲到的引擎中的各种Cache,比如TextureCache缓存Texture2D其实封装了一个GL Texture纹理对象(虽然只是一个GLuint),因此我们只需要管理好Texture2D对象的生命周期,在析构的时候就会glDeleteTexture清除这个纹理对象。

压缩纹理

压缩纹理,指上传至GL显存中的纹理数据就是一种压缩的格式,这种格式的数据可以直接被GPU读取采样(GPU芯片支持的压缩格式),这既可以减少压缩纹理的文件大小,也可以减少它的内存占用和CPU至GPU的带宽占用,所以我们应该大大地使用压缩纹理。

  • Android
    • ETC1:POT,GLES 2.0及以上支持,不支持Alpha Channel,cocos已有支持。
    • ETC2:NPOT,支持Alpha Channel,GLES 3.0及以上支持。
    • ASTC:Android 5.0/OpenGLES 3.1及以上支持,市面上98.5%的Android手机支持。
  • IOS
    • ASTC:iphone 6以上支持。
    • PVRTC2:POT,iphone都支持。

注意POT纹理像素宽高必须是2的次幂,美术设计的就要注意,否则会被压缩工具自主拉伸。

字体选择

性能Charmap>BMFont>TTF,打死不用systemFont,每个SystemFont都是一个纹理,内容改变就要生成新的纹理。

  • TTF:根据显示字符,动态生成纹理,计算量最大,纹理会随着显示的字符类型数目增大而增大,如果是英文版那就舒服了,字符类型就那么几个,可以就用charmap/bmfont,好处是TTF可以支持各种字符(各种语言、符号)。
  • BMFont:折中。
  • Charmap:等宽的纹理组成的大纹理,根据偏移量显示指定纹理区域,性能最好,但是字符固定,也没有特效。

字符的特效酌情使用:

  • outline,两个drawcall完成文本渲染
  • shadow,两个drawcall
  • glow,, 一个drawcall
  • shadow + outline, 三个drawcall。
  • 没有特效时,TTF自动合并drawcall。

    等等(待添加)

    骨骼动画代替帧数多的帧动画。

多多使用九宫格图。

设置默认纹理转化格式。

  1. // 设置好加载纹理文件时,默认转化成的纹理格式。
  2. // 默认是RGBA888,可以考虑RGBA4444
  3. Texture2D::setDefaultAlphaPixelFormat(PixelFormat::RGBA4444);
  4. enum class PixelFormat
  5. {
  6. //! auto detect the type
  7. AUTO,
  8. //! 32-bit texture: BGRA8888
  9. BGRA8888,
  10. //! 32-bit texture: RGBA8888
  11. RGBA8888,
  12. //! 24-bit texture: RGBA888
  13. RGB888,
  14. //! 16-bit texture without Alpha channel
  15. RGB565,
  16. //! 8-bit textures used as masks
  17. A8,
  18. //! 8-bit intensity texture
  19. I8,
  20. //! 16-bit textures used as masks
  21. AI88,
  22. //! 16-bit textures: RGBA4444
  23. RGBA4444,
  24. //! 16-bit textures: RGB5A1
  25. RGB5A1,
  26. //! 4-bit PVRTC-compressed texture: PVRTC4
  27. PVRTC4,
  28. //! 4-bit PVRTC-compressed texture: PVRTC4 (has alpha channel)
  29. PVRTC4A,
  30. //! 2-bit PVRTC-compressed texture: PVRTC2
  31. PVRTC2,
  32. //! 2-bit PVRTC-compressed texture: PVRTC2 (has alpha channel)
  33. PVRTC2A,
  34. //! ETC-compressed texture: ETC
  35. ETC,
  36. //! S3TC-compressed texture: S3TC_Dxt1
  37. S3TC_DXT1,
  38. //! S3TC-compressed texture: S3TC_Dxt3
  39. S3TC_DXT3,
  40. //! S3TC-compressed texture: S3TC_Dxt5
  41. S3TC_DXT5,
  42. //! ATITC-compressed texture: ATC_RGB
  43. ATC_RGB,
  44. //! ATITC-compressed texture: ATC_EXPLICIT_ALPHA
  45. ATC_EXPLICIT_ALPHA,
  46. //! ATITC-compressed texture: ATC_INTERPOLATED_ALPHA
  47. ATC_INTERPOLATED_ALPHA,
  48. //! Default texture format: AUTO
  49. DEFAULT = AUTO,
  50. NONE = -1
  51. };

3、计算优化(CPU)

draw call

一帧的draw call次数最好在50以内。
看下面一段OpenGL伪代码,理解何为draw call(绘制)以及batch draw call(批绘制)。

  1. // ******************************************
  2. // 下面代码为绘制2个Sprite的cocos2dx整体OpenGL伪代码
  3. // ******************************************
  4. void main()
  5. {
  6. intGLLib(); // 初始化GL库
  7. auto window = glCeateWindow(......); // 创建GL窗口
  8. initGLState(); // 初始化OpenGL的初始State
  9. while(!window->shouldClose()){ // 一个循环就是一帧
  10. ......;
  11. // ********************
  12. // 普通方法绘制两个Sprite
  13. // ********************
  14. for(......) drawSprite(); // 循环两次,2个draw call
  15. // ********************
  16. // 批绘制绘制两个Sprite
  17. // ********************
  18. drawSpriteBatch(); // 一个draw call搞定。
  19. ......;
  20. }
  21. }
  22. // ******************************************
  23. // 绘制一个Sprite
  24. // ******************************************
  25. void drawCall(){
  26. // 第一步、切换至绘制这个Sprite的GL State
  27. glBindBuffer(...); // 绑定VBO、EBO(顶点数据、索引数据)
  28. glBindVertexArray(...); // 绑定VAO(shader的attribs)
  29. glUseProgram(...); // 设置shader
  30. glUniform(...); // 设置shader的uniforms(矩阵、texture sampler等)
  31. glBindTexture(...); // 绑定Texture纹理
  32. glBlendFunc(...); // 混合,如果有开启的话
  33. // 第二步、执行draw call
  34. glDrawElements(GL_TRIANGLES,...); // 顶点数据开始进入渲染管线,绘制Sprite,这就是一个draw call
  35. }
  36. // ******************************************
  37. // 批绘制,一个draw call 绘制两个Sprite
  38. // ******************************************
  39. void drawCallBatch(){
  40. // 第一步、切换至绘制这两个Sprite的GL State(代价高昂的)
  41. // 在这个VBO顶点数据中包含了这两个Sprite的顶点数据,EBO根据这个VBO动态生成
  42. glBindBuffer(...); // 绑定VBO、EBO(顶点数据、索引数据)
  43. // 两个Sprite的shader必须相同,这是批绘制的前提条件(1/3)
  44. glBindVertexArray(...); // 绑定VAO(shader的attribs)
  45. glUseProgram(...); // 设置shader4
  46. // 两个Sprite的纹理是同一张,这是批绘制的前提条件(2/3)
  47. // 在顶点数据中已经指定了每个Sprite的采样区域
  48. glUniform(...); // 设置shader的uniforms(矩阵、texture sampler等)
  49. glBindTexture(...); // 绑定Texture纹理
  50. // 两个Sprite的混合必须一样,这是批绘制的前提条件(3/3)
  51. glBlendFunc(...); // 混合,如果有开启的话
  52. // 第二步、执行draw call
  53. glDrawElements(GL_TRIANGLES,...); // 绘制真正绘制Sprite
  54. }

GL State的切换是代价高昂的,第二个批绘制少了一次GL State的切换,将明显提高绘制速度。这就是draw call优化(尽量减少每帧的draw call次数),实际上就是draw call的合并。需要注意的是,满足以下条件的两个draw call才能进行合并:

  1. 这两个draw call必须是先后连续执行的。
  2. 两个绘制之间的shader必须相同
  3. 两个绘制之间的texture必须相同
  4. 两个绘制之间的混合(blending)必须相同

我们要做的draw call优化,就从如何促成这4个条件下手。

  • 将同一图层的图像TP打包成大图(texture相同),根据够用原则,还可以用RGBA4444+抖动代替RGBA8888

  • 将相同类型的Node尽量放一起add(shader相同)

  • 万不得已,别碰混合(Blending)

    I/O操作

    一般就指文件加载(纹理、字体、配置、音频、视频),比较耗时。文件加载其实也包含在上面的资源管理方案中:

    • 预加载,在进入指定界面前预加载,比如界面的生命周期函数中执行。
    • 异步加载(多线程)
    • 分帧加载,避免峰值。

这里注意下大图的异步加载:

  1. auto textureCache = TextureCache::getInstance();
  2. auto spFrameCache = SpriteFrameCache::getInstance();
  3. spFrameCache->addSpriteFramesWithFile(plist); // 这个加载并不是异步加载。
  4. textureCache->addImageAsync(plistPNG, [](Texture2D* texture){
  5. spFrameCache->addSpriteFramesWithFile(plist, texture); // 这样就可以异步加载。
  6. });

visit和draw

Node的visit和draw方法是每帧调用的,见下面GL伪代码:

  1. Scene* runningScene = createMainScene(); // 当前UI树的根节点
  2. void main() {
  3. while(!window->shouldClose()){ // 一个循环一帧
  4. ......;
  5. runningScene->visit(); // 每帧执行draw
  6. ......;
  7. }
  8. }
  9. // UI树的遍历是左->根->右的深度遍历顺序。
  10. void Scene::visit(){
  11. for(...) child->visit(); // localZOrder<0的child
  12. this->draw(); // 绘制本节点
  13. for(...) child->visit(); // localZOrder>0的child
  14. }

因此当我们要重写这两个方法时,最好将他们的代码优化到极致,在这里面非常值得用空间换时间:

  • 避免繁重计算
  • CC_USE_CULLING,遮盖剔除,默认是开启的 ,可以考虑关闭。
  • 尽量少破坏UI树的结构,减少UI树的深度

    • 不要频繁地addchild setlocalZOrder,Opacity代替Visible。

      动态帧率

      不同机型、是否挂机、不同界面,可以适当调整帧率。

      数学计算优化

      位移代替乘除,乘法代替开方。

      等等(待添加)

      4、计算优化(GPU)

      主要是VS和FS两个着色器的代码优化。

    • 避免使用复杂的像素着色器

      • 一般的2D游戏都不会用到什么复杂着色器。
    • 使用bake烘焙光照,而不是动态光照
      • 一般2D游戏用不上。
    • 避免在VS中,discard和alpha test,
      • 会破坏GPU自身的 depth testing 优化,比如 PowerVR 的 HSR。
    • ClippingNode尽量少用

      • 会clear、读、写stencil,可以考虑RenderTexture代替。

        5、资源优化

        够用原则。
    • 图片压缩

      • PNG压缩
      • 压缩纹理
      • 图片设计像素宽高别过大
    • 音频压缩

等等(待添加)。

四、其他优化

使用 armabi-v7a构建Android工程,这会有更好的性能表现,因为在此架构下面 Cocos2d-x 会启用 neon指令集,矩阵运算的效率会大大提高。

等等(待添加)。