使用 Unity 開發 Android 遊戲時如何追蹤效能問題

oschina發表於2015-08-24

前言

兩週前我開始用 Unity 開發一個叫 SkyBlocks 的 Android 遊戲。遊戲已經在 Google Play 上架了,如果你有時間可以下載來玩一玩兒。

開發的過程中遇到的最大的問題就是效能問題。我開始慢慢嘗試分析到底是什麼導致的效能問題以及我該怎麼解決它。

Sky Blocks 遊戲機制

這個遊戲(SkyBlocks)有點像倒過來的俄羅斯方塊和太空入侵者的合體。遊戲的玩法就是把方塊擺成一行,這是這行方塊就會移到遊戲皮膚的最上方。但是這行方塊不會像俄羅斯方塊那樣完全消失。你有60秒的時候來擺出行數儘可能多的方塊。UFO 會入侵“地面”(遊戲皮膚的下方),還會極力破壞你建好的一切東西。一旦它們穿過你的防禦,就開始破壞地球,當地球血量到 0 的時候,遊戲就結束了。
聽起來簡單,做出來難,但是非常有意思,有趣極了!

不要忘記做設計

重要的事情是永遠不要忘記先做設計。當我開始開發 SkyBlocks 是我也不知道我想要做什麼,我更不知道這個遊戲應該是個什麼樣子, 但是我從沒想過該怎麼去處理這個問題。幸好我以前用 JavaScript 和 HTML5 做過俄羅斯方塊,我僅僅通過複製貼上,並且修改了一些小 BUG,像旋轉時的碰撞檢測的方式把這些寫過的程式碼移植到 C# 而沒有考慮從 2D 到 3D 的區別。

自從在每次更新的時候,我不再一次性的繪製整個遊戲皮膚,我就不得不建立每一行到一個 GameObject 中,並且用建立的簡單的立方體渲染在網格中已經被鎖定的塊。網格每次更新的時候我都得銷燬所有的塊,並且又從新建立這些塊。對於我來說,我覺得這已經夠好了,遊戲在電腦上執行的還挺好的。

然而,我沒考慮到的是,遊戲皮膚(網格)有 10 行 20 列,這有可能要有 200 個立方體不停地渲染,銷燬,重建。這還不是最壞的,如果有必要的話行數會變得更多。並且每個立方體也有它自己的引用資源這使得每個立方體都會呼叫一次繪圖。 想象一下,鋪滿一個遊戲皮膚大約需要 150 到 200 個塊被渲染。這就需要大約呼叫 200 次繪圖。

如果在我移植程式碼之前做了設計,我就知道這個遊戲不能長時間的執行。如果在開始動手之前有這就有想法,我就不會浪費那麼多時間了。

解決問題

解決問題的最好的辦法是先勾勒出你的想法,然後再逐步深入。怎樣讓各個部分結合起來像一個整體一樣的工作?這些不同的部分都做些什麼?在 Sky Blocks 專案裡,部分指的是遊戲皮膚,防禦線和 UFO。

遊戲皮膚僅僅是為了控制遊戲的,在遊戲皮膚上有移動的塊和已經被鎖定的靜止的塊。而防禦線僅僅是那10個立方體的靜止的線。UFO 是一個可以移動到防禦線上方的組合的網格。而抓住這些部分我就接近勝利了。

減少繪圖呼叫次數

我在前面已經提到過,在這個遊戲裡我呼叫了太多次的繪圖,我在我的 Android 機器上(a samsung galaxy s4)上測試我的遊戲,隨著功能的完善,我發現我的遊戲執行的越來越慢,跟蝸牛一樣。

為了讓遊戲執行的更好,減少繪圖呼叫是一項重要的任務。我不得不在網上找答案。繪圖呼叫消耗多少效能?是什麼引起了繪圖呼叫?怎樣才能減少呼叫?

在效能的提升上,我設計不出一個好的實驗方案,但是我找到了一個可以在 CPU 和 GPU 上都能執行的方案。雖然一些實驗中效能上沒有明顯的區別,但是在我將遊戲繫結到 FPS 上的大部分的實驗會讓遊戲執行的緩慢一些

主要的原因是很容易發現的,是由於所有的立方體被分開使用素材渲染。

為了在遊戲皮膚上減少繪圖呼叫,我決定減少物件的數量和不同種素材的數量。

因此我試著實現了一個功能,這個功能在遊戲皮膚上替換了以前可能分開的繪製200個立方體,而現在只需繪製一整塊網格。然後我選擇用頂點顏色替換了單色紋理。並且將素材著色器改變成我在網上找的無光源頂點顏色。現在我把遊戲皮膚上多次的繪圖呼叫變成僅僅一次。

對於防禦線,我做了類似的事情,我把所有的素材修改成相同的無光源頂點顏色,但是我沒有讓它們作為一個網格來渲染,我沿用以前的每 10 個立方體一個防禦線的策略,這是因為我已經把素材變成共享的了,這就可以把之前多次防禦線的繪圖呼叫變成一次呼叫。

