Unity2017 优化总结

Unity2017游戏优化总结

前言

         “过早的优化是万恶之源”。

在问题成为真正的性能瓶颈之前,不要为了优化而优化。首先定位到了真正的性能问题,再从相应的问题出发,去解决性能问题。

         在Unity中,我们可以通过引擎提供的工具Profile 和 Frame Debugger,来定位到大部分的性能问题。

         以下所总结的优化点,可以在大家碰到相应的性能瓶颈时,找到对应的优化策略,去做相应的优化。

         (所有优化策略均总结自《Unity 2017 Game Optimization , Second Edition》一书)

 

脚本优化策略

  • 使用gameobject 进行判空时,即gameobject == null 操作,会造成一定的overhead。是因为gameobject是unity的对象,它除了在托管内存区域有一份对象外,还在native内存区域存在一份对象,这两份对象通过一个我们可以称为内存桥(Memory Bridge)来交流,这个多少会造成一些overhead。因此,我们在做这些操作时,可以用System.Object.RefereenceEquals(gameobject,null)来进行判空操作。(虽然这种提升非常非常小,不过也算是知道有这么回事)
  • 在使用gameobject对象进行获取string类型的操作时,(比如gameobject.name,gameobject.tag)时,都会造成上面的Memory Bridge的overhead,以及会产生gc。如果一定用到了unity的tagsystem去比较tag,尽量用unity提供的CompareTag()方法去比较tag。
  • 避免在运行时更改transform的parent。要么就在生成时直接指定它的父对象,要么就在生成时指定预估transform的子物体数量,预估transform的capacity属性。原因和unity组织transform时,这些对象在内存的布局有关,具体可搜索相关的内容。
  • 在每次使用transform对象改变物体的position,rotation,scale时,unity都会通知其他引擎子系统,比如物理系统,渲染系统等。因此在我们每帧都需要改变一个物体的transform属性时,好的办法是先把计算结果储存起来,在帧的最后时刻再赋值给transform属性。
  • 不要使用Gameobject.Find 和 SendMessage方法。
  • 不在摄像机视野内的脚本可以Disable掉,以免造成不必要的额外损耗,用BecameVisible和BecameInVisible Call.不过这俩callback必须在此gameobject上有render组件时才会生效
  • 加载大的prefab时,可以分解成多个小的prefab进行加载。加载一般使用异步的方式去加载prefab
  • 加载场景时以异步的形式去加载场景,且加载模式建议使用addtive模式去加载。
  • 尽量少用monobehaviour中的update方法。即减少monobehaviour中的update方法的个数。实验表明,调用常规方法1000次比调用update方法1000次要省时

 

Draw Call

Draw Call 的优化主要是在Batch 上,一共有Dynamic Batch 和Static Batch,这些优化的策略是非常常见,官方文档中也有很详细的说明,也属于老生常谈的范畴。这里就不作过多的笔墨了。官方文档链接:https://docs.unity3d.com/Manual/DrawCallBatching.html

 

使用static batch时,要将这些物体最开始就放入场景中,并勾选static才能使static batch生效。在运行时自动生成的物体,就算勾选了static batch,也不会和场景中默认的static batch 的物体合并到一起。

 

如果必须在运行时生成static batch 对象,那可以使用staticBatchUtillityCombine方法,这个方法比较耗时。注意,若使用了这个方法,某些原先没有标志为static的对象,它们是none-static的,但是它们的render组件将会保持static,因此可能会出现你移动了这个物体,而它在游戏中的表现并未移动。

 

 

