Unity性能優化

Unity性能優化

首先祝你們國慶、中秋雙節快樂!時隔挺久沒寫東西了,一方面加班太多,其實另外一方面也是本身懶惰了,不過還好一直都在堅持鍛鍊,身體和心靈總要有一個在路上。大好假期,外面人太多了,仍是在家裏學習學習,看看電影來的舒服~這一篇主要是總結了《Unity性能優化》的一些筆記,加之一些其餘地方看的內容,僅供學習參考!c#

1、Profile的使用

1.1 Unity Profile

Profile能夠收集Unity中不一樣子系統中的數據,大體以下:數組

  • CPU消耗量
  • 渲染信息和GPU信息
  • 運行時的內存分配和總消耗量
  • 音頻源/數據的使用狀況
  • 物理引擎(2D/3D)的使用狀況
  • 網絡消息傳遞和活動的狀況
  • 視頻回放的使用狀況
  • UI性能
  • 全局光照統計數據

經過Profile咱們能夠經過觀察目標函數調用的行爲,分配了多少內存來觀察程序的工做狀況,這種方法爲指令注入(instrumentation);另外一種方式爲基準分析(benchmarking),這種方法的重要指標爲渲染幀率(Frames Per Second,FPS)、整體內存消耗和CPU活動(尋找活動中較大的峯值)。相比第一種方式,第二種方式更爲經常使用,從長遠看,它會節省大量時間,由於它確保了咱們只用關注性能有問題的地方。通常在大致基準分析後,才深刻地使用指令注入去改善性能問題。緩存

image-20200914192142758

此外,由於在IDE下會帶來一些額外的開銷或隱藏真實程序中的一些潛在的條件,所以在應用程序以獨立格式在目標硬件上運行時,應將分析工具掛接到應用程序中。性能優化

1.2 掛接Profile

如下步驟爲發佈PC程序時所需的步驟,在發佈程序時(以Windows爲例)須要將Development BuildAutoconnect Profile勾選,以下圖:網絡

image-20200914193132077

發佈程序後,在IDE中啓動 Profile(Ctrl+7),並啓動應用程序,則Profile會自動鏈接應用程序並開始收集數據。數據結構

此外,還能夠鏈接WebGL實例、遠程鏈接iOS設備、遠程鏈接Android設備,在此再也不贅述。併發

1.3 性能分析方法

通常咱們的目標是使用基準分析來觀察應用程序,尋找問題行爲實例,而後使用指令注入工具在代碼中尋找問題的緣由。但咱們經常被無效的數據分散注意力或忽略了一些細微的細節而得出結論。如下爲通用的解決步驟:異步

  • 驗證目標腳本是否出如今場景中
  • 驗證腳本的在場景中出現的次數是否正確
  • 驗證事件的正確順序
  • 最小化正在進行的代碼更改
  • 減小內部干擾
  • 減小外部干擾

須要注意一下幾點:ide

  1. 能夠採用IDE調試或Debug.Log,可是注意Unity中日誌記錄在CPU和內存中都很是昂貴,所以不要Debug很頻繁,應該只針對代碼中最相關的部分進行有針對性的記錄,而且對調試的Debug及時註釋。
  2. Unity的Update()次數即便在相同的硬件上,也可能次數不一樣,例如在這一秒調用60個更新,而在下一秒有可能則調用59個更新,在下一秒可能調用62個更新,所以,程序最好不要依賴於某個對象Update()的特定調用次數。
  3. 若是一幀須要很長時間處理,好比程序出現明顯的卡頓,那麼Profiler可能沒法獲取結果記錄在Profiler窗口中。
  4. 程序若是啓動了垂直同步(Vsync,用於將應用程序的幀率匹配到它將顯示到的設備幀率,例如顯示器的幀率爲60Hz,而程序的渲染循環比60Hz快,則程序會等到,指導輸出渲染爲止),在Profiler中可能會在WaitForTargetFPS下的「CPU使用狀況」區域中產生過多嘈雜峯值,所以在查看監視CPU使用狀況時,能夠先禁用VSync,具體在Edit|Project Settings|Quality中禁用VSync。

1.4 代碼片斷針對性分析

根據Profiler窗口能夠快速肯定哪一個MonoBehaviour或方法致使了問題,而後咱們須要肯定問題是否能夠重現,在什麼狀況下出現性能瓶頸,以及問題代碼塊中問題的確切來源,爲此,咱們須要對代碼片斷進行一些分析,通常分爲兩類:函數

  • 腳本代碼控制Profiler
  • 自定義定時和日誌記錄

1.4.1 Profiler腳本控制

