兩周前我開始用 Unity 開發一個叫 SkyBlocks 的 Android 游戲。游戲已經在 Google Play 上架了,如果你有時間可以下載來玩一玩兒。
開發的過程中遇到的最大的問題就是性能問題。我開始慢慢嘗試分析到底是什麼導致的性能問題以及我該怎麼解決它。
這個游戲(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 個立方體一個防御線的策略,這是因為我已經把素材變成共享的了,這就可以把之前多次防御線的繪圖調用變成一次調用。
不幸的是,由於我沒對調整之前的游戲截屏,但是我又實在不想調整成以前的解決方案,因此不能向各位展示調整前後的區別。
下面這張圖片是優化後的截屏,但它也只能是張圖,你可以通過這張圖片了解我的游戲。
活動塊調用了一次繪圖指令,參考(Batches: 20 或者 SetPass calls)。就像我前面所說的,以前每個塊包包含 4-5 個獨立的立方體並且每個都含有素材引用。因此正如你看到的每個塊本身都要通過至少四次才能創建出來。
而現在在頂端的兩個被鎖定的塊,我們僅僅使用了一次額外的繪圖調用。而這些塊還是活動塊時, 都是由原始的立方體組成並且每個立方體至少要經過一次處理才能創建出來。
防御線使用相同的模式,但是這裡只有 10 個原始的使用頂點顏色和共享素材的四方體。實際上,在這裡我們不需要在一個網格裡繪制完整的防御線,Unity 幫我們自動的將完整的防御線添加到“通過批處理保存”。
UFO 比較靈活些。每個 UFO 被分成 3 個獨立的網格: 上,中,下。
由於我想隨機的出現 UFO,並且隨機的讓 UFO 一部分活動 。 因此每個 UFO 的每個部分有3-4個素材。一個 UFO 大概有 12-17 次的繪圖調用,然而我卻發現每個 UFO 實際上有 17-30 次的繪圖調用。而我大概會有 2-3 個 UFO 幾乎同時出現在屏幕上,因此就有大概 50-100 次的繪圖調用。好疼啊!
而在此刻,我非常非常渴望我能減少任何我能減少的繪圖調用。因此我在網上找到了一個可以將所有網格合並成一個的腳本。但是這個這個腳本不能真正的適當的處理素材,所以我只能使用一種顏色的 UFO。我只能放棄多彩的漂亮的 UFO,而選用單調的討厭的單一顏色的 UFO。通過對這個腳本的調整,我可以使用至少 2 種不同的顏色和一個紋理。 值得嗎?當然了。30 次繪圖調用聽起來很多,也確實是。但是它表現的更好些,盡管我任然不能確信是否比之前好了很多。但是我將 UFO 繪圖調用的次數減少到了僅僅 10 次左右。
是否所有的繪圖調用的減少都能讓我們的游戲運行的跟快呢?不一定。 如果你能減少繪圖調用,這很棒!但是對於那些更靈活,微妙的部分,如果你願意犧牲一些靈活而去減少繪圖調用,也不是不行。
目前我已經把游戲運行期間的平均 150-200 次的繪圖調用減少到了僅僅 75-90 次。這已經減少了很多次了!
最後一部分,UFO 射出的激光,我在深入的研究後也解決了繪圖調用的問題。所有的激光也都有素材引用,這些UFO的射擊間隔很短“哒-哒”。在全力射擊下每個激光會有30-40次的繪圖調用。還好,這比創建初始方塊要容易多了,使用相同的無光源頂點顏色著色器,再分配一些頂點顏色到網格就好了。現在所有的激光只需要一次繪圖調用,就算是UFO“哒-哒-哒”不停地射擊也沒問題。 ;-)
現在我已經將整個游戲的繪圖調用降低到 30-45 次了,怎麼樣?還行吧!
其他的繪圖調用是 UI 引起的,我本來打算減少 UI 對象的數量來提高速度,但是我現在的效果我覺得挺好的了。游戲運行的比以前更流暢了。
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 是可以對素材分組並且批處理所有對象來使用這個素材。自從我用了無光源著色器就沒有在代碼中修改過任何屬性,素材也從來沒有被實例化過而所有的素材都是一起被批處理的。
注意了,我將要發布一篇較詳細信息的文章來介紹對於不同的對象,我是如何提高性能的,包括更多的代碼實例。
但是現在,祝大家永遠開心,快樂。
更多Android相關信息見Android 專題頁面 http://www.linuxidc.com/topicnews.aspx?tid=11
英文原文:How I tackled my performance issues developing an Android game in Unity