Unity 2017 Game Optimization 讀書筆記 Scripting Strategies Part 5

一. Disable unused scripts and objects

場景中激活的物體或者腳本越多,開銷越大。對於很多並沒有產生作用的腳本和物體,可以隱藏掉從而提升性能,比如FPS遊戲中視野外的部分。

1.Disabling objects by visibility

有時我們希望腳本和物體在不可見的時候能處於disabled狀態。Unity在內部渲染時提供了Frustum Culling技術來裁剪攝像機視野外的部分,也提供了Occlusion culling來剔除被遮擋的部分。但是這兩個技術只是渲染上的優化,並沒有影響到物體上運行在cpu的腳本,比如AI腳本邏輯腳本都,都依然會運行產生開銷,我們需要自己控制這些行爲。

一個不錯的解決方法是使用OnBecameVisibe()和OnBecameInvisible()函數。這倆個函數會在renderable物體變看見或者不可見時被調用,對於多個相機的場景,當物體被任意一個相機可見,就會調用OnBecameVisible;當物體在所有相機都不可見時,會調用OnBecameInvisible。

要注意的是這倆個函數必須和渲染管線打交道,所以必須要有個renderable  component在物體上,比如MeshRender。

實現腳本的關啓或者整個Gameobject是否enable的代碼可以如下:

要注意的一點是對於隱藏掉的gameobject,就無法再被becameInvisible調用,所以一種解決方法是我們需要將腳本放在子物體上,將renderable 物體一直保持visible。

2.Disabling objects by distance

有時我們希望對於距離玩家足夠遠的腳本或物體變成disabled狀態,一個很好的例子是AI巡邏功能,當離玩家很遠時,可以保持Idle狀態不進行AI處理。下邊的代碼是一個簡單的示例:

3.Consider using distancesquared over distance

CPU計算開放運算的開銷要遠大於乘法,當調用Distance函數或者Vector3的magnitude函數時,都會進行開方運算。Vector3還提供了sqrMagnitude屬性,這個數值是未開方的,這意味着使用它進行一些距離的判斷在大多數情況下會得到同樣的結果,但是開銷卻小很多。舉個例子:

在絕大多數情況下,使用平方進行距離的判斷會得到同樣正確的結果,但是當需要精度極高時,會產生誤差,因爲使用平方會減少精度。

對於除了distance 外其他的開方操作,這個技巧同樣適用,sqrMagnitude property就是Unity爲我們提供的使用這個技巧的一個方式。

二. Minimize Deserialization behavior

Unity的序列化系統主要用於場景,prefabs,ScriptableObjects和各種各樣的Asset類型。當這些object 類型被保存到硬盤中時,會序列化成YAML格式的文件,YAML可以在之後被反序列化回原始的類型。

當一個prefab或者場景被序列化時,它所有的gameobjects和腳本都將被序列化,包括private和protected類型的字段,所有的子物體及子物體上的腳本。

當應用被構建時,這些序列化的數據會被一起打包成一個大的二進制文件,從disk讀取和反序列化這些數據相當慢,開銷很大,會造成明顯的性能開銷。

當我們調用Resources.Load時,就會產生反序列化操作,一旦數據從disk加載到內存後,再次加載這個引用的數據就會很快。數據越大,加載越慢,比如UI prefab上層級結構越複雜,就會開銷越大。第一次加載大的序列化數據時有可能產生很大的CPU開銷,導致掉幀,接下來介紹幾種可以降低這種反序列化方法的開銷。

1.Reduce serialized object size

儘量減小序列化物體的大小,或者將它們拆分成更小的單元,使得它們能以更小的單元加載。Unity不支持嵌套prefab(最新的已經支持),UI prefabs是很好的優化對象,因爲我們大多數情況下,在某一時刻我們並不需要整個UI,可以每次加載一些所需的。

2.Load serialized objects asynchronously

prefabs和其他序列化數據可以通過Resources.LoadAsync方法進行異步加載,這將會減輕主線程的負擔。使用異步方法時,需要一些時間來處理,使得序列化object變成可用狀態。

這種方式對於遊戲一開始就立刻需要的prefabs不太合適,但是之後所有的prefabs都是很好的異步加載候選對象。

3.Keep previously loaded serialized objects in memory

一旦序列化object被載入到內存後,將會一直保持在內存中,可以通過instantiating複製更多的prefab。通過Resources.Unload,將會釋放掉序列化object所佔用的內存空間。

如果我們遊戲的內存預算還很充足,可以考慮將序列化Object常駐內存中,這樣可以在需要使用時不必每次都要從硬盤中讀取,減少讀取數據所帶來的時間損耗,但是這種方案也給內存管理帶來了風險,隨着序列化數據的越來越多,所佔用的內存將會越來越多,所以我們應該具體情況具體分析,在有需要的時候使用這個方式。

4.Move common data into ScriptableObjects

如果我們有很多不同的prefab,但是都帶有包含了很多共享數據的腳本,比如遊戲策劃使用的數值比如速度,力量等,這些數據都將會被序列化到每一個使用它們的prefab中。對於這種,可以考慮將共享的數據通過ScriptableObject序列化成一個通用的數據,這將減少序列化數據的量以及可以明顯的縮短加載的時間。