利用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();
}

image-20200917192518076

1.4.2 自定義CPU分析

除了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");
}

運行後,在程序中以下圖:

image-20200918075420525

由此可分析出某方法較爲精確的耗時。

2、腳本策略

2.1 獲取組件優化

Unity中獲取組件GetComponent()有3個可用的重載,分別是GetComponent(string),GetComponent< T >()和GetComponent(typeof(T))。在這三個方法中,最好使用GetCompnent< T >()重載。

此外,GetComponent()方法也不該該運用在相似Update()逐幀計算中,最好的方法是初始化過程當中(Awake或Start等)就獲取引用並緩存它們,直到須要使用它們爲止。一樣的技巧也適用於在運行時決定計算的任何數據,不須要要求CPU在每次執行Update()時都從新計算相同的值,所以能夠提早將其緩存到內存中。

2.2 移除空的回調定義

在MonoBehaviour腳本中經常使用其周期函數,經常使用的有Awake()、Start()、Update()、FixedUpdate()等,這些回調函數會在場景第一次實例化時添加到一個函數指針列表中,又由於在全部的Update()回調(包括場景中全部的MonoBehaviour)完成以前,渲染管線不容許呈現新幀,所以當場景中有大量MonoBehaviour腳本時(包含空的Start()或Update()),場景的初始化以及每幀都會嚴重消耗資源從而影響幀率。所以咱們須要在編寫腳本時注意刪除空的周期函數,例如Start(),Update()等。

2.3 Update、Conroutines和InvokeRepeating

當咱們嘗試在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值指示的次數,在此以前它一直處於空閒,從而減小對大多數幀的性能影響。然而協程也有如下缺點:

  • 與標準函數調用相比,啓動攜程會帶來額外開銷成本(大約是標準函數調用的三倍),同時還會分配一些內存,將當前狀態存儲在內存中,直到下一次調用它。並且這種開銷也不是一次性的,由於協程會不斷調用yield,這會一次又一次的形成相同的開銷成本,因此須要確保下降頻率的好處大於此成本;
  • 協程運行獨立於MonoBehaviour組件中Update()回調的觸發,無論組件是否禁用,都將繼續調用攜程;
  • 協程會在包含它的GameObject變成不活動的那一刻自動中止(不管該GameObject被設置爲不活動仍是它的一個父對象被設置爲不活動),且若是GameObject再次被設置爲活動,協程也不會自動從新啓動;
  • 將方法轉換爲協程,可減小大部分幀中的性能損失,但若是方法體的單次調用突破了幀率預算,則不管該方法的調用次數再怎麼少,都將超過預算,所以這種方法只適用於因爲在給定幀中調用方法次數過多而致使幀率超出預算的狀況,而不適合因爲原方法自己太昂貴的狀況;
  • 協程較難調試,它不遵循正常的執行流程,且在調用棧上沒有調用者。若是使用協程,最好使它們儘量簡單,且獨立於其餘複雜的子系統。

實際上,針對老是在WaitForSecondsWaitForSecondsRealtime上調用yield協程,能夠一般替換成InvokeRepeating()調用,它的創建更簡單,且開銷較協程小一些,以下:

void Start()
{
    InvokeRepeating("DoSomething",0f,_delayTime);
}

InvokeRepeating()與協程的重要區別是,InvokeRepeating()徹底獨立與MonoBehaviour和GameObject的狀態外。此外,中止InvokeRepeating()調用有兩個方法:第一種方法是調用CancelInvoke(),它會中止給定MonoBehaviour發起所的全部InvokeRepeating()回調(不能單獨取消某個);第二種方法是銷燬關聯的MonoBehaviour或它的父GameObject。注意,禁用MonoBehaviour或GameObject都不會中止InvokeRepeating()

2.4 GameObject本機-託管橋接

與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屬性。

2.5 運行時修改Transform的父節點

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));

2.6 減小對Transform的改變

不斷更改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的改變。

2.7 避免在運行時使用Find()和SendMessage()

SendMessage()GameObject.Find()方法很是昂貴,應不惜一切代價儘可能避免使用。Find()會迭代場景中的每一個GameObject對象。不過,在場景初始化期間調用Find()有時是能夠的,例如在Awake()或Start()中。

2.8 Vector計算

若是須要比較距離而非計算距離,用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;
    }

最終結果以下:

image-20200928112412526

由結果能夠看出,以上兩個方法結算結果相同,可是Func2卻比Func1耗時少,由於後者比前者少了一次向量乘法。因此,應該儘量合併數字乘法,最後再進行向量乘

2.9 循環

