除了Unity的一些組件優化技巧之外,更多的細節處於代碼層面上
最近學習優化,看到一篇文章,寫的很詳細,從底層原理到我們
的實際處理,都有一些非常好的建議,可以推薦給小伙伴們看看
https://www.jianshu.com/p/289de89a6609
===========如何定位程序的哪一個環節產生了過大的開銷============
使用Uinty的Profiler工具,可以比較精准快速的定位程序的哪一個位置產生了大開銷
首先在build setting里面勾選Autoconnect Profiler

然后在windows欄選擇proflier即可

打開之后即可監控游戲每一幀的運行狀態了,可以根據FPS和每一幀運行的時間判斷游戲的卡頓程度
確定某一幀的占用率過高之后

點擊波狀圖,就會暫停游戲

下方就會列出當前游戲運行的消耗詳細信息了
跟進total欄的百分比,即可定位是哪個位置產生了最大的消耗,從而進行相應優化了
這個工具是用來定位優化點的,具體的優化方案可以參考下面的優化方式
由於篇幅有限,而且這個工具應該大部分人都知道而且會用,就不過多贅述了。
不明白的可以參閱這篇作者的 https://www.jianshu.com/p/a7cee5e548cf
寫的非常詳細
==================代碼層優化====================
一、內存管理
1:GC原理
C#的垃圾回收是自動托管的,垃圾回收系統也有一套生命周期和統計流程,下面就是關於GC的整體流程:
1)一次GC的過程分為2個階段:
標記清除階段,GC會假設堆中所有對象都可以被回收,然后找出不能回收的對象,打上清除標記,剩下的就是要被回收的了。找的過程就是檢查對象 有沒有被其他的對象引用的過程,如果這個對象在程序中沒有被引用到,那么就會被打上清除的標記。
重新地址排列階段,標記清除的對象被清除之后,堆里面的空間就會變成不是連續的了,GC的第二部就會開始重新排列還存在的對象,使堆中的地址 分配變成連續的。
2)整個.net的GC流程:
在進行.Net的GC階段,是不止一次GC操作的,C#采用了分代算法,先對程序里面的內存進行分代管理,再根據不同的代,進行不同力度的清理。
這里面的生命周期為3代, 第0代是新創建的代碼,在達到了0代集合的閾值之后,觸發0代的GC,幸存的對象會進入1代集合, 同理,在1代集合達到了閾值 的時候,也會進行GC,但是這次GC是 0代和1代一起執行,以此類推,2代集合GC的時候也會進行1代 2代的CG操作。按消耗量的比例應該是 1:10:100
這種分代算法是基於 老的對象生命周期一般都比新的對象生命周期長,就像公司的員工一樣,時間較長的員工公司看來一般都比新進來的員工穩定性要大。
2:優化策略
明白了大致GC的流程之后會發現,GC會消耗大量的CPU性能,因為這里面會經歷很多次的運算以及遍歷等
接下來是弄清楚GC什么時候被觸發,以及如何規避影響用戶體驗的GC操作
1:跟進分代算法,在容量達到閾值的時候會發生,
2:GC會不時的自動運行(頻率因平台而異)。
3:手動強制調用GC
大致的優化思路就是 降低每一次GC的運行時間(減少垃圾對象,使GC的過程中盡量少的遍歷),降低GC的頻率(降低觸發GC機制的次數),在加載地圖等需要用戶等待的游戲流程里面主動GC。
接下來就是跟進3種觸發機制做我們代碼上的優化了
1)、盡量少new不必要的字段
1 object obj = null; 2 update() 3 { 4 object = somebody; 5 } 6 7 update() 8 { 9 object obj =somebody; 10 }
上面的賦值方式只會在開辟一個內存空間,第二種會反復開辟內存空間,這些空間一般在一代GC里面就會被釋放掉。屬於最無用的代碼方式(沒必要的 情況下)。
2).使用對象池
對象池的使用會大大降低新內存空間的使用,他會在一個內存空間反復給新的值。
3).盡量使用緩存機制,少使用Instantiate實例化新對象,因為這里Unity會初始化他身上的組件以及各種序列化的操作,各個物體會根據自身的組件,創建耗時都 不相同
4).字符串的操作
string的拼接操作是在內部重新new 一個新的出來,因為string在C#是不可變更到,所以在每一次的+= 就相當於new了一個新的字符串出來,如果出現比較頻繁 的拼接操作,stringBuilder會比string 更好,但是string在對字符串的操作上會比stringBuider好,至於使用哪一種就看具體的需求了。
5).在地圖加載等需要等待的過程中主動進行GC。
避免內存消耗還有很多的方式,我也是剛剛開始比較全面的學習性能優化,在以上也是我在網上搜集了一些自己能理解的處理方式。
CPU性能管理:
一、代碼層:
1、 盡量避免空的Update(),只要寫了Update(),不管是不是空的,Unity都會去執行,這里會增加開銷
2、Find getcomponent 等查找的方法,Unity都會去遍歷場景對象和組件,在大型項目中,場景對象一旦變多就會產生很大的開銷了,盡量在start里面調用而不 是update里面反復使用
3、Update里面盡量少做遍歷,一次UpDate 就會遍歷一次,這是一個很大的開銷了。
4、在業務需求不影響的情況下,可以讓Update里面的邏輯使用計時器,增加間隔,1秒調用一次,2秒調用一次等。
5、在可以知道觸發條件的情況下,盡量使用委托的方式處理觸發效果,而不是一次次的遍歷目標的觸發狀態
6、和上面一樣,盡量在設計代碼的時候,使用觀察者模式,理論上來說,游戲的大部分邏輯都可以使用觸發后再執行,特別是UI。(在使用lua熱更新的項目 中,大部分的游戲邏輯流程都是通過事件消息的觸發來完成的,因為里面沒有Uinty那么專業的生命周期)
7、for 和 foreach 的取舍 :
在固定長度或長度不需要計算的時候for循環效率高於foreach.
在不確定長度,或計算長度有性能損耗的時候,用foreach比較方便
二、渲染層:
渲染層的優化包括 圖集的結構設計、模型的處理、LOD、mipMap、烘焙等、陰影處理
圖集的結構設計是為了減少額外的draw call,盡量以模塊划分,因為原則上每個模塊之間的UI元素是不會互相耦合的。Unity的texture的大小是根據2的冪來計算到。如果你的圖片真實大小是1025,那么他會創建一塊2048的texture,也是浪費開銷的行為。
模型的處理要結合LOD的使用,在固定視角、固定攝像機深度的游戲中,LOD的發揮效果不是很大,更多的是制作模型的時候就確定了模型精度
LOD和MipMap的使用會帶來很大的性能提升,但是會受項目影響,具體看項目而定
烘焙就是在游戲發布之前,場景融合光照提前產生新的貼圖,使這個場景不用進行動態光照計算 就可以達到光照的效果,降低了CPU計算的開銷,但是對用戶的體驗肯定沒有實時光照那么好,一般都是出現在手游中
陰影和烘焙的道理差不多,為了降低實時光照的計算量,可以將陰影設置為假陰影,陰影在是圓的情況,不會產生旋轉的變化,同時因為烘焙的原因,光線沒有入射角的變化,也不會產生陰影的投射角度變化,也是在優化性能上比較常用的方式
