日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

浅谈Unity中的优化

發布時間:2023/12/14 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 浅谈Unity中的优化 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

    • 0-前言
    • 1-GC介紹
      • 1.1-腦圖
      • 1.1-介紹
      • 1.2-Unity內存管理機制簡介
      • 1.3-棧內存分配和回收機制
      • 1.4-堆內存分配和回收機制
      • 1.5-垃圾回收時的操作
      • 1.6-何時會觸發垃圾回收
      • 1.7-GC操作帶來的問題
    • 2-GC分析及優化
      • 2.1-分析GC帶來的問題
      • 2.2-分析堆內存的分配
        • 2.2.1-堆內存和堆棧內存分配的變量類型
        • 2.2.2-利用profiler window 來檢測堆內存分配
      • 2.3-降低GC的影響的方法
      • 2.4-減少內存垃圾的數量
        • 2.4.1-緩存
        • 2.4.2-不要在頻繁調用的函數中反復進行堆內存分配
        • 2.4.3-清除鏈表
        • 2.4.4-對象池
      • 2.5-造成不必要的堆內存分配的因素
        • 2.5.1-字符串
        • 2.5.2-Unity函數調用
        • 2.5.3-裝箱操作
        • 2.5.4-協程
        • 2.5.5-函數引用
        • 2.5.6-LINQ和常量表達式
      • 2.6-重構代碼來減小GC的影響
      • 2.7-定時執行GC操作
        • 2.7.1-主動調用GC操作
    • 3-解讀Unity性能優化
      • 3.1-常見的Unity性能問題
      • 3.2-Unity運行時的內存占用情況
      • 3.3-內存標準
      • 3.4-Mono內存管理策略
      • 3.5-Mono內存泄漏分析
      • 3.6-優化
        • 3.6.1-DrawCall優化
        • 3.6.2-CPU優化最直接的方法
        • 3.6.3-GPU優化
      • 3.7-研發團隊需要關注的引擎模塊
      • 3.8-開始優化工作
    • 4-Unity3D性能優化——初識Unity-Statistics
    • 5-Unity3D性能優化——Unity篇
      • 5.1-Unity 腳本開發方面
      • 5.2-Unity資源相關優化
        • 5.2.1-音頻
        • 5.2.2-Texture
        • 5.2.3-模型
        • 5.2.4-文本文件優化
        • 5.2.5-AssetBundle管理
        • 5.2.6-資源工作流管理
      • 5.3-Unity相機優化
        • 5.3.1-Occlusion Culling遮擋剔除
        • 5.3.2-Frustum Culling視錐體剔除
      • 5.4-UnityUGUI優化
      • 5.5-C# 語言層面的優化
      • 5.6-圖形渲染層面
    • 6-Unity3D性能優化——工具篇
      • 6.1-游戲性能簡述
      • 6.2-Unity3d性能分析工具
        • 6.2.1-Unity Profile
        • 6.2.2-其他性能分析工具

0-前言

此文章為網上轉載收集而成,非原創文章,請尊重別人的勞動成果,讓分享成為一種美德,歡迎轉載。另外,文章在表述和代碼方面如有不妥之處,歡迎批評指正。同時大家有更好的優化方案,或者自己獨立見解的優化想法,也請發相關郵件于我,我將持續更新這篇文章,努力將“淺談”轉變為“深入”!
觀前提示:本文篇幅較長,請耐心觀看或收藏

本文鏈接https://blog.csdn.net/qq_23420435/article/details/110109812

當前版本:V0.0.0 更新時間:2020-11-25 更新內容:首次整合更新 更新管理:小小Editor

收集整合

18328685848@163.com----轉載收集整合 小小Editor

章節一《CG介紹》、章節二《GC分析及優化》

http://www.cnblogs.com/msxh/p/6531725.html https://blog.csdn.net/worisaa/article/details/64121436----原創 路人張德帥----轉載 worisaa

章節三《解讀Unity性能優化》

https://www.jianshu.com/p/e427a38e91c3----Jumbo

章節四《Unity3D性能優化——初識Unity-Statistics》

https://blog.csdn.net/wdmzjzlym/article/details/51335915----萌萌的一天

章節五《Unity3D性能優化——Unity篇》

https://blog.csdn.net/weixin_43967425/article/details/84928476----原創 Kyle_GISer----原創 網絡

章節五點二《5.2-Unity資源相關優化》

https://blog.csdn.net/u012565990/article/details/51794486 https://blog.csdn.net/u013709166/article/details/54959464?----原創 CZandQZ----原創 ResetOTER

章節五點三點一《Occlusion Culling遮擋剔除

https://blog.csdn.net/qq_37672438/article/details/100933982----原創 路人張德帥

章節五點三點二《Frustum Culling視錐體剔除》

https://blog.csdn.net/xinjay1992/article/details/107550433?----原創 程序員茶館

章節五點四《5.4-UnityUGUI優化》

https://mp.weixin.qq.com/s?__biz=MzU5MjQ1NTEwOA==&mid=2247495094&idx=1&sn= 4a948884855c5a6f26f73ff7845c8caf&chksm=fe1dd91dc96a500baf2970fed3b13009e89 7997472f6596325102dff0cad616defd7a2470d52&token=2019102974&lang=zh_CN#rd----原創 Unity官方平臺----原創 網絡

章節六《Unity3D性能優化——工具篇》

https://zhuanlan.zhihu.com/p/39529241----朔宇

1-GC介紹

1.1-腦圖

1.1-介紹

在游戲運行的時候,數據主要存儲在內存中,當游戲的數據不在需要的時候,存儲當前數據的內存就可以被回收再次使用。內存垃圾是指當前廢棄數據所占用的內存,垃圾回收(GC)是指將廢棄的內存重新回收再次使用的過程。

Unity中將垃圾回收當作內存管理的一部分,如果游戲中垃圾回收十分復雜,則游戲的性能會受到極大影響,此時垃圾回收會成為游戲性能的一大障礙點。

下面我們將會學習垃圾回收的機制,掌握垃圾回收如何被觸發以及如何提高垃圾回收效率來減小其對游戲行性能的影響。

1.2-Unity內存管理機制簡介

要想了解垃圾回收如何工作以及何時被觸發,我們首先需要了解unity的內存管理機制。Unity主要采用自動內存管理的機制,開發時在代碼中不需要詳細地告訴unity如何進行內存管理,unity內部自身會進行內存管理。

unity的自動內存管理可以理解為以下幾個部分:

  • unity內部有兩個內存管理池:堆內存和堆棧內存。堆棧內存(stack)主要用來存儲較小的和短暫的數據片段,堆內存(heap)主要用來存儲較大的和存儲時間較長的數據片段。
  • unity中的變量只會在堆?;蛘叨褍却嫔线M行內存分配。
  • 只要變量處于激活狀態,則其占用的內存會被標記為使用狀態,則該部分的內存處于被分配的狀態,變量要么存儲在堆棧內存上,要么處于堆內存上。
  • 一旦變量不再激活,則其所占用的內存不再需要,該部分內存可以被回收到內存池中被再次使用,這樣的操作就是內存回收。處于堆棧上的內存回收及其快速,處于堆上的內存并不是及時回收的,其對應的內存依然會被標記為使用狀態。
  • 垃圾回收主要是指堆上的內存分配和回收,unity中會定時對堆內存進行GC操作。

在了解了GC的過程后,下面詳細了解堆內存和堆棧內存的分配和回收機制的差別。

1.3-棧內存分配和回收機制

棧上的內存分配和回收十分快捷簡單,主要是棧上只會存儲短暫的較小的變量。內存分配和回收都會以一種可控制順序和大小的方式進行。

棧的運行方式就像 stack :只是一個數據的集合,數據的進出都以一種固定的方式運行。正是這種簡潔性和固定性使得堆棧的操作十分快捷。當數據被存儲在棧上的時候,只需要簡單地在其后進行擴展。當數據失效的時候,只需要將其從棧上移除復用。

1.4-堆內存分配和回收機制

堆內存上的內存分配和存儲相對而言更加復雜,主要是堆內存上可以存儲短期較小的數據,也可以存儲各種類型和大小的數據。其上的內存分配和回收順序并不可控,可能會要求分配不同大小的內存單元來存儲數據。

堆上的變量在存儲的時候,主要分為以下幾步:

  • 首先,unity檢測是否有足夠的閑置內存單元用來存儲數據,如果有,則分配對應的內存單元;
  • 如果沒有足夠的存儲單元,unity會觸發垃圾回收來釋放不再被使用的堆內存。這步操作是一步緩慢的操作,如果垃圾回收后有足夠的內存單元,則進行內存分配。
  • 如果垃圾回收后并沒有足夠的內存單元,則unity會擴展堆內存的大小,這步操作會很緩慢,然后分配對應的內存單元給變量。

堆內存的分配有可能會變得十分緩慢,特別是需要垃圾回收和堆內存需要擴展的情況下。

1.5-垃圾回收時的操作

當一個變量不再處于激活狀態的時候,其所占用的內存并不會立刻被回收,不再使用的內存只會在GC的時候才會被回收。

每次運行GC的時候,主要進行下面的操作:

  • GC會檢查堆內存上的每個存儲變量;
  • 對每個變量會檢測其引用是否處于激活狀態;
  • 如果變量的引用不再處于激活狀態,則會被標記為可回收;
  • 被標記的變量會被移除,其所占有的內存會被回收到堆內存上。

GC操作是一個極其耗費的操作,堆內存上的變量或者引用越多則其運行的操作會更多,耗費的時間越長。

1.6-何時會觸發垃圾回收

主要有三個操作會觸發垃圾回收:

  • 在堆內存上進行內存分配操作而內存不夠的時候都會觸發垃圾回收來利用閑置的內存;
  • GC會自動的觸發,不同平臺運行頻率不一樣;
  • GC可以被強制執行。

GC操作可以被頻繁觸發,特別是在堆內存上進行內存分配時內存單元不足夠的時候,這就意味著頻繁在堆內存上進行內存分配和回收會觸發頻繁的GC操作。

1.7-GC操作帶來的問題

在了解GC在unity內存管理中的作用后,我們需要考慮其帶來的問題。最明顯的問題是GC操作會需要大量的時間來運行,如果堆內存上有大量的變量或者引用需要檢查,則檢查的操作會十分緩慢,這就會使得游戲運行緩慢。其次GC可能會在關鍵時候運行,例如CPU處于游戲的性能運行關鍵時刻,其他的任何一個額外的操作都可能會帶來極大的影響,使得游戲幀率下降。

另外一個GC帶來的問題是堆內存碎片。當一個內存單元從堆內存上分配出來,其大小取決于其存儲的變量的大小。當該內存被回收到堆內存上的時候,有可能使得堆內存被分割成碎片化的單元。也就是說堆內存總體可以使用的內存單元較大,但是單獨的內存單元較小,在下次內存分配的時候不能找到合適大小的存儲單元,這就會觸發GC操作或者堆內存擴展操作。

堆內存碎片會造成兩個結果,一個是游戲占用的內存會越來越大,一個是GC會更加頻繁地被觸發。

2-GC分析及優化

2.1-分析GC帶來的問題

GC操作帶來的問題主要表現為幀率運行低,性能間歇中斷或者降低。如果游戲有這樣的表現,則首先需要打開unity中的profiler window來確定是否是GC造成。

了解如何運用profiler window,可以參考此處,如果游戲確實是由GC造成的,可以繼續閱讀下面的內容。

2.2-分析堆內存的分配

如果GC造成游戲的性能問題,我們需要知道游戲中的哪部分代碼會造成GC,內存垃圾在變量不再激活的時候產生,所以首先我們需要知道堆內存上分配的是什么變量。

2.2.1-堆內存和堆棧內存分配的變量類型

在Unity中,值類型變量都在堆棧上進行內存分配,其他類型的變量都在堆內存上分配。如果你不知道值類型和引用類型的差別,可以查看此處。

下面的代碼可以用來理解值類型的分配和釋放,其對應的變量在函數調用完后會立即回收:

下面的代碼可以用來理解值類型的分配和釋放,其對應的變量在函數調用完后會立即回收:

void ExampleFunciton() {int localInt = 5; }

對應的引用類型的參考代碼如下,其對應的變量在GC的時候才回收:

void ExampleFunction() {List localList = new List(); }

2.2.2-利用profiler window 來檢測堆內存分配

我們可以在profier window中檢查堆內存的分配操作:在CPU usage分析窗口中,我們可以檢測任何一幀cpu的內存分配情況。其中一個選項是GC alloc,通過分析其來定位是什么函數造成大量的堆內存分配操作。一旦定位該函數,我們就可以分析解決其造成問題的原因從而減少內存垃圾的產生。

2.3-降低GC的影響的方法

大體上來說,我們可以通過三種方法來降低GC的影響:

  • 減少GC的運行次數;
  • 減少單次GC的運行時間;
  • 將GC的運行時間延遲,避免在關鍵時候觸發,比如可以在場景加載的時候調用GC

