首先祝你们国庆、中秋双节快乐!时隔挺久没写东西了,一方面加班太多,其实另外一方面也是本身懒惰了,不过还好一直都在坚持锻炼,身体和心灵总要有一个在路上。大好假期,外面人太多了,仍是在家里学习学习,看看电影来的舒服~这一篇主要是总结了《Unity性能优化》的一些笔记,加之一些其余地方看的内容,仅供学习参考!c#
Profile能够收集Unity中不一样子系统中的数据,大体以下:数组
经过Profile咱们能够经过观察目标函数调用的行为,分配了多少内存来观察程序的工做状况,这种方法为指令注入(instrumentation);另外一种方式为基准分析(benchmarking),这种方法的重要指标为渲染帧率(Frames Per Second,FPS)、整体内存消耗和CPU活动(寻找活动中较大的峰值)。相比第一种方式,第二种方式更为经常使用,从长远看,它会节省大量时间,由于它确保了咱们只用关注性能有问题的地方。通常在大致基准分析后,才深刻地使用指令注入去改善性能问题。缓存
此外,由于在IDE下会带来一些额外的开销或隐藏真实程序中的一些潜在的条件,所以在应用程序以独立格式在目标硬件上运行时,应将分析工具挂接到应用程序中。性能优化
如下步骤为发布PC程序时所需的步骤,在发布程序时(以Windows为例)须要将Development Build
和Autoconnect Profile
勾选,以下图:网络
发布程序后,在IDE中启动 Profile(Ctrl+7),并启动应用程序,则Profile会自动链接应用程序并开始收集数据。数据结构
此外,还能够链接WebGL实例、远程链接iOS设备、远程链接Android设备,在此再也不赘述。并发
通常咱们的目标是使用基准分析来观察应用程序,寻找问题行为实例,而后使用指令注入工具在代码中寻找问题的缘由。但咱们经常被无效的数据分散注意力或忽略了一些细微的细节而得出结论。如下为通用的解决步骤:异步
须要注意一下几点:ide
Edit|Project Settings|Quality
中禁用VSync。根据Profiler窗口能够快速肯定哪一个MonoBehaviour或方法致使了问题,而后咱们须要肯定问题是否能够重现,在什么状况下出现性能瓶颈,以及问题代码块中问题的确切来源,为此,咱们须要对代码片断进行一些分析,通常分为两类:函数
利用UnityEngine.Profiling.Profiler类中的BeginSample()
和EndSample()
,可使方法运行时激活和禁用分析功能的分隔符方法。
如如下代码:
private void DoSomething() { Profiler.BeginSample("Test Profiler Sample"); var lt = new List<string>(); for (int i = 0; i < 10000000; i++) { lt.Add(i.ToString()); } Profiler.EndSample(); }
除了Unity Profiler以外,还能够利用System.Diagnostics中的Stopwatch
类,可是该类最多精确到1/10毫秒,所以为了提升精度,能够利用屡次相同测试的平均值来计算平均调用时间,即在一个合理的时间内运行相同测试代码成千上万次,而后总消耗时间除以测试运行次数,以此获得较为精确的单次运行次数。能够自定义定时器,以下:
using System; using System.Diagnostics; /// <summary> /// 自定义方法测试定时器 /// </summary> public class CustomTestTimer : IDisposable { private string _timerName;//计时器名称 private int _numTests;//测试次数 private Stopwatch _wathc;//计时器 public CustomTestTimer(string timerName, int numTests) { _timerName = timerName; _numTests = numTests; if (numTests <= 0) _numTests = 1; _wathc = Stopwatch.StartNew(); } public void Dispose() {//当引用using()块结束时调用 _wathc.Stop(); float ms = _wathc.ElapsedMilliseconds; UnityEngine.Debug.LogFormat("{0} 测试完成,总计用时:{1:0.00}ms,每次测试平均用时:{2: 0.000000}ms,一共测试{3}次", _timerName, ms, ms / _numTests, _numTests); } }
若是要测试某方法,可采用如下方式:
int numTests = 100000; using (new CustomTestTimer("Controlled Test", numTests)) { for (int i = 0; i < numTests; i++) { TestFunction(); } }; private void TestFunction() { Debug.Log("123"); }
运行后,在程序中以下图:
由此可分析出某方法较为精确的耗时。
Unity中获取组件GetComponent()有3个可用的重载,分别是GetComponent(string),GetComponent< T >()和GetComponent(typeof(T))。在这三个方法中,最好使用GetCompnent< T >()重载。
此外,GetComponent()方法也不该该运用在相似Update()逐帧计算中,最好的方法是初始化过程当中(Awake或Start等)就获取引用并缓存它们,直到须要使用它们为止。一样的技巧也适用于在运行时决定计算的任何数据,不须要要求CPU在每次执行Update()时都从新计算相同的值,所以能够提早将其缓存到内存中。
在MonoBehaviour脚本中经常使用其周期函数,经常使用的有Awake()、Start()、Update()、FixedUpdate()等,这些回调函数会在场景第一次实例化时添加到一个函数指针列表中,又由于在全部的Update()回调(包括场景中全部的MonoBehaviour)完成以前,渲染管线不容许呈现新帧,所以当场景中有大量MonoBehaviour脚本时(包含空的Start()或Update()),场景的初始化以及每帧都会严重消耗资源从而影响帧率。所以咱们须要在编写脚本时注意删除空的周期函数,例如Start(),Update()等。
当咱们尝试在Update()中执行某方法时,例如:
void Update() { DoSomething(); }
若是该方法占用太多帧率预算,那么提升性能的一个方法是简单地减小DoSomething()的调用频率:
private float _delayTime=0.2f; private float _timer=0; void Update() { _timer+=Time.deltaTime; if(_timer>_delayTime) { DoSomething(); _time-=_delayTime; } }
修改后,该方法由每秒调用60次变为每秒调用5次。以上方法乍一看改进了以前的情形,但代价是须要一些额外的内存来存储浮点数据,且Unity仍要调用一个空的回调函数。咱们还能够继续对其进行更改,将其改成协程:
void Start() { StartCoroutine(DoSomethingCoroutine()); } IEnumerator DoSomethingCoroutine() { while(true) { DoSomething(); yield return new WaitForSeconds(_delayTime); } }
以上提到的协程,应于线程进行区别:线程以并发方式在彻底不一样的CPU内核上运行,并且多个线程能够同时运行,而协程是以顺序的方式在主线程上运行,这样在任何给定时刻都只有一个协程在处理。以上用协程改进后好处是该函数只调用_delayTime值指示的次数,在此以前它一直处于空闲,从而减小对大多数帧的性能影响。然而协程也有如下缺点:
实际上,针对老是在WaitForSeconds
或WaitForSecondsRealtime
上调用yield协程,能够一般替换成InvokeRepeating()
调用,它的创建更简单,且开销较协程小一些,以下:
void Start() { InvokeRepeating("DoSomething",0f,_delayTime); }
InvokeRepeating()与协程的重要区别是,InvokeRepeating()彻底独立与MonoBehaviour和GameObject的状态外。此外,中止InvokeRepeating()调用有两个方法:第一种方法是调用CancelInvoke(),它会中止给定MonoBehaviour发起所的全部InvokeRepeating()回调(不能单独取消某个);第二种方法是销毁关联的MonoBehaviour或它的父GameObject。注意,禁用MonoBehaviour或GameObject都不会中止InvokeRepeating()。
与C#对象相比,GameObject和MonoBehaviour是特殊对象,由于它们在内存中有两个表示:一个表示存在于管理C#代码相同系统管理的内存中,C#代码是用户编写的(托管代码),另外一个表示存在于另外一个单独处理的内存空间中(本机代码)。数据能够再这两个内存之间移动,所以每次移动都会致使额外的CPU开销和 可能的额外内存分配,这种效果通常称为跨越本机-托管的桥接。
由以上理论,触发这种额外开销的有如下两种常见状况:
对GameObject空引用检查
通常咱们使用如下方式对GameObject空引用检查:
if(gameObject!=null){ //DoSomething }
另外一种更好地方式是利用System.Object.ReferenceEquals(),其运行速度大约是上边的两倍:
if(!System.Object.ReferenceEquals(gameObject,null)) { //DoSomething }
以上方式也适用于MonoBehaviour。
GameObject的字符串属性
从GameObject中检索字符串属性是另外一种意外跨越本机-托管桥接的方式。一般使用的两个属性是tag和name,所以使用这两个属性是很差的,然而GameObject提供了CompareTag()
方法,它则彻底避免了本机-托管的桥接。
即便用gameObject.CompareTag("tag")而不是使用gameObject.tag=="tag"。除此以外,name属性没有对应方法,所以尽量使用Tag属性。
Transform组件的父-子关系比较像动态数组,所以Unity尝试将全部共享相同父元素的Transform按顺序存储在预先分配的内存缓冲区中,并在Hierarchy窗口中根据父元素下面的深度进行排序。这种数据结构容许整个组中进行更快的迭代,对于物理和动画等多个子系统有利,可是若是将一个GameObject的父对象从新指定为另外一个对象,父对象必须将子对象放入预先分配的缓冲区中,并根据新的深度对全部Transform进行排序。另外若是父对象没有预先分配足够的空间,就必须扩展缓冲区。对于较深、复杂的GameObject结构,这须要一些时间来完成。
经过GameObject.Instantiate()实例化新的GameObject时,想为其设置一个父物体,在咱们使用时不少状况会写成相似如下代码:
GameObject listItem = (GameObject)Instantiate(Resources.Load("Prefabs/UI/Items/PersonListItem")); listItem.transform.SetParent(m_PersonSelectContnt, false);
以上状况在listItem实例化以后当即将Transform的父元素从新修改成另外一个元素,它将丢弃一开始分配的缓冲区,为了不这种状况,应该将父Transform参数提供给GameObject.Instantiate()
调用,这调用可跳过这个缓冲区分配步骤,从而提高一部分性能:
GameObject listItem = (GameObject)Instantiate(Resources.Load("Prefabs/UI/AMMT/Items/PersonListItem", m_PersonSelectContnt, false));
不断更改Transform组件属性,也同时会向其余组件(如Collider、Rigidbody、Light、Camera等)发送内部通知,这些组件也必须进行处理,由于物理和渲染系统都须要知道Transform的新值,并相应进行更新。
在复杂的过程当中,在同一帧中屡次替换Transform组件的属性很常见,每次Transform发生改变时,都会触发内部消息。所以,应该尽可能减小修改Transform属性的次数,方法是将其变化缓存在一个成员变量中,只在帧的末尾修改Transform值,以下所示:
private bool _positionChanged; private Vector3 _newPosition; public void SetPosition(Vector3 pos) { _newPosition = pos; _positionChanged = true; } private void FixedUpdate() { if (_positionChanged) { transform.position = _newPosition; _positionChanged = false; } }
用以上逻辑仅在下一个FixedUpdate()中提交对position的更改,从而减小对Transform的改变。
SendMessage()
和GameObject.Find()
方法很是昂贵,应不惜一切代价尽可能避免使用。Find()会迭代场景中的每一个GameObject对象。不过,在场景初始化期间调用Find()有时是能够的,例如在Awake()或Start()中。
若是须要比较距离而非计算距离,用SqrMagnitude
代替Magnitude
能够避免一次耗时的开放运算。
在进行向量乘法计算时,有一点须要注意乘法顺序,由于向量乘比较耗时,因此咱们应该尽量减小向量乘法运算。能够基于以前CustomTestTimer来作一个实验:
private void Start() { int numTests = 1000000; using (new CustomTestTimer("向量在中间", numTests)) { for (int i = 0; i < numTests; i++) { Func1(); } } using (new CustomTestTimer("向量在最后", numTests)) { for (int i = 0; i < numTests; i++) { Func2(); } } } private void Func1() { Vector3 a = 3 * Vector3.one * 2; } private void Func2() { Vector3 a = 3 * 2 * Vector3.one; }
最终结果以下:
由结果能够看出,以上两个方法结算结果相同,可是Func2却比Func1耗时少,由于后者比前者少了一次向量乘法。因此,应该尽量合并数字乘法,最后再进行向量乘。
在无可奈何须要写多重循环时,应该尽可能把遍历次数较多的循环放在内层。作测试以下:
private void Start() { int numTests = 10000000; using (new CustomTestTimer("大循环在外", numTests)) { for (int i = 0; i < numTests; i++) { for (int j = 0; j < 2; j++) { int k = i * j; } } } using (new CustomTestTimer("大循环在内", numTests)) { for (int i = 0; i < 2; i++) { for (int j = 0; j < numTests; j++) { int k = i * j; } } } }
测试结果以下:
首先介绍一下批处理,批处理主要是指将大量任意数据块组合在一块儿,并将它们做为单个大数据块进行处理的过程。在Unity中的批处理主要分为动态批处理和静态批处理,这两种方法本质是几何体合并的两种不一样形式,用于将多个对象的网格数据合并到一块儿,并在单一指令中渲染它们,而不是单独准备和绘制每一个几何体。
批处理的主要目的便是减小Draw Call,Draw Call是指一个从CPU发送到GPU用于绘制对象的请求。这里注意的是,若Draw Call太高致使画面帧率变低,是因为CPU的提交速度瓶颈致使,而不是GPU。
减小Draw Call的开销:
动态批处理有如下优势:
动态批处理是Unity自动生成的,功能开关在Edit|Project Settings|Player|Other Settings|Dynamic Batching
。
使用动态批处理的要求以下:
动态批处理在渲染大量简单网格时是很是有用的工具,在工程中,动态批处理的自动进行的,而咱们须要注意一点:能够阻止两个简单对象动态批处理的惟一条件是,它们使用了不一样的纹理,所以,咱们应该将它们的纹理合并(一般称为图集),并从新生成网格UV,以便进行动态批处理。固然这样可能会牺牲纹理的质量,或者纹理文件会变大。
对动态批处理相对,静态批处理功能相似于动态批处理,可是它只处理标记为Static的对象。静态批处理的要求:
在上一章已经提到过一些关于艺术资源的优化,例如合并贴图、减小网格等,下面咱们详细看一下Unity中艺术资源的优化。
通常纹理是一张图片,它会告诉插值程序,图像的每一个像素应该是什么颜色。下面直接来说纹理优化的要点。
减少纹理文件的大小
给定的纹理文件越大,推送纹理所消耗的GPU内存带宽就越多。若是每秒推送的总内存超过显卡的总内存带宽,就会产生瓶颈,由于在下一个渲染过程开始以前,GPU必须等待全部纹理都上传完毕。减少纹理大小的方式不少,能够有如下两点:
使用图集
图集能够将许多较小的、独立的纹理合并到一个较大的文理文件中,从而最小化材质的数量,所以最小化所需使用的Draw Call数量。这样作的额外工做是须要修改网格或精灵对象的UV,只采样大纹理文件中所需的部分。但好处也是明显的,这样会减小Draw Call下降CPU工做负载,提高帧率。注意,因为推送到GPU的数据是同样的,所以图集不会减小内存带宽消耗,它只是将多张图片打包到一张更大的文理文件中。
固然图集只是当全部给定的纹理须要相同的着色器时采用的一种方法,若是一些纹理须要经过着色器应用独立的图形效果,它们就必须分离到本身的材质中,并在单独的组中打图集。
调整非正方形纹理的压缩率
纹理文件一般以正方形、2的n次方幂的格式保存,要避免非2的n次幂的纹理。
模型网格也是影响性能的另外一个资源。下面来说一下网格优化的一些注意点。
减小网格多边形数量
这是提高性能最明显的方法之一,一般模型采用的是精细的纹理和复杂的阴影来提供大部分细节,这样咱们就能够从网格上去掉许多顶点从而优化模型和性能。
恰当使用Read-Write Enabled
Read-Write Enabled标志容许在运行时经过脚本读取/修改网格,禁用改选项会使Unity在肯定要使用的最终网格后,从内存中丢弃原始网格数据,所以若是在整个过程当中只是用网格的等比缩放版本,则禁用该选项会节省运行时的内存。但若是模型网格须要在运行时以不一样的比例从新出现,那么Unity会在该选项禁用时每次从新导入网格从新加载网格数据,还须要同时生成从新缩放的副本,所以启用Read-Write Enable是明智的。
合并网格
将多个模型网格合并成单个的大型网格,便于减小Draw Call,特别是当网格对于动态批处理来讲过大,不能与其余静态批处理组很好地配合时。
这里是我经过项目实践的内容,属于内部资料,所以不详细写了,主要目的其实就是在建模时,应对模型的材质和贴图要求复用,相同的材质、贴图不能重复,除此以外,须要对导入Unity的模型、贴图、材质进行管理,主要是要创建材质库,使得新导入的模型尽量地引用已有的材质球。这样作也能够将材质与模型分离,达到在Unity中能够编辑模型材质的优势。
Unity中的内存空间本质上能够划分为3个不一样的内存域,每一个域存储不一样的数据类型,关注不一样的任务集。
以上托管域也包含存储在本地域中的对象描述的包装器,所以当和Transform等组件交互时,大多数指令会请求Unity进入它的本地代码,在那里生成结果,而后再将结果复制回托管域,这正是本地-托管桥的由来。当两个域对相同实体有本身的描述时,跨越它们须要内存进行上下文切换,从而会带来一些严重的潜在性能问题。
垃圾回收策略
最小垃圾回收问题的一种策略实在合适的时间手动触发垃圾回收,当肯定用户不会注意到这种行为时就能够偷偷触发垃圾回收,垃圾回收能够经过System.GC.Collect()
手动调用。甚至能够在运行时使用Profiler.GetMonoUsedSize()
和Profiler.GetMonoHeapSize()
方法决定是否须要调用垃圾回收。固然,最好的垃圾回收策略是避免垃圾回收。
字符串
字符串本质是字符数组,所以字符串在内存中是连续的,当字符串再分配内存后就不可变了,即字符串是不可变的引用类型。对字符串的修改、合并、链接等操做都须要建立新的字符串。所以字符串的使用须要注意如下几点:
Unity API中的数组
Unity API中有不少指令会致使堆内存分配,本质上包括了全部返回数组数据的指令,例如如下方法:
GetComponents< T >(); //(T[])
Mesh.vertices; //(Vector3[])
Camear.allCameras; //(Camear[])
每次调用这类API方法时,都会致使分配该数据的新内存,这些方法应该尽量避免,或者仅调用不多次数并缓存结果,避免比实际所须要更频繁的内存分配。
循环子物体
有时咱们迭代子物体时,可能会使用foreach写成如下相似形式:
foreach(Transform child in transform){ //Dosomething with 'child' }
以上写法会致使堆分配问题,所以应避免以上代码风格,而是用如下形式:
for(int i=0;i<transform.childCount;i++){ var child = transform.GetChild(i); //Dosomething with 'child' }
Read/Write Enabled
:若是不须要运行时读取图片的像素信息的话,禁用,不然启用后纹理的内存消耗会增长一倍。Generate Mip Maps
:Mipmaps和模型的LOD相似,会根据相机距离远近下降或提高贴图像素,可是会多出三分之一的内存开销,若是不是模型贴图,则能够禁用,此外,UI的贴图基本用不到,能够禁用。Max Size
:视状况而定,在2019.4版本Unity中最大能够达到8192*8192,但通常不要过大,不然贴图单个文件大小过大。Mesh Compression
:压缩比越高模型文件越小,须要根据项目实际效果决定,咱们项目目前都将其设为Off
。Read/Write Enable
:若是不须要修改模型时,能够禁用,不然启用后模型内存消耗会增长一倍。可是注意,以前也说过,因为项目中使用了Runtime Editor插件,与该插件须要配合的模型要将此项启用。Optimize Mesh
:默认Everything,能够提高GPU性能。Normals
:若是模型没有法线信息,能够将其设置为None
,减少模型大小。Animation Type
:若是模型没有动画,将其设置为None
。Optimize Game Objects
:在使用Animator制做动画时,将该项启用,能够将暴露在Hierarchy的子节点移除,极大减小了模型的层级和Children的数量,从而提高运行时的性能。若有挂节点需求,在Extra Transform to Expose
中添加须要暴露的子节点便可。Pixel Light Count
:场景使用正向渲染时的最大像素光源数。该值太小的话,假如在某个范围内有多个光源,则这个范围只会有最多设置值个光源产生光照做用,随机某些光源不会发光。可是因为实时光照性能消耗过大,叠加光照对于性能消耗呈指数级增加,所以该值也不宜设置太大,根据项目需求设置。
Texture Quality
:贴图质量,若是选择Half Res
,这样速度会更快,可是贴图质量会降低。
Anisotropic Textures
:是否启用各向异性纹理,若是选择Forced On
,则为始终启用。该项针对如下问题时可能产生效果:
能够看到在开启前画面有模糊,开启后被修正为正常的,即该选项能够修正曲面倾斜后的贴图。
Anti Aliasing
:抗锯齿级别设置,有Disable,2x,4x和8x,倍数越高画面锯齿感越低,可是性能相对越低。
Shadow Resolution
:阴影分辨率,分辨率越高,开销越大。采用Medium Resolution
便可。
Shadow Distance
:相机与阴影可见距离的最大距离,超出此距离则阴影不会渲染。
VSync Count
:垂直同步选项,该选项能够与显示设备的刷新速率同步,防止出现“画面撕裂”。根据咱们项目的需求,建议设置为Don't Sync
。
上传管线AUP相关设置
Async Upload Time Slice
:该参数设定渲染线程中每帧上传纹理和网格数据所用的时间总量,以毫秒为单位。当异步加载操做时,该系统会执行两个该参数大小的时间切片,默认值为2毫秒。若是该值过小,可能会在纹理/网格的GPU上传遇到瓶颈。若是该值太大,可能会形成帧率陡降。Async Upload Buffer Size
:该参数设定环形缓冲区的大小,以MB为单位。当上传时间切片在每帧发生时,要确保在环形缓冲区有足够的数据利用整个时间切片。若是环形缓冲区太小,上传时间切片会被缩短。该值默认为4MB,可适当提升至16MB。Async Upload Persistent Buffer
:该选项决定在完成全部待定读取工做时,是否释放上传时使用的环形缓冲区。分配和释放该缓冲区常常会产生内存碎片,所以一般将其设置为True。若是须要在未加载时回收内存,能够将该值设为False。Scripting Backend
:能够选IL2CPP
,转成C++代码后性能获得提高,同时也变相提供了C#代码的混淆。C++ Compiler Configuration
:默认选择Release
,若是发布的话,能够改为Master
,这样打包速度虽然会慢一些,可是编译的C++代码会更加优化一些。Prebake Collision Meshes
:启用,用构建的时间换运行时的性能。Keep Loaded Shaders Alive
:启用,由于Shader的加载和解析很耗时,因此不但愿Shader被卸载。Optimize Mesh Data
:启用,减小没必要要的Mesh数据,下降包的大小。写文不易~所以作如下申明:
1.博客中标注原创的文章,版权归原做者 煦阳(本博博主) 全部;
2.未经原做者容许不得转载本文内容,不然将视为侵权;
3.转载或者引用本文内容请注明来源及原做者;
4.对于不遵照此声明或者其余违法使用本文内容者,本人依法保留追究权等。