Unity性能优化总结

一般游戏的性能指标有:帧率、稳定性、流畅性、加载时间(loading)、内存占用(这一项在移动设备上比较重要,有很多闪退原因就是由此造成的)、安装包大小、网络延迟、耗电量等。
程序:代码优化
美术:资源优化
策划:合理和设计方案以避免性能开销
性能优化主要从以下几个方面展开: CPU、GPU、内存

一、CPU

CPU的性能开销主要来自于以下两个方面:
1.引擎模块自身的性能开销:渲染(Draw Call)、动画、物理引擎、UI模块、粒子系统、资源加载、GC(Garbage Collection)。
2.游戏自身代码的性能开销:代码结构、循环、数据结构等。

渲染:

1.降低Draw Call
CPU每次在准备数据(顶点位置、法线、颜色、坐标纹理等)并通知GPU渲染的过程称为一次Draw Call,其实就是CPU对底层图形程序接口的调用。
Draw Call的消耗来源:如果每次Draw Call只提交少量的数据将导致CPU瓶颈,CPU无法将GPU填满。Draw Call对GPU的耗费在于硬件一直等待CPU提交数据,而无法得到有效利用。GPU大量的时间耗费在不断切换状态和正确性检测上。
降低Draw Call有以下几种方法:
①减少所渲染物体的材质种类 → 通过把纹理打包成图集来尽量减少材质的使用。
②通过Draw Call Batching来减少其数量 → 静态批处理和动态批处理。
批处理的思想:在每次调用Draw Call时尽可能多地处理多个物体(多个物体最好一起渲染,将批处理之前需要很多次调用的物体合并,之后只需要调用一次底层图形的接口就行),减少每一帧需要的Draw Call数目。
静态批处理的优点:自由度高,限制很少。
静态批处理的缺点:可能会占用更多的内存(额外的内存开销来存储合并后的几何数据),而经过静态批处理后的所有物体都不可以再移动了(即使在脚本中尝试改变物体的位置也是无效的)。
静态批处理的时间点:⑴在游戏导出的时候,在player setting中勾选static batching,这样导出包的时候就进行批处理,包体较大。⑵在游戏场景中勾选物体的static选项,在加载该场景的时候,会进行一次静态批处理的合并,这样导出来的包不大,但是在加载的时候会使得内存变大。
动态批处理的优点:Unity自动完成,实现方便,经过批处理的物体仍然可以移动,这是由于在处理每一帧时Unity都会重新合并一次网格。
动态批处理的缺点:限制有很多,可能一不小心就破坏了这种机制,导致Unity无法批处理一些使用了相同材质的物体。
③尽量少使用反光、阴影等,因为这样会使得物体多次渲染。
2.简化资源
3.LOD
Levels Of Detail是一种优化游戏效率的常用方法,它是根据物体在游戏画面中所占视图的百分比来调用不同复杂度的模型的,其缺点在于会占用大量内存,使用这个技术一般是在解决运行时流畅度的问题,以空间交换时间。
4.剔除Culling

UI模块:

1.动静分离
2.预加载、常驻、即时释放
3.图集
4.内存池
5.Active/Deactive
6.UISprite、Texture
7.不移动、不可见UI不更新
8.对于不交互的UI元素,关闭Raycast Target
9.资源预加载
10.shader预加载

