Cocos 小白的性能优化探索

近期使用了 cocos creator 来开发一些游戏化的课中互动。Cocos 是一个优秀的国产游戏引擎,能够经过 javascript 写出跨平台的游戏。看完文档,吭哧吭哧搞完,看似完美运行,然而体验会上,你们提出加载时黑屏时间长、手机发烫严重、闪退、卡顿等问题。头疼,只能想办法优化。javascript

通过几天的优化,性能才渐渐达标,其间踩了很多坑,因此打算将一些性能问题排查和优化的手段记录起来,分享给有须要的同窗。html

虽然 Cocos 属于游戏开发范畴,但与前端开发中遇到的性能问题仍是有不少共通之处,无非是加载速度、CPU、内存这三个指标。接下来分别从这三个指标来阐述一些优化手段。前端

1. 加载速度优化

Cocos 的启动大体能够分为5个阶段:java

Cocos 启动流程

其中 Cocos 引擎加载和运行的耗时,业务侧是没法改动的,这部分黑屏时间没法优化。那么黑屏时间优化就只剩 Cocos 静态资源加载了。git

静态资源加载的手段有两个:github

资源加载优化

资源压缩主要是针对图片资源的压缩,tinify支持 png 和 jpg 格式图片的在线压缩,通常能够压缩掉 75% 的大小,而且在视觉上不会有明显的差别,十分推荐。算法

若是接受必定程度的失真,在 cocos creator 编辑器中也可以对 png 和 jpg 图片进行压缩。 canvas

若是是 png 格式图片就 png,jpg 格式则选 jpg,选择后能够调整图片质量,图片质量越低,大小越小,失真也会越多。 浏览器

资源缓存分为硬盘缓存和内存缓存。缓存

对于原生端,资源自己是存在本地的。对于 Web 端,能够经过 http 的缓存,或者 PWA 来实现资源在硬盘的缓存。

资源还能够缓存在内存中,通常来讲,游戏中会有多个场景,例如游戏中会有不少关卡,每一个关卡一个场景。若是一个场景不会重复进入,那么场景资源能够不用缓存。若是场景须要重复进入,那么缓存一下,能够加速第二次打开的速度。

通常来讲,硬盘的存储空间比较大,多作硬盘的存储问题不大。可是内存通常空间比较宝贵,不能啥资源都一股脑往里塞,容易形成内存占用率高,而且可能存在内存泄漏的风险,因此通常来讲只缓存一些常驻的资源。

2. CPU 优化

因为游戏中须要大量的计算与绘制,自己是比较吃cpu的。因此在游戏过程当中, CPU 的优化是很是重要的。若是 CPU 负载太高,会形成设备发热严重、帧率下降甚至是卡退。

CPU 是负责解析执行指令的,那么cpu高负载的缘由主要就是须要执行的指令过多,尤为是一些耗时的指令。在游戏中,主要是绘制指令的调用,也就是 drawcall。还有其余的一些计算量比较大的系统,例如物理系统、碰撞系统。另外就是结点的建立与销毁,以及业务代码中一些 update 逻辑。

对于 drawcall 的优化,理想的状况是 drawcall 的次数越少越好。要了解优化 drawcall 的意义和方法,首先要知道在执行 drawcall 后, CPU 作了什么操做。

CPU 对于图形处理不太擅长,因此通常都是将图形处理丢给 GPU (Graphics Processing Unit,图形处理器)去作,这就是为何打大型游戏须要比较好的显卡的缘由,其实就是须要性能更强大的gpu。

CPU 要将数据交给 GPU 渲染,也不是啥都不用干的。CPU 须要把要渲染的数据,写入到数据缓冲区(显存),并设置渲染状态(纹理、着色器等),而后 GPU 才去取数据计算并渲染。

因为 GPU 的图形处理能力强,因此每次给一点数据和一次性给一堆数据处理速度是差很少的。可是对于 CPU 来讲,若是频繁调用 drawcall,每次一点点数据,那么 CPU 就会忙得焦头烂额。因此优化 drawcall 的最有效方式就是批处理了。

