Cocos Creator 游戏性能优化指南

性能优化

  • 引言
  • 一、减少Draw Call
    • 什么是Draw Call?
    • 为什么要减少Draw Call?
    • 减少Draw Call的方法
        • 1、剔除
          • I、视锥剔除:摄像机的位置和视角形成一个视锥体,只有位于视锥体内的对象才会被渲染。可以通过检查对象的包围盒(Bounding Box)是否与视锥体相交来判断对象是否需要渲染。
          • II、遮挡剔除:如果一个对象被其他对象完全遮挡,则该对象不需要被渲染。
          • III、背面剔除:对于封闭的几何体来说,朝向摄像机背面的面不需要被渲染。
          • IV、距离剔除:距离相机太远的物体不需要渲染。
        • 2、LOD
        • 3、图集
          • I、静态图集
          • II、动态图集
          • III、示例
  • 二、使用对象池
    • 1、什么是对象池?
    • 2、对象池的优缺点
    • 3、对象池的实现
  • 三、优化资源加载
    • 1、延迟加载
    • 2、预加载资源
  • 四、减少动画开销
          • 1、**减少动画帧数**:减少动画帧数可以减少CPU和GPU的负担。对于快速动作,玩家不会注意到少量帧的减少。
          • 2、**骨骼动画优化**:骨骼动画可以通过控制少量骨骼来驱动大量顶点,减少计算量。减少骨骼数量和复杂度可以进一步降低计算开销。
          • 3、**使用对象池技术复用动画对象**:通过对象池技术复用动画对象,避免频繁创建和销毁动画对象,减少内存分配和回收的开销。
  • 五、代码优化

引言

游戏开发中,流畅的游戏体验是玩家最关心的问题之一。一个卡顿的游戏会严重影响玩家的体验,甚至让玩家失去继续玩的兴趣。因此,优化游戏性能是每个游戏开发者必须掌握的技能。本文将详细介绍在使用Cocos Creator进行游戏开发时的一些性能优化技巧。

一、减少Draw Call

什么是Draw Call?

Draw Call是指CPU向GPU发出的命令,用来告诉GPU绘制某个图形图像。

为什么要减少Draw Call?

每一次Draw Call都会涉及到CPU和GPU之间的通信,这种通信是有成本的。大量的Draw Call会导致以下问题:

  • CPU开销:每个Draw Call都需要CPU发出命令,如果命令太多,会占用大量的CPU资源。
  • GPU开销:GPU需要处理每一个Draw Call的命令,导致GPU资源的浪费。
    在这里插入图片描述

GPU的绘制能力非常强大,能一次处理大量数据。但每一次Draw Call前,CPU都要做一系列的准备工作,才能让GPU正确渲染出图像。举个例子,假设你的网速是10M/s,传输一个1000M的压缩包,和传输1000个1M的文件,谁的速度快。【传输 1 个 1000M 的文件要比传输 1000个 1M 的文件要快得多得多】。因为在每一个文件传输前,CPU 都需要做许多额外的工作来保证文件能够正确地被传输,而这些额外工作造成了大量额外的性能和时间开销,导致传输速度下降。

减少Draw Call的方法

1、剔除

将不满足条件的对象整体移除,以下是实现算法:

I、视锥剔除:摄像机的位置和视角形成一个视锥体,只有位于视锥体内的对象才会被渲染。可以通过检查对象的包围盒(Bounding Box)是否与视锥体相交来判断对象是否需要渲染。
// 伪代码示例
public isInFrustum(camera, object) {
let frustum = camera.getFrustum();
let boundingBox = object.getBoundingBox();
return frustum.intersects(boundingBox);
}
II、遮挡剔除:如果一个对象被其他对象完全遮挡,则该对象不需要被渲染。
III、背面剔除:对于封闭的几何体来说,朝向摄像机背面的面不需要被渲染。
IV、距离剔除:距离相机太远的物体不需要渲染。
2、LOD

LOD算法是一种优化技术,用于减少远处或不重要物体的渲染复杂度,以提高整体渲染性能。它通过动态调整模型的细节级别,根据对象与摄像机的距离或重要性来选择不同的细节级别进行渲染。

在这里插入图片描述

  • 为同一个物体,配置不同材质(少Pass、少贴图、少计算)
  • 为同一个物体,配置不同的Mesh。例如高细节(High Poly)、中细节(Medium Poly)和低细节(Low Poly)。在渲染时,根据对象与摄像机的距离选择合适的细节级别进行渲染。
3、图集
I、静态图集

将多个小图片合并成一个大图片的方法,这样在渲染多个小图片时,只需要一次Draw Call就可以完成绘制。

Cocos Creator提供了自动图集(Auto Atlas)功能,可以方便地将多个小图自动合并成一个大图。在Cocos Creator中,可以通过资源管理器创建自动图集:

  1. 打开资源管理器,右键选择要合并的小图片文件夹。
  2. 选择“创建 -> 自动图集”。
  3. 在自动图集属性中,可以调整合并规则和参数。

使用图集

// 示例代码
resources.load('path/to/atlas', cc.SpriteAtlas, (err, atlas) => {
    if (err) {
        console.error(err);
        return;
    }
    let frame = atlas.getSpriteFrame('sprite_name');
    this.node.getComponent(cc.Sprite).spriteFrame = frame;
});
II、动态图集

