|
近期使用了cocoscreator来开发一些游戏化的课中互动。Cocos是一个优秀的国产游戏引擎,可以通过Javascript写出跨平台的游戏。看完文档,吭哧吭哧搞完,看似完美运行,然而体验会上,大家却提出加载时黑屏时间长、手机发烫严重、闪退、卡顿等问题。头疼,只能想办法优化。经过几天的优化,性能才渐渐达标,其间踩了不少坑,所以打算将一些性能问题排查和优化的手段记录起来,分享给有需要的同学。虽然Cocos属于游戏开发范畴,但与前端开发中遇到的性能问题还是有很多共通之处,无非是加载速度、CPU、内存这三个指标。接下来分别从这三个指标来阐述一些优化手段。1.加载速度优化Cocos的启动大致可以分为5个阶段:Cocos启动流程其中Cocos引擎加载和运行的耗时,业务侧是无法改动的,这部分黑屏时间无法优化。那么黑屏时间优化就只剩Cocos静态资源加载了。静态资源加载的手段有两个:资源加载优化资源压缩主要是针对资源的压缩,tinify 支持png和jpg格式的在线压缩,一般可以压缩掉75%的大小,并且在视觉上不会有明显的差异,十分推荐。如果接受一定程度的失真,在cocoscreator编辑器中也能够对png和jpg进行压缩。如果是png格式就png,jpg格式则选jpg,选择后可以调整质量,质量越低,大小越小,失真也会越多。资源缓存分为硬盘缓存和内存缓存。对于原生端,资源本身是存在本地的。对于Web端,可以通过http的缓存,或者PWA来实现资源在硬盘的缓存。资源还可以缓存在内存中,一般来说,游戏中会有多个场景,例如游戏中会有很多关卡,每个关卡一个场景。如果一个场景不会重复进入,那么场景资源可以不用缓存。如果场景需要重复进入,那么缓存一下,可以加速第二次打开的速度。一般来说,硬盘的存储空间比较大,多做硬盘的存储问题不大。但是内存一般空间比较宝贵,不能啥资源都一股脑往里塞,容易造成内存占用率高,并且可能存在内存泄漏的风险,所以一般来说只缓存一些常驻的资源。2.CPU优化由于游戏中需要大量的计算与绘制,本身是比较吃 CPU 的。所以在游戏过程中,CPU的优化是非常重要的。如果CPU负载过高,会造成设备发热严重、帧率降低甚至是卡退。CPU是负责解析执行指令的,那么 CPU 高负载的原因主要就是需要执行的指令过多,尤其是一些耗时的指令。在游戏中,主要是绘制指令的调用,也就是 drawcall。还有其他的一些计算量比较大的系统,例如物理系统、碰撞系统。另外就是结点的创建与销毁,以及业务代码中一些 update 逻辑。对于drawcall的优化,理想的情况是drawcall的次数越少越好。要了解优化drawcall的意义和方法,首先要知道在执行drawcall后,CPU做了什么操作。CPU对于图形处理不太擅长,所以一般都是将图形处理丢给GPU(GraphicsProcessingUnit,图形处理器)去做,这就是为什么打大型游戏需要比较好的显卡的原因,其实就是需要性能更强大的 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去渲染。字体分为两种实现方式,一种是位图字体 (Bitmapfont),一种是 Freetype 字体。所谓位图字体,就是将所有字符全部都打到一张中,这样做简单粗暴,效率也比较高,因为相当于字体都是预渲染好的。缺点是在字符集比较大时,例如所有汉字,那么字符的可能会比较大,内存占用率会比较高。并且不够灵活,因为的分辨率固定,在高分屏中,位图字体会出现一些锯齿。另外一种是 Freetype 字体,例如ttf格式的字体。不同于位图字体使用像素来表示字体,Freetype字体只是定义了字体的渲染数据,需要在运行时实时计算然后渲染。这样的字体就不存在放缩问题,但需要一定的计算消耗,所以一般需要通过缓存来优化。对于只有数字和英文字母,并且文本结点比较多或者经常变化的情况,可以考虑使用位图字体进行优化,可以有效降低文字渲染造成的drawcall数。我们来看看这样一个简单例子。场景中有3个label结点,字体的格式为ttf格式。预览一下,发现drawcall是4,前面提到了相机默认会有一次drawcall,说明3个文本结点带来了3次drawcall,如果是大量文本结点或者文本结点经常变化,将会造成大量的drawcall。如果我们使用BMFont,可以看到drawcall立即降为2,也就是3个结点只绘制了1次,带来的drawcall优化非常可观。对于系统自带字体,Cocos也会为每个label组件创建字符纹理,并且默认不参加合图。Cocos为label组件提供了类似BMFont的功能,我们可以使用 CacheMode 来优化CPU。CacheMode值为 NONE的时候,Cocos会为每个label组件的文本创建字符纹理,并且默认不参加合图。值为 BITMAP 的时候,Cocos会为每个label组件的文本创建字符纹理,但是可以参加动态合图(后面会讲到),批量绘制。值为 CHAR 的时候,Cocos会为字体生成一张单独的字符图集,并缓存起来。后续的新的文本,可以直接从字符图集缓存中获取,不需要重新渲染。(事实上Cocos官方文档对此的描述是”下次遇到相同字符不再重新绘制”,但就我的理解来说还是需要绘制的,否则为什么屏幕显示的文字会更新呢,所以应该只是复用了渲染的数据)。相较于自动图集这种静态合图方式,CacheMode为 BITMAP 使用的是动态合图。静态合图的方式是在构建时生成合图,而动态合图是运行时生成合图。静态合图会减少一些运行时的消耗,但是一些动态加载资源没办法应用静态合图,这时候可以通过动态合图进行优化。关于如何使用动态合图,Cocos官方文档已经讲得很详细,这里不再赘述,可以直接查看文档。前面我们说到合图是降低drawcall是一种常见并且有效的手段,但是使用合图的方式会占用一定的内存,所以同时要关注内存指标。另外需要注意的是,合图之后并不意味着就能够批量渲染,参与合图的sprite或者label结点的需要是连续的。还是上面那个星星的例子,场景中有3颗星星,也就是3个sprite,原本需要3次drawcall,合图之后只需要1次drawcall。我们在第一和第二个星星中间,加入一个sprite结点,批量渲染就会被打破:插入红色小方块后,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 知识,如果不想深入细节可以直接跳过:什么是模板测试?模板测试其实就是通过模板缓冲区中的设置,来决定某些区域要不要渲染。详细学习请见:OpenGL文档。使用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地址:链接,可以方便我们查看结点的结构和数量,判断是否存在结点过多的情况。如果发现结点数量过多,并且结点频繁创建销毁,例如游戏中的小怪、子弹等数量比较多的重复物体,通常可以通过回收工厂进行优化。回收工厂就是结点用完之后,不销毁,而是缓存起来,下次获取结点可以直接复用缓存中的结点,而不需要重新创建。Cocos本身提供了回收工厂的接口 NodePool,可以了解一下:Cocos文档。游戏中的碰撞检测,也会比较耗性能。我们可以尽量使用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能否解码,所以针对不同的平台,需要使用不同的纹理压缩算法。关于纹理压缩算法的介绍,推荐看这篇文章。Etc1绝大部分的安卓设备支持,PVRTC所有的iOS设备支持。如果不需要支持alpha通道,安卓选择 Etc1RGB、iOS选择 VRTC4bitsRGB 即可。如果需要支持alpha通道,安卓选择 Etc1RGBSeparateA,iOS选择 VRTC4bitsRGBASeparateA。对于不用的内存,我们也要及时释放,防止内存泄漏。分自动释放和手动释放两种。对于静态资源的释放,可以通过勾选场景自动释放选项来实现:这样在场景切换后,场景中的静态资源就会被自动释放了。如果不想等到切换场景才释放静态资源,也可以使用 cc.assetManager.releaseAsset 进行手动释放。有一个坑点是,动态加载的资源无法在场景切换时,跟随静态资源自动释放。需要通过 cc.setAutoReleaseRecursively 手动设置一下:这样资源在场景切换时,会自动释放这部分动态加载的资源。也可以通过 cc.loader.releaseRes 手动释放动态加载资源。紧追技术前沿,深挖专业领域扫码关注我们吧!
|
|