首先祝你們國慶、中秋雙節快樂!時隔挺久沒寫東西了,一方面加班太多,其實另外一方面也是本身懶惰了,不過還好一直都在堅持鍛鍊,身體和心靈總要有一個在路上。大好假期,外面人太多了,仍是在家裏學習學習,看看電影來的舒服~這一篇主要是總結了《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.對於不遵照此聲明或者其餘違法使用本文內容者,本人依法保留追究權等。