在使用UGUI的过程中,有以下几点可能会成为影响游戏性能的原因:
⑴图集整理不规范
在UI界面设计的时候要考虑到重用性,比如一些公用的UI资源或使用频率较高的资源可列为公共资源,放在若干张大图集当中(1~3张)作为重用图集,在游戏开始时进行加载,必要的情况考虑常驻内存使用;对于一些特殊的、使用情况不多的UI,可对其按照使用功能划分为功能图集,在特定的时间进行加载;对于一些同时使用到重用图集与功能图集的UI,在功能图集的“留白”部分较多的情况下,可以考虑将部分在重用图集中出现的元素单独提取出来,合并到功能图集中,从而做到让UI只依赖功能图集,这样可以省去部分加载重用图集的资源消耗(可接受的冗余换取性能)。
UGUI自动打包图集时,有时候同一个Tag会自己打出多个Group图集,导致Draw Call增加,产生Group的主要原因有两种:
①纹理的格式不同
②纹理量太大,一个Group放不进
⑵UI的层级深度
有相同材质和纹理的UI元素是可以Batch的,可以Batch的UI(注意UI的层级)上下叠在一起不会影响性能,但是不能Batch的UI元素叠在一起就会增加Draw Call。要注意UI元素之间的层叠关系,有一些UI会有透明的部分,在设计和开发的过程中可能会在透明的部分上面叠加了其他的UI元素,这样就有可能造成Draw Call的增加,比如排列、列表、背包等情况。
有些情况可以考虑人为增加层级从而减少Draw Call,比如一个Text的层级为0,另一个可以Batch的Text叠在了图片A上,层级为1,那此时有两个Text因为层级不同会安排2个Draw Call,但如果在第一个Text下放一张透明图片(可以和图片A进行Batch),那两个Text的层级就一致了,Draw Call就可以减少一个。
⑶图文交叉
Image和Text组件,当Text叠在Image上面(比如Button),然后Text上又叠了一张图片,就会至少多2个Draw Call,这种情况可以考虑将字体直接印在下面的图片上。
⑷Mask
应避免使用Mask,其实Mask组件功能有时候可以变通,比如设计一个边框,让这个边框叠在最上面,底下的UI移动时,就会被这个边框给遮住,Mask以内的和Mask以外的UI无法Batch
如果需要使用Mask,需要评估一下Mask会带来的性能消耗,如果Mask内的UI是动态生成的话,需要注意UI之间是否有重叠 → 可能某些透明部分存在重叠,需要细致观察。
⑸Raycast Target属性
用不上的UI尽量关闭这个属性
⑹Alpha = 0
在某些情况下可能会使用到将Canva Group组件中的Alpha设置为0来隐藏UI,虽然不会增加draw call,但在引擎处理的时候实际上还是画了一个透明度为0的面片,这种隐藏方法依然会触发GPU渲染。

加载模块

主要出现于场景切换处,且CPU占用峰值均较高 → 前一场景的场景卸载和下一场景的场景加载。
⑴场景卸载
调用SceneManger.LoadScene时,引擎即会对上一场景进行处理
其主要开销如下:
--Destroy
引擎在切换场景时会收集未标识成"DontDestroyOnload"的GameObject,然后进行Destroy。同时,代码中的OnDestroy被触发执行,这里的性能开销主要取决于OnDestroy回调函数中的代码逻辑。
--Resource.UnloadUnusedAssets
一般情况下,场景切换过程中,该API会被调用两次,一次为引擎在切换场景时自动调用,另一次则为用户手动调用(一般出现在场景加载后,用户调用他拉确保上一场景中的资源被卸载干净),其耗时开销主要取决于场景中Asset和Object的数量,数量越多、耗时越长。
⑵场景加载
--资源加载(90%以上)
其加载效率主要取决于资源的加载方式(R.L和AB加载)、加载量(纹理、网格、材质等资源数据的大小)和资源的格式(纹理格式、音频格式等)。
--Instantiate实例化
①资源加载,在Instantiate实例化时,引擎底层会查看其相关的资源是否已经被加载,如果没有,则会先加载其相关资源,再进行实例化 → Instantiate耗时的根本原因。
②除此之外,Instantiate实例化的性能开销还体现在脚本代码的序列化(当GameObject上Component数目比较多时,其Instantiate实例化性能会受到影响)和构造函数的执行上。(Awake和Start函数中的代码逻辑,其产生的开销也会被计算在Instantiate实例化内)
资源加载是加载模块中最为耗时的部分,其CPU开销在Unity中主要体现在Loading.UpdatePreloading和Loading.ReadObject

