返回「计算机、信息技术与工程」

Unity性能优化知识体系

更多
Markdown 结构化数据
本文目录 55 个章节

Unity性能优化知识体系

warning · 历史框架,阈值需重建 本文保留早期 Unity 项目的优化分类框架。文中的设备型号、插件名、接口和毫秒/内存数值是历史案例,不是跨版本通用标准。当前项目应在目标 Unity 版本、脚本后端、渲染管线、设备档位与构建配置上重新采样。

当前项目的可复现工作流

  1. 定义代表性场景、设备档位、画质、帧率目标和构建提交。
  2. 先用系统工具确认 CPU、GPU、内存、I/O 或热/功耗中的主要约束。
  3. 在 Development Build 与目标设备上采集多轮数据,记录中位数、尾部帧和峰值。
  4. 用 Unity Profiler、Memory Profiler、Frame Debugger 或平台 GPU 工具定位具体模块。
  5. 每次只改变一个变量,保留对照数据和回滚点。
  6. 在低、中、高档目标设备复测,避免只在编辑器或单一设备得出结论。

创建时间:2020/9/7 11:11

By Prin@UWA

  • Unity性能优化知识体系
    • CPU
      • 分类方式
      • 渲染模块
        • 降低Draw Call
          • Draw Call耗时过高的原因
        • Triangle
        • 不透明与半透明渲染
          • ParticleSystem
        • 其他优化方法
          • 简化场景和模型的相关资源
          • 根据某些高耗时的函数来优化
          • 根据手机机型的不同采用不同的渲染LOD
      • UI模块
        • UGUI
          • 优化方法
        • NGUI
      • 加载
        • 场景卸载
          • Destroy
          • Resources.UnloadUnusedAssets
        • 场景加载
          • 资源加载
          • Instantiate实例化
      • 逻辑代码
      • 物理模块
        • 引起 Physics.Simulate 开销较大的几个因素
          • Rigidibody
          • Contacts & Colider
        • 注意点
      • 动画
        • Mecanim动画系统的优势
        • 优化方案
          • 简化Mesh
          • Optimize Game Objects
          • SkinnedMeshRenderer.BakeMesh
          • Culling
      • ParticleSystem
        • 优化建议
        • 粒子系统拼合(Batch)
      • GC
    • 内存
      • 内存部分
        • Reserved Total
        • Mono堆内存
      • Texture
      • Mesh
        • Tangent属性
        • Color属性
      • Animation
      • Material
      • RenderTexture
    • GPU
      • Overdraw
    • Ref

性能优化是游戏项目开发过程中一个永恒的话题。玩家的需求和项目的要求永远在不停增长,同屏人数、屏幕特效和场景复杂度永远在向着“榨干”硬件的趋势逼近。所以,无论硬件设备发展到何种程度,无论研发团队有多么丰富的经验积累,性能优化永远是一个非常棘手而又无法绕开的问题。

该领域的知识结构特征:

  • 类别

  • 模块

  • 相关概念与知识点

  • 耗时原因

  • 指标与建议

  • 优化方法

CPU

分类方式

  • 引擎模块性能开销

    • TOP1: 渲染模块

    • TOP2: UI模块

    • TOP3: 加载模块

    • 逻辑代码

    • 动画模块

    • 物理模块

    • 粒子系统

    • GC调用

  • 自身代码性能开销

渲染模块

降低Draw Call

具体知识详见《Draw Call与Batching》

每次引擎准备数据并通知GPU的过程称为一次Draw Callthe command that tells the GPU to render a certain set of vertices as triangles with a certain state (shaders, blend state and so on)

在没有进行拼合的情况下,这一过程是逐个物体进行的,对于每个物体,不只GPU的渲染,CPU重新 设置材质/Shader 也是一项非常耗时的操作。 Draw Call的耗时其实是CPU耗时: The main reason to make fewer draw calls is that graphics hardware can transform and render triangles much faster than you can submit them. If you submit few triangles with each call, you will be completely bound by the CPU and the GPU will be mostly idle. The CPU won’t be able to feed the GPU fast enough.