不幸的是,由於我沒對調整之前的遊戲截圖,但是我又實在不想調整成以前的解決方案,因此不能向各位展示調整前後的區別。

下面這張圖片是優化後的截圖,但它也只能是張圖,你可以通過這張圖片瞭解我的遊戲。

使用 Unity 開發 Android 遊戲時如何追蹤效能問題活動塊呼叫了一次繪圖指令,參考(Batches: 20 或者 SetPass calls)。就像我前面所說的,以前每個塊包包含 4-5 個獨立的立方體並且每個都含有素材引用。因此正如你看到的每個塊本身都要通過至少四次才能建立出來。

使用 Unity 開發 Android 遊戲時如何追蹤效能問題而現在在頂端的兩個被鎖定的塊,我們僅僅使用了一次額外的繪圖呼叫。而這些塊還是活動塊時, 都是由原始的立方體組成並且每個立方體至少要經過一次處理才能建立出來。

使用 Unity 開發 Android 遊戲時如何追蹤效能問題防禦線使用相同的模式,但是這裡只有 10 個原始的使用頂點顏色和共享素材的四方體。實際上,在這裡我們不需要在一個網格里繪製完整的防禦線,Unity 幫我們自動的將完整的防禦線新增到“通過批處理儲存”。

UFO 比較靈活些。每個 UFO 被分成 3 個獨立的網格: 上,中,下。

由於我想隨機的出現 UFO,並且隨機的讓 UFO 一部分活動 。 因此每個 UFO 的每個部分有3-4個素材。一個 UFO 大概有 12-17 次的繪圖呼叫,然而我卻發現每個 UFO 實際上有 17-30 次的繪圖呼叫。而我大概會有 2-3 個 UFO 幾乎同時出現在螢幕上,因此就有大概 50-100 次的繪圖呼叫。好疼啊!

使用 Unity 開發 Android 遊戲時如何追蹤效能問題

而在此刻,我非常非常渴望我能減少任何我能減少的繪圖呼叫。因此我在網上找到了一個可以將所有網格合併成一個的指令碼。但是這個這個指令碼不能真正的適當的處理素材,所以我只能使用一種顏色的 UFO。我只能放棄多彩的漂亮的 UFO,而選用單調的討厭的單一顏色的 UFO。通過對這個指令碼的調整,我可以使用至少 2 種不同的顏色和一個紋理。 值得嗎?當然了。30 次繪圖呼叫聽起來很多,也確實是。但是它表現的更好些,儘管我任然不能確信是否比之前好了很多。但是我將 UFO 繪圖呼叫的次數減少到了僅僅 10 次左右。

使用 Unity 開發 Android 遊戲時如何追蹤效能問題

是否所有的繪圖呼叫的減少都能讓我們的遊戲執行的跟快呢?不一定。 如果你能減少繪圖呼叫,這很棒!但是對於那些更靈活,微妙的部分,如果你願意犧牲一些靈活而去減少繪圖呼叫,也不是不行。

目前我已經把遊戲執行期間的平均 150-200 次的繪圖呼叫減少到了僅僅 75-90 次。這已經減少了很多次了!

最後一部分,UFO 射出的鐳射,我在深入的研究後也解決了繪圖呼叫的問題。所有的鐳射也都有素材引用,這些UFO的射擊間隔很短“噠-噠”。在全力射擊下每個鐳射會有30-40次的繪圖呼叫。還好,這比建立初始方塊要容易多了,使用相同的無光源頂點顏色著色器,再分配一些頂點顏色到網格就好了。現在所有的鐳射只需要一次繪圖呼叫,就算是UFO“噠-噠-噠”不停地射擊也沒問題。 ;-)

現在我已經將整個遊戲的繪圖呼叫降低到 30-45 次了,怎麼樣?還行吧!

其他的繪圖呼叫是 UI 引起的,我本來打算減少 UI 物件的數量來提高速度,但是我現在的效果我覺得挺好的了。遊戲執行的比以前更流暢了。

使用 Unity 開發 Android 遊戲時如何追蹤效能問題 UFO 也使用了很少的繪圖呼叫。但是想想大約 10 次和 30 次比較,還是有很大的區別的。

減少繪圖呼叫的最重要的規則是使用盡可能少的素材。如果可以的話儘量使用共享素材而不是引用的素材,這些一定會幫助你減少你的繪圖呼叫。

確保你只載入了一次資源

在我的程式碼中我使用了一下資源載入,但是 Unity 沒有快取載入結果,因此導致了多次的載入了相同的資源。這個功能消耗了大量的效能。 我以前也是多次的載入了相同的素材到我的鐳射武器上,像往常一樣遊戲在電腦上執行的十分好,但是在 Android 上,就不行了。