基于此,我們可以采用三種策略:

  • 對游戲進行重構,減少堆內存的分配和引用的分配。更少的變量和引用會減少GC操作中的檢測個數從而提高GC的運行效率。
  • 降低堆內存分配和回收的頻率,尤其是在關鍵時刻。也就是說更少的事件觸發GC操作,同時也降低堆內存碎片。
  • 我們可以試著測量GC和堆內存擴展的時間,使其按照可預測的順序執行。當然這樣操作的難度極大,但是這會大大降低GC的影響。

2.4-減少內存垃圾的數量

減少內存垃圾主要可以通過一些方法來減少:

2.4.1-緩存

如果在代碼中反復調用某些造成堆內存分配的函數但是其返回結果并沒有使用,這就會造成不必要的內存垃圾,我們可以緩存這些變量來重復利用,這就是緩存。

例如下面的代碼每次調用的時候就會造成堆內存分配,主要是每次都會分配一個新的數組:

void OnTriggerEnter(Collider other) {Renderer[] allRenderers = FindObjectsOfType<Renderer>();ExampleFunction(allRenderers); }

對比下面的代碼,只會生產一個數組用來緩存數據,實現反復利用而不需要造成更多的內存垃圾:

private Renderer[] allRenderers;void Start() {allRenderers = FindObjectsOfType<Renderer>(); }void OnTriggerEnter(Collider other) {ExampleFunction(allRenderers); }

2.4.2-不要在頻繁調用的函數中反復進行堆內存分配

在MonoBehaviour中,如果我們需要進行堆內存分配,最壞的情況就是在其反復調用的函數中進行堆內存分配,例如Update()和LateUpdate()函數這種每幀都調用的函數,這會造成大量的內存垃圾。我們可以考慮在Start()或者Awake()函數中進行內存分配,這樣可以減少內存垃圾。

下面的例子中,update函數會多次觸發內存垃圾的產生:

void Update() {ExampleGarbageGenerationFunction(transform.position.x); }

通過一個簡單的改變,我們可以確保每次在x改變的時候才觸發函數調用,這樣避免每幀都進行堆內存分配:

private float previousTransformPositionX;void Update() {float transformPositionX = transform.position.x;if(transfromPositionX != previousTransformPositionX){ExampleGarbageGenerationFunction(transformPositionX); previousTransformPositionX = trasnformPositionX;} }

另外的一種方法是在update中采用計時器,特別是在運行有規律但是不需要每幀都運行的代碼中,例如:

void Update() {ExampleGarbageGeneratiingFunction() }

通過添加一個計時器,我們可以確保每隔1s才觸發該函數一次:

private float timeSinceLastCalled; private float delay = 1f; void Update() {timSinceLastCalled += Time.deltaTime;if(timeSinceLastCalled > delay){ExampleGarbageGenerationFunction();timeSinceLastCalled = 0f;} }

通過這樣細小的改變,我們可以使得代碼運行的更快同時減少內存垃圾的產生。

2.4.3-清除鏈表

在堆內存上進行鏈表的分配的時候,如果該鏈表需要多次反復的分配,我們可以采用鏈表的clear函數來清空鏈表從而替代反復多次的創建分配鏈表。

void Update() {List myList = new List();PopulateList(myList); }

通過改進,我們可以將該鏈表只在第一次創建或者該鏈表必須重新設置的時候才進行堆內存分配,從而大大減少內存垃圾的產生:

private List myList = new List(); void Update() {myList.Clear();PopulateList(myList); }

2.4.4-對象池

即便我們在代碼中盡可能地減少堆內存的分配行為,但是如果游戲有大量的對象需要產生和銷毀依然會造成GC。對象池技術可以通過重復使用objects來降低堆內存的分配和回收頻率。對象池在游戲中廣泛的使用,特別是在游戲中需要頻繁的創建和銷毀相同的游戲對象的時候,例如槍的子彈。

要詳細的講解對象池已經超出本文的范圍,但是該技術值得我們深入的研究This tutorial on object pooling on the Unity Learn site對于對象池有詳細深入的講解。

2.5-造成不必要的堆內存分配的因素

我們已經知道值類型變量在堆棧上分配,其他的變量在堆內存上分配,但是任然有一些情況下的堆內存分配會讓我們感到吃驚。下面讓我們分析一些常見的不必要的堆內存分配行為并對其進行優化。

2.5.1-字符串

在c#中,字符串是引用類型變量而不是值類型變量,即使看起來它是存儲字符串的值的。這就意味著字符串會造成一定的內存垃圾,由于代碼中經常使用字符串,所以我們需要對其格外小心。

c#中的字符串是不可變更的,也就是說其內部的值在創建后是不可被變更的。每次在對字符串進行操作的時候(例如運用字符串的“加”操作),unity會新建一個字符串用來存儲新的字符串,使得舊的字符串被廢棄,這樣就會造成內存垃圾。

我們可以采用以下的一些方法來最小化字符串的影響:

1)減少不必要的字符串的創建,如果一個字符串被多次利用,我們可以創建并緩存該字符串。

2)減少不必要的字符串操作,例如如果在Text組件中,有一部分字符串需要經常改變,但是其他部分不會,則我們可以將其分為兩個部分的組件。

3)如果我們需要實時的創建字符串,我們可以采用StringBuilderClass來代替,StringBuilder專為不需要進行內存分配而設計,從而減少字符串產生的內存垃圾。

4)移除游戲中的Debug.Log()函數的代碼,盡管該函數可能輸出為空,對該函數的調用依然會執行,該函數會創建至少一個字符(空字符)的字符串。如果游戲中有大量的該函數的調用,這會造成內存垃圾的增加。

在下面的代碼中,在Update函數中會進行一個string的操作,這樣的操作就會造成不必要的內存垃圾:

public Text timerText; private float timer; void Update() {timer += Time.deltaTime;timerText.text = "Time:"+ timer.ToString(); }

通過將字符串進行分隔,我們可以剔除字符串的加操作,從而減少不必要的內存垃圾:

public Text timerHeaderText; public Text timerValueText; private float timer; void Start() {timerHeaderText.text = "TIME:"; }void Update() {timerValueText.text = timer.ToString(); }

2.5.2-Unity函數調用

在代碼編程中,我們需要知道當我們調用不是我們自己編寫的代碼,無論是Unity自帶的還是插件中的,我們都可能會產生內存垃圾。Unity的某些函數調用會產生內存垃圾,我們在使用的時候需要注意它的使用。

這兒沒有明確的列表指出哪些函數需要注意,每個函數在不同的情況下有不同的使用,所以最好仔細地分析游戲,定位內存垃圾的產生原因以及如何解決問題。有時候緩存是一種有效的辦法,有時候盡量降低函數的調用頻率是一種辦法,有時候用其他函數來重構代碼是一種辦法。現在來分析unity中中常見的造成堆內存分配的函數調用。

在Unity中如果函數需要返回一個數組,則一個新的數組會被分配出來用作結果返回,這不容易被注意到,特別是如果該函數含有迭代器,下面的代碼中對于每個迭代器都會產生一個新的數組:

void ExampleFunction() {for(int i=0; i < myMesh.normals.Length;i++){Vector3 normal = myMesh.normals[i];} }

對于這樣的問題,我們可以緩存一個數組的引用,這樣只需要分配一個數組就可以實現相同的功能,從而減少內存垃圾的產生:

void ExampleFunction() {Vector3[] meshNormals = myMesh.normals;for(int i=0; i < meshNormals.Length;i++){Vector3 normal = meshNormals[i];} }

此外另外的一個函數調用GameObject.name 或者 GameObject.tag也會造成預想不到的堆內存分配,這兩個函數都會將結果存為新的字符串返回,這就會造成不必要的內存垃圾,對結果進行緩存是一種有效的辦法,但是在Unity中都對應的有相關的函數來替代。對于比較gameObject的tag,可以采用GameObject.CompareTag()來替代。

在下面的代碼中,調用gameobject.tag就會產生內存垃圾:

private string playerTag="Player"; void OnTriggerEnter(Collider other) {bool isPlayer = other.gameObject.tag ==playerTag; }

采用GameObject.CompareTag()可以避免內存垃圾的產生:

private string playerTag = "Player"; void OnTriggerEnter(Collider other) {bool isPlayer = other.gameObject.CompareTag(playerTag); }

不只是GameObject.CompareTag,unity中許多其他的函數也可以避免內存垃圾的生成。比如我們可以用Input.GetTouch()和Input.touchCount()來代替Input.touches,或者用Physics.SphereCastNonAlloc()來代替Physics.SphereCastAll()。

2.5.3-裝箱操作

裝箱操作是指一個值類型變量被用作引用類型變量時候的內部變換過程,如果我們向帶有對象類型參數的函數傳入值類型,這就會觸發裝箱操作。比如String.Format()函數需要傳入字符串和對象類型參數,如果傳入字符串和int類型數據,就會觸發裝箱操作。如下面代碼所示:

void ExampleFunction() {int cost = 5;string displayString = String.Format("Price:{0} gold",cost); }

在Unity的裝箱操作中,對于值類型會在堆內存上分配一個System.Object類型的引用來封裝該值類型變量,其對應的緩存就會產生內存垃圾。裝箱操作是非常普遍的一種產生內存垃圾的行為,即使代碼中沒有直接的對變量進行裝箱操作,在插件或者其他的函數中也有可能會產生。最好的解決辦法是盡可能的避免或者移除造成裝箱操作的代碼。

2.5.4-協程

調用 StartCoroutine()會產生少量的內存垃圾,因為unity會生成實體來管理協程。所以在游戲的關鍵時刻應該限制該函數的調用?;诖?#xff0c;任何在游戲關鍵時刻調用的協程都需要特別的注意,特別是包含延遲回調的協程。

yield return 0;

由于需要返回0,引發了裝箱操作,所以會產生內存垃圾。這種情況下,為了避免內存垃圾,我們可以這樣返回:

yield return null;

另外一種對協程的錯誤使用是每次返回的時候都new同一個變量,例如:

while(!isComplete) {yield return new WaitForSeconds(1f); }

我們可以采用緩存來避免這樣的內存垃圾產生:

WaitForSeconds delay = new WaiForSeconds(1f); while(!isComplete) {yield return delay; }

如果游戲中的協程產生了內存垃圾,我們可以考慮用其他的方式來替代協程。重構代碼對于游戲而言十分復雜,但是對于協程而言我們也可以注意一些常見的操作,比如如果用協程來管理時間,最好在update函數中保持對時間的記錄。如果用協程來控制游戲中事件的發生順序,最好對于不同事件之間有一定的信息通信的方式。對于協程而言沒有適合各種情況的方法,只有根據具體的代碼來選擇最好的解決辦法。

2.5.5-函數引用

函數的引用,無論是指向匿名函數還是顯式函數,在unity中都是引用類型變量,這都會在堆內存上進行分配。匿名函數的調用完成后都會增加內存的使用和堆內存的分配。具體函數的引用和終止都取決于操作平臺和編譯器設置,但是如果想減少GC最好減少函數的引用。

2.5.6-LINQ和常量表達式

由于LINQ和常量表達式以裝箱的方式實現,所以在使用的時候最好進行性能測試。

2.6-重構代碼來減小GC的影響

即使我們減小了代碼在堆內存上的分配操作,代碼也會增加GC的工作量。最常見的增加GC工作量的方式是讓其檢查它不必檢查的對象。struct是值類型的變量,但是如果struct中包含有引用類型的變量,那么GC就必須檢測整個struct。如果這樣的操作很多,那么GC的工作量就大大增加。在下面的例子中struct包含一個string,那么整個struct都必須在GC中被檢查:

public struct ItemData {public string name;public int cost;public Vector3 position; } private ItemData[] itemData;

我們可以將該struct拆分為多個數組的形式,從而減小GC的工作量:

private string[] itemNames; private int[] itemCosts; private Vector3[] itemPositions;

另外一種在代碼中增加GC工作量的方式是保存不必要的Object引用,在進行GC操作的時候會對堆內存上的object引用進行檢查,越少的引用就意味著越少的檢查工作量。在下面的例子中,當前的對話框中包含一個對下一個對話框引用,這就使得GC的時候回去檢查下一個對象框:

public class DialogData {private DialogData nextDialog;public DialogData GetNextDialog(){return nextDialog;} }

通過重構代碼,我們可以返回下一個對話框實體的標記,而不是對話框實體本身,這樣就沒有多余的object引用,從而減少GC的工作量:

public class DialogData {private int nextDialogID;public int GetNextDialogID(){return nextDialogID;} }

當然這個例子本身并不重要,但是如果我們的游戲中包含大量的含有對其他Object引用的object,我們可以考慮通過重構代碼來減少GC的工作量。

2.7-定時執行GC操作

2.7.1-主動調用GC操作

如果我們知道堆內存在被分配后并沒有被使用,我們希望可以主動地調用GC操作,或者在GC操作并不影響游戲體驗的時候(例如場景切換的時候),我們可以主動的調用GC操作:

System.GC.Collect()

通過主動的調用,我們可以主動驅使GC操作來回收堆內存。

3-解讀Unity性能優化

3.1-常見的Unity性能問題

VSS:Virtual Set Size,虛擬耗用內存。它是一個進程能訪問的所有內存空間地址的大小。這個大小包含了 一些沒有駐留在RAM中的內存,就像mallocs已經被分配,但還沒有寫入。VSS很少用來測量程序的實際使 用內存。

RSS:Resident Set Size,實際使用物理內存。RSS是一個進程在RAM中實際持有的內存大小。RSS可能會 產生誤導,因為它包含了所有該進程使用的共享庫所占用的內存,一個被加載到內存中的共享庫可能有很 多進程會使用它。RSS不是單個進程使用內存量的精確表示。

PSS:Proportional Set Size,實際使用的物理內存,它與RSS不同,它會按比例分配共享庫所占用的內存。 例如,如果有三個進程共享一個占30頁內存控件的共享庫,每個進程在計算PSS的時候,只會計算10頁。 PSS是一個非常有用的數值,如果系統中所有的進程的PSS相加,所得和即為系統占用內存的總和。當一個 進程被殺死后,它所占用的共享庫內存將會被其他仍然使用該共享庫的進程所分擔。在這種方式下,PSS 也會帶來誤導,因為當一個進程被殺后,PSS并不代表系統回收的內存大小。

VSS:Virtual Set Size,虛擬耗用內存。它是一個進程能訪問的所有內存空間地址的大小。這個大小包含了 一些沒有駐留在RAM中的內存,就像mallocs已經被分配,但還沒有寫入。VSS很少用來測量程序的實際使 用內存。

RSS:Resident Set Size,實際使用物理內存。RSS是一個進程在RAM中實際持有的內存大小。RSS可能會 產生誤導,因為它包含了所有該進程使用的共享庫所占用的內存,一個被加載到內存中的共享庫可能有很 多進程會使用它。RSS不是單個進程使用內存量的精確表示。

PSS:Proportional Set Size,實際使用的物理內存,它與RSS不同,它會按比例分配共享庫所占用的內存。 例如,如果有三個進程共享一個占30頁內存控件的共享庫,每個進程在計算PSS的時候,只會計算10頁。 PSS是一個非常有用的數值,如果系統中所有的進程的PSS相加,所得和即為系統占用內存的總和。當一個 進程被殺死后,它所占用的共享庫內存將會被其他仍然使用該共享庫的進程所分擔。在這種方式下,PSS 也會帶來誤導,因為當一個進程被殺后,PSS并不代表系統回收的內存大小。

3.2-Unity運行時的內存占用情況

3.3-內存標準

  • 限定內存占用不超過200M(iPhone4接近容易Crash,低端機型)
  • 項目中Reserved Total(總體分配)內存盡量控制在150M以內,如下 Texture 50M Mesh 20M AnimationClip 15M AudioClip 15M Mono堆內存 40M 字體等 10M
  • 項目中盡量嚴格控制,即使在中高端機型可較大內存運行。

3.4-Mono內存管理策略

  • 字符串連接處理,建議StringBuilder
  • 盡量不使用foreach,Unity5.4以上解決了GC問題
  • 不要頻繁實例化和銷毀對象,建議對象池管理
  • 場景切換時,主動調用System.GC.Collect(),及時清理內存

  • Mono通過垃圾回收機制(Garbage Collect,簡稱GC)對內存進行管理。Mono內存分為兩部分,已用內存(used)和堆內存(heap),已用內存指的是mono實際需要使用的內存,堆內存指的是mono向操作系統申請的內存,兩者的差值就是mono的空閑內存。當mono需要分配內存時,會先查看空閑內存是否足夠,如果足夠的話,直接在空閑內存中分配,否則mono會進行一次GC以釋放更多的空閑內存,如果GC之后仍然沒有足夠的空閑內存,則mono會向操作系統申請內存。

3.5-Mono內存泄漏分析

  • Mono通過引用關系,判斷哪些內存不再使用
  • 【Mono內存泄漏】對象已經不再使用,卻未被GC回收
  • Mono內存泄漏使空閑內存減少,GC頻繁,mono堆不斷擴大,最終導致游戲內存占用的增大
  • 大部分mono內存泄漏的情況都是由于靜態對象的引用引起
  • 不再需要的對象將其引用設置為null,使其可以被GC及時回收

3.6-優化

3.6.1-DrawCall優化

  • 先了解下DrawCall相關概念,便于優化

    DrawCall是CPU調用底層圖形接口的操作

    DrawCall_Num = 25K * CPU_Frame * CPU_Percentage / FPS

    DrawCall_Num : DrawCall數量(最大支持)

    CPU_Frame : CPU 工作頻率(GHz單位)

    CPU_Percentage:CPU 分配在DrawCall這件事情上的時間率 (百分比) FPS:希望的游戲幀率

  • DrawCall Batching(DC批處理)

    Dynamic Batching(動態批處理)

    Static Batching(靜態批處理)

  • Bus總線帶寬

    CPU完成一次DrawCall,除了需要調用一次DrawCall的命令之外,還需要把內存中頂點數據、紋理貼圖、shader參數通過bus總線拷貝到內存分配給GPU的顯存之中,注意這是拷貝,不是指針傳遞,速度不快。項目中不會同時出現的資源不要打包到一起,保證單張合并紋理不大于1024*1024一般就不會有問題了。

3.6.2-CPU優化最直接的方法

  • VSync(垂直同步)是CPU優化最直接的方式(發熱、耗電原因之一)

  • 打開Edit-Project Settings-Quality找到V Sync Count

    V Sync Count

    Don’t Sync 不同步

    Every V Blank 每一個垂直同步

    Every Second V Blank 每一秒垂直同步

通常我們選擇Don’t Sync,同時Application.targetFrameRate設置目標FPS,讓性能保持一個好的狀態。注意選擇其他項,Application.targetFrameRate設置不生效。

垂直同步講會在下節“5.2-Unity Profile中詳細介紹”

3.6.3-GPU優化

渲染流程

GPU接收頂點數據作為輸入傳遞給頂點著色器。頂點著色器的處理單元是頂點,輸入進來的每個頂點都會調用一次頂點著色器。(頂點著色器本身不可以創建或銷毀任何頂點,并無法得到頂點與頂點之間的關系)。頂點著色器是完全可編程的,它主要完成的工作有:坐標變換和逐頂點光照。 坐標變換:就是對頂點的坐標進行某種變換—把頂點坐標從模型空間轉換到齊次裁剪空間。頂點的多少直接決定了三角形面的多少,也直接決定了GPU的渲染流水線的工作量,所以減少頂點數是一個比較重要的優化點。那么減少頂點怎么操作呢,又有哪些途徑?

  • 頂點著色器 優化基本幾何體(模型減面減頂點) 使用LOD(Level of detail)技術 使用遮擋剔除(Occlusion culling)技術
  • 中間操作 曲面細分著色器:是一個可選的著色器,主要用于細分圖元 幾何著色器:是一個可選的著色器,可用于執行逐圖元的著色操作,或者被用于產生更多的圖元。 裁剪:這一階段是可配置的。目的是把那些不在視野內的頂點裁剪掉,并剔除某些三角形圖元的面片。部分在視野內的圖元需要做裁剪處理,在裁剪邊緣產生新的頂點和三角形進行處理。 屏幕映射:這一階段是可配置和編程的,負責把每個圖元的坐標(三維坐標系)轉換成屏幕坐標(二維坐標系)。
  • 三角形設置:開始進入光柵化階段,不再是數學上點了,而會把所有的點都映射到屏幕的具體像素坐標上,計算每條邊上的像素坐標而得到三角形邊界的表示方式即為三角形設置。 三角形遍歷:這一階段會檢查每個像素是否被一個三角風格所覆蓋。如果覆蓋的話,就會生成一個片元(一個片元并不是真正意義上的像素,而是包含了很多狀態的集合,這些狀態用于計算每個像素的最終顏色。這些狀態包括了屏幕坐標、深度信息,及從幾何階段輸出的頂點信息,如法線和紋理坐標等。),這樣一個查找哪些像素被三角形覆蓋的過程就是三角形遍歷。
  • 片元著色器 盡量減少overdraw 減少實時光照 不要使用動態陰影 盡量使用簡單的shader

片元著色器的輸入就是上一階段對頂點信息插值得到的結果,更具體點說,是根據從頂點著色器中輸出的數據插值得到的。而這一階段的輸出是一個或者多個顏色值。這一階段可以完成很多重要的渲染技術,如紋理采樣,但是它的局限在于,它僅可以影響單個片元。片元著色器是比較花時間的,因為它是最終顏色的計算者,在某些情況下,例如復雜燈光環境下,片元著色器會出現GPU流水線主要的拖后腿的存在。為了讓片元著色器的計算更加快,我們需要從很多方面進行提前的優化:片元著色器最容易拖后腿的情況就是,overdraw!和Android app的開發一樣,就是同一個像素點繪制了多次,某些情況會造成計算力的浪費,增加耗電量。前面提到的遮擋剔除有減少overdraw非常有用。在PC上,資源無限,為了得到最準確的渲染結果,繪制順序可能是從后往前繪制不透明物體,然后再繪制透明物體進行混合。但是在移動平臺上,對于不透明物體,我們可以設置從前往后繪制,對于有透明通道的物體(很多UI紋理就是含有透明通道的),再設置從后往前繪制。unity中shader設置為“Geometry” 隊列的對象總是從前往后繪制的,而其他固定隊列(如“Transparent”“Overla”等)的物體,則都是從后往前繪制的。這意味這,我們可以盡量把物體的隊列設置為“Geometry” 。對于GUI,尤其要注意和設計師商量,能用不透明的設計就用不透明的,對于粒子效果,也要注意不要引入透明值,多半情況下,移動平臺的粒子效果透明值沒有作用。

移動平臺的最大敵人。一個場景里如果包含了三個逐像素的點光源,而且使用了逐像素的shader,那么很有可能將Draw Calls提高了三倍,同時也會增加overdraws。這是因為,對于逐像素的光源來說,被這些光源照亮的物體要被再渲染一次。更糟糕的是,無論是動態批處理還是動態批處理(其實文檔中只提到了對動態批處理的影響,但不知道為什么實驗結果對靜態批處理也沒有用),對于這種逐像素的pass都無法進行批處理,也就是說,它們會中斷批處理。所以當你需要光照效果時,可以使用Lightmaps,提前烘焙好,提前把場景中的光照信息存儲在一張光照紋理中,然后在運行時刻只需要根據紋理采樣得到光照信息即可。當你需要金屬性強(鏡面)的效果,可以使用Light Probes。當你需要一束光的時候,可以使用體積光去模擬這個效果。