DrawCall耗时的地方: There are some real costs with making draw calls, it requires setting up a bunch of state (which set of vertices to use, what shader to use and so on), and state changes have a cost both on the hardware side (updating a bunch of registers) and on the driver side (validating and translating your calls that set state).

The main cost of draw calls only apply if each call submits too little data , since this will cause you to be CPU-bound, and stop you from utilizing the hardware fully.

Making a single draw call with two triangles is cheap, but if you submit too little data with each call, you won’t have enough CPU time to submit as much geometry to the GPU as you could have.

(我还不太懂的)draw calls can also cause the command buffer to be flushed, but in my experience that usually happens when you call SwapBuffers, not when submitting geometry. Video drivers generally try to buffer as much as they can get away with (several frames sometimes!) to squeeze out as much parallelism from the GPU as possible.

目前,我们建议DrawCall的主体范围(5%~95%) 控制在[0,150]范围内。

方法:减少所渲染物体的材质种类,并通过Draw Call Batching 来减少其数量。

Draw Call耗时过高的原因
  1. 低端设备上GPU压力过高,导致低端设备Draw Call传输过慢,从而增大了耗时。

  2. DrawCall数量过大。

* UI重建耗时

*

Triangle

面片数过高会影响渲染效率

不透明与半透明渲染

一般来说,在移动游戏项目中,半透明物体渲染开销均要大于不透明物体的渲染开销 半透明渲染的开销:

  • UI界面

  • 粒子系统

  • 半透明场景物体(花草、光束等)

MMO游戏可能不透明渲染较高,那么需要尝试简化场景中的不透明模型网格、动态加载超大地形。

ParticleSystem

粒子系统常因透明混合、过度绘制、填充率与粒子数量产生显著 GPU 开销。历史项目曾以主体采样区间 0—3 ms 作为观察目标;当前预算应从整帧目标、设备档位和其他 GPU 阶段反推。

其他优化方法

简化场景和模型的相关资源

LOD(Levels of Detail)技术: 指根据物体模型的节点在显示环境中所处的位置和重要度,决定物体渲染的资源分配,降低非重要物体的面数和细节度,从而获得高效率的渲染运算。

SimpleLOD、Simplygon等简化工具来对网格模型进行简化,从而进一步加快渲染的效率,如果可以,建议蒙皮网格降低到1500面以下,场景渲染面片峰值低于10万面。

去除过量的网格资源、不合规的纹理资源等。

根据某些高耗时的函数来优化

查看某些耗时较高的渲染函数,如Camera.Render等 。

  • Occlusion Culling

  • Culling Distance

根据手机机型的不同采用不同的渲染LOD

低端机上使用更为简单的材质,从而来降低低端设备的GPU压力。

UI模块

目的:如何让UI系统使用尽可能小的CPU开销来达到设计师所设计的表现力

UGUI

主要看两个函数:

  • Canvas.SendWillRenderCanvases 该函数为UI元素自身发生变化(比如被Enable或者被缩放, 移动并不算 )时所产生的调用。发生在canvas被渲染之前。

  • Canvas.BuildBatch 该函数为UI元素合并的Mesh需要改变时所产生的调用。通常之前所提到的Canvas.SendWillRenderCanvases()的调用都会引起Canvas.BuildBatch的调用。另外,Canvas中的UI元素发生 移动也会引起 Canvas.BuildBatch的调用。

优化方法