资源相关设置

  • 对文件比较大,且使用比较频繁的音频文件,勾选音频文件的loadType 设置为compressed in memory
  • 使用3D Sounds时,将Audio Clip的设置“Force to Mono”打开,能节省50%的disk和内存开销
  • 优化音频时可以降低音频的采样率,一般22050赫兹就可以使用于人声或者音乐文件。降低采样可以减少音频大小以及运行时的内存占用。不过采样太低会影响音质。因此具体的数值需要测试迭代。设置的方法是:将音频的Sample Rate Setting 改为Override Sample Rate.
  • 明智的使用贴图的mip map level设置。勾选了mip map会导致贴图的内存占用增高原来的30%。因此,那些可以确保不会随着游戏的进行,与摄像机的距离发生明显变化的物体,例如UI,天空盒,主角等不需要勾选mip map。其他的物体,比如3D世界中的场景物件可以按需勾选mip map。
  • 适当降低texture的分辨率,节省空间。但是降低之前确保降低后的texture画质不受影响
  • Asynchronous Texture Uploading:可以使贴图文件在渲染线程中异步从硬盘上传到内存中,而不占用主线程资源。但如果贴图是通过代码加载到内存中,或者贴图文件开启了Read/Write选项,则这个贴图不会享受到Asynchronous Texture Uploading功能。相关设置在Quality Setting的other菜单下。
  • 在效果可以接受的情况下,减少模型的面数。
  • 比较简单的物体或者面数比较少的物体,且有不复杂的动作时可以考虑使用baked Animations来替代,这个操作必须得在动画制作工具中进行处理,好处是可以减少文件大小和内存占用

 

 

渲染相关优化

  1. 要优化渲染,首先必须定位到具体得性能瓶颈到底是在CPU端还是在GPU端。一个方法是直接打开profile窗口查看相关得cpu usage 和 gpu usage参数。

如果是cpu 得耗时高,gpu得耗时少,那结果显而易见;

若cpu 和 gpu得耗时都高,那么就查看cpu得耗时具体在哪些方法上。如果是一项叫做gfx.waitForPresent比较高,说明cpu在等待gpu得渲染完毕,因此瓶颈是在GPU端(前提是关闭了垂直同步,垂直同步有可能使gfx.waitForPresent耗时较高);

   如果还不能看出来,就使用暴力测试得方法来找问题。

   首先是明显得降低draw call,若帧数没有明显变化,则说明问题在gpu端。但有可能我们得draw call已经降到很低了,没法再降低了,那我们就增加一小部分draw call数值(比如不使用static batch 或 破坏部分dynamic batch),看帧数有没有明显的变化。若变低了,就说明问题可能是cpu端,若没有变化,则问题可能是再GPU端。

  1. 如果问题是在GPU端,那么大部分情况下可以从两个方面来优化,一个是GPU的填充率(Fill Rate),一个是Gpu 的内存带宽。

要看具体的问题是在填充率上,还是内存带宽上,我们只需要将游戏的分辨率降低,若帧数明显上升,则说明是填充率的问题;

若没变化,我们再看看是不是内存带宽的问题。可以通过减少贴图的分辨率来判断,若减少之后帧数明显上升,则说明问题是再内存带宽上。做这个操作有一个全局比较快速的设定,在Editor-Project Setting-Quality Setting-Texture Quality中可以快速设置贴图的质量。

若都不是以上的问题,则再看看是否发生再顶点着色器阶段(一般这种情况很少见),看是不是有非常复杂的顶点着色器代码或者导入了太多的mesh 数据到gpu中。

  1. 优化的方式
  • 减少几何体(模型)的复杂度,用工具降低面数,或者使用LOD相关功能
  • 使用GPU Instancing功能,减少Draw Call,这种一般用于绘制大量简单的相似的物体,比如成片的草地,树木等。
  • 使用LOD是用一定的内存和CPU时间,来换取Draw call,填充率和现存带宽。但这项技术建议不要一开始就使用,只有当我们意识到性能问题再GPU端时,且有一定的内存和CPU预算时,才考虑使用LOD功能。可以看到的是:如果游戏有很丰富的场景,摄像机可以看到很远时,就可以在早期就使用LOD,若是俯视角,固定视角,室内等场景时,或许并不需要使用LOD
  • 使用Culling Group来判断场景中的物体是否需要显示
  • 使用Occlusion Culling。它将消耗一定的内存和CPU时间以及硬盘空间,如果我们的场景不是那种有很多物体相互遮挡类型的,比如俯视角,则使用Occlusion Culling会是负优化。若场景是那种比如有林立的建筑,或者熙攘的人群等,则使用Occlusion Culling会减少填充率,draw call 以及overdraw.

 