在無可奈何須要寫多重循環時,應該儘可能把遍歷次數較多的循環放在內層。作測試以下:

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;
                }
            }
        }
    }

測試結果以下:

image-20200928114131466

3、批處理

首先介紹一下批處理,批處理主要是指將大量任意數據塊組合在一塊兒,並將它們做爲單個大數據塊進行處理的過程。在Unity中的批處理主要分爲動態批處理和靜態批處理,這兩種方法本質是幾何體合併的兩種不一樣形式,用於將多個對象的網格數據合併到一塊兒,並在單一指令中渲染它們,而不是單獨準備和繪製每一個幾何體。

3.1 Draw Call

批處理的主要目的便是減小Draw Call,Draw Call是指一個從CPU發送到GPU用於繪製對象的請求。這裏注意的是,若Draw Call太高致使畫面幀率變低,是因爲CPU的提交速度瓶頸致使,而不是GPU。

減小Draw Call的開銷:

  • 避免使用大量很小的網格。當不能避免須要使用很小的網格時,能夠考慮是否能夠合併網格。
  • 避免使用過多的材質。儘可能在不一樣的網格之間共用同一材質。

3.2 動態批處理

動態批處理有如下優勢:

  • 在運行時生成(批處理是動態生成的)
  • 批處理中包含的對象在不一樣幀之間可能有所不一樣,這取決於哪些網格在主相機視圖中當前是可見的(批處理的內容是動態的)
  • 在場景中運動的對象也能夠批處理(對動態對象有效)

動態批處理是Unity自動生成的,功能開關在Edit|Project Settings|Player|Other Settings|Dynamic Batching

image-20200924200616428

使用動態批處理的要求以下:

  • 全部網格實例必須使用相同的材質引用(此處爲「材質引用」,若是兩個不一樣的材質但它們設置相同,渲染管線仍是不能執行動態批處理);
  • 只有ParticleSystem和MeshRenderer組件進行動態批處理。SkinnedMeshRenderer組件(用於角色動畫)和其餘可渲染的組件類型不能進行批處理;
  • 每一個網格至多有300個頂點;
  • 着色器使用的頂點屬性(例如頂點位置、發現向量、UV座標、顏色信息等等)不能大於900;
  • 全部網格示例要麼使用等比縮放,要麼使用非等比縮放,但不能二者混用;
  • 網格實體應該引用相同的光照紋理文件;
  • 材質的着色器不能依賴多個過程;
  • 網格實體不能接受實施投影;
  • 整個批處理中網格索引的總數有上限,這與所用的Graphics API和平臺有關,通常索引值在32~64K之間。

動態批處理在渲染大量簡單網格時是很是有用的工具,在工程中,動態批處理的自動進行的,而咱們須要注意一點:能夠阻止兩個簡單對象動態批處理的惟一條件是,它們使用了不一樣的紋理,所以,咱們應該將它們的紋理合並(一般稱爲圖集),並從新生成網格UV,以便進行動態批處理。固然這樣可能會犧牲紋理的質量,或者紋理文件會變大。

3.3 靜態批處理

對動態批處理相對,靜態批處理功能相似於動態批處理,可是它只處理標記爲Static的對象。靜態批處理的要求:

  • 網格實體必須標記爲Static,其反作用便是任何想要使用靜態批處理的對象都不能經過任何方式移動、旋轉和縮放;
  • 每一個被靜態批處理的對象都須要額外的內存;
  • 合併到靜態批處理中的頂點數量是有上限的,與Graphics API和平臺的不一樣而不一樣,通常爲32~64K個頂點;
  • 網格實例能夠來自任何模型,可是它們必須使用相同的材質引用。

4、藝術資源優化

在上一章已經提到過一些關於藝術資源的優化,例如合併貼圖、減小網格等,下面咱們詳細看一下Unity中藝術資源的優化。

4.1 紋理貼圖

通常紋理是一張圖片,它會告訴插值程序,圖像的每一個像素應該是什麼顏色。下面直接來說紋理優化的要點。

  1. 減少紋理文件的大小

    給定的紋理文件越大,推送紋理所消耗的GPU內存帶寬就越多。若是每秒推送的總內存超過顯卡的總內存帶寬,就會產生瓶頸,由於在下一個渲染過程開始以前,GPU必須等待全部紋理都上傳完畢。減少紋理大小的方式不少,能夠有如下兩點:

    • 下降紋理的分辨率
    • 下降紋理的位數(最低爲8)
  2. 使用圖集

    圖集能夠將許多較小的、獨立的紋理合併到一個較大的文理文件中,從而最小化材質的數量,所以最小化所需使用的Draw Call數量。這樣作的額外工做是須要修改網格或精靈對象的UV,只採樣大紋理文件中所需的部分。但好處也是明顯的,這樣會減小Draw Call下降CPU工做負載,提高幀率。注意,因爲推送到GPU的數據是同樣的,所以圖集不會減小內存帶寬消耗,它只是將多張圖片打包到一張更大的文理文件中。

    固然圖集只是當全部給定的紋理須要相同的着色器時採用的一種方法,若是一些紋理須要經過着色器應用獨立的圖形效果,它們就必須分離到本身的材質中,並在單獨的組中打圖集。

  3. 調整非正方形紋理的壓縮率

    紋理文件一般以正方形、2的n次方冪的格式保存,要避免非2的n次冪的紋理。