一般来说,建议把UGUI的堆内存分配控制在3MB以内。

  1. Unity 5.2以后的版本,UI的几何创建和合批都由多线程完成,性能明显提升。具体可参见Unity官方博客:http://blogs.unity3d.com/cn/2015/09/07/making-the-ui-backend-faster/

  2. 尽可能将静态UI元素和动态UI元素分开,存放于不同的Canvas下。同时,对于不同频率的动态元素也建议存放于不同的Canvas中。

  3. 严格控制一个非静态的Canvas(即Canvas中会频繁出现变换的元素)中的UI元素数量,因为Canvas中所有的UI元素都会合并到一个Mesh中,一旦某个元素发生变化,则会引起Mesh发生变化,从而造成开销。

  4. 尽可能减少Mask组件的使用,使用Mask不仅会增加GPU端渲染的压力,同时也会造成CPU端DrawCall的明显上升。可尝试用RectMask2D来进行替换。

  5. 对UI纹理资源进行详细检测,查看其分辨率、格式等是否足够精简和优化。

其他:

  • 简化UI元素几何(如Tiled的Sprite、文本Outline等)

  • 在中低端设备上减少UI的数量避免战斗中过多的UI Rebuild消耗

UI性能问题统计定位方式: 不断点击UI模块的每个按钮,分析其对应的底层性能开销是否合理,比如:切换服务器时、进入主界面时、主界面与人物界面的切换时、人物界面切换到主界面时、主界面与商城界面的切换时、商城界面切换到主界面时、战斗界面等等时,通过在各个界面的测试来统计定位出CPU开销和堆内存分配的性能瓶颈,并给出详细的优化方案。

NGUI

UI性能相关函数:

  • UICamera.Update() 该函数通常在点击时出现开销。因此,当该函数的CPU开销较高时,通常都是因为调用了其他的较为耗时的函数引起。

  • UIRect.Update() 该函数通常在需要更新锚点位置时出现开销。因此,当该函数的CPU开销持续较高时,通常是因为当前场景中有较多的UI元素绑定了OnUpdate模式的锚点。

  • UIPanel.LateUpdate () 该函数为NGUI最主要的CPU开销,包含了 对所有UI界面包括其下UI元素的状态更新、网格重建、DrawCall合并等操作 。大量的UI变动或者不合理的UIPanel布局都有可能导致该函数出现较高的峰值。

  • UIRect.Start() 该函数主要涉及到UI元素的 初始化 操作,通常在UI界面被实例化时出现并产生一定的CPU开销。

UIPanel.LateUpdate是NGUI中CPU开销最大的函数。对它的的优化,主要着眼于UIPanel的布局,原则如下:

  1. 尽可能将动态UI元素和静态UI元素分离到不同的UIPanel中(UI的重建以UIPanel为单位),从而尽可能将因为变动的UI元素引起的重构控制在较小的范围内

  2. 尽可能让动态UI元素按照同步性进行划分,即运动频率不同的UI元素尽可能分离放在不同的UIPanel中

  3. 控制同一个UIPanel中动态UI元素的数量,数量越多,所创建的Mesh越大,从而使得重构的开销显著增加。

  4. 尽可能减少Mask组件的使用,使用Mask不仅会增加GPU端渲染的压力,同时也会造成CPU端DrawCall的明显上升。可尝试用RectMask2D来进行替换。

  5. 对UI纹理资源进行详细检测,查看其分辨率、格式等是否足够精简和优化。

  6. 对于UI元素,OnEnable和OnDisable都会进行较多的操作,因此即使不涉及到资源的加载,依然会有较大的CPU开销,因此,对于切换非常频繁的UI界面,我们建议更高效的做法:

* 改变UI的位置(以UIPanel为单位)来实现UI的隐藏和显示,因为是位置移动,所以并不产生多余的CPU消耗,同时又可以节省Enable和Disable的CPU开销。

* 通过设置相机的Culling mask 来实现 UI 界面的隐藏和显示,同样避免Enable/Disable操作。该做法可能会一定程度地提高内存的开销(UIDrawCall中存储的Mesh),因此可以根据UI切换的频率来决定是否要进行优化。

例如:HUD运动血条可能会出现较多,此时,建议研发团队将运动血条分离成不同的UIPanel,每组UIPanel下5~10个动态UI为宜。这种做法,其本质是从概率上尽可能降低单帧中UIPanel的重建开销。 Batch 合并Drawcall绘制HUD

加载