批处理的方式就是合图了。所谓合图,就是将要渲染的纹理图合成一个大的图集,一次性送给 GPU 去渲染。例若有 3 个 sprite,3 个 sprite 有本身的纹理,若是不合图,那么就须要 3 次 drawcall。若是开启了合图,那么只须要 1 次 drawcall。

3 个星星图标的 sprite,显示 drawcall 是 4,为何不是 3 呢,由于相机的背景自己须要一次 drawcall,因此星星总共须要 3 次 drawcall。

添加图集后,能够看到 drawcall 就变成 2 了,说明星星如今只须要 1 次 drawcall。

除了 sprite 能够合图,label 组件 (font) 也能支持合图。实际上,渲染字体也是将纹理送到gpu去渲染。

字体分为两种实现方式,一种是位图字体 (Bitmap font),一种是 Free type 字体。

所谓位图字体,就是将全部字符所有都打到一张图片中,这样作简单粗暴,效率也比较高,由于至关于字体都是预渲染好的。缺点是在字符集比较大时,例如全部汉字,那么字符的图片可能会比较大,内存占用率会比较高。而且不够灵活,由于图片的分辨率固定,在高分屏中,位图字体会出现一些锯齿。

另一种是 Free type 字体,例如ttf格式的字体。不一样于位图字体使用像素来表示字体,Free type 字体只是定义了字体的渲染数据,须要在运行时实时计算而后渲染。这样的字体就不存在放缩问题,但须要必定的计算消耗,因此通常须要经过缓存来优化。

对于只有数字和英文字母,而且文本结点比较多或者常常变化的状况,能够考虑使用位图字体进行优化,能够有效下降文字渲染形成的 drawcall 数。

咱们来看看这样一个简单例子。场景中有 3 个 label 结点,字体的格式为 ttf 格式。

预览一下,发现 drawcall 是 4,前面提到了相机默认会有一次 drawcall,说明 3 个文本结点带来了 3 次 drawcall,若是是大量文本结点或者文本结点常常变化,将会形成大量的 drawcall。

若是咱们使用 BMFont,能够看到 drawcall 当即降为 2,也就是 3 个结点只绘制了 1 次,带来的 drawcall 优化很是可观。

对于系统自带字体,Cocos 也会为每一个 label 组件建立字符纹理,而且默认不参加合图。

Cocos 为 label 组件提供了相似 BMFont 的功能,咱们可使用 Cache Mode 来优化 CPU 。

Cache Mode 值为 NONE 的时候,Cocos 会为每一个 label 组件的文本建立字符纹理,而且默认不参加合图。

值为 BITMAP 的时候,Cocos 会为每一个 label 组件的文本建立字符纹理,可是能够参加动态合图(后面会讲到),批量绘制。

值为 CHAR 的时候,Cocos 会为字体生成一张单独的字符图集,并缓存起来。后续的新的文本,能够直接从字符图集缓存中获取,不须要从新渲染。(事实上 Cocos 官方文档对此的描述是”下次遇到相同字符再也不从新绘制”,但就个人理解来讲仍是须要绘制的,不然为何屏幕显示的文字会更新呢,因此应该只是复用了渲染的数据)。

相较于自动图集这种静态合图方式, Cache Mode 为 BITMAP 使用的是动态合图。静态合图的方式是在构建时生成合图,而动态合图是运行时生成合图。静态合图会减小一些运行时的消耗,可是一些动态加载图片资源没办法应用静态合图,这时候能够经过动态合图进行优化。关于如何使用动态合图,Cocos 官方文档已经讲得很详细,这里再也不赘述,能够直接查看:docs.cocos.com/creator/man…

前面咱们说到合图是下降 drawcall 是一种常见而且有效的手段,可是使用合图的方式会占用必定的内存,因此同时要关注内存指标。另外须要注意的是,合图以后并不意味着就可以批量渲染,参与合图的 sprite 或者 label 结点的须要是连续的。仍是上面那个星星的例子,场景中有 3 颗星星,也就是 3 个 sprite,本来须要 3 次 drawcall,合图以后只须要 1 次 drawcall。咱们在第一和第二个星星中间,加入一个 sprite 结点,批量渲染就会被打破:

1