4.2 模型網格

模型網格也是影響性能的另外一個資源。下面來說一下網格優化的一些注意點。

  1. 減小網格多邊形數量

    這是提高性能最明顯的方法之一,一般模型採用的是精細的紋理和複雜的陰影來提供大部分細節,這樣咱們就能夠從網格上去掉許多頂點從而優化模型和性能。

  2. 恰當使用Read-Write Enabled

    Read-Write Enabled標誌容許在運行時經過腳本讀取/修改網格,禁用改選項會使Unity在肯定要使用的最終網格後,從內存中丟棄原始網格數據,所以若是在整個過程當中只是用網格的等比縮放版本,則禁用該選項會節省運行時的內存。但若是模型網格須要在運行時以不一樣的比例從新出現,那麼Unity會在該選項禁用時每次從新導入網格從新加載網格數據,還須要同時生成從新縮放的副本,所以啓用Read-Write Enable是明智的。

  3. 合併網格

    將多個模型網格合併成單個的大型網格,便於減小Draw Call,特別是當網格對於動態批處理來講過大,不能與其餘靜態批處理組很好地配合時。

4.3 項目實踐

這裏是我經過項目實踐的內容,屬於內部資料,所以不詳細寫了,主要目的其實就是在建模時,應對模型的材質和貼圖要求複用,相同的材質、貼圖不能重複,除此以外,須要對導入Unity的模型、貼圖、材質進行管理,主要是要創建材質庫,使得新導入的模型儘量地引用已有的材質球。這樣作也能夠將材質與模型分離,達到在Unity中能夠編輯模型材質的優勢。

5、內存管理

5.1 Unity中的內存域

Unity中的內存空間本質上能夠劃分爲3個不一樣的內存域,每一個域存儲不一樣的數據類型,關注不一樣的任務集。

  • 託管域,該域是Mono平臺工做的地方,咱們編寫的任何MonoBehaviour腳本和自定義的C#類在運行時都會在此域實例化對象 ,託管域的內存空間會被自動被垃圾回收管理。
  • 本地域,該域咱們會間接與之交互。Unity一些底層代碼由C++編寫,並根據目標平臺編譯到不一樣的應用程序中。該域關係Unity內部內存空間的分配,如爲各類子系統(如渲染管線、物理系統、UI等)分配資源數據(如紋理、網格、音頻等)合內存空間。此外,它還包括GameObject和Component等重要遊戲對象的部分本地描述,也是大多數內建Unity類(如Transform、RigidBody等)保存數據的地方。
  • 外部庫,例如DirectX和OpenGL庫,也包括項目中不少自定義的庫和插件。

以上託管域也包含存儲在本地域中的對象描述的包裝器,所以當和Transform等組件交互時,大多數指令會請求Unity進入它的本地代碼,在那裏生成結果,而後再將結果複製回託管域,這正是本地-託管橋的由來。當兩個域對相同實體有本身的描述時,跨越它們須要內存進行上下文切換,從而會帶來一些嚴重的潛在性能問題。