粒子系统优化点

         在播放粒子系统时,默认会播放粒子所有子孙物体的particle system,它会导致每个子物体都调用GetComponent<ParticleSystem>()方法。如果粒子层级比较深,则有可能会造成性能问题。建议在播放时,调用播放的API中输入一个可选参数false(具体可以查看相关的粒子系统播放或者暂停的api)。若要播放全部子物体的粒子系统,可以先缓存这个粒子系统所有的particle system组件。(个人觉得这个优化点可以在真遇到这样的粒子时考虑这样做)

 

 

UGUI优化

  • UI动静分离。UGUI中,一个Canvas下,有任何一个UI元素的状态发生改变(几乎是除了颜色属性改变以外所有其他的变动)都会触发canvas下所有UI元素的Rebuild。Rebuild是一个比较耗时的操作。当一个canvas下有大量的UI元素时,每次rebuild所消耗的时间也会更长。
  • 一般的做法是在项目中,将Canvas分为三个,Static ,Incidental Dynamic,和Continuous Dynamic. Static 的Canvas下主要放一些背景图片的元素,基本上从来都不会动的。Incidental Dynamic的canvas下主要放一些响应事件的UI,比如提示框,包含UI按钮的UI等。Continuous Dynamic下放一些频繁变化的UI,比如进度条或者有动画的UI元素
  • 对于不交互的UI元素,要禁用Ratcast Target选项
  • 避免对UI元素使用Animator动画组件,它会持续改变UI的状态
  • 为World  Canvas指定一个Camera,否则它会每帧都执行Camera.Main,而Camera.Main会持续调用Gameobj.Find方法。
  • 如果项目中使用Scroll Rect组件,要把它放在一个单独的Canvas下,并禁用这个canvas的pixel perfect选项。当scroll Rect下的UI元素,降低到一定程度时,用代码主动暂停Scroll Rect的运动,这样可以避免一些不必要的消耗。

 

 

Shader 优化

  • shader中尽量使用较小的数据类型。一般在移动平台上,Gpus计算小的数据类型能够更有效率一些,所以我们要适当对float (32 bit), half(16 bit) ,fixed(12bit)进行转换。一般颜色和单位向量尽量使用fixed,而其他情况尽量使用half(-6万多 ~ 6万多的取值范围),否则才使用float
  • 在Swizzling 时,不要改变精度。例如:

float4  input = float4(1.0,2.0,3.0,4.0);

float2  temp1=input.xz;                   (这个过程就是swizzling,这里精度未变化)

half2  temp2 = input.xz;                  (这里精度由float 变为了 half,避免这样做)

  • 禁用不必要的shader特征,比如ZWriting,Alpha Testing等等
  • 移除shader中不需要的数据,Property等
  • 减少复杂的数学计算,而是将数学计算结果保存在一张图的某个通道中
  • 如非必要,尽量不要使用条件分支语句(if-else)。
  • 尽量减少shader代码中数据的依赖关系,最大化利用GPU多核并行的优势
  • 使用Shader Based LOD可以适当的降低Fill Rate
  • 使用更少的贴图数据。即降低贴图的分辨率,或者使用贴图的mip map level,可以减少显存到Texture Cache之间的传输数据量
  • 关注显存的容量。显存中的Texture大部分是在场景初始化时从CPU提交到GPU的,在运行时,若有新的Texture需要显示(即Instantiate 新的prefab等),也会从Cpu提交到GPU中,这个过程一般是异步发生的(Asynchonous Texture Uploading),我们需要避免在运行时太频繁的上传Texture 到GPU端

 

 

Lighting 优化

  • 适当使用实时阴影,选择合适的Shadow Distance。更高的Shadow Resolution和Shadow Cascades将会增加填充率和显存带宽
  • Soft Shadow 并不会比 Hard Shadow消耗更多的内存和CPU,唯一的区别是Soft Shadow有更复杂的shader,如果游戏的填充率不是瓶颈,则可以使用soft shadow来提升画质
  • 使用light组件中的Culling Mask属性,让光线只影响指定的物体
  • 使用Baked  LightMap光照图,烘焙场景。

 

 