加载模块的性能开销比较集中,主要出现于 场景切换处 ,且CPU占用峰值均较高。 性能开销主要体现在两个方面:前一场景的场景卸载和下一场景的场景加载。

场景卸载

对于Unity引擎而言,场景卸载一般是由引擎自动完成的,即当我们调用类似Application.LoadLevel的API时,引擎即会开始对上一场景进行处理,其性能开销主要被以下几个部分占据:

Destroy

引擎在切换场景时会收集未标识成“DontDestoryOnLoad”的GameObject及其Component,然后进行Destroy。同时,代码中的OnDestory被触发执行,这里的性能开销主要 取决于OnDestroy回调函数中的代码逻辑

Resources.UnloadUnusedAssets

一般情况下,场景切换过程中,该API会被调用两次,一次为引擎在切换场景时自动调用,另一次则为用户手动调用(一般出现在场景加载后,用户调用它来确保上一场景的资源被卸载干净)。其耗时开销主要取决于场景中Asset和Object的数量。

场景加载

场景加载的性能开销又可分为:

资源加载

资源加载几乎占据了整个加载过程的90%时间以上,其加载效率主要取决于资源的加载方式(Resource.Load或AssetBundle加载)、加载量(纹理、网格、材质等资源数据的大小)和资源格式(纹理格式、音频格式等)等等。

Instantiate实例化

在Instantiate实例化时,引擎底层会查看其相关的资源是否已经被加载,如果没有,则会先加载其相关资源,再进行实例化,这大多数“Instantiate耗时问题”的根本原因

因此,提倡AssetBundle资源依赖关系打包并进行预加载,从而来缓解Instantiate实例化时的压力

Instantiate实例化的性能开销还体现在脚本代码的序列化上,如果脚本中需要序列化的信息很多,则Instantiate实例化时的时间亦会很长。最直接的例子就是NGUI,其代码中存在很多SerializedField标识,从而在实例化时带来了较多的代码序列化开销。

逻辑代码

绝大多数的项目中其性能开销都遵循着“二八原则”,即80%的性能开销都集中在20%的函数上。

耗时明显增高可能的原因:ILRuntime由于需要动态编译,代码运行效率非常低,一般不建议使用。

物理模块

CPU 均值:可重点观察 Physics.Simulate 的平均与尾部耗时。历史材料引用过 3 ms 目标,但它只能作为特定帧预算案例;当前项目应按帧率目标、物理复杂度和平台重新分配预算。

Unity 4.x版本所使用的物理引擎为Nvidia PhysX 2.8 5.x版本所使用的物理引擎为Nvidia PhysX 3.3

引起 Physics.Simulate 开销较大的几个因素

Rigidibody

该组件可使得游戏对象在物理系统的控制下运动。

移动设备,建议Rigidibody数量控制在50以下。

用Rigidbody组件配合CapsuleCollider,通过RigidBody.velocity来移动,会造成物理计算,特别是网格有很多Mesh Colider的时候,物理计算相当高

  • 建议尽量用Transform.Position代替物理计算。

  • 如果地形是凹凸不平又要有重力的表示,也可以用Navmesh去做,它所引起的工作量在于烘焙Navmesh,并且尽可能地贴合地表 ,可以完全不用物理计算。

Contacts & Colider

Contacts数量为碰撞对(Contact Pair)数量。任意两个发生碰撞的碰撞体都会产生一个“碰撞对”。一般来说,Contacts数量越大,则表明碰撞物体的数量越多,即物理系统的CPU开销越大。

注意点

  1. 如果你是用NGUI制作UI,则在NGUI界面打开后,往往会有Physics一下涨高的情况。这是因为NGUI为了实现点击事件的检测,在每个UI上都设有BoxCollider,所以当UI Widgets摆在同一深度并存在相互叠加的情况时,会造成较多不必要的Contacts。但实际上UI根本不需要任何物理计算。需要看看能否把UI层之间的碰撞检测关掉。

  2. OntriggerXXX(如OntriggerEnter)。这种情况一般是在脚本中触发了其他的逻辑调用,例如在主角被碰撞从而受到伤害时,创建一个伤害数字的UI,会造成逻辑计算开销,这些也会算到Physics开销中。所以如果你报告中的物理模块的数据都是正常的,但是物理开销依然很大,则需要大家再着重检测这个部分逻辑代码了。