5.2 內存管理性能加強

  1. 垃圾回收策略

    最小垃圾回收問題的一種策略實在合適的時間手動觸發垃圾回收,當肯定用戶不會注意到這種行爲時就能夠偷偷觸發垃圾回收,垃圾回收能夠經過System.GC.Collect()手動調用。甚至能夠在運行時使用Profiler.GetMonoUsedSize()Profiler.GetMonoHeapSize()方法決定是否須要調用垃圾回收。固然,最好的垃圾回收策略是避免垃圾回收。

  2. 字符串

    字符串本質是字符數組,所以字符串在內存中是連續的,當字符串再分配內存後就不可變了,即字符串是不可變的引用類型。對字符串的修改、合併、鏈接等操做都須要建立新的字符串。所以字符串的使用須要注意如下幾點:

    • 若是提早大體知道字符串的最終大小,那麼可使用StringBuilder類提早分配一個適當的緩衝區來存儲或修改字符串。
    • 若是不知道結果字符串的最終大小,使用StringBuilder可能不會生成大小合適的緩衝區,所以能夠採用string.Format()、string.Join()和string.Concat()。
  3. Unity API中的數組

    Unity API中有不少指令會致使堆內存分配,本質上包括了全部返回數組數據的指令,例如如下方法:

    GetComponents< T >(); //(T[])

    Mesh.vertices; //(Vector3[])

    Camear.allCameras; //(Camear[])

    每次調用這類API方法時,都會致使分配該數據的新內存,這些方法應該儘量避免,或者僅調用不多次數並緩存結果,避免比實際所須要更頻繁的內存分配。

  4. 循環子物體

    有時咱們迭代子物體時,可能會使用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'
    }

6、程序設置

6.1 紋理

image-20200928134345119

  • Read/Write Enabled:若是不須要運行時讀取圖片的像素信息的話,禁用,不然啓用後紋理的內存消耗會增長一倍。
  • Generate Mip Maps:Mipmaps和模型的LOD相似,會根據相機距離遠近下降或提高貼圖像素,可是會多出三分之一的內存開銷,若是不是模型貼圖,則能夠禁用,此外,UI的貼圖基本用不到,能夠禁用。
  • Max Size:視狀況而定,在2019.4版本Unity中最大能夠達到8192*8192,但通常不要過大,不然貼圖單個文件大小過大。

6.2 模型

image-20200928135145583

Model

  • Mesh Compression:壓縮比越高模型文件越小,須要根據項目實際效果決定,咱們項目目前都將其設爲Off
  • Read/Write Enable:若是不須要修改模型時,能夠禁用,不然啓用後模型內存消耗會增長一倍。可是注意,以前也說過,因爲項目中使用了Runtime Editor插件,與該插件須要配合的模型要將此項啓用。
  • Optimize Mesh:默認Everything,能夠提高GPU性能。
  • Normals:若是模型沒有法線信息,能夠將其設置爲None,減少模型大小。

Rig

image-20200928135802410

  • Animation Type:若是模型沒有動畫,將其設置爲None
  • Optimize Game Objects:在使用Animator製做動畫時,將該項啓用,能夠將暴露在Hierarchy的子節點移除,極大減小了模型的層級和Children的數量,從而提高運行時的性能。若有掛節點需求,在Extra Transform to Expose中添加須要暴露的子節點便可。

6.3 其餘

Quality

image-20200928140325827

  • Pixel Light Count:場景使用正向渲染時的最大像素光源數。該值太小的話,假如在某個範圍內有多個光源,則這個範圍只會有最多設置值個光源產生光照做用,隨機某些光源不會發光。可是因爲實時光照性能消耗過大,疊加光照對於性能消耗呈指數級增加,所以該值也不宜設置太大,根據項目需求設置。

  • Texture Quality:貼圖質量,若是選擇Half Res,這樣速度會更快,可是貼圖質量會降低。

  • Anisotropic Textures:是否啓用各向異性紋理,若是選擇Forced On,則爲始終啓用。該項針對如下問題時可能產生效果:

    image-20200928142044013

    能夠看到在開啓前畫面有模糊,開啓後被修正爲正常的,即該選項能夠修正曲面傾斜後的貼圖。

  • 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。
    • 關於AUP的其餘內容可參考如下網站內容:優化加載性能:瞭解異步上傳管線AUP

Player Settings

image-20200928153840009

  • Scripting Backend:能夠選IL2CPP,轉成C++代碼後性能獲得提高,同時也變相提供了C#代碼的混淆。
  • C++ Compiler Configuration:默認選擇Release,若是發佈的話,能夠改爲Master,這樣打包速度雖然會慢一些,可是編譯的C++代碼會更加優化一些。

image-20200928154803237

  • Prebake Collision Meshes:啓用,用構建的時間換運行時的性能。
  • Keep Loaded Shaders Alive:啓用,由於Shader的加載和解析很耗時,因此不但願Shader被卸載。
  • Optimize Mesh Data:啓用,減小沒必要要的Mesh數據,下降包的大小。

寫文不易~所以作如下申明:

1.博客中標註原創的文章,版權歸原做者 煦陽(本博博主) 全部;

2.未經原做者容許不得轉載本文內容,不然將視爲侵權;

3.轉載或者引用本文內容請註明來源及原做者;

4.對於不遵照此聲明或者其餘違法使用本文內容者,本人依法保留追究權等。

相關文章
相關標籤/搜索