Loading.UpdatePreloading
Loading.UpdatePreloading这一项仅在调用类似LoadLevel(Async)的接口处出现,主要负责卸载当前场景的资源并且加载下一场景中的相关资源和序列化信息等。下一场景中,自身所拥有的GameObject和资源越多,其加载开销越大。(在很多项目中,存在另外一种加载方式,及场景为空场景,绝大部分资源和GameObject都是通过OnLevelwasloaded回调函数进行加载、实例化和拼合的,对于这种情况,Loading.UpdatePreloading的开销会很小)

Loading.ReadObject
Loading.ReadObject这一项记录的则是资源加载时的真正资源读取性能开销,基本上引擎的主流资源(纹理、资源、网络资源、动画片段等)读取均是通过该项来进行体现的。可以说这一项很大程度上决定了项目场景的切换效率。
另外,在使用Resources.UnloadUnusedAssets()时有可能造成一定的卡顿,尽量不要主动使用,在切换场景时会自动调用该API。

从点击应用到出现游戏画面,加载时间受哪些方面的影响?
①Resource文件夹中的资源数量。在游戏启动时,Unity引起会为Resource文件夹下的资源建立一个查找树来存放与其对应的索引,便于后续资源的加载。一般来说,Resource文件夹下资源数量越多,其构建时间越长,应用启动也就越慢。
②首场景的资源加载和相关代码的初始化工作。如果首场景的资源量越多,其脚本初始化的任务越繁重,则应用的启动时间也会越慢。

物理:

1.少用或不用mesh colider(网格碰撞器)
太复杂,网格碰撞器利用一个网格资源并在其上构建碰撞器。对于复杂网状模型上的碰撞检测,它要比应用原型碰撞器精确的多。
2.设置一个合适的Fixed Timestep
Fixed Timestep和物理计算有关,若计算的频率太高,增加CPU的开销
3.优化物理算法

GC(Garbage Collection)