动画

动画模块的开销表现在:

  • 动画片段数量

  • Animator.Update CPU耗时

  • Animation.Update CPU耗时

  • MeshSkinning.Update CPU耗时

Animation.Update 和 Animator.Update 分别表示Unity引擎两个不同动画模块的性能开销。前者对应Unity老版本的动画系统,后者对应Unity新动画系统Mecanim。

Mecanim动画系统的优势

  1. Mecanim动画系统的多线程计算性能较之老版本的单线程计算性能要好;

  2. Mecanim动画系统可以对GameObject开启 “Optimize Game Object” 选项。该选项为Unity引擎在4.3版本中加入的新功能,旨在优化Mecanim动画系统的底层计算开销。开启该选项,Animator.Update和MeshSkinning.Update的CPU占用均存在一定程度的降低;

  3. Mecanim动画系统的Retargeting功能可以让多个不同的角色使用同一套的AnimationClip资源,比如主城中的NPC角色,其大部分共性动画可尝试使用一套Idle、Wave等动画片段,从而进一步降低动画资源的内存开销;

  4. Unity引擎已经不再对老版本动画系统进行维护。

优化方案

简化Mesh

在尽可能保证动画效果的同时,对模型的网格进行简化,建议尝试使用Unity Asset Store中的SimpleLOD插件来对骨骼蒙皮网格进行简化;

Optimize Game Objects

关于MeshSkinning.Update, 一般来说该值的大小取决于蒙皮网格(Skinned Mesh)的面片数和骨骼数。所以如果该值过高,我们的建议是减面。同时我们建议开发团队勾选“Optimize Game Objects”,该方法是将fbx生成的GameObject的层级关系移除,使动画系统不用每帧再去更新这些骨骼节点(GameObject)的Transform,因此能一定程度上降低CPU开销,此优化选项默认是关闭的),该方法特别适合于在配置较低的手机上运用骨骼角色多的情况。

SkinnedMeshRenderer.BakeMesh

将一个蒙皮动画的某个时间点上的动作,Bake成一个不带蒙皮的Mesh,从而可以通过自定义的采样间隔,将一段动画转成一组Mesh序列帧。而后在播放动画时只需选择最近的采样点(即一个Mesh)进行赋值即可,从而省去了骨骼更新与蒙皮计算的时间(几乎没有动画,只是赋值的动作)。 作用:用内存换取计算时间,省去了蒙皮计算。在场景中大量出现同一个带动画的模型时,效果会非常明显。 该方法的缺点是内存的占用极大地受到模型顶点数、动画总时长及采样间隔的限制。因此,该方法只适用于顶点/面片数较少,且动画总时长较短的模型。 同时,Bake的时间较长,需要在加载场景时完成。

UWA曾经测试在红米2上跑了100个不到300面的角色,非常流畅。特别推荐下:该方法比较经典的适用场景为MOBA游戏中的小兵。

Culling

控制场景中具有Animator Controller组件的GameObject,建议通过调整其Culling选项来降低视锥体或一定范围外的动画组件更新。

ParticleSystem

粒子系统的开销在Unity 5.3以前的版本主要体现在ParticleSystem.Update CPU耗时上。

游戏打怪过程中渲染模块的主要开销为半透明渲染所占据。原因主要在于是技能特效播放时的粒子特效。

优化建议

