CPU性能优化

使用外部工具

  • 如果游戏出现性能问题,切忌自行猜测或揣测成因,一定要使用Unity Profiler、FrameDebug和平台专属工具来准确找出卡顿的问题来源。

代码优化

  • 降低内存垃圾回收(GC)对性能的影响
    Unity使用的是Boehm-Demers-Weiser垃圾回收器 ,它会中止主线程代码运行,在垃圾回收工作完成后再让其恢复运行。

    请注意,部分多余的托管内存分配会造成GC耗能高峰:

    • Strings(字符串):在C#中,字符串属于引用类型,而非值类型。我们需要减少不必要的字符串创建或更改操作,尽量避免解析JSON和XML等由字符串组成的数据文件,将数据存储于ScriptableObjects,或以MessagePack或Protobuf等格式保存。如果你需要在运行时构建字符串,可使用StringBuilder类。

    • Unity函数调用:部分函数会涉及托管内存分配。我们需要缓存数组引用,避免在循环进行中进行数组的内存分配,且尽量使用那些不会产生垃圾回收的函数。比如使用GameObject.CompareTag,而不是使用GameObject.tag 手动比对字符串(因为返回一个新字符串会产生垃圾数据)。

    • Boxing(打包):避免在引用类型变量处传入值类型变量,因为这样做会导致系统创建一个临时对象,在背地里将值类型转换为对象类型(如int i = 123; object o = i ),从而产生垃圾回收的需求。尽量使用正确的类型覆写来传入想要的值类型。泛型也可用于类型覆写。

    • Coroutines(协同程序):虽然yield不会产生垃圾回收,但新建WaitForSeconds对象会。我们可以缓存并复用WaitForSeconds对象,不必在yield中再度创建。

    • LINQ与Regular Expressions(正则表达式):这两种方法都会在后台的数据打包期间产生垃圾回收。如果需要追求性能,请尽量避免使用LINQ和正则表达式,转而使用for循环和列表来创建数组。

    • 定时处理垃圾回收
      如果你确定垃圾回收带来的卡顿不会影响游戏特定阶段的体验,你可以使用System.GC.Collect来启动垃圾数据收集。

    • 增量式垃圾回收不会在程序运行期间长时间地中断运行,而会将总负荷分散到多帧,形成零碎的收集流程。如果垃圾数据收集对性能产生了较大的影响,可以尝试启用这个选项来降低GC的处理高峰。

  • 降低每帧的代码量

    • 有许多代码并非要在每帧上运行,这些不必要的逻辑完全可以在Update、LateUpdate和FixedUpdate中删去。这些事件函数可以保存那些必须每帧更新的代码,任何无须每帧更新的逻辑都不必放入其中,只有在相关事物发生变化时,这些逻辑才需被执行。

    如果必须要使用Update,可以考虑让代码每隔n帧运行一次。这种划分运行时间的方法也是一种将繁重工作负荷化整为零的常见技术。

  • 避免在Start/Awake中加入繁重的逻辑
    当首个场景加载时,每个对象都会调用如下函数:

    • Awake

    • OnEnable

    • Start
      在应用完成第一帧的渲染前,我们须避免在这些函数中运行繁重的逻辑。否则,应用的加载时间会出乎意料地长。

  • 避免加入空事件

    • 即使是空的MonoBehaviours也会占用资源,因此我们应该删除空的Update及LateUpdate方法。

    • 如果你想用这些方法进行测试,请使用预处理指令(preprocessor directives)

  • 删去Debug Log语句

    • Log声明(尤其是在Update、LateUpdate及FixedUpdate中)会拖慢性能,因此我们需要在构建之前禁用Log语句。

  • 使用哈希值、避免字符串

    • Unity底层代码不会使用字符串来访问Animator、Material和Shader属性。出于提高效率的考虑,所有属性名称都会被哈希转换成属性ID,用作实际的属性名称。

    在Animator、Material或Shader上使用Set或Get方法时,我们便可以利用整数值而非字符串。后者还需经过一次哈希处理,并没有整数值那么直接。

    使用Animator.StringToHash来转换Animator属性名称,用Shader.PropertyToID来转换Material和Shader属性名称。

  • 选择正确的数据结构

  • 避免在运行时添加组件

    • 在运行时调用AddComponent会占用一定的运行成本,Unity必须检查组件是否有重复或依赖项。

  • 缓存GameObjects和组件

    • 用Vector3.zero代替 new Vector(0, 0, 0)

    • 调用GameObject.Find、GameObject.GetComponent和Camera.main(2020.2以下的版本)会产生较大的运行负担,因此这些方法不适合在Update中调用,而应在Start中调用并缓存。

  • 对象池(Object Pool)

  • 使用ScriptableObjects(可编程对象)

  • 通过一系列取巧(符合一定规律)或作假的手段,达到性能消耗少的同时,效果和原来几乎一致。

    • 如:使用精灵图集、List.Clear代替New List,防止出现GC。

  • 平衡(消耗转移)

    • 增加一项指标,减少另一项(超标)指标。
      如:静态合批就是增加内存,减少DrawCall。

  • GetComponent
    性能:(泛型) > typeof(T) > string
    频繁调用GetComponent方法(一般为碰撞)会造成CPU的开销,但是对GC几乎没有影响

  • 定时器
    优势:Update定时器 > Invoke > 协程
    若非长时间异步操作(Http传输、Asset加载、文件I/O),否则不要用协程

  • 字符串连接效率
    String. >Format(或Join、Concat)> StringBuilder > +号
    注意:都会产生GC