插入红色小方块后,drawcall 变成4。分别是相机背景 drawcall + 第一个星星 drawcall + 红色方块 drawcall + 第三和第四个星星的 drawcall。第一个星星原本能够和第三和第四个星星一块儿批量渲染的,被红色方块的渲染打断了。

咱们再将小方块的位置调整一下,调到第一个星星的前面。

能够看到,尽管显示上没有任何变化,可是 drawcall 变成了3次。

因此,尽可能让参与合图的结点连续,中间不插入其余的 sprite 类的结点,以避免打破批次渲染。

此外,mask 组件也多是 drawcall 数量上升的元凶之一。mask 在 Cocos 中,主要是用来实现一些形状,例如圆角。

为何这么说呢,咱们来看个例子:

场景中有一个白色方块。

总的 drawcall 是 2,因此渲染方块须要 1 次 drawcall。

若是想要显示圆形,能够经过加 mask 组件来遮罩。

能够看到 drawcall 从 2 变成了 4,说明使用了 mask 以后,会产生 2 次 drawcall。很神奇哦,这是什么原理呢?

cocos文档中的解释是这样的:

结论就是使用 mask 组件的结点,绘制总共须要 3 次 drawcall,使用 mask 组件不能与相邻的结点合批渲染,即便它们使用的是相同的图集。因此,尽可能少用 mask,若是要实现圆角等效果,结点的尺寸也比较固定,可让设计同窗直接给图。

固然若是你和我同样想细扣里面的细节,什么是模板缓冲?为何必定要 3 次 drawcall ?能够看接下的详细解释,须要一点 OpenGL 知识,若是不想深刻细节能够直接跳过:

  1. 什么是模板测试?

    模板测试其实就是经过模板缓冲区中的设置,来决定某些区域要不要渲染。

详细学习请见:[learnopengl-cn.readthedocs.io/zh/latest/0…learnopengl-cn.readthedocs.io/zh/latest/0… Advanced OpenGL/02 Stencil testing/)

  1. 使用 mask 组件的结点渲染三步骤

    能够经过spector.JS来查看渲染帧信息。这是圆形渲染相关的三个帧:

第 1 帧渲染:

渲染命令以下,意思是经过 6 个顶点画出 2 个三角形,实际上就是本来的小方块。

可是实际上这里并无将小方块真正渲染出来。

模板缓冲状态为

这里的意思是将小方块区域对应的模板缓冲区位置的值直接置为 0,也就是刷新该区域的模板缓冲区。

第 2 帧渲染:

渲染命令以下,意思是经过 186 个顶点,画出 n(不少)个三角形,其实就是画出圆形,由于在 OpenGL(Webgl)中,各类形状都是经过三角形去拼出来的。

模板缓冲状态为

直接将圆形遮罩对应的模板缓冲区位置的值设成 1。

第 3 帧渲染:

渲染命令以下,与第一帧同样,都是渲染出小方块,此次会将方块渲染出来。

模板缓冲状态以下,意思是只有缓冲区对应位置的值为 1,才会渲染出来,因此方形被遮罩出了圆形。

除了 drawcall,一些逻辑计算也会影响cpu的使用率。例如 widget 组件的计算时机:

若是选择了 ALWAYS,那么每一帧都会从新计算结点的位置、大小,因此比较耗计算。能够只选择 ON_WINDOW_RESIZE,只在窗口大小变化时,才会从新计算。若是还须要在其余时机计算 widget,能够按需手动调用 widget.updateAlignment

另外,因为 update 这个生命钩子在每一帧都会调用,因此也须要注意在 update 中的逻辑是否执行过于频繁,例如不停地打 log,或者不停地计算,都会影响 CPU 的性能。

结点的建立以及销毁也是比较耗费性能的,因此要避免频繁地进行结点的建立和销毁操做,而且应该尽可能减小结点的数量。

因为 Cocos 在 Web 中经过 canvas 进行绘制,没办法使用浏览器的开发者调试工具去查看结点,这里推荐一个 Cocos 插件 ccc-devtools,github 地址:github.com/potato47/cc…