建议研发团队根据移动设备对粒子系统进行管理,对于低端设备尽可能降低粒子系统的复杂程度和屏幕覆盖面积,从而降低其渲染方面的开销,提升低端设备的运行流畅性。 具体优化做法:

  • 在中低端机型上 降低粒子数 、同屏粒子数,比如仅显示“关键粒子特效”或自身角色释放的粒子特效等,从而降低Update的CPU开销;

  • 尝试 关闭离当前相机较远的粒子系统 ,离近后再进行开启,从而避免不必要的粒子系统Update的开销;

  • 尽可能 降低粒子特效在屏幕中的覆盖面积 ,覆盖面积越大, 层叠数 越多,其渲染开销越大;

粒子系统拼合(Batch)

引擎会将若干个材质相同且深度相同的粒子系统在渲染前进行合批(Batch),从而通过一个Draw Call来对其粒子系统进行渲染,进而降低粒子系统的渲染开销。

关于 渲染顺序与动态合批 :粒子系统的Draw Call动态拼合与 半透明物体的动态拼合 机制相当(粒子基本都是半透明材质)。而对半透明物体,由于其渲染顺序的限制(必须从后向前渲染,以保证渲染结果的正确性),动态拼合 只能对渲染顺序相邻且材质相同的物体有效 。而在决定半透明物体的 渲染顺序 时,Unity首先会按Shader中的RenderQueue进行排序;其次(相同RenderQueue时),会根据每个半透明物件到屏幕的距离,距离大的优先渲染。

因此,需要尽可能地将相同材质的粒子系统放在比较接近的深度下,才能更多地使动态拼合生效。 但通常由于相机的运动、粒子系统的分散分布等原因造成粒子系统之间的穿插,能够动态拼合的数量往往都是很少的。

GC

测试过程中系统垃圾回收操作(Garbage Collection)的调用次数 每次GC调用均会造成一定程度上的卡顿,降低了项目运行的流畅度。如果开发人员的逻辑代码分配堆内存过大过快的话,则GC调用的次数也会随之增加。 建议两次GC间隔在1000帧以上

内存

内存部分

Reserved Total

对于目前市场主流的Unity游戏来说,其内存占用主要集中在120~200MB。 我们建议把内存把控在150MB以内。这是经过了大量的项目优化后的经验数值。 本段原以 iPhone 4 和 512/768 MB Android 设备为目标,曾设置 Reserved Total 150 MB、进程内存约 200 MB 的历史项目预算。该设备与渠道条件已经过时,不能作为现代项目通用安全线。当前预算应覆盖目标设备最低规格、系统终止策略、峰值场景、图形驱动和第三方原生内存,并保留安全余量。 考虑到现在设备越来越高端,不少开发团队为了彰显美术表现力而舍弃了低端设备。 个人也觉得稍微高出这个标准也是无可厚非的事。

Mono堆内存

旧版 Unity/Mono 中,托管堆扩张后常不会立即把全部保留空间返还给系统。当前应区分 Used 与 Reserved,并验证 Mono 或 IL2CPP 后端、GC 模式和 Unity 版本;“只升不降”不是可跨版本套用的绝对规律。

Texture

使用RGBA32/ARGB32/RGB24等格式,不仅占用内存大,加载效率明显低于的硬件支持格式的纹理资源,因此建议尽可能少使用这种格式的纹理。建议使用Dither或ChromaPack压缩格式,这两种格式在遵守一定的使用限制后,可以减小内存占用,效果也比较好。 纹理格式问题参考UWA问答:https://answer.uwa4d.com/search/question?content=%E7%BA%B9%E7%90%86%E6%A0%BC%E5%BC%8F

Mesh

Tangent属性

Tangent属性一般为导入引擎时生成,会增大网格的内存占用,一般来说对于不使用法线贴图的网格,都是不需要导入Tangents属性。

Color属性

Color属性一般为需要顶点色数据、或需要存储在顶点色的特殊数据以实现复杂效果时需要导入。

Animation

Material

RenderTexture

GPU

Overdraw

Ref

https://blog.uwa4d.com/archives/Simple_PA_Rendering.html https://blog.uwa4d.com/archives/optimzation_cpu.html https://blog.uwa4d.com/archives/Simple_PA_Physics.html https://blog.uwa4d.com/archives/Simple_PA_Animation.html