移动平台的渲染优化

  • 避免Alpha Testing,当前Alpht Testing在移动平台上性能不佳
  • 使Draw Call最小化
  • 使同时使用的材质数量最小化
  • 使用最合适的Texture Size。不同的OpenGL ES版本支持不同的Max Texture Size。若我们使用的texture  size超过了目标版本支持的最大size,则Cpu会在提交到GPU前自动将Texture size缩小。这会增加加载时间,且影响到画质。
  • 使用POT(Power of Two)的正方形贴图。这会有利于引擎将贴图压缩成对GPU友好的贴图格式
  • Shader中尽量使用最合适精度的变量。比如能用fixed的地方,就别用half。

 

 

 

内存管理

Unity的内存区域一共分为三个区域:

  1. 托管区域:这个区域包括所有的MonoBehaviour脚本和自定义的C#类,都会放进这个区域。同样,这个区域的内存释放也都有垃圾收集器来管理。
  2. Native区域:这个区域包括用C++编写的引擎代码,以及引擎内部的资源数据(贴图,音频,网格等),还有就是引擎的各个子系统(渲染,物理,input system等),还有一部分是一些重要的游戏对象的内部数据结构,比如Gameobject,Component等数据的C++表示,这些对象往往在托管区域都有一层对它们的包装(Wrapper),因此我们才可以在托管区域用C#对象引用到它们
  3. 外部库的内存区域。比如DirectX 和 OPEN GL库,还有一些在项目中包含的插件等(Plugins文件夹下)

 

运行时的GC:

         在游戏中,我们进行内存申请时(new class操作),一般会进行以下的流程:

  1. 确保是否有足够大小的连续空间来分配给新对象
  2. 若没有,则遍历所有直接引用或间接的引用,找到所有能达到的对内存区域,并把它们设置为可达到
  3. 再次遍历这些引用。将那些没有标记为可达到的内存区域标记为可回收状态
  4. 遍历这些被回收的区域,看回收了这部分区域是否有足够大小的连续空间来创建新的对象
  5. 若没有,则向操作系统申请一块新的内存来扩展当前的堆
  6. 分配新的内存空间给新的对象

 

定位代码问题:

         这个问题一般主要在Profile中查看GC的值就好了,可以在Cpu usage和Memory Usage的窗口中查看到。看是否存在较大的GC。这是一个长期的过程,我们要时刻保持警惕。

 

GC的优化:

  • 在性能不敏感的时期主动出发垃圾回收(System.GC.Collect方法),比如在切换场景,暂停游戏时等
  • 在使用string时,若有多字符的链接操作(即+操作符),若要处理较多字符串的连接,则使用stringBuilder类;若较少,则使用String.Format方法或者String.Concact方法。多字符连接数大于2时,就避免使用+操作符了。
  • 在调用UnityAPI时,那些会返回一个数组的API都会在堆上分配新的内存空间,比如GetComponents<T>(),mesh.vertices等。每次调用都会创建一个全新的数据,因此,我们必须谨慎的调用这些方法,并缓存结果。
  • 必要情况下,在Dictionary的键中,我们要缓存Unity对象时,使用UnityObject的InstanceID来作为字典的键,而不是直接使用Object的引用
  • 如果内存消耗或者GC问题比较严重时,也应该避免在运行时频繁的使用Coroutines和StartCoroutines方法
  • 在项目代码中,不要使用.NetFrameWork中的LINQ和正则表达式的相关方法特性,这些方法会造成明显的性能问题。(个人觉得在写编辑器拓展工具时可以一定程度的使用:))
  • 运行时使用各种对象池,包括c# Object和unity 中Prefab 的对象池,避免频繁的创建和删除所带来的GC。

        

总结

         以上内容为本人做出的相关总结,只是列出了书中一部分内容。在原书中会对这些内容做更详细的阐述,以及还有其他方面的介绍。如果有时间的话,建议阅读原书。

         另外,列出一些unity官方的关于优化相关的视频网址(自备梯子=。=):

                   https://www.youtube.com/watch?v=n-oZa4Fb12U

                   https://www.youtube.com/watch?v=_wxitgdx-UI&t=604s

                   https://www.youtube.com/watch?v=W45-fsnPhJY

                            PS:Youtube有自动翻译功能(英文字幕),这样看会好理解很多

 

         以上为本文档所有内容,如果文档内有错误,请大家踊跃指出,互相学习!

 

 

 

 

猜你喜欢

转载自blog.csdn.net/q568360447/article/details/82222016