若是发现结点数量过多,而且结点频繁建立销毁,例如游戏中的小怪、子弹等数量比较多的重复物体,一般能够经过回收工厂进行优化。回收工厂就是结点用完以后,不销毁,而是缓存起来,下次获取结点能够直接复用缓存中的结点,而不须要从新建立。Cocos 自己提供了回收工厂的接口 NodePool,能够了解一下:docs.cocos.com/creator/man…

游戏中的碰撞检测,也会比较耗性能。咱们能够尽可能使用box或者circle碰撞器,而少用多边形碰撞器

3. 内存优化

游戏中比较占用资源的主要是资源的缓存,例如图片资源缓存。而资源分为静态资源和动态资源。

静态资源指的是,场景一开始进入时便当即加载的资源。动态资源是指在场景中异步加载的资源,例如一些网络图片、音频等经过 cc.loader.load 或者 cc.loader.loadRes 加载的资源。

咱们能够经过 cc.loader._cache 查看当前场景下面的资源列表

也能够经过前面提到的 ccc-devtool 可视化地查看资源列表,而且还能看到纹理资源的大小:

注意到一张图片在内存中是比存在磁盘中要大不少的,由于在图片存在磁盘中时,是通过编码的,例如使用 png 和 jpg,数据量会小不少。可是存在内存中时,是解码成像素值的,因此须要占据的空间比较大。

内存要降下来,也无非两种方式,一是减小没必要要的资源、二是资源压缩。

减小没必要要的资源,例如:场景中的背景图,在移动端中是一套,在 PC 端是一套。那么应该是经过代码判断是什么平台,而后再动态加载对应资源的方式实现,而不是在场景中同时放置移动端和 PC 端的背景,而后控制显隐的方式实现。这样能够减小一套资源的内存占用。

对于背景,通常来讲由设计直接给图会比较大,若是是只是纯色或者经过简单的背景重复或者变换能够实现,能够由开发来实现,这样能够把大背景图优化掉。

另外,合图的时候咱们注意只将比较相关的图片进行合图,不然意味着可能加载一整张合图,只是用到其中的一个小图,会形成不少内存空间的浪费。

资源压缩,主要是指对图片资源的压缩,也称纹理压缩

单纯使用 tinify 等工具,对图片大小进行压缩,若是不改变图片尺寸,是不会减小图片资源在内存中的体积的,只能减少图片在磁盘中的存储体积。对于分辨率要求不高的资源,可使用2倍图或者1倍图,能够减少资源在内存中的体积。

纹理压缩算法,例如 Etc1, Etc2, PVRTC 等,能够优化图片在内存中的体积。jpg 和 png 格式虽然可以对图片数据进行压缩,可是并不能被gpu读取,因此是须要 CPU 解码以后再给到 GPU 渲染的。而通过纹理压缩算法压缩后的数据,是可以直接给gpu渲染的,因此纹理压缩不只可以优化内存,还能优化 CPU。

须要注意的是,纹理压缩通常都是有损压缩,能够选择压缩率。另外,纹理压缩的算法依赖于设备的 GPU 可否解码,因此针对不一样的平台,须要使用不一样的纹理压缩算法。

关于纹理压缩算法的介绍,推荐看这篇文章:zhuanlan.zhihu.com/p/237940807…

Etc1 绝大部分的安卓设备支持,PVRTC 全部的 iOS 设备支持。

若是图片不须要支持 alpha 通道,安卓选择 Etc1 RGB、iOS 选择 PVRTC 4bits RGB 便可。若是须要支持 alpha 通道,安卓选择 Etc1 RGB Separate A,iOS 选择 PVRTC 4bits RGBA Separate A

对于不用的内存,咱们也要及时释放,防止内存泄漏。分自动释放和手动释放两种。

对于静态资源的释放,能够经过勾选场景自动释放选项来实现:

这样在场景切换后,场景中的静态资源就会被自动释放了。

若是不想等到切换场景才释放静态资源,也可使用 cc.assetManager.releaseAsset 进行手动释放。

有一个坑点是,动态加载的资源没法在场景切换时,跟随静态资源自动释放。须要经过 cc.setAutoReleaseRecursively 手动设置一下:

这样资源在场景切换时,会自动释放这部分动态加载的资源。也能够经过 cc.loader.releaseRes 手动释放动态加载资源。

相关文章
相关标签/搜索