最終我避免了重複載入資源,並且刪掉了專案中資源載入的地方。但是在這之前,我建立了一個靜態的字典,字串作為 Key(資源的名稱),資源作為 Value,然後,我使用的時候都會檢查字典裡是否已經存在 Key,如果沒有就載入資源,否則從字典快取裡獲取資源。

我建議你可以試著用這個方法載入舞臺。

儘可能避免例項化

我從來沒有考慮過物件的例項化會消耗多少效能,我幾乎在所有地方都例項化了。我只是覺得它跟建立一個新的類的引用消耗的效能類似。但是我錯了,事實上,程式花費了一些時間在 CPU 上例項化一個物件,又花費相同時間去銷燬這個物件。問題在於我發現每次 UFO 攻擊的時候,他媽的都會讓我建立大量的鐳射。每個鐳射的例項化和銷燬間隔很短很短的時間。我算了一下,大約 20-40 物件的例項化和銷燬耗時 1.5 秒。減少了繪圖呼叫是很好,但是我從未意識到 UFO 出現後實際上是例項化消耗了大量的效能。

能解決這個問題的唯一的辦法是建立有序的物件池。我在場景裡面建立了一個新的空的物件並呼叫ProjectilePool。在程式碼裡建立了一些新的 Projectiles ,我廢棄了以前在 Projectiles  list 裡去查詢Projectile,而是在 ProjectilePool 裡線查詢有沒有可用的 Projectile,如果有,就取得這個 Projectile 並且從新設定它的位置和狀態。這樣就能從新使用這個就舊的 Projectile 了。

如果我沒有在 List 中找到 Projectile,我就像以前一樣建立一個。但是這時 Projectile 通常會被銷燬掉,而我把 Projectile 新增到 ProjectilePool 並且使它不活動。因此我現在可以將 UFO 攻擊期間的CPU 的使用率降低到幾乎 25%-30%。現在我的遊戲執行的超級好。

總結

繪圖呼叫等於怪獸。如果你想盡可能的減少。最好的方式是面對他們,減少你的物件使用的素材的數量。使用較少不同的紋理,試著並且調整儘量多的紋理到地圖集中。如果你正在實現 2D 並且不要使用太多的光源或者像我一樣,只使用只有一種顏色的紋理。然後使用使用無光源頂點顏色,沒有任何引數可以被用到所有的類。這很可能減少很多次繪圖呼叫。如果你願意犧牲一漂亮的視覺效果,你也可以合併網格或許這還是有用的。

例項化很慢,非常慢。試著儘可能的避免例項化。試著在初始化的時候載入盡肯能多的物件,然後當你想使用的時候在引用它們。另一個很好的方式用一個物件池迴圈的使用舊的物件來減少例項化的數量。

當然繪圖呼叫和例項化不僅僅是唯一的惡棍。你得記著繪圖呼叫使用了 CPU 和 GPU,而例項化使用了 CPU。

如果在你的遊戲中你有大的複雜的模組或者太多的處理要執行。僅僅減少繪圖呼叫是不能幫助你提供速度,當然這也會使你的遊戲執行的快些但不總是這樣。CPU 有時是你最大的敵人。先看看你的程式碼,然後試著找出執行的糟糕的地方然後讓它執行的更好些。

在我的遊戲中,例項化,銷燬和 Web 請求時最大問題。

這篇文章的結束只是下一篇開始

Sky Blocks 在 Google Play 上的下載地址
https://play.google.com/store/apps/details?id=com.Shinobytes.SkyBlocks

我使用的無光源頂點著色器
http://pastebin.com/RMm5a4Zv

減少繪圖呼叫到底有多重要?

        “雖然繪圖呼叫可以成為一個瓶頸,但是記住幀頻才是王道。如果你的幀頻是夠好,那就沒必要擔心繪圖呼叫。繪圖呼叫被請求的數量是否嚴重的影響了效能,很大程度上取決於硬體的狀況和每一幀所做的所有的事情”
— Daniel Brauer, Unity Technologies

例項化素材 VS 共享素材

例項化素材的主要的特點是一個可以讓任何屬性改變的素材。一個例項化素材僅僅為了一個特殊的類被例項化一次。每次的例項化都有可能會觸發一次繪圖呼叫。但是例項化之後改變他的屬性是不會建立新的例項的,而僅僅是修改了當前的例項。然而共享的素材是使用了相同的著色器和其他相同的屬性的素材。Unity 是可以對素材分組並且批處理所有物件來使用這個素材。自從我用了無光源著色器就沒有在程式碼中修改過任何屬性,素材也從來沒有被例項化過而所有的素材都是一起被批處理的。

注意了,我將要釋出一篇較詳細資訊的文章來介紹對於不同的物件,我是如何提高效能的,包括更多的程式碼例項。

但是現在,祝大家永遠開心,快樂。

相關文章