火星時代老師總結使用Unity 3D優化遊戲執行效能的經驗
流暢的遊戲玩法來自流暢的幀率,而我們即將推出的動作平臺遊戲《Shadow Blade》已經將在標準iPhone和iPad裝置上實現每秒60幀視為一個重要目標。
以下是我們在緊湊的優化過程中提升遊戲執行效能,並實現目標幀率時需要考慮的事項。
當基本遊戲功能到位時,就要確保遊戲執行表現能夠達標。我們衡量遊戲執行表現的一個基本工具是Unity內建分析器以及Xcode分析工具。使用Unity分析器來分析裝置上的執行程式碼真是一項寶貴的功能。
我們總結了這種為將目標裝置的幀率控制在60fps而進行衡量、調整、再衡量過程的中相關經驗。
一、遇到麻煩時要呼叫“垃圾回收器”(Garbage Collector,無用單元收集程式,以下簡稱GC)
由於具有C/C++遊戲程式設計背景,我們並不習慣無用單元收集程式的特定行為。確保自動清理你不用的記憶體,這種做法在剛開始時很好,但很快你就公發現自己的分析器經常顯示CPU負荷過大,原因是垃圾回收器正在收集垃圾記憶體。這對移動裝置來說尤其是個大問題。要跟進記憶體分配,並儘量避免它們成為優先數,以下是我們應該採取的主要操作:
1.移除程式碼中的任何字串連線,因為這會給GC留下大量垃圾。
2.用簡單的“for”迴圈代替“foreach”迴圈。由於某些原因,每個“foreach”迴圈的每次迭代會生成24位元組的垃圾記憶體。一個簡單的迴圈迭代10次就可以留下240位元組的垃圾記憶體。
3.更改我們檢查遊戲物件標籤的方法。用“if (go.CompareTag (“Enemy”)”來代替“if (go.tag == “Enemy”)” 。在一個內部迴圈呼叫物件分配的標籤屬性以及拷貝額外記憶體,這是一個非常糟糕的做法。
4.物件庫很棒,我們為所有動態遊戲物件製作和使用庫,這樣在遊戲執行時間內不會動態分配任何東西,不需要的時候所有東西反向迴圈到庫中。
5.不使用LINQ命令,因為它們一般會分配中間緩器,而這很容易生成垃圾記憶體。
二、謹慎處理高階指令碼和本地引擎C++程式碼之間的通訊開銷。
所有使用Unity3D編寫的遊戲玩法程式碼都是指令碼程式碼,在我們的專案中是使用Mono執行時間處理的C#程式碼。任何與引擎資料的通訊需求都要有一個進入高階指令碼語言的本地引擎程式碼的呼叫。這當然會產生它自己的開銷,而儘量減少遊戲程式碼中的這些呼叫則要排在第二位。
1.在這一情景中四處移動物件要求來自指令碼程式碼的呼叫進入引擎程式碼,這樣我們就會在遊戲玩法程式碼的一個幀中快取某一物件的轉換需求,並一次僅向引擎傳送一個請求,以便減少呼叫開銷。這種模式也適用於其他相似的地方,而不僅侷限於移動和旋轉物件。
2.將引用本地快取到元件中會減少每次在一個遊戲物件中使用 “GetComponent” 獲取一個元件引用的需求,這是呼叫本地引擎程式碼的另一個例子。
三、物理效果
1.將物理模擬時間步設定到最小化狀態。在我們的專案中就不可以將讓它低於16毫秒。
2.減少角色控制器移動命令的呼叫。移動角色控制器會同步發生,每次呼叫都會耗損極大的效能。我們的做法是快取每幀的移動請求,並且僅運用一次。
3.修改程式碼以免依賴“ControllerColliderHit” 回撥函式。這證明這些回撥函式處理得並不十分迅速。
4.面對效能更弱的裝置,要用skinned mesh代替physics cloth。cloth引數在執行表現中發揮重要作用,如果你肯花些時間找到美學與執行表現之間的平衡點,就可以獲得理想的結果。
5.在物理模擬過程中不要使用ragdolls,只有在必要時才讓它生效。
6.要謹慎評估觸發器的“onInside”回撥函式,在我們的專案中,我們儘量在不依賴它們的情況下模擬邏輯。
7.使用層次而不是標籤。我們可以輕鬆為物件分配層次和標籤,並查詢特定物件,但是涉及碰撞邏輯時,層次至少在執行表現上會更有明顯優勢。更快的物理計算和更少的無用分配記憶體是使用層次的基本原因。
8.千萬不要使用Mesh對撞機。
9.最小化碰撞檢測請求(例如ray casts和sphere checks),儘量從每次檢查中獲得更多資訊。
四、讓AI程式碼更迅速
我們使用AI敵人來阻攔忍者英雄,並同其過招。以下是與AI效能問題有關的一些建議:
1.AI邏輯(例如能見度檢查等)會生成大量物理查詢。可以讓AI更新迴圈設定低於影象更新迴圈,以減少CPU負荷。
五、最佳效能表現根本就不是來自程式碼!
沒有發生什麼情況的時候,就說明效能良好。這是我們關閉一切不必要之物的基本原則。我們的專案是一個側邊橫向卷軸動作遊戲,所以如果不具有可視性時,就可以關閉許多動態關卡物體。
1.使用細節層次的定製關卡將遠處的敵人AI關閉。
2.移動平臺和障礙,當它們遠去時其物理碰撞機也會關閉。
3.Unity內建的“動畫挑選”系統可以用來關閉未被渲染物件的動畫。
4.所有關卡內的粒子系統也可以使用同樣的禁用機制。
六、回撥函式!那麼空白的回撥函式呢?
要儘量減少Unity回撥函式。即使敵人回撥函式存在效能損失。沒有必要將空白的回撥函式留在程式碼庫中(有時候介於大量程式碼重寫和重構之間)。
七、讓美術人員來救場
在程式設計師抓耳撓腮,絞盡腦汁去想該如何讓每秒執行更多幀時,美術人員總能神奇地派上大用場。
1.共享遊戲物件材料,令其在Unity中處於靜止狀態,可以讓它們繫結在一起,由此產生的簡化繪圖呼叫是呈現良好移動執行效能的重要元素。
2.紋理地圖集對UI元素來說尤其有用。
3.方形紋理以及兩者功率的合理壓縮是必不可少的步驟。
4.我們的美術人員移除了所有遠處背景的網格,並將其轉化為簡單的2D位面。
5.光照圖非常有價值。
6.我們的美術人員在一些關口移除了額外頂點。
7.使用合理的紋理mip標準是一個好主意(遊戲邦注:要讓不同解析度的裝置呈現良好的幀率時尤其如此)。
8.結合網格是美術人員可以發揮作用的另一個操作。
9.我們的動畫師盡力讓不同角色共享動畫。
10.要找到美學/效能之間的平衡,就免不了許多粒子效果的迭代。減少發射器數量並儘量減少透明度需求也是一大挑戰。
八、要減少記憶體使用
使用大記憶體當然會對效能產生負面影響,但在我們的專案中,我們的iPod由於超過記憶體上限而遭遇了多次崩潰事件。我們的遊戲中最耗記憶體的是紋理。
1.不同裝置要使用不同的紋理大小,尤其是UI和大型背景中的紋理。《Shadow Blade》使用的是通用型模板,但如果在啟動時檢測到裝置大小和解析度,就會載入不同資產。
2.我們要確保未使用的資產不會載入記憶體。我們必須遲一點在專案中找到僅被一個預製件例項引用,並且從未完全載入記憶體中例項化的資產。
3.去除網格中的額外多邊形也能實現這一點。
4.我們應該重建一些資產的生週期管理。例如,調整主選單資產的載入/解除安裝時間,或者關卡資產、遊戲音樂的有效期限。
5.每個關卡都要有根據其動態物件需求而量身定製的特定物件庫,並根據最小記憶體需求來優化。物件庫可以靈活一點,在開發過程中包含大量物件,但知道遊戲物件需求後就要具體一點。
6.保持聲音檔案在記憶體的壓縮狀態也是必要之舉。
加強遊戲執行效能是一個漫長而具有挑戰性的過程,遊戲開發社群所分享的大量知識,以及Unity提供的出色分析工具為《Shadow Blade》實現目標執行效能提供了極大幫助。