GC:主要作用在于从已用内存中找出那些不再使用的内存,并进行释放
GC是Mono运行时的机制,而非Unity3D游戏引擎的机制,故GC不是用来处理引擎的Assets的内存释放的。
什么东西会被分配到托管堆上? → 引用类型(类的实例、字符串、数组等)
而值类型是分配到堆栈上而非堆上。
所以GC的优化其实就相当于是代码的优化。
下列两种情况会触发GC:
⑴当空闲内存不足时会触发GC。(PS:GC释放的内存只会留给Mono使用,并不会交还给,因此Mono堆内存是只增不减的)
⑵在代码中通过调用GC.Collect()手动进行GC,但是GC本身是比较耗时的操作,而且由于GC会暂停那些需要Mono内存分配的线程(C#代码创建的线程和主线程),因此无论是否在主线程中调用,GC都会导致游戏一定程度的卡顿,需要谨慎处理。
Mono内存泄漏:对象不再使用却没有被GC回收的情况 → 会导致空闲内存减少,GC频繁,Mono堆不断扩充,最终导致游戏占用的内存升高。
游戏中大部分Mono内存泄漏的情况都是由于静态对象的引用而引起的,因此对于静态对象尽量少用,对于不再使用的静态对象将其引用设置为null,使其可以即使被GC回收。
GC使用注意点:
⑴字符串连接的处理。因为将两个字符串连接的过程,其实是生成一个新的字符串的过程。而作为引用类型的字符串,其空间是在堆上分配的,被弃置的旧的字符串的空间会被GC当做垃圾回收。
⑵尽量不用使用foreach,foreach循环将会在每一次迭代中创建一个enumerator对象,这时候GC会对enumerator对象进行回收处理,消耗资源。
⑶不要直接访问gameobject的tag属性。比如if (MyGameObject.tag == “Player”)最好换成if (MyGameObject.CompareTag (“Player”))。因为访问物体的tag属性会在堆上额外的分配空间。
⑷使用对象池 → 实现空间的复用
⑸最好不用LINQ的命令,因为它们会分配临时的空间,同样也是GC收集的目标。
⑹尽量减少代码堆内存分配,应避免频繁创建和开辟空间,防止频繁触发GC,同时在Loading或者对性能不敏感的时候主动GC。

二、GPU

GPU的性能瓶颈主要存在于以下几个方面:
1.Fill Rate(填充率),是指显卡每帧或者说每秒能够渲染的像素数。在每帧的绘制中,如果一个像素被反复绘制(overdraw)的次数越多,那么它占用的资源也必然越多。目前在移动设备上,FillRate的压力主要来自半透明物体。因为多数情况下,半透明物体需要开启Alpha Blend并且关闭ZTest和ZWrite(shader中的渲染队列),同时如果我们绘制像alpha = 0这种实际上不会产生效果的颜色上去,也会有Blend操作,这是一种极大的浪费。
2.像素的复杂度,比如动态阴影、光照、复杂的shader等。
3.几何体的复杂度(顶点数量)。
4.GPU的显存带宽。

降低填充率
在开发过程中应注意避免overdraw和尽量降低overdraw,降低overdraw有以下几种方法:
⑴尽量减少alpha = 0的资源的使用,因为这种资源也会参与绘制,占用一定的GPU。
⑵制作图集的时候,尽量使小图排布紧凑,尽量图集中大面积留白,理由同上。
⑶避免无用对象及组件的过度使用(比如新手教学部分用了很多“不可见”的Image作为交互响应的控件;但这些东西虽然画上去没有效果,依然占用了显卡资源,特别是有很多大块的区域),这种情况下可以实现一个只在逻辑上响应Raycast但是不参与绘制的组件即可。
具体可参考以下文章:https://blog.uwa4d.com/archives/fillrate.html
⑷通过修改渲染队列也能降低overdraw。
⑸注意UI文本的空白区域引起的overdraw。UI文本字形是作为独立的面片(quad)进行渲染的,每个字符都是一个面片。这些面片通常含有大量的空白区域围绕着字体,空白区域的大小取决于字形的形状,在放置文本时很容易就会忽略(破坏其他UI的批处理,所以对字体尽可能预留一定的空间。
⑹Image Sliced模式(九宫格拉伸),情况允许下取消勾选Fill Center(中心镂空,以其他UI元素覆盖),可以减少overdraw。

减少顶点数量
⑴保持材质的数目尽可能少,使Unity容易批处理 → 尽可能减少模型中三角形的数目,尽可能重用顶点。(“软边”→减少顶点数目,并且可以使得渲染效果更加平滑。)
⑵使用纹理图集(一张大贴图里面包含了很多子贴图)来代替一系列单独的小贴图。它们可以更快地被加载,具有很少的状态转换,而且对批处理更友好。
⑶如果使用了纹理图集和共享材质,如果使用了纹理图集和共享材质,使用Renderer.sharedMaterial 来代替Renderer.material。
⑷使用光照纹理(LightMap)而非实时灯光。(LightMap是一种很常见的优化策略,它主要用于场景中整体的光照效果。这种技术主要是提前把场景中的光照信息存储在一张光照纹理中,然后在运行时刻只需要根据纹理采样得到光照信息即可。)
⑸使用LOD,好处就是对那些离得远,看不清的物体的细节可以忽略。(但如果没有调整好距离的话可能会造成模型的突变,使用时需要注意)
⑹遮挡剔除(Occlusion culling),这里需要注意的是,合并方式也会影响Culling,例如把整个游戏所有的树都合并成一个DC,DC是下降了,但是只要有一棵树在摄像机里,所有合并的树模型都会被渲染,增大了渲染带宽和负载,需要权衡使用。
⑺使用mobile版的shader。因为简单。

优化显存带宽
压缩图片,减小显存带宽的压力。
显存带宽的瓶颈:
①尺寸很大且未压缩的纹理。(理解为一个通道,太大难过去)
②分辨率过高的framebuffer
优化方法:
⑴设置格式压缩
Android → ETC1
IOS → PVRTC
但对于透明纹理,ETC1不支持,而PVRTC则可能会有较大失真,因此更推荐使用RGBA16(要注意“色阶问题”,即色彩过渡不均匀,应避免大量的过渡色使用),RGBA32比较占内存,不推荐使用。
另外,针对Android上带alpha通道的图片,还有一种比较常见的做法:即把alpha通道独立出来作为另外一张纹理,从而将RGB部分和alpha部分分别采用ETC1来压缩,但渲染时就需要自定义的shader来处理。
⑵Mipmap
Mipmap中每一个层级的小图都是主图的一个特定比例的缩小细节的复制品。因为存了主图和它的那些缩小的复制品,所以内存占用会比之前大。但是为何又优化了显存带宽呢?因为可以根据实际情况,选择适合的小图来渲染。所以,虽然会消耗一些内存(大概增加30%),但是为了图片渲染的质量(比压缩要好),这种方式也是推荐的。(一般UI没有必要开启Mipmap)

三、内存

Unity内存占用主要来自一下三个方面:
1.资源内存占用
2.引擎模块自身内存占用
3.托管堆内存占用,高频率地New Class/Container/Array等,注意尽量不要在Update等高频函数中开辟新内存。
PS:Log输出也会占用少量内存,当有大量Log输出时需要注意。

资源内存占用
⑴纹理(Texture)
①尽可能根据硬件的种类选择硬件支持的纹理格式
②纹理尺寸越大,则占用内存越大,必要时可以使用九宫格拉伸
③Mipmap,此项开启后内存消耗会增加1/3,UI不开启,2D游戏所有图片不开启,3D场景内贴图开启,角色和特效根据实际情况开启
④Read & Write,此项开启后内存消耗会增大一倍(开启后可以在运行时进行贴图合并操作)
⑵网格(Mesh)
Color数据、Normal数据、Tangent数据,面数越多加载越慢,LOD
⑶动画片段(AnimationClip)
①压缩格式
②数据精度(衡量概率曲线会随着精度的上升增加)
③动画的类型:Generic OR? Humanoid
⑷音频片段(AudioClip)
受LoadType和Compression Format影响
首推Mp3
默认情况下Load in Background不开启
非及时音效建议开启,如bgm
大量频繁使用的音效不开启,如音效资源,选择Decompressed On Load来降低CPU的开销
对于Quality,建议选择50%(此效果是非线性的),在某些极端情况下或对音质要求不高的情况下可以选择1%
⑸材质(Material)
⑹着色器(Shader)
⑺字体资源(Font)
⑻文本资源(TextAsset)
另外,尽量减少在Hierarchy对资源的直接引用,使用动态有效的管理方式去管理当前场景用到的资源文件。

Unity官方给出的一些优化建议:
1.PC平台的话保持场景中显示的顶点数少于200K~3M,移动设备的话少于10W,一切取决于你的目标GPU与CPU。
2.如果你用U3D自带的SHADER,在表现不差的情况下选择Mobile或Unlit目录下的。它们更高效。
3.尽可能共用材质。
4.将不需要移动的物体设为Static,让引擎可以进行其批处理。
5.尽可能不用灯光。
6.动态灯光更加不要了。
7.尝试用压缩贴图格式,或用16位代替32位。
8.如果不需要别用雾效(fog)
9.尝试用OcclusionCulling,在房间过道多遮挡物体多的场景非常有用。若不当反而会增加负担。
10.用天空盒去“褪去”远处的物体。
11.shader中用贴图混合的方式去代替多重通道计算。
12.shader中注意float/half/fixed的使用。
13.shader中不要用复杂的计算pow,sin,cos,tan,log等。
14.shader中越少Fragment越好。
15.注意是否有多余的动画脚本,模型自动导入到U3D会有动画脚本,大量的话会严重影响消耗CPU计算。
16.注意碰撞体的碰撞层,不必要的碰撞检测请舍去。

你可能感兴趣的:(Unity性能优化总结)