5.Load scenes additively and asynchronously

加載場景可以使用替換掉當前場景的方式,也可以使用增量加載的方式加載新增的內容到當前場景中,不卸載之前的場景。可以通過勾選SceneManager.LoadScene中的LoadSceneMode來啓用這個功能。

加載場景還可以選擇是異步加載還是同步加載,通常最好的方式是兩種混合使用。

通過SceneManager.LoadScene可以進行同步加載,同步加載將會阻塞主線程直到所需的場景完全加載完畢。這通常使得用戶體驗很差。同步加載最好用在我們想讓用戶儘快操作或者沒有時間去等待場景物體出現時,這通常被用在加載遊戲的第一個場景或者返回到主菜單時。

通過SceneManager.LoadSceneAsync進行異步加載,可以讓場景的加載在背後偷偷進行,用戶沒有明顯的感知,可以有效提升用戶體驗。

值得注意的是場景並不等同於遊戲的關卡,在大多數遊戲中,玩家在某一時刻只在一個關卡中,但是Unity可以通過增量加載的方式支持多個場景同時被加載,每個場景只是關卡中的一部分。例如,我們可以在剛開始時加載第一個場景Scene-1-1a,當玩家接近下一個區域時,異步增量加載下一個場景Scene-1-1b,當玩家在關卡中游玩時重複這個操作。

想實現這個功能需要一套可以實時檢查關卡中player位置的系統,當playe接近下個區域時,異步增量加載下個場景,但要注意的是異步加載需要一部分時間來處理,也就是下個場景中的物體需要一些幀數後才加載好,因此一定要保證在觸發加載時有足夠的時間提前量來讓異步加載完成,來避免用戶看到物體是突然出現在場景中的。

場景可以通過卸載來釋放內存。卸載同樣也可以有兩種方式,同步卸載和異步卸載。卸載時要注意對於大場景,如果進行卸載就會卸載全部物體,如果想分部卸載,需要將原始場景切分成多個小場景。卸載時還要注意確保玩家確實看不到該場景的所有內容,否則玩家會看到物體突然消失的這種現象。另一個要注意的是卸載場景時會銷燬物體釋放很多內存,有可能觸發GC,因此也需要對內存進行高效的管理來滿足多場景加載的這種方案。

這種方案需要很強的場景設計,代碼編寫,測試等工作,但是對於用戶體驗的提升也是很顯著的,平滑的場景區域過渡常常能收到用戶和鑑賞家的讚賞,如果方案能使用得當,還可以大大提高運行時的性能表現,更加提升了用戶體驗。

三. Create a custom Update() layer

假設上千個MonoBehaviour腳本在場景一開始一起進行初始化,同時各自啓動一個Coroutine來處理耗時500ms的AI運算任務,它們會在同一幀中觸發,這很有可能產生CPU一個瞬時的巨大開銷,隨後cpu的開銷會降低,等到下個AI運算循環時又會產生極大的CPU瞬時開銷。

有三種解決方法:

(1)每次生成隨機的時間去等待Coroutine觸發

(2)將Coroutine的初始化時間點分散開來,來使得每一幀只有部分在處理

(3)用God Class來進行控制,將調用Update的責任丟給God Class,來限制每幀最多被調用的數量。

前兩個方法非常有吸引力是因爲非常簡單,但是這些方法會有不少隱患和副作用。

最好的方式就是根本不要用Update,或者準確的說只用一次。Unity調用Update時會有很多副作用,它需要之前提到過的Native-Managed Bridge來完成,因此開銷會比普通函數大很多,大概是1000倍。因此我們應該儘可能減少調用Native-Managed Bridge,自定義Update系統來替代調用Unity的Upate。

實際上很多Unity開發者很喜歡在項目之初就設計使用它們自己的Update系統,這可以讓他們控制Update何時在系統中傳播,控制菜單暫停,控制重要tasks的優先級等,比如發現在當前幀中cpu消耗超過了預算,可以將低優先級的任務在之後幀中再運行。

接下來我們實現一個簡易的Update 系統:

1.IUpdateable接口,規定了實現該接口的類需要定義OnUpdate函數

2.UpdateableComponet,繼承Monobehaviour以及IUpdateable接口,定義OnUpdate的virtural方法,以便繼承類可以自定義實現該方法。 

2.

3.註冊Update系統

4.定義Initialize virtual函數,爲繼承類提供初始化的功能,避免繼承類覆蓋Start函數

5.用單例模式實現GameLogic Update系統

如果場景中有n個繼承於UpdateableComponent的類,通過我們自定義的Update系統,可以將調用Native-Managed Bridge的次數從n次減少爲1次,效率將大大提升。這個系統還可以擴展成提供優先級功能的系統,以及加入其他更多功能。

對於已經開發比較久的項目,加入自定義的Update系統會是一件複雜耗時的工作,但是帶來的好處也是大大的,我們可以自己評估花些時間來進行這部分改造是否值得。

四. Summary

這一章提供了許多Unity中關於提升編碼實踐的方法,來提高性能。但是其中的一些技巧並不是什麼時候都適用,有些時候工作流的順暢與性能和設計同樣重要,所以在決定使用哪些優化方法前,需要先思考這些犧牲是否值得

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章