動態陰影很酷,但是對于片元著色器來說是災難,陰影計算是三角投影計算,非常耗性能。如果想要陰影,可以使用

  • 簡單的使用一個帶陰影的貼圖
  • 烘焙場景,拿到lightmaps
  • 創建投影生成器的方法
  • 使用ShadowMap的方法
  • 建議盡量使用Unity自帶mobile版本的(built-in)Shader,這些大大提高了頂點處理的性能。當然也會有一些限制。
  • 自己寫的shader請注意復雜操作符計算,類似pow,exp,log,cos,sin,tan等都是很耗時的計算,最多只用一次在每個像素點的計算,還有有些除法運算盡量該能乘法運算等。
  • 避免透明度測試著色器,因為這個非常耗時,使用透明度混合的版本來代替。
  • 浮點類型運算:精度越低的浮點計算越快。
  • 不要在Shader中添加不必要的Pass.
  • 3.7-研發團隊需要關注的引擎模塊

    3.8-開始優化工作

    前面已經介紹了性能相關概念以及需關注模塊,接下來該開始優化工作,具體步驟如下:

    4-Unity3D性能優化——初識Unity-Statistics

    當運行一個U3D場景后,可以在界面右上方看到一個叫做"Status"的按鈕,點開它就會出現一個重疊界面顯示出實時統計數據,比如下圖這種樣子:

    ? 如果你是一名U3D開發新手,或者對此功能非常不熟悉,那么你可能會在游戲優化過程中遇到很多麻煩。接下來的篇幅著重講講該窗口的作用和必要的相關名詞解釋。

    ? Statistics窗口,全稱叫做 Rendering Statistics Window,即渲染統計窗口(或渲染數據統計窗口),窗口中羅列出關于聲音、圖像、網絡狀況等多種統計信息

    • FPS(Time per frame andFPS):frames per seconds表示引擎處理和渲染一個游戲幀所花費的時間,該數字主要受到場景中渲染物體數量和 GPU性能的影響,FPS數值越高,游戲場景的動畫顯示會更加平滑和流暢。一般來說,超過30FPS的畫面人眼不會感覺到卡,由于視覺殘留的特性,光在視網膜上停止總用后人眼還會保持1/24秒左右的時間,因此游戲畫面每秒幀數至少要保證在30以上。另外,Unity中的FPS數值僅包括此游戲Scene里更新和渲染的幀,編輯器中繪制的Scene和其它監視窗口的進程不包括在內。
    • CPU:獲取到當前占用CPU進行計算的時間絕對值,或時間點,如果Unity主進程處于掛斷或休眠狀態時,CPU time將會保持不變。
    • Render thread:GPU渲染線程處理圖像所花費的時間,具體數值由GPU性能來決定,
    • Batches:即Batched Draw Calls,是Unity內置的Draw Call Batching技術。

    ? 首先解釋下什么叫做“Draw call”,CPU每次通知GPU發出一個glDrawElements(OpenGl中的圖元渲染函數)或DrawIndexedPrimitive(DirectX中的頂點繪制方法)的過程稱為一次Draw call,一般來說,引擎每對一個物體進行一次DrawCall,就會產生一個Batch,這個Batch里包含著該物體所有的網格和頂點數據,當渲染另一個相同的物體時,引擎會直接調用Batch里的信息,將相關頂點數據直接送到GPU,從而讓渲染過程更加高效,即Batching技術是將所有材質相近的物體進行合并渲染。

    ? 對于含有多個不同Shader和Material的物體,渲染的過程比較耗時,因為會產生多個Batches。每次對物體的材質或者貼圖進行修改,都會影響Batches里數據集的構成。因此,如果場景中有大量材質不同的物體,會很明顯的影響到GPU的渲染效率。這里說幾點關于Batches優化相關的方案。

    雖然Unity引擎自帶Draw Call Batching技術,我們也可以通過手動的方式合并材質接近的物體;

    盡量不要修改Batches里物體的Scale,因為這樣會生成新的Batch。

    為了提升GPU的渲染效率,應當盡可能的在一個物體上使用較少的材質,減少Batches過多的開銷;

    對于場景中不會運動的物體,考慮設置Static屬性,Static聲明的物體會自動進行內部批處理優化。

    • Verts:攝像機視野(field of view)內渲染的頂點總數。
    • Tris: 攝像機視野(field of view)內渲染的的三角面總數量。

    ? 關于Tris和Verts,突然想到一些問題,這里需要多嘴說幾句:

    Camera的渲染性能受到Draw calls的影響。之前說過,對一個物體進行渲染,會生成相應的Draw call,處理一個Draw Call的時間是由它上邊的Tris和Verts數目決定。盡可能得合并物體,會很大程度的提高性能。舉個很簡單例子,比如場景一種有1000個不同的物體,每個物體都有10個Tris;場景二中有10個不同的物體,每個物體有1000個Tris。在渲染處理中,場景一中會產生1000個Draw Calls,它的渲染時間明顯比場景二慢。

    Unity stats 視圖中的 Tris 和 Verts 并不僅僅是視錐中的梯形內的 Tris 和 Verts,而是Camera中 field of view所有取值下的tris和verts,換句話說,哪怕你在當前game視圖中看不到這個 cube,如果當你把 field of view調大到 179 過程中都看不到這個cube,stats面板才不會統計,GPU才不會渲染,否則都會渲染,而且unity不會把模型拆分,這個模型哪怕只有1個頂點需要渲染,unity也會把整個模型都渲出來。(參考自Mess的《Unity Camera組件部分參數詳解》)

    之前有童鞋問過我,新建一個空的場景,里邊沒有添加任何物體,為什么Status面板上顯示有1.7k Tris以及5.0kVerts。這是因為空的場景自帶默認的天空盒。點擊Windows—Lighting打開Lighting下的Scene面板,把Skybox里的材質設為空,比如像我下圖這樣:**

    ? 可以看到,場景中的Tris數量變為2,Verts數量變為了4,這是由于攝像機存在的關系,刪掉它,你就會發現Tris 和 Verts 都變為0了。

    • Screen:獲當前Game屏幕的分辨率大小,后邊的2.1MB表示總的內存使用數值。
    • SetPass calls:又碰到一個神奇的詞“SetPass calls”。如果你是一個Unity的老用戶,你可能會注意到原來的Stats面板的第一項是“Draw calls”,然而到了Unity5.X版本,Stats上沒有了“Draw calls”,卻多出來一項”SetPass calls“,那么這個玩意到底是做什么的???(你猜… )← ←!

    ? 感覺又要說一大堆東西了…之前有講到Batches,比如說場景中有100個gameobject,它們擁有完全一樣的Material,那么這100個物體很可能會被Unity里的Batching機制結合成一個Batch。所以用“Batches”來描述Unity的渲染性能是不太合適的,它只能反映出場景中需要批處理物體的數量。那么可否用“Draw calls”來描述呢?答案同樣是不適合。每一個“Draw calls”是CPU發送個GPU的一個渲染請求,請求中包括渲染對象所有的頂點參數、三角面、索引值、圖元個數等,這個請求并不會占用過多的消耗,真正消耗渲染資源的是在GPU得到請求指令后,把指令發送給對應物體的Shader,讓Shader讀取指令并通知相應的渲染通道(Pass)進行渲染操作。

    • Shadow casters:表示場景中有多少個可以投射陰影的物體,一般這些物體都作為場景中的光源。
    • visible skinned meshed:渲染皮膚網格的數量。
    • Animations:正在播放動畫的數量。

    5-Unity3D性能優化——Unity篇

    5.1-Unity 腳本開發方面

    基本思想減少動態內存分配和釋放,減少耗時函數的調用,盡可能的使用緩存。

    1:控制Startcorountien 的使用次數,避免頻繁的開啟協程,開啟一個協程至少分配37B的內存空間(corountien類的實例 分配21B Enumerator 分配16B)
    如果僅僅是延遲運行,定時運行函數,可以使用invoke invokerepeating 代替

    2:對象查找 gameobject.find(); 全局的查找方式,無法查找隱藏的對象,效率低下
    Transform.find 從自身位置查找,性能較優的推薦方法
    Gameobject.findobjectswithTag 通過標簽查找,效率較高,但是需要設置標簽稍顯麻煩。
    建議在Awake 或者Start 函數中查找對象并保存引用,切忌在Update中動態的查找

    3:緩存組件
    注意獲取組件的方法GetCompent 方法大約會分配39B的堆內存,并且GetCompent是去訪問unity原生代碼效率很低,使用緩存訪問,性能相差幾十倍。緩存訪問性能要高得多。
    (a)盡量避免在Update函數中做計算,可以使用InvokeRepeating,間隔一段計算一次。
    (b)避免使用SendMessage 性能底下,使用了反射機制,要使用委托 delegate的事件模擬消息機制。
    (c)刪除無用的預制體上的組件,刪除類中無用的函數。
    (d)腳本的禁用,腳本使用的時候開啟,不用的時候關閉,需要專門寫一套管理機制。

    4.減少.Count .Length的調用

    for(int i = 0; i < list.Count; i++) { //do something}

    應改為:

    for(int i = 0, j = list.Count; i < j ; i++) { //do something}

    5.減少 gameobject,transform,GetComponent 的使用

    我們可以在Start()方法中預先存儲好這些值,之后使用的時候調用預先存儲好的值即可。

    GameObject m_gameObject; Transform m_transform; Text m_text; void Start () { m_gameObject = gameObject; m_transform = transform; m_text = GetComponent<Text>(); }

    6.減少SetActive(bool)的使用

    對于要頻繁顯示隱藏的物體,我們可以減少使用SetActive(bool),而是通過transform.scale信息將其縮小為0,例如UI的隱藏。

    7.如果某個活動狀態(gameObject.active == true)的GameObject上的腳本中含有Awake()方法,即使這個腳本沒有被啟用(enabled==false),Awake()方法也會執行。如果游戲中含有非常多的帶有Update()方法的MonoBehavior,應該嘗試改變代碼結構來減少開銷,

    8.Camera.main所引起的問題與Find()方法類似,應該避免使用Camera.main并且手動管理對相機的引用。

    9.Update()和LateUpdate()等事件方法的每次調用都需要引擎代碼與托管代碼之間進行通信,還要Unity進行安全檢查(GameObject狀態是否合法等),即使這些事件方法的方法體是空的,引擎任然會對其進行調用。因此,為避免浪費CPU時間,應該 刪除空的事件方法。

    10.設置position和rotation會觸發內部的OnTransformChanged事件并傳播到所有的子級對象中,對于含有非常對子物體的對象來說,這種操作開銷很大,應該 減少對position和rotation的修改。

    11.嘗試使用localPosition替代position。localPosition存儲在transform中,訪問該值時,Unity會直接將其返回,而position在每次訪問時都會重新計算,如果要經常獲取position,可以將其緩存起來。

    12.不要附加 Animation Component 在靜態實體上附加 Animation 部件雖然對結果沒有影響,但卻會增加一定的 CPU 開銷來調用這一組件,所以盡量去掉該組件。

    13.關于相機相機
    裁剪平面
    將遠平面設置成合適的距離。遠平面過大會將一些不必要的物體加入渲染,降低效率。
    根據不同的物體設置不同的遠裁剪平面
    Unity 提供了可以根據不同的 layer 來設置不同的 view distance ,所以我們可以實現將物體進行分層,大物體層設置的可視距離大些,而小物體層可以設置地小些,另外,一些開銷比較大的實體(如粒子系統)可以設置得更小些等等。

    14.非運動物體盡量打上 Static 標簽
    Unity 在運行時會對 static 物體進行自動優化處理,所以應該盡可能將非運行實體勾上 static 標簽。

    15.不要實例化(Instantiate)對象,事先建好對象池,并使用Translate“生成”對象;

    16.使用 Resource.Load 方法在需要的時候再讀取資源;各種資源在使用完成后,盡快用Resource.UnloadAsset和UnloadUnusedAsset卸載掉;

    17.靈活運用AssetBundle的Load和Unload方法動態加載資源,避免主要場景內的初始化內存占用過高;(實現起來真的很難…)

    18.采用www加載了AssetBundle后,要用www.Dispose 及時釋放;

    19.在關卡內謹慎使用DontDestroyOnLoad,被標注的資源會常駐內存;

    20.盡量少使用FindObjectsOfType函數,這個函數非常慢,盡量少用且一定不要在Update里調用;

    21.場景中若存在不需要顯示的模型,盡量不使用SetActive方法,而是將其Scale置為Vectory.Zero,并禁用其MeshRenderer

    24.場景中絕對靜態的模型可以使用合并Mesh操作來進行減少DrawCall

    private void CombineMesh(){ for (int i = 0; i < rootTr.Count; i++){GameObject rootModel = rootTr[i].gameObject;//給父物體添加meshfilterMeshFilter combineMeshFilter = rootModel.GetComponent<MeshFilter>();if (combineMeshFilter == null){combineMeshFilter = rootModel.AddComponent<MeshFilter>();}//獲取子物體中所有的meshfiltervar filters = rootModel.GetComponentsInChildren<MeshFilter>();//將子物體的meshfilter添加到combineInstance中CombineInstance[] combines = new CombineInstance[filters.Length];for (int j = 0; j < filters.Length; j++){combines[j].mesh = filters[j].sharedMesh;combines[j].transform = filters[j].transform.localToWorldMatrix;}//合并mesh并將mesh賦值給父物體Mesh finalmesh = new Mesh(); finalmesh.CombineMeshes(combines);rootModel.gameObject.GetComponent<MeshFilter>().sharedMesh = finalmesh;}}

    25.支持分級Log(自定義logger),避免大量且頻繁的Log,在構建時屏蔽log。

    26.使用gameObject.CompareTag(“XXX”)而非gameObject.tag,后者會產生額外的內存與性能消耗。

    27.使用內建的常量,例如Vector3.zero等等,避免頻繁創建相同的對象。

    5.2-Unity資源相關優化

    這節主要講unity資源優化,先學習一個概念:資源管道,原始文件通過內容管道變成了一個可被Unity高效使用的中間文件 ,這不是Unity里面的一個概念,但Unity的工作行為和它很類似;Unity在導入資源的時候可以使用類型豐富的文件,這不意味著在我們生成的App中同樣也是這些文件,資源通過Unity的資源管道變成了較為統一的格式。我們在導入資源的時候有很多參數可以調整,

    5.2.1-音頻

    在Unity中導入聲音文件我們能看到類似下面的面板 , 我們先看第一個選擇Load Type,他有三個可值:

    Decompress On Load,Compressed In Memory ,Streaming。

    Decompress On Load

    在硬盤上壓縮這個文件,并在第一次加載到內存的時候解壓它,這是加載聲音文件的默認選項,大多數情況下我們應該使用這個選擇 。

    加載后解壓縮聲音,聲音文件將在他們加載不久后就解壓,這個選項適用于較小的壓縮聲音,以避免即時解壓縮的性能開銷。要知道在加載時解壓 Vorbis編碼的聲音將使用的內存是壓縮狀態的十倍或更多(ADPCM編碼大概3.5倍)所以不要使用此選項用于大文件。

    Decompress On Load 默認選項適用于小文件。

    ? Compressed In Memory

    ? 保持聲音在內存中是壓縮的并在播放時解壓縮。這有更多的CPU開銷(尤其是OGG / Vorbis格式的壓縮文件),但可以提高加載速度并減少內存消 耗,因此這個選項適用于大文件。在性能窗口可以看"DSP CPU"。

    ? Compressed In Memory 適用于大文件。

    ? Streaming 選項

    ? 直接從磁盤流音頻數據。這只使用了原始聲音占內存大小的很小一部分。 該方法使用最少的內存和最多的CPU,它有個很明顯的缺點就是不能被引用超過一次。試著讓 Audio Clip產生多個副本的時候會每個都產生數據緩沖區,如果非要這么做會產生大量的內存和cpu消耗。因此這個選擇最好是給單實例的Audio Clip,如背景和環境音效。對于手游而言不要優先考慮使用這種方式。

    在場景中把一個Audio Clip賦值給Audio Source組件,這個音頻文件將在場景初始化的時候加載到內存中。但是如果它啟用了,加載Audio Clip就變成了一個后臺任務它將推遲到場景初始化完成后再加載,換句話說就是在游戲開始后開始加載。 通過啟用這個選擇我們可以提高場景的啟動速度,但Play聲音的時候它還在后臺加載的話播放將推遲到加載完成,我們可以使用AudioClip對象的loadState 屬性來檢查是否加載完成以保證聲音在不會在一個不恰當的時間播放。

    ? Preload Audio Data 默認是啟用的,它表示Unity自動在場景初始化時加載文件。禁用此選項將推遲加載直到 AudioSource的 Play() 或 PlayOneShot() 方法執行的時候。從硬盤加載聲音文件,然后解壓,再push到內存里然后再播放,這么一系列的動作可能 會導致 CPU的峰值。

    ? 由于播放延遲和性能消耗,不建議在播放的瞬間加載。我們應該控制加載在播放之前的某些方便的時間使用AudioClip對象的LoadAudioData()方法加載。我們也可以用AudioClip對象的UnloadAudioData()方法手動控制聲音文件的內存釋放。

    ? 聲音文件的編碼格式和質量

    ? Unity支持三種聲音文件編碼格式的,由于平臺依賴性在某些特殊情況下會有其他的選項(如Xbox One的XMA和PS Vita的HEVAG )

    • Vorbis/MP3
    • PCM
    • ADPCM

    ? 我們導入到Unity的音頻文件可能是各種各樣常見的音頻格式,但通過內容管道,Standalone、WebGL和其他的一些非移動平臺使用Ogg-Vorbis格式的壓縮,而移動平臺使用MPEG-3(MP3)格式。

    ? PCM 提供高品質但犧牲文件大小最適合使用在很短的音效上。

    ? ADPCM 這種格式適用于大量音效上如腳步爆破和武器,它比PCM小3.5倍但CPU使用率遠低于Vorbis/MP3

    ? Vorbis/MP3 比PCM小但是品質比PCM低,比ADPCM消耗更多CPU。但大多數情況下我們還是應該使用這種格式,這個選擇還多了個Quality可以調節質量改變文件大小增強音頻性能

    **盡量減少音頻數量 **

    ? 因為每個 Audio Source的播放都會消耗一定量的cpu,所以我們可以控制場景中 Audio Source的數量來節省cpu。

    ? 一種方式是控制我們的音頻來源,這種方式我們硬盤上 的AudioClip可以同時播放的數量和總共播放的總數進行節流控制。我們通常會做一個AudioPool做這些節流操作,這很適合2d聲音和單實例的3d聲音(3d聲音在播放的時候仍然要放在場景中具體的位置 )。 更甚至是移除一些音頻,這種做法會影響到用戶體驗,我們應該在質量和性能上做權衡。

    減少AudioClip引用

    ? 場景中每個Audio Source的Audio Clip引用和Preload Audio Data 啟用都將消耗一定量的內存(壓縮解壓或者緩存),這些內存將貫徹整個場景,如果兩個或兩個以上的Audio Source引用相同的Audio Clip沒有額外的內存消耗。Audio Clips在Unity中是非托管資源這意味著他們不會通過設置為null釋放內存。

    ? Unity希望我們能夠加載和釋放這些資源,經常使用的音效長久的在內存中保存文件是合理的,因為每次加載一個文件到

    ? 內存中都會消耗CPU。然而,如果我們發現因為音效而使用太多的內存的時候,我們必須做出艱難的選擇是降低音頻質量還是完全移除他們來節省內存。另一方面,保存不常用的音效在一個很長的場景中將會造成重大問題。

    ? 我們可能會有很多一次性音效,如對話片段,它們沒有需 要長久保存在內存中。創建Audio Sources并分配一個AudioClip如果只是這樣即使在游戲中只有一次使用也會導致內存消耗過剩,我們應該利用Resources.Load()和Resources.UnloadAsset()來保持需要播放的音頻數據在內存中,一旦它不在需要就立即釋放它。

    5.2.2-Texture


    ? 術語“Texture” 和“Sprite”新手在游戲開發中經常會產生困惑,所以值得區分下,在Unity3d中Texture僅僅是個圖片,我們習慣讀作貼圖。本質上來講它是一個大的顏色數組告訴程序每個像素是什么顏色。

    ? 而Sprite是一個2d網格,一個平對著當前攝像機的quad,我們習慣讀作精靈。

    ? 還有一種東西叫做Sprite Sheets,我們稱它為圖集;它用一張大圖包含了很多圖片,最常見的使用時2d角色動畫。這些文件可以用Unity的Sprite Editor工具分割成小圖做幀動畫。

    ? 別管這些令人困擾的命名,來簡單談下Texture:我們導入的這些圖片文件一般都生成于Adobe Photoshop國內基本都是或者一些小眾軟件Gimp等。在運行的時候這些文件被加載到內存中,然后Push到GPU,應用Shader完成一次Draw Call。一般情況下,渲染一次擁有一個網格并攜帶一種材質的物體便會使用一次Draw Call,Draw Call的次數是決定性能比較重要的指標。

    ? 壓縮格式

    ? 像音頻文件一樣,Unity為我們多種文件壓縮技術來更有效的儲存文件。在導入Texture文件時,有幾個選項我們可以設置。首先是Texture Type ,這個設置不會影響到文件本身而是說Unity將如何解析,操作和壓縮它。

    ? 在Texture Type為Texture模式下Unity只給我們看到四個格式選項

    Compressed, 16-bit,True Color和Crunched

    ? 如果我們將Texture Type設置為Advanced,那么我們就有了更多的設置選項,它給了我們更多的 Texture解析的控制權。

    ? 紋理的壓縮方式取決于大小和質量的平衡,更多的需要做的是了解每種格式來選擇最適合的。但是這里著重要提一下的是Crunched格式,這個格式壓縮需要很長時間,但在運行時減壓是非??臁T谛掳姹镜腢nity中加入了這個選擇可以看到它被壓縮成PVRTC格式,推薦在開發手機應用程序時使用 PVRTC 紋理。你應該知道當使用 PVRTC 時與標準JPEG或PNG圖像相比有可能有些圖像質量的下降。是否值得在你的程序中做出一些犧牲取決于一些因素,但使用 PVRTC 紋理可以節省大量的內存空間,PVRTC 是個很好的選擇。

    ? 明智的使用 Mip Maps

    ? 呈現小物件的時候,像巖石樹木這樣的遠處物體,一個高精度的貼圖對于玩家來說是沒有意義的,因為看不到那么多的細節。但是如果使用這張高精度的貼圖會損失原本不應該的性能,這時候需要一個較少細節的紋理來提升程序性能。

    ? Mip Maps的發明就是來解決這個問題 的,通過啟用 Generate Mip Maps

    ? 聽著好像很爽的樣子但是他在我們手游中幾乎沒用,啟用Mip Maps還會讓最后生成的紋理文件增大33%左右。那他存在的意義何在如何什么時候使用它呢?它唯一有用的地方是攝像機需要渲染不同距離的紋理時。如果我們的紋理總是呈現在攝像頭的同樣距離,這時候開啟Mip Map就一點用都沒只是在浪費空間。還有如果只有單一的一種遠距離物體我們也應該禁用它,應該從原紋理縮放一個小紋理給這個遠處物體。

    ? **以下時候應該禁用這個選項: **

    • 在2d游戲中的幾乎所有的紋理(2d游戲正交投影不存在近大遠小)
    • UI界面
    • Mesh貼圖,Sprites,Particle Effects,因為他們總是在攝像機差不多的距離渲染不會有很明顯的距離差

    ? 考慮把圖片打包起來變成圖集

    ? 把圖塊變成圖集之后會減少Draw Calls的次數(其實就是減少Material 數量)明顯減少CPU消耗,內存消耗上基本相同。一個圖集下的所有貼圖都應該是使用相同Shader的,不然后果很嚴重做了還不如不做。

    ? 圖集經常使用在UI界面,2d游戲中,它在手游中幾乎是必不可少的,因為Draw Calls往往會成為手游的瓶頸。

    ? 我們不應該手動去生成圖集,有很多現成的工具有Unity自己的有第三方的還有一些源碼工程。要注意圖集不能大于目標平臺的最大要求,圖集最好合理的分布不要為了節省空間而視圖把圖集塞滿。如果渲染器需要從不同的圖集中調用紋理會導致大量的高速緩存命中失敗,從而有引發內存帶寬不足等等問題。 如果做pc游戲的話圖集就沒有特別的必要了,因為Draw Calls很少會成為pc游戲的性能瓶頸。正確的使用圖集它不是僅僅把圖片堆起來。

    ? 對于通用紋理,盡可能的使用九宮格。如果用大塊的紋理則會占用較大的內存空間。而針對對稱紋理則可以使用shader或者Scale翻轉等方法來重復利用以減小內存消耗。

    ? **調整非正方形紋理的壓縮率 **

    ? 不建議導入非正方形或者非2的次冪的紋理到我們的應用程序,因為GPU往往需要把紋理是變成正方形和2的次冪導致不必要的處理畸形的紋理尺 寸工作量。如果紋理不是2的次冪,Unity會scale、pad紋理,以使得達到2的次冪,這樣會花費更多內存,讓加載更慢,所以建議是避免導入非正方形和2的次冪的紋理。

    ? Other

    • 通過設置“MaxSize”來限制圖片的大小,一般來說我們應該講圖片縮小到肉眼剛好看不出壓縮的程度。圖片不要超過2048.
    • “Format”則表示壓縮的方式,不同的平臺我們應該用不同的壓縮方式來進行壓縮。例如:安卓使用ETC而IOS則使用PVRTC。
    • 一般情況下我們需要關閉圖片的Read&Write選項,否則內存會大一倍。除非我們對圖像需要進行讀寫操作。
    • 針對顏色范圍不大的素材,我們可以降低圖片的色階以減小圖片的大小。

    5.2.3-模型

    • 保證不可讀寫。
    • 將沒有動畫的模型上的動畫腳本去除,否則會消耗cpu。
    • 多個角色分享一套rig,可以解決資源。
    • 使用MeshCompression
    • 模型莫名期末有很多particle view物體。在3DMAX中當你按下6時候,會彈出particle view窗口。同時會生成一個多余的particle view物體(這個物體在大綱中是找不到的,但當使用Ctrl+A的時候,他就會被選到,而且這個物體會占用一定的文件空間),在導出時particle view會嚴重擾亂物體的軸心。決方法很簡單,只要在max導出之前,按下f11鍵,進入max的腳本編輯器,輸入delete $’particle view’,回車,此時下一行會提示有幾個particle view被刪除了。看到這個數字即可以放心導出了。
    • 當動畫播放出現褶皺、破損、奇葩的時候,估計是點受骨骼影響太多了,U3D設置中一個點最多只能受4個骨骼影響。
    • 在UI中最好不要使用Mesh特效,否則無法判斷深度。除非使用RendererTexture來適應,盡可能讓美術用別的方案來替代。

    5.2.4-文本文件優化

    • 解析文本是比較慢的。
    • 將文本轉成二進制文件,可以有效提高讀寫速度。
    • 將文本分成小塊,只讀取需要的部分。讀入之后如有必要則Cache起來。
    • 利用線程進行多線程讀取。

    5.2.5-AssetBundle管理

    目前市面上的大多數游戲都要涉及到熱更新,從Unity5.0開始,簡化的API讓AssetBundle的打包似乎不再是一件困難的工作,但是想要管理好AssetBundle的關系卻并不是那么容易的一件事。

    在游戲中加載資源我使用過兩種做法,都需要自己對加載方式進行封裝,通過多態來靈活加載工程內或者熱更的資源:

  • 一種是講所有需要加載的資源,例如prefab、文本等等放入到Resources下面(盡可能減少資源否則會影響啟動速度),而依賴的資源放在別的文件夾下,熱更的時候將資源加載到沙盒目錄下。在資源加載的時候,首先對沙盒目錄進行檢查,若有的話,則使用熱更資源,否則就加載Resource下的資源。優點是在工程內不需要打包,而缺點是由于沒有打包,導致在最后出包的時候打包緩慢。

  • 而另一種是將所有的資源打成AssetBundle放入StreamingAssets下,熱更的時候同樣十八AssetBundle下載到沙盒目錄下。資源加載的時候,首先對沙盒目錄進行檢查,若有的話使用熱更資源,否則就加載StreamingAssets下的資源。在加載的時候需要提供資源名和包名,在編輯器下可以通過AssetDatabase來直接從編輯器中加載資源,通過將場景加入到BuildSetting中來加載場景,避免每次進行修改的時候都需要重新打AssetBundle。這種方法在最后出包的時候比較快,在最終確定下資源正確性的時候構建AssetBundle。

    通過延遲加載來對AssetBundle進行加載,在一般的使用場景下,我們并不需要將所有的AssetBundle進行載入。在游戲中,我們將建立一張常用的Bundle列表,用于進入場景時加載該場景中的常駐資源。而不一定會出現的資源則在需要的時候進行即時加載,并且放入Bundle池中隨時準備取用,當Bundle閑置達到一定的時間后對其進行自動的卸載,這取決于該Bundle的使用頻度。在切換場景之后卸載該場景的常用Bundle而去加載另一個場景的常用Bundle。

    要注意Bundle的細粒度,如果Bundle的細粒度超過一定數量的話必然會引起熱更包體積過大,玩家的更新需要下載更多的資源包,而在場景中也需要加載更多原本并不被需要的資源,而過小細粒度則會造成場景加載的緩慢,給管理上也會增加難度。所以適當的細粒度在AssetBundle的分包中也非常重要。

    將公用的資源單獨打成包:如果一個資源本身沒有標記任何的Bundle包,而被許多別的Bundle包引用則會被打入每一個引用它的Bundle中,造成包體積的膨脹。例如Shader這樣的材質就很有必要單獨打成一個包用作公用,否則包體積和游戲內存占用會變成一個大問題。

    當手機的容量較小時,可以通過WWW.LoadFromCacheOrDownload來進行加載,而不必擔心內存不足的問題。

    在將代碼打包到Prefab的時候對于Component要用動態加載的方式,考慮使用lua腳本進行操作,或者是直接動態從程序集加載,這樣可以避免資源與代碼不同步的情況發生??梢灾粚Υa進行修改而不需要重新進行資源打包。

    在使用第二種方案建立項目的時候可以建立一個或者幾個資源項目,用于大量資源的打包,用于將AssetBundle打包并放入主項目中,在主項目的在打包的時候不必再對所有的AssetBundle資源進行再打包,可以大大提高打包效率,并且可以將工作流放入到資源項目當中,提高資源的迭代效率。

    在為資源分包的時候可以按照文件夾來進行區分,以便于管理。

    當在一個地方需要用到包中的一小個資源,例如一個2048*2048圖集中的一個小icon??截愐环?#xff0c;并且放入到目前需要使用的包中,避免由于小的資源需求而引入大內存消耗。

  • 5.2.6-資源工作流管理

    作為程序,我們在資源上面花的精力自然是越少越好,但是如果沒有好的工具鏈,好的流程,我們必定將會困在永遠做不完的資源管理中。美術發過來的max文件或許需要經過你的導出、導入到Unity、拖成預制體、掛動畫、掛碰撞盒等等的操作之后才能成為一個真正可用的資源。這個時候一個好的工具顯得格外重要。

    • uTomate,用于管理流程的Unity插件,我們可以通過簡單的節點連接來對我們的資源進行一系列操作的配置。除此之外,我們還可以用它來做一鍵打包等功能。
    • Defaulter – Custom Importer Defaults ,用于管理資源的導入統一化,通過路徑來決定其中資源的格式,例如:貼圖對應著的MaxSize、Format、Read&Write等等,還支持其他很多的資源,通過這個我們不再需要對每一個導入的資源進行手動的設置了,由于其開源的有點,我們也可以根據我們自己的需要進行優化。由于作者不再維護了,所以我們或許需要自己來進行編寫。
    • 熟悉一些簡單的程序腳本,例如maxscript或者是ps中的ExtendScript ToolkitCS6。我就曾經自己寫過一個自動切圖的小插件,不過效率不是很行,但是語言本身并不難學,能給美術和程序自己帶來很多方便,通過C#對命令行調用的方式集成到Unity中,相信整個工作會輕松不少。
    • 利用Jenkins或者Hudson進行持續集成。人肉集成是對人力與資源的一種浪費,極其容易出現錯誤,而本地打包則大大占用了程序員的本地帶寬,讓程序員無法繼續進行工。而通過配置Jenkins來自動實現可參數化的、穩健的、可持續的集成,項目組可以集中更多的力量來進行產品的迭代。
    • 使用Spine或者Unity Anima2D來制作2D動畫,可以事倍功半。
    • 當然最靠譜的還是自己用C#來寫工具,雖然會花一些時間,但是磨刀不誤砍柴工,花一天時間寫工具,今后的幾個月當中可能會減少你好幾天的工作量。好工具所帶來的生產力可能大大超出你的想象。
      資源的工作流除了用于生成資源,對于資源的管理也是非常的重要。

    5.3-Unity相機優化

    5.3.1-Occlusion Culling遮擋剔除

    unity 中的剔除包括兩種,一種是視角剔除,凡是不在攝像機視野內的物體,不進行渲染,第二種就是遮擋剔除,被擋住的物體不被渲染,即使它在相機視野內,兩種方法可以共存

    不進行視角剔除,也不進行遮擋剔除

    只進行視角剔除

    二者都進行

    Occlusion Culling使用兩種存儲方式, 一個為 View Cells (靜態物體) 另一種為Target Cells (移動的物體).二者的數據分別單獨存儲

    較大的物體不易當被遮擋物,較小的物體不易當遮擋物

    可以通過‘overdraw’來觀看場景中的遮擋情況,注意該模式下視野就代表攝像機,

    注意:黃色的區域為遮擋剔除區域,只有在這個區域內,遮擋剔除效果才會顯現

    一.Occlusion Culling Window

    在 Window > Rendering打開Occlusion Culling Window

    Object菜單欄:scene Filter:文件過濾器,all 就是顯示場景中所有的物體,renderers 顯示場景中帶有render組建的物體,occlusion Areas 顯示場景中帶有occlusion Area組件的物體

    選中一個物體后,也可以在這個面板設置它的Occluder Static和Oculudee Static

    NOTE: Whenever your camera is outside occlusion areas, occlusion culling will not be applied. It is important to set up your Occlusion Areas to cover the places where the camera can potentially be, but making the areas too large incurs a cost during baking.

    當你的相機在遮擋區域之外時,遮擋剔除將不被應用。設置遮擋區域以覆蓋相機可能出現的位置是很重要的,但是在烘焙過程中,使遮擋區域過大會帶來成本。

    當使用l (LOD) 時. (LOD0) 作為 Occluder.也就是說如果當前時Lod1則不會發生遮擋

    Smallest Occluder:遮擋物的最小體積,小于這個數則不會遮擋其他物體

    Smallest Hole:有的墻上有縫隙,你可以通過這個縫隙看到后面的物體,這個值是縫隙的直徑

    Backface Threshold:背面閾值,有些背面你是不可以到達的,比如說地形的下面,一個封閉房間的外面,這些地方你始終看不了,但是烘焙之后這些數據是包含在unity里面的,100代表不剔除這些數據,值越小數據就會越小,只是剔除背面數據

    Clear:清除烘焙數據 Bake:烘焙數據

    二.Occlusion Area

    Occlusion Area 遮擋區域:應用于遮擋剔除移動的物體. (移動的物體不能設置為 static). 你可以在一個空物體上面添加組件 Occlusion Area (Component -> Rendering -> Occlusion Area ).

    添加Occlusion Area組件之后, 點擊 Is View Volume 檢測遮擋移動的物體

    Size:遮擋區域的大小

    Center:遮擋區域的位置

    Is View Volume:是否應用遮擋剔除移動的物體

    遮擋區域可以有多個

    在該區域內的物體,可以移動,不用設置靜態,只要被遮擋就不會渲染,不被遮擋就渲染,事先也得烘焙

    5.3.2-Frustum Culling視錐體剔除

    一.應用背景

    在現代游戲中,游戲資源越來越多,游戲場景也越來越大越來越復雜,雖說硬件設備更新迭代很快,性能也日漸強大,但這還遠不能緩解復雜繁多的資源帶來的性能壓力,因而性能優化仍然很有必要。場景資源的剔除是性能優化的一個重要方面,剔除方式也有很多,比如OcclusionCulling、Frustum Culling、layerCullingDistance等。由于項目的需要,這里重點關注Frustum Culling(視錐體剔除)。

    視錐體剔除的基本思想:判斷對象是否在相機視錐體內(相交也算),在則不剔除,不在則剔除。判斷的方法也有很多,比較常見的方法是判斷對象的BoundingBox與相機視錐體的六個剪裁平面的關系,來判斷對象是否在視錐體中。為此Unity也提供了原生API以支持基于視錐體的剔除方案。

    二.問題要點

    這里基于Unity提供的原生API來探討基于視錐體的剔除流程,需要使用到GeometryUtility中提供的API。

    1.獲取相機的剪裁平面:

    有多個API可獲得剪裁平面:

    ①public static Plane[] CalculateFrustumPlanes(Camera camera);

    ② public static Plane[] CalculateFrustumPlanes(Matrix4x4 worldToProjectionMatrix);

    ③ public static void CalculateFrustumPlanes(Camera camera, Plane[] planes);

    ④ public static void CalculateFrustumPlanes(Matrix4x4 worldToProjectionMatrix, Plane[] planes);

    前三個API最終都是調用了④來實現剪裁面獲取功能的,其中①和②由于在內部創建了Plane數組,并返回,因此存在GC,而③和④需要預先定義一個長度為6的Plane數組,并傳入方法,方法內部會修改這些對象的值,因而不存在GC。所以建議使用③或者④。

    通過上述API獲取的剪裁平面的順序依次是:左、右、下、上、近、遠。

    2.傳入需要檢測對象的BondingBox:

    public static bool TestPlanesAABB(Plane[] planes, Bounds bounds);

    調用上述API,傳入通過①獲取的剪裁平面及對象的BoundingBox即可檢測出該對象是否在視錐體內。

    三.Demo源碼

    using System.Collections.Generic; using UnityEditor; using UnityEngine; using System.Linq;public class FrustumTest : MonoBehaviour {public Camera CulingCamera;public Renderer[] CullingTestObjects;private Plane[] planes;void OnEnable(){planes = new Plane[6];}void Update(){GeometryUtility.CalculateFrustumPlanes(CulingCamera, planes);for (var index = 0; index < CullingTestObjects.Length; index++){var bounds = CullingTestObjects[index].bounds;var result = GeometryUtility.TestPlanesAABB(planes, bounds);CullingTestObjects[index].enabled = result;}}[MenuItem("Test/Create")]static void Create(){var gos = new List<GameObject>();var root = new GameObject("Root").transform;for (var i = 0; i < 10; i++){for (var j = 0; j < 10; j++){for (var k = 0; k < 10; k++){var go = GameObject.CreatePrimitive(PrimitiveType.Cube);go.transform.position = new Vector3(i, j, k) * 2;go.transform.parent = root;gos.Add(go);}}}var test = new GameObject("FrustumTest").AddComponent<FrustumTest>();test.CulingCamera = Camera.main;test.CullingTestObjects = gos.Select(item => item.GetComponent<Renderer>()).ToArray();} }

    四.實驗效果

    五.存在的問題:

    通過上述API獲取剪裁面時,只能一次性獲所有的剪裁面,而在一些特殊情況下我們往往只需要部分剪裁面即可。同時上述API底層采用了P/Invoke方式調用了非托管C++庫來實現剪裁面的計算,頻繁調用會有一定的性能損耗。為了實現更加個性化的基于視錐體的裁剪方案,我們往往需要自行計算剪裁面,并進行包含檢測。

    5.4-UnityUGUI優化

    1.UGUI中若不需要使用到RaycastTarget的,務必將其關閉,一般UI里也就是按鈕才需要接收響應事件,那么大部分image和text是是不需要開RaycastTarget的。

    但是問題就來了,Unity默認在hierarchy窗口Create->UI->Image 、Text的時候就會自動幫我們勾選上RaycastTarget, 一個復雜點的界面至少也300+個Image和Text, 總不能一個個取消吧。 所以我們可以重寫Create->UI->Image的事件。

    [MenuItem("GameObject/UI/Image")]static void CreatImage(){if(Selection.activeTransform){if(Selection.activeTransform.GetComponentInParent<Canvas>()){GameObject go = new GameObject("image",typeof(Image));go.GetComponent<Image>().raycastTarget = false;go.transform.SetParent(Selection.activeTransform);}}}

    2.禁用空的Image。在Unity項目中,有時候會用空的Image并將alpha設置為0來接收點擊事件。這樣會產生不必要的overdraw,增加性能負擔,可以使用以下腳本,只監聽事件,不畫網格,從而減少overdraw。

    using UnityEngine.UI;public class EmptyRaycast : Graphic {public override void SetMaterialDirty(){}public override void SetVerticesDirty(){}protected override void OnPopulateMesh(VertexHelper vh){vh.Clear();} }

    按如下使用方式,可以實現點擊事件的監聽,并且不產生overdraw。

    而如果是使用Image的話,則會產生overdraw。

    3.場景中應盡量保持少許的Canvas,新增一個Canvas,都會增加一個Batches.

    4.UI 上的的Color屬性 不建議直接修改,修改會導致重繪mesh,建議修改材質球顏色不會出現這個問題

    5.在Loading界面使用動態打圖集技術 可以很大的優化DC。但是動態打圖集很增加Loading時間。

    6.設置UI點擊其他區域退出當前UI這樣的類似功能 會產生大量Overdraw,最好的方法寫個代碼 繼承Image 重新 OnPopulateMesh 方法,只寫一行 tofill.clear(); 就搞定了

    7.Sprite導入 UI一般關閉 Read/Write 和 Generate MipMaps

    8.GameObject.SetActive 盡量少使用。因為在OnEnable 和OnDisenable 都會重新設置一遍所有的臟標志. 尤其不要對大量的 text 進行 setactive。單個UI建議使用 canvas renderer的.cull屬性。如果多個UI建議使用canvas group中的alpha 設置0。

    9.避免使用Camera.main

    當設置畫布進行渲染時,不管該畫布是在世界空間還是攝像機的屏幕空間,都可以指定用于為UI中Graphic Raycaster生成交互事件的攝像機。渲染模式為“Screen Space - Camera”的畫布需要使用該設置,該設置名為“Render Camera”。

    然而在渲染模式為“World Space”的畫布上,該設置是可選的,名為“Event Camera”。

    如果將世界空間畫布的Event Camera字段留空,這不意味著該畫布不會接收事件。它會使用游戲的主攝像機。為了確定哪個攝像機是主攝像機,該畫布會訪問Camera.main屬性。

    根據Unity所使用的代碼路徑,每幀中每有一個Graphic Raycaster和世界空間畫布,該畫布會訪問7到10次Camera.main。每次訪問Camera.main都會調用Object.FindObjectWithTag。這個做法在運行時并不合適。

    解決方案:避免使用Camera.main

    緩存攝像機的引用,然后創建系統來跟蹤主攝像機。如果使用世界空間畫布,要指定Event Camera,不要將該屬性留空。如果需要修改Event Camera,編寫代碼來更新Event Camera屬性。

    10.避免使用布局分組

    問題:每個影響布局的UI元素都會至少執行一次GetComponents調用。

    當修改布局系統的一個或多個子元素時,會使布局變臟。修改后的子元素會使擁有該元素的布局系統(Layout System)無效化。

    簡單介紹一下布局系統:布局系統是一組連續的布局分組(Layout Group),它們在布局元素(Layout Element)之上。布局元素不只是名為Layout Element的組件,它們還包括UI圖像、文字和Scroll Rect組件,而且Scroll Rect同時也是布局分組。

    回到問題本身,每個使布局變臟的UI元素都會至少執行一次GetComponents調用,該調用會在布局元素父對象上尋找有效的布局分組。找到有效布局分組后,它會繼續遍歷Transform層級,直到停止尋找分組或是到達層級的根部分,無論先滿足哪個條件都會停止尋找過程。因此。每個布局分組會給每個子布局元素的改變過程添加一次GetComponents調用,使嵌套布局分組的性能變差。

    解決方案:避免使用布局分組。

    使用錨點進行比例布局。在擁有動態元素數量的活躍UI上,考慮編寫代碼來計算布局,僅在需要時運行該代碼,而不是每次發生改變的時候。

    11.Other
    動態打圖集算法

    Github: unityRuntimeSpriteSheetsGenerator

    圖片壓縮格式選擇

    • 安卓主要用RGB ETC 4bit 不支持A通道,如果透明 用ETC2 8bit 或者 再用一張ETC 1通道
    • Crunched 是untiy的二次壓縮方式,缺點就是壓縮時間過久
    • IOS主要用 RGBAPVRTC4 其次選用 SATC
    • 大部分壓縮格式要求 寬高是2的整數次冪(POT) 或4的整數倍數 .如果不能滿足,就單獨打圖集
    • 安卓 RGB ETC4通道分離 split alpha channel。要注意在Project setting中graphics shader 中添加 DefaultETC1。否則在真機可能出錯 。或直接用ETC2(優先選擇)

    打圖集的規則

    • 盡量把同一個界面放在一個圖集
    • 盡量不要把同一個圖放在多個公用圖集
    • 使用頻率高的圖 放在一個圖集
    • 大圖UI盡量不打進圖集
    • 建議使用插件打包圖集,效果會略微比Unity自帶的圖集打包好一點。節省空間很大一點

    UI 合批流程

    • 遍歷UI
    • 合批測試(當前UI會判斷底下UI是否可以合批 如果不合批 深度底下最大的+1)
    • 判斷順序 如果深度一樣 然后判斷材質是否一樣 再判斷 圖片是否一樣 最后再根據面板順序排序,如果深度為-1 則不渲染
    • 得出排序數組 看相鄰元素是否一樣 然后合批

    會打斷合批的操作

    父物體 的 Pos 改變z軸 , 旋轉改變 x y 軸,會退出批處理 會打斷上下合批(UI和其他UI不在同一平面就不能合批)

    UI 網格重建流程

    某ui改變----將此UI設置臟標志—添加臟標志隊列–下一幀重建mesh

    5.5-C# 語言層面的優化

    主要思想是減少存的動態分配和釋放,以及內存泄露,減少垃圾回收。

    1:
    (1)字符串的處理 使用stringbuild 類代替string 進行多個字符串的拼接。
    (2)String.format();
    避免使用“+”“aaa”+“bbb”這種方式 原因是“+”對字符串進行拼接,會導致臨時
    堆string 對象發生頻繁的堆對象的分配和釋放。

    2.盡可能使用for循環代替foreach
    數組 泛型list<> 一律使用for ,字典使用foreach
    原因: 每次foreach產生一個臨時的迭代器對象,迭代器會額外的分配內存。

    3:頻繁調用的函數中,如果有臨時變量是引用類型,要將其改為成員變量。避免頻繁的堆對象的創建和釋放。

    4:如果函數運行需要一個List 需要設置List為成員變量 獨立與函數的運行,可以
    通過clear()函數清空,重復使用全局成員集合,清空函數不會刪除內存空間,大大的減輕性能的負擔。

    5:避免使用Lambda表達式,存在內存泄露的內存隱患。

    6:盡量使用對象池 避免對象的頻繁創建和釋放,導致大量無效內存的積累,引發GC(垃圾回收)操作,造成性能的降低

    7:避免裝箱和拆箱的操作,會導致堆內存的分配。

    8:避免使用AraayList會把所有插入的數據當作object類處理,存在一個裝箱的過程。盡量使用泛型List<>;

    9:避免使用反射機制,運行時獲取類型信息,性能低下。

    10:即時的將對象設置為null、 委托事件即時注銷。

    11.別把成員變量聲明為 public 或 protected。都聲明為 private 而使用 public/protected 的屬性

    12.不在代碼中使用具體的路徑和驅動器名。 使用相對路徑,并使路徑可編程。

    13.避免不必要的調用 ToUpper 或 ToLower 方法
    String是不變類,調用ToUpper或ToLower方法都會導致創建一個新的字符串。如果被頻繁調用,將導致頻繁創建字符串對象。這違背了“避免頻繁創建對象”這一基本原則。
    例如,bool.Parse方法本身已經是忽略大小寫的,調用時不要調用ToLower方法。
    另一個非常普遍的場景是字符串比較。高效的做法是使用 Compare 方法,這個方法可以做大小寫忽略的比較,并且不會創建新字符串。
    還有一種情況是使用 HashTable 的時候,有時候無法保證傳遞 key 的大小寫是否符合預期,往往會把 key 強制轉換到大寫或小寫方法。實際上 HashTable 有不同的構造形式,完全支持采用忽略大小寫的 key: new HashTable(StringComparer.OrdinalIgnoreCase)。

    14…絕對不要在循環中使用try-Catch.

    15.利用using和try/finally語句來清理資源

    16.盡量少用模運算和除法運算,比如a/5f,一定要寫成a*0.2f。

    17.不要濫用靜態對象:由于靜態對象始終存在于內存當中,過度濫用的話容易出現占用內存過多的情況。當不再需要的時候,將靜態大對象置空,保證GC能夠正常進行。

    18.使用尾遞歸而非其他的遞歸,尾遞歸的性能好于頭遞歸。

    5.6-圖形渲染層面

    1.不要使用實時陰影,使用陰影紋理或者簡單的圖片模擬陰影。

    2.減少頂點,減少三角面數,峰值小于10w面/每幀,使用LOD進行多層次模型設置,進行遮擋。

    3.減少Drawcall 峰值<200 對場景物件 比如說:石頭,樹木等使用相同材質相同貼圖的物體進行批處理,降低DC。人物模型(人物模型一般都是帶骨骼的)SkinMesh。即便使用相同的材質貼圖,DC也不能進行批處理。使用Lightmap技術降低DC。 UI圖素進行合并(打包圖集)降低DC。

    4.盡量使用簡單材質,適合移動平臺(手機)Mobile這種Shader或者使用自定義一些優化過的Shader,高級Shader嚴重降低幀率,加大能耗,或者不可用。

    5.在紋理和質量之間做平衡,在保證質量不受太大影響的情況下,盡量使用小尺寸的紋理,提升效率。減少安裝包的大小,九宮格等

    6.減少骨骼數量的使用15個上下左右,減少粒子系統,粒子的數量。盡量減少粒子數量。

    7.共享材質 多一個材質DC就會上升 使用共享材質 將多張貼圖畫在一張較大的貼圖上面,達到共享材質的作用。

    8.減少,水面折射效果,或者霧效果。

    9.嘗試使用遮擋剔除Occlusion Culling進行渲染上面優化。

    10.使用LOD技術,在LOD2和LOD1可以修改貼圖的品質,Cast Shadow和 ReceiveShadow可以移除

    11.能不使用實時光渲染則盡量不要使用實時光渲染,使用光照貼圖(LightMap)替代

    12.將場景中的靜態物體進行合并Mesh處理

    13.場景中如果沒有使用燈光和像素燈,就不要使用法線貼圖,因為法線效果只有在有光源(Direct Light/Point Light/Angle Light/Pixel Light)的情況下才有效果。

    14.如果硬陰影可以解決問題就不要用軟陰影,并且使用不影響效果的低分辨率陰影;

    6-Unity3D性能優化——工具篇

    6.1-游戲性能簡述

    提起游戲性能,首先要提到的就是,不僅開發人員,所有游戲玩家都應該會接觸到的一個名詞:幀率(Frame rate)。

    幀率是衡量游戲性能的基本指標。在游戲中,“一幀”便是繪制到屏幕上的一個靜止畫面。繪制一幀到屏幕上也叫做渲染一幀。每秒的幀數(fps)或者說幀率表示GPU處理時每秒鐘能夠更新的次數。高的幀率可以得到更流暢、更逼真的動畫。

    現階段大多數游戲的理想幀率是60FPS,其帶來的交互感和真實感會更加強烈。通常來說,幀率在30FPS以上都是可以接受的,特別是對于不需要快速反應互動的游戲,例如休閑、解密、冒險類游戲等。有些項目有特殊的需求,比如VR游戲,至少需要90FPS。當幀率降低到30FPS以下時,玩家通常會有不好的體驗。

    而現階段隨著支持144HZ刷新率的硬件設備的涌現,能否維持對應高幀率又是一項新的指標,尤其是在電競領域

    但在游戲中重要的不僅僅幀率的速度,幀率同時也必須非常穩定。玩家通常對幀率的變化比較敏感,不穩定的幀率通常會比低一些但是很穩定的幀率表現更差。

    雖然幀率是一個我們談論游戲性能的基本標準,但是當我們提升游戲性能時,更因該想到的是渲染一幀需要多少毫秒。幀率的相對改變在不同范圍會有不同的變化。比如,從60到50FPS呈現出的是額外3.3毫秒的運行時間,但是從30到20FPS呈現出的是額外的16.6毫秒的運行時間。在這里,同樣降低了10FPS,但是渲染一幀上時間的差別是很顯著的。

    我們還需要了解渲染一幀需要多少毫秒才能滿足當前幀率。通過公式 1000/(想要達到的幀率)。通過這個公式可以得到,30FPS必須在33.3毫秒之內渲染完一幀,60FPS必須在16.6毫秒內渲染完一幀。

    渲染一幀,Unity需要執行很多任務。比如,Unity需要更新游戲的狀態。有一些任務在每一幀都需要執行,包括執行腳本,運行光照計算等。除此之外,有許多操作是在一幀執行多次的,例如物理運算。當所有這些任務都執行的足夠快時,我們的游戲才會有穩定且理想的幀率。當這些任務執行不滿足需求時,渲染一幀將花費更多的時間,并且幀率會因此下降。

    知道哪些任務花費了過多的時間,是游戲性能問題的關鍵。一旦我們知道了哪些任務降低了幀率,便可以嘗試優化游戲的這一部分。這就是為什么性能分析工具是游戲優化的重點之一。

    6.2-Unity3d性能分析工具

    工欲善其事必先利其器,這里我們來講解Unity3D優化所需的工具

    如果游戲存在性能問題,游戲運行就會出現緩慢、卡頓、掉幀甚至直接閃退等現象。在我們嘗試解決問題前,需要先知道其問題的起因,而嘗試不同的解決方案。若僅靠猜測或者依據自身原有的經驗去解決問題,那我們可能會做無用功,甚至引申出更復雜的問題。

    在這些時候我們就需要用到性能分析工具,性能分析工具主要測試游戲運行時各個方面性能,如CPU、GPU、內存等。通過性能分析工具,我們能夠透過游戲運行的外在表現,獲取內在信息,而這些信息便是我們鎖定引起性能問題的關鍵所在。

    在我們進行Unity性能優化的過程中,最主要用的到性能分析工具包括,Unity自帶的Unity Profile,IOS端的XCode ,以及一些第三方插件,如騰訊推出的UPA性能分析工具。

    我們主要針對Unity Profile進行講解,之后也會略微介紹另外一些性能分析工具。

    6.2.1-Unity Profile

    Unity Profile是Unity中最常用的官方性能分析工具,在使用Unity開發游戲的過程中,借助Profiler來分析CPU、GPU及內存使用狀況是至關重要的。

    首先我們來了解Unity Profile的面板:
    我們通過Window——>Profiler來激活Unity Profile面板

    在下圖中我們可以看到Unity Profile面板,其中有很多profilers,每個profiler顯示我們當前項目一個方面的信息,如CPU、GPU、渲染(Rendering)、內存(Memory)、聲音(Audio)、視屏(Video)、物理(Physics)、ui及全局光照(global illumination)。

    當項目運行時,每個profilers會隨著運行時間來顯示數據,有些性能問題是持續性的,有些僅在某一幀中出現,還有些性能問題可能會隨時間推移而逐漸顯出出來。

    在面板的下半部分顯示了我們選中的profilers當前幀的詳細內容,我們可以通過選擇列標題,通過這一列的信息值來排序。
    在CPU usage profiler中的列表題分別為:
    Total:當前任務的時間消耗占當前幀cpu消耗的時間比例。
    Self:任務自身時間消耗占當前幀cpu消耗的時間比例。
    Calls:當前任務在當前幀內被調用的次數。
    GC Alloc:當前任務在當前幀內進行過內存回收和分配的次數。
    Time ms:當前任務在當前幀內的耗時總時間。
    Self ms:當前任務自身(不包含內部的子任務)時間消耗。

    當我們在層級視圖中點擊函數名字時,CPU usage profiler將在Profiler窗口上部的圖形視圖中高亮顯示這個函數的信息。比如我們選中Cameta.Render,Rendering的信息就會被高亮顯示出來。

    我們可以Profiler的左下的下拉菜單中選擇Timeline。

    Timeline顯示了兩件事:cpu任務的執行順序和哪個線程負責什么任務。線程允許不同的任務同時執行。當一個線程執行一個任務時,另外的線程可以執行另一個完全不同的任務。和Unity的渲染過程相關的線程有三種:主線程,渲染線程和worker threads。了解哪個線程負責哪些任務的用處非常之大,一旦我們知道了在哪個線程上的任務執行的速率最低,那么我們就應該集中優化在那個線程上的操作。

    以上所顯示的數據依賴于我們當前選擇的profiler。例如,當選中內存時,這個區域顯示例如游戲資源使用的內存和總內存占用等。如果選中渲染profiler,這里會顯示被渲染的對象數量或者渲染操作執行次數等數據。

    這些profiler會提供很多詳細信息,但是我們并不總需要使用所有的profiler。實際上,我們在分析游戲性能時通常只是觀察一個或者兩個profiler,而不需要觀察的我們可以通過右上角的”X”關閉,如果需要在添加回來,可以通過左上角Add Profiler。
    例如,當我們的游戲運行的比較慢時,我們可能一開始先查看CPU usage profiler,CPU usage profiler也是在我們進行優化分析時最常用的Profiler。

    當然,除了CPU usage profiler,Unity Profiler中其他的Profiler在一些場合也非常的有用,比如GPU、內存、渲染等,其使用方法和CPU usage profiler也是大同小異,可以按照以上的步驟來查看并學習。

    我們在觀察數據時,需要觀察的目標有如下幾點:

    CPU:

    • GC Allow: 任何一次性內存分配大于2KB的選項。
      每幀都具有20B以上內存分配的選項 。

    GC相關的問題和優化,在之后我們會詳細的介紹。

    • Time ms:

      注意占用5ms以上的選項

    內存 :

    • Texture: 檢查是否有重復資源和超大內存是否需要壓縮等.
    • AnimationClip: 重點檢查是否有重復資源.。
    • Mesh: 重點檢查是否有重復資源。

    實際項目中的優化建議

    在了解了Unity Profiler后,現在我們在一個實際項目中來進行一次性能分析。同時來了解一般在實際項目中,主要會引起也是我們主要去觀察的性能問題出現在什么地方。

    以下是我做的一個簡單的游戲項目,并未做任何性能優化并且有大量引起性能問題的代碼,可以更方便大家觀察其性能問題。

    我們來看一下在CPU usage profiler面板中的可觀察項,在項目中我們可以先關閉VSync垂直同步來提高幀率。

    下圖中我關閉了除VSync之外的顯示,可以看到VSync的消耗

    具體步驟是edit->project settings->Quality,在Inspector面板中,V Sync count選擇don’t Sync.

    我們來簡單的介紹一下什么是垂直同步,以及關閉它之后會發生什么。

    要理解垂直同步,首先明白顯示器的工作原理。

    顯示器上的所有圖像都是單個像素組成了水平掃描線,水平掃描線在垂直方向的堆積形成了完整的畫面,無論是隔行掃描還是逐行掃描,顯示器都有兩種同步參數——水平同步和垂直同步。
    垂直和水平是CRT顯示器中兩個基本的同步信號,水平同步信號決定了CRT畫出一條橫越屏幕線的時間,垂直同步信號決定了CRT從屏幕頂部畫到底部,再返回原始位置的時間,而垂直同步代表著CRT顯示器的刷新率水準。
    在游戲項目中,如果我們選擇等待垂直同步信號也就是打開垂直同步,在游戲中或許性能較強的顯卡會迅速的繪制完一屏的圖像,但是沒有垂直同步信號的到達,顯卡無法繪制下一屏,只有等85單位的信號到達,才可以繪制。這樣FPS自然要受到刷新率運行值的制約。
    而如果我們選擇不等待垂直同步信號,那么游戲中繪制完一屏畫面,顯卡和顯示器無需等待垂直同步信號就可以開始下一屏圖像的繪制,自然可以完全發揮顯卡的實力。
    但是,正是因為垂直同步的存在,才能使得游戲進程和顯示器刷新率同步,使得畫面更加平滑和穩定。取消了垂直同步信號,固然可以換來更快的速度,但是在圖像的連續性上勢必會打折扣。

    需要注意,LCD顯示器其實也是存在刷新率的,但其機制與CRT不同,這里不做過多贅述,但是垂直同步和水平同步對于LCD顯示器來說,一樣是有必要的。

    在關閉垂直同步后,我們繼續看我們的項目

    可以看到,我們以Total和Time ms排序,在圖中拉黑的項(Camera Render)始終排在最前面。
    Camera Render是相機渲染工作的CPU占用量,在實際項目中,渲染是最常見的引起性能問題的原因。 而因為渲染而引起的性能問題的優化是一個非常大的工程,這方面的優化方法在我們后續的文章中會有詳細的教程去學習和分析。在這里,我們只需要先了解。
    我們這個項目的優化中,無疑,渲染造成的性能損耗是一個大頭。

    如果說,在我們性能分析中,渲染已經沒有什么問題,那么我們接下來要重點觀察的就是GC,也就是垃圾回收性能分析。
    我們按照GC Alloc的順序來顯示,可以看到下圖。

    在之前我們提到過,GC Alloc中,任何一次性內存分配大于2KB的選項,每幀都具有20B以上內存分配的選項 ,是需要我們重點關注的,顯而易見,我們的項目中,對于GC的優化,也有很大的問題。

    這里我們大致介紹一下GC的機制,要想了解垃圾回收如何工作以及何時被觸發,我們首先需要了解unity的內存管理機制。Unity主要采用自動內存管理的機制,開發時在代碼中不需要詳細地告訴unity如何進行內存管理,unity內部自身會進行內存管理。
    Unity內部有兩個內存管理池,堆內存和棧內存,垃圾回收主要是指堆上的內存分配和回收,unity中會定時對堆內存進行GC操作。
    當堆內存上一個變量不再處于激活狀態的時候,其所占用的內存并不會立刻被回收,不再使用的內存只會在GC的時候才會被回收。
    每次運行GC的時候,GC會檢查堆內存上的每個存儲變量,對每個變量會檢測其引用是否處于激活狀態,如果變量的引用不再處于激活狀態,則會被標記為可回收,被標記的變量會被移除,其所占有的內存會被回收到堆內存上。

    GC操作是一個極其耗費的操作,堆內存上的變量或者引用越多則其運行的操作會更多,耗費的時間越長。

    如果我們也排除了GC的問題, 那么再接下來,我們就要考慮到是否是腳本的一些問題造成性能損耗。

    這里的腳本,可能是我們自己寫的代碼,也有可能是我們使用的一些插件的代碼。在CPU usage profiler面板中,我們可以關注Script這一項。

    如果在一個很慢的幀中,一大部分時間被腳本運行所消耗,這意味著這些慢的腳本可能就是引起性能問題的主因。我們可以更加深入的分析數據來確認。

    首先我們按照Time ms來排序,然后選擇信息列表中的項目,如果是用戶腳本的函數,那么在Profiler上方會有高亮腳本的部分。這種情況,說明游戲的性能問題是和用戶腳本相關的,如下圖中的顯示,這部分腳本性能問題一定是與我們FixedUpdate有關。

    同時,我們還可以再關注一些物理、ui方面的性能問題。

    在上面我們討論的,是幾種最常見的性能問題,在實際項目優化中,如果有性能問題也逃不開這些,如果在這些方向都已經達到了我們的要求,但我們的游戲仍然有性能問題,我們應該遵循上面的方法解決問題:收集數據——>使用CPU usage profiler查看信息——>找到引起問題的函數。一旦我們知道了引起問題函數的名字,我們便可以針對性的,對其進行優化處理。

    6.2.2-其他性能分析工具

    在開頭我們說過,在我們進行Unity性能優化的過程中,最主要用的到性能分析工具包括,Unity自帶的Unity Profile,IOS端XCode Capture GPU frame以及一些第三方插件,如騰訊推出的UPA性能分析工具。

    這里我們簡單的介紹一下XCode和UPA.

    Xcode是 Mac OS X上的集成開發工具。在我們打包Unity IOS的項目時,必須使用到Xcode來幫助我們打包及發布。

    Xcode的功能也十分的強大,在我們開發IOS端時,可以使用其GPU frame Capture 功能為我們的項目進行性能優化分析。

    在unity中,我們打包時在Run In Xcode as 選擇debug模式,并且勾選Development Build

    打包完成后,使用Xcode打開文件,在Xcode中選擇Product ——> Scheme——> Manage Schemes

    然后會出現如下界面

    我們雙擊這個項目會出現如下界面

    然后我們在左側選中Run,然后在右側面板選擇Options

    在GPU frame Capture中選擇OpenGL ES或者Metal。

    在Debug模式下運行項目,當項目在真機上完全加載后,就可以進入Debug Navigator(View ——> Navigators ——> Show Debug Navigator)

    以下是GPU frame Capture具體功能的界面,在圖形化界面中,可以在游戲運行時清晰的了解到CPU、GPU、內存的使用情況。


    XCode的Capture GPU frame功能能高效且定量地定位到GPU中shader的消耗。

    UPA是騰訊和Unity聯合打造的一款性能分析工具,據說王者榮耀的性能優化分析就有使用到UPA,具體的使用方法可以通過客戶端性能測試【騰訊WeTest】去了解

    總結

    以上是生活随笔為你收集整理的浅谈Unity中的优化的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。