Cocos Creator 提供了动态图集功能,实现机制如下:

  1. 从动态图集中获得一张 2048 x 2048 的空白纹理
  2. 当渲染一张小图时(任何一边不超过 512),如果这张小图没有被合并过,则将这张小图合并到这张空白纹理上(如果图集空间不够,则会新开空白纹理)
  3. 修改当前 2D 元素的 uv和图集信息

这样一来,小图就可以根据顺序实现自动图集分布,相邻的两个元素使用的图集会尽可能的一致。

动态图集功能在 Web 端默认开启,如果想要禁用,则需要调用:

// 禁用动态图集
DynamicAtlasManager.instance.enabled = false;

这个机制会有几个缺点。

  1. 需要开启图片内存缓存,会增一倍的内存开销
  2. 由于需要内存数据支持,目前PVR/ETC等GPU压缩格式的纹理,不支持动态图集
  3. 如果需要渲染的2D元素过多,会很容易导致图集交叉使用的情况
  4. 图集只会在场景切换时清空,对单场景不友好
III、示例

cocos creator的渲染流程中,会先渲染父节点,然后逐个渲染子节点。简单讲:深度优先。

在这里插入图片描述

因为 item 节点下的 Sprite 与 Label 节点渲染类型不同,并相互间隔排列,引擎无法向 GPU 批量提交渲染数据。

因此渲染一个 item 需要 DrawCall 4次:Sprite → Label → Sprite → Label。

优化:将Cache Mode模式改成BITMAP。Cache Mode有三种模式。

在这里插入图片描述

  • NONE:每一个 Label 都会生成为一张单独的位图,且不会参与动态合图,所以每一个 Label 都会打断渲染合批
  • BITMAP:当 Label 组件开启 BITMAP 模式后,文本同样会生成为一张位图,只要符合动态合图要求就可以参与动态合图,和周围的精灵合并 DrawCall(注意 BITMAP 模式只适用于不频繁更改的文本)。
  • CHAR:当 Label 组件开启 CHAR 模式后,引擎会将该 Label 中出现的所有字符缓存到一张全局共享的位图中,相当于是生成了一个 BMFont(适用于文本频繁更改的情况,对性能和内存最友好)。

二、使用对象池

1、什么是对象池?

对象池技术(Object Pooling)是一种优化方法,用于管理和重复利用对象,减少频繁创建和销毁对象的开销,特别适用于需要频繁生成和回收对象的场景,如子弹、敌人、特效等。通过对象池技术,可以显著提升游戏性能,减少内存碎片和垃圾回收的压力。

2、对象池的优缺点

优点

  • 提高性能:对象池通过重复利用已经创建的对象,避免了频繁的对象创建和销毁操作,从而提高了系统的性能。相比于每次都创建新的对象,从对象池中获取已经存在的对象可以节省系统开销,并显著减少了系统响应时间。
  • 节约内存和减少垃圾回收:对象池可以减少垃圾回收的频率,因为对象的重复利用降低了新对象的创建量。这样可以减少系统对内存的占用,提高内存的利用效率。
  • 资源管理和控制:对象池可以对对象进行统一的管理和控制,包括对象的创建、初始化、回收和销毁。通过对象池,可以有效地管理系统对资源的占用和释放,避免资源泄露和浪费。

缺点

  • 增加初始内存占用:对象池在初始化时会创建大量对象,占用一定的内存。

3、对象池的实现

具体实现请移步:对象池的制作

三、优化资源加载

1、延迟加载

延迟加载是指在需要使用资源时才进行加载,而不是在游戏启动时一次性加载所有资源。这样可以减少初始加载时间,提高游戏的启动速度。可以使用场景切换或事件触发时进行加载。

2、预加载资源

在游戏加载阶段预先加载一些必要资源,以减少游戏运行时的卡顿。

图像:常用的装备图片、角色展示图片等常用显示图片可以在加载场景的时候就先预加载,存放在Map里面,使用时直接获取。

// 示例代码
resources.preload(['path/to/resource1', 'path/to/resource2'], (err, assets) => {
    // 资源预加载完成
});

四、减少动画开销

1、减少动画帧数:减少动画帧数可以减少CPU和GPU的负担。对于快速动作,玩家不会注意到少量帧的减少。
// 示例代码
let animation = node.getComponent(cc.Animation);
let clip = animation.getClip('animationName');
clip.sample = 10; // 将帧率降低到10帧每秒
animation.play('animationName');
2、骨骼动画优化:骨骼动画可以通过控制少量骨骼来驱动大量顶点,减少计算量。减少骨骼数量和复杂度可以进一步降低计算开销。
3、使用对象池技术复用动画对象:通过对象池技术复用动画对象,避免频繁创建和销毁动画对象,减少内存分配和回收的开销。

五、代码优化

1、尽量减少临时对象的创建,避免频繁触发垃圾回收。

for (let i = 0; i < 1000; i++) {
       let obj = getObject(); // 避免在循环中频繁创建新对象
   }

2、减少不必要的更新逻辑。确保update函数中只包含必要的逻辑,避免频繁调用耗时的操作。少在UPDATE里面刷新内容。多用事件触发等方式刷新。

//减少不必要的更新逻辑。确保update函数中只包含必要的逻辑,避免频繁调用耗时的操作。
update(dt) {
    if (this.needUpdate) {
        this.performUpdate(); // 仅在需要时执行更新逻辑
    }
}
//多用事件触发等方式刷新内容,避免频繁调用 update。
this.node.on('customEvent', this.onCustomEvent, this);
上一篇:puppeteer 爬虫初探


下一篇:排序矩阵查找-解答思路