Unity编辑器优化

  • 物理优化

    1. 碰撞盒的使用(在大量使用的情况下才需要考虑优化)
      精度:MeshCollider > 其他Collider
      消耗:MeshCollider >> 其他Collider

    2. 碰撞检测
      精度:连续动态 > 连续 > 离散
      消耗:连续动态 >> 连续 > 离散

    3. 设置碰撞矩阵(Layer C ollision Matrix)

    4. 射线
      设置射线有限的长度(避免额外的消耗)
      设置碰撞层(layerMask)

减少DrawCall

  1. 合批

    为啥要合批?
    批量渲染是通过减少CPU向GPU发送渲染命令(DrawCall)的次数,以及减少GPU切换渲染状态的次数,尽量让GPU一次多做一些事情,来提升逻辑线和渲染线的整体效率。

    1. 离线合批(Offline Batch)
      离线合批就是在游戏运行前,先用工具把相关资源做合批处理,以减轻引擎实时合批的负担。
      适合离线合批的是静态模型和场景物件。如场景地表装饰面:石头/砖块等等。

      离线合批方式有:

      1. 美术利用专业建模工具合批。如3D Max/Maya等。

      2. 利用引擎插件或工具。如Unity的插件MeshBaker和DrawCallMinimizer,可以将静态物体进行合批。

      3. 自制离线合批工具。如果第三方插件无法满足项目需求,就要程序专门实现离线合批工具。

    2. 实时合批(Runtime Batch)
      Unity引擎内建了两种合批渲染技术:Static batching(静态合批)和Dynamic batching(动态合批)。

      1. 静态合批

        • PlayerSettings中开启static batching,对需要静态合批物体的Static打钩即可\

        • 前提:
          共享相同的材质
          运行时不能移动,旋转或缩放

        • 缺点:打包之后体积增大,应用运行时所占用的内存体积也会增大。
          需要额外的内存来存储合并的几何体。

      2. 动态合批

        • Unity自动处理

        • 限制:

          1. 900个顶点以下的模型。

          2. 如果我们使用了顶点坐标,法线,UV,那么就只能最多300个顶点。

          3. 如果我们使用了UV0,UV1,和切线,又更少了,只能最多150个顶点。

          4. 如果两个模型缩放大小不同,不能被合批的,即模型之间的缩放必须一致。

          5. 合并网格的材质球的实例必须相同。即材质球属性不能被区分对待,材质球对象实例必须是同一个。

          6. 如果他们有Lightmap数据,必须相同的才有机会合批。

          7. 使用多个pass的Shader是绝对不会被合批。因为Multi-pass Shader通常会导致一个物体要连续绘制多次,并切换渲染状态。这会打破其跟其他物体进行Dynamic batching的机会。

          8. 延迟渲染是无法被合批。

  • GPU Instancing (新领域 学习成本高)

    • 如何使用GPU Instancing:
      -Unity自带的支持标准表面着色器,通过
      Create->Shader->StandardSurfaceShader(Instanced)

GPU性能优化

  • 减少顶点数量,简化计算复杂度。

  • 压缩图片,以适应显存带宽。

    • OpenGL ES 2.0使用ETC1、PVRTC格式压缩等等,在打包设置那里都有。

    • 使用mipmap。

  • 粒子系统

    • 灵活使用UWA的性能分析工具,可以有效定位对GPU压力贡献大的粒子系统。
      一种做法是,建立一个专门的空的测试场景,在其中顺次播放我们项目中要用到的粒子系统,然后使用UWA SDK进行打包测试提交GOT Online Overview报告,就可以在GPU耗时曲线处,结合测试截图找到播放时GPU耗时较高的粒子系统了。

    • 在中低端机型上对粒子系统的Max Particles最大粒子数量进行限制

    • UWA尤其关注Fragment Shader的屏占比、指令数和时钟周期数,渲染的像素越多、复杂度越高,说明该Shader资源越需要予以优化。

    • 一个特效可能包含有二三十个组件,这个组件有些是很耗的,在我们特效全开的时候,或者高档机上,我们会把它全部打开。但是有些中低端的机器,我们希望有选择性在这二十多个组件里面会有一些被disabled,我们让美术去挂接一个组件,这个组件自己来决定说,我在开中档特效的时候,这个特效的组件需不需要生效,我们通过修改组件的属性,让它播放的时候,能够有一部分是有效,有一部分是无效的,这个就是特效的分级。

  • UI

    • 当某个全屏UI打开时,可以将被背景遮挡住的其他UI进行关闭。

    • 对于Alpha为0的UI,可以将其Canvas Renderer组件上的CullTransparent Mesh进行勾选,这样既能保证UI事件的响应,又不需要对其进行渲染。

    • 尽可能减少Mask组件的使用,不仅提高绘制的开销,同时会造成DrawCall上升。在Overdraw较高的情况下,可以考虑使用RectMask2D代替。

    • 在URP下需要额外关心是否有没必要的Copy Color或者Copy Depth存在。尤其是在UI和战斗场景中的相机使用同一个RendererPipelineAsset的情况下,容易出现不必要的渲染耗时和带宽浪费,这样会对GPU造成不必要的开销。通常建议UI相机和场景相机使用不同的RendererData。

Life is a Rainmeter