Unity+NGUI性能優化方法總結

 一共9招。

 

1 資源分離打包與加載

 

  遊戲中會有很多地方使用同一份資源。比如,有些界面會共用同一份字體、同一張圖集,有些場景會共用同一張貼圖,有些會怪物使用同一個Animator,等等。可以在製作遊戲安裝包時將這些公用資源從其它資源中分離出來,單獨打包。比如若資源A和B都引用了資源C,則將C分離出來單獨打一個bundle。在遊戲運行時,如果要加載A,則先加載C;之後如果要加載B,因爲C的實例已經在內存,所以只要直接加載B,讓B指向C即可。如果打包時不將C從A和B分離出來,那麼A的包裏會有一份C,B的包裏也會有一份C,冗餘的C會將安裝包撐大;並且在運行時,如果A和B都加載進內存,內存裏就會有兩個C實例,增大了內存佔用。

 

  資源分離打包與加載是最有效的減小安裝包體積與運行時內存佔用的手段。一般打包粒度越細,這兩個指標就越小;而且當兩個renderQueue相鄰的DrawCall使用了相同的貼圖、材質和shader實例時,這兩個DrawCall就可以合併。但打包粒度也並不是越細就越好。如果運行時要同時加載大量小bundle,那麼加載速度將會非常慢——時間都浪費在協程之間的調度和多批次的小I/O上了;而且DrawCall合併不見得會提高性能,有時反而會降低性能,後文會提到。因此需要有策略地控制打包粒度。一般只分離字體和貼圖這種體積較大的公用資源。

 

  可以用AssetDatabase.GetDependencies得知一份資源使用了哪些其它資源。

 

2  貼圖透明通道分離,壓縮格式設爲ETC/PVRTC

 

  最初我們使用了DXT5作爲貼圖壓縮格式,希望能減小貼圖的內存佔用,但很快發現移動平臺的顯卡是不支持硬件解壓DXT5的。因此對於一張1024x1024大小的RGBA32貼圖,雖然DXT5可將它從4MB壓縮到1MB,但系統將它送進顯卡之前,會先用CPU在內存裏將它解壓成4MB的RGBA32格式(軟件解壓),然後再將這4MB送進顯存。於是在這段時間裏,這張貼圖就佔用了5MB內存和4MB顯存;而移動平臺往往沒有獨立顯存,需要從內存裏摳一塊作爲顯存,於是原以爲只佔1MB內存的貼圖實際卻佔了9MB!

 

  所有不支持硬件解壓的壓縮格式都有這個問題。經過一番調研,我們發現安卓上硬件支持最廣泛的格式是ETC,蘋果上則是PVRTC。但這兩種格式都是不帶透明(Alpha)通道的。因此我們將每張原始貼圖的透明通道都分離了出來,寫進另一張貼圖的紅色通道里。這兩張貼圖都採用ETC/PVRTC壓縮。渲染的時候,將兩張貼圖都送進顯存。同時我們修改了NGUI的shader,在渲染時將第二張貼圖的紅色通道寫到第一張貼圖的透明通道里,恢復原來的顏色:

 

[plain] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. fixed4 frag (v2f i) : COLOR  
  2. {  
  3.     fixed4 col;  
  4.     col.rgb = tex2D(_MainTex, i.texcoord).rgb;  
  5.     col.a = tex2D(_AlphaTex, i.texcoord).r;  
  6.     return col * i.color;  
  7. }  

 

  這樣,一張4MB的1024x1024大小的RGBA32原始貼圖,會被分離並壓縮成兩張0.5MB的ETC/PVRTC貼圖(我們用的是ETC/PVRTC 4 bits)。它們渲染時的內存佔用則是2x0.5+2x0.5=2MB。

 

3 關閉貼圖的讀寫選項

 

  Unity中導入的每張貼圖都有一個啓用可讀可寫(Read/Write Enabled)的開關,對應的程序參數是TextureImporter.isReadable。選中貼圖後可在Import Setting選項卡中看到這個開關。只有打開這個開關,纔可以對貼圖使用Texture2D.GetPixel,讀取或改寫貼圖資源的像素,但這就需要系統在內存裏保留一份貼圖的拷貝,以供CPU訪問。一般遊戲運行時不會有這樣的需求,因此我們對所有貼圖都關閉了這個開關,只在編輯中做貼圖導入後處理(比如對原始貼圖分離透明通道)時打開它。這樣,上文提到的1024x1024大小的貼圖,其運行時的2MB內存佔用又可以少一半,減小到1MB。

 

4 減少場景中的GameObject數量

 

  有一次我們將場景中的GameObject數量減少了近2萬個,遊戲在iPhone 3S上的內存佔用立馬減了20MB。這些GameObject雖然基本是在隱藏狀態(activeInHierarchy爲false),但仍然會佔用不少內存。這些GameObject身上還掛載了不少腳本,每個GameObject中的每個腳本都要實例化,又是一比不菲的內存佔用。因此後來我們規定場景中的GameObject數量不得超過1萬,並且將GameObject數量列爲每週版本的性能監測指標。

 

5 整理圖集

 

  整理圖集的主要目的是節省運行時內存(雖然有時也能起到合併DrawCall的作用)。從這個角度講,顯示一個界面時送進顯存的圖集尺寸之和是越小越好。一般有如下方法可以幫助我們做到這點:

 

  1)在界面設計上,儘量讓美術將控件設計爲可以做九宮格拉伸,即UISprite的類型爲Sliced。這樣美術就可以只切出一張小圖,我們在Unity中將它拉大。當然,一個控件做九宮格也就意味着其頂點數量從4個增加到至少16個(九宮格的中心格子採用Tiled做平鋪類型的話,頂點數會更多),構建DrawCall的開銷會更大(見第6點),但一般只要DrawCall安排合理(同樣見第6點)就不會有問題。

 

  2)同樣是在界面設計上,儘量讓美術將圖案設計成對稱的形式。這樣切圖的時候,美術就可以只切一部分,我們在Unity中將完整的圖案拼出來。比如對一個圓形圖案,美術可以只切出四分之一;對一張臉,美術可以只切出一半。不過,與第1)點類似,這個方法同樣有其它性能代價——一個圖案所對應的頂點數和GameObject數量都增多了。第4點已經提到,GameObject數量的增多有時也會顯著佔用更多內存。因此一般只對尺寸較大的圖案採用這個方法。

 

  3)確保不要讓不必要的貼圖素材駐留內存,更不要在渲染時將無關的貼圖素材送進顯存。爲此需要將圖集按照界面分開,一般一張圖集只放一個界面的素材,一個界面中的UISprite也不要使用別的界面的圖集。假設界面A和界面B上都有一個小小的一模一樣的金幣圖標,不要因爲在製作時貪圖方便,就讓界面A的UISprite直接引用界面B中的金幣素材;否則界面A顯示的時候,會將整個界面B的圖集也送進顯存,而且只要A還在內存中,B的圖集也會駐留內存。對於這種情況,應該在A和B的圖集中各放一個一模一樣的金幣圖標,A中的UISprite只使用A的圖集,B中的UISprite只使用B的圖集。

 

  不過,如果兩個界面之間存在大量相同的素材,那麼這兩個界面就可以共用同一張圖集。這樣可以減少所有界面的總內存佔用量。具體操作時需要根據美術的設計進行權衡。一般界面之間相同的通用的素材越多,程序的內存負擔就越小。但界面之間相同的東西太多的話,美術效果可能就不生動,這是美術和程序之間又一個需要尋求平衡的地方。

 

  另外,數量龐大的圖標資源(如物品圖標)不要做在圖集裏,而應該採用UITexture。

 

  4)減少圖集中的空白地方。圖集中完全透明的像素和不透名的像素所佔的內存空間其實是一樣的。因此在素材量不變的情況下,要儘量減少圖集中的空白。有時一張1024x1024的圖集中,素材所佔的面積還沒超過一半,這時可以考慮將這張圖集切成兩張512x512的圖集。(可能有人會問爲什麼不能做成一張1024x512的圖集,這是因爲iOS平臺似乎要求送進顯存的貼圖一定是方形。)當然,兩張不同圖集的DrawCall是無法合併的,但這並不是什麼問題(見第6點)。

 

  應該說,圖集的整理在具體操作時並沒有一成不變的標準,很多時候需要權衡利弊來最終決定如何整理,因爲不管哪種措施都會有別的性能代價。

 

6 根據各個UI控件的設計安放Panel,隔開DrawCall

 

  有一次我們發現NGUI的UIPanel.LateUpdate函數的CPU開銷非常大。仔細研究之後,發現是合併了太多的DrawCall所致,尤其是將運行時會運動變化的UI控件和靜止不變的UI控件的DrawCall合在了一起。當一個UI控件(UIWidget)的位置、大小或顏色等屬性發生變化時,UIPanel就需要重建這個控件所用的DrawCall,某些情況下還要重建Panel上的所有DrawCall。有時重建一個DrawCall會消耗不少CPU開銷,它需要重新計算這個DrawCall上所有控件的頂點信息,包括頂點位置、UV和顏色等。如果很多控件都集中在同一個DrawCall上,那麼只要一個控件有一點點變化,這個DrawCall上的所有控件的頂點就都要重新遍歷一邊;而我們的UI又大量採用了九宮格拉伸,使控件的頂點數量變得更多,因此重建一個DrawCall的開銷就更大。

 

  因此我們將UI控件分組,將一段時間內會發生變化的控件——比如怪物頭頂的血條和傷害跳字放在同一個Panel上,並且這個Panel上只有這些控件,其餘基本不變化的控件就放在別的Panel上。這樣兩類控件就被隔開到不同的DrawCall不同的Panel中,當一個控件發生變化而導致DrawCall重建時,就不需要遍歷那些沒有變化的控件。因爲在美術設計上,一段時間內在變化的控件總是少數,所以優化效果十分明顯,節省的CPU佔用率能達到25%。

 

  這種方法會增加一些DrawCall,但不會有什麼影響。我們項目中前期曾經過於重視DrawCall數量的壓縮,但後來發現增加幾個DrawCall並不是那麼可怕的事情。主程有一次甚至用Cocos2d-x做過試驗,即使在500個DrawCall的情況下,動畫依然可以跑得很流暢,相比之下貼圖大小對流暢度的影響要大得多。

 

7 優化錨點內部邏輯,使其只在必要時更新

 

  在上一點優化了Panel的DrawCall重建效率之後,我們發現NGUI錨點自身的更新邏輯也會消耗不少CPU開銷。即使是在控件靜止不動的情況下,控件的錨點也會每幀更新(見UIWidget.OnUpdate函數),而且它的更新是遞歸式的,使CPU佔用率更高。因此我們修改了NGUI的內部代碼,使錨點只在必要時更新。一般只在控件初始化和屏幕大小發生變化時更新即可。不過這個優化的代價是控件的頂點位置發生變化的時候(比如控件在運動,或控件大小改變等),上層邏輯需要自己負責更新錨點。

 

8 降低貼圖素材分辨率

 

  這一招說白了其實就是減小貼圖素材的尺寸。比如對一張在原畫裏尺寸是100x80的貼圖,我們將它導入Unity後會把它縮小到50x40,即縮小兩倍。遊戲實際使用的是縮小後的貼圖。不過這一招是必然會顯著降低美術品質的,美術立馬會發現畫面變得更模糊,因此一般不到程序撐不住的時候不會採用。

 

9 界面的延遲加載和定時卸載策略(暫未實施)

 

  如果一些界面的重要性較低,並且不常被使用,可以等到界面需要打開顯示的時候才從bundle加載資源,並且在關閉時將自己卸載出內存,或者等過一段時間再卸載。不過這個方法有兩個代價:一是會影響體驗,玩家要求打開界面時,界面的顯示會有延遲;二是更容易出bug,上層寫邏輯時要考慮異步情況,當程序員要訪問一個界面時,這個界面未必會在內存裏。因此目前爲止我們仍未實施該方案。目前只是進入一個新場景時,卸載上一個場景用到但新場景不會用到的界面。

 

  以上的9個方法中,4、5、6需要在一定程度上從策劃和美術的角度考慮問題,並且需要持續保持監控以維護優化狀態(因爲在設計上總是會有新界面的需求或改動老界面的需求);其它都是一勞永逸的解決方案,只要實施穩定後,就不需要再在上面花費精力。不過2和8都是會降低美術品質的方法,尤其是8。如果美術對品質的降低程度實在忍不了的話,也可能不會允許採用這兩個方法。

 

後記

 

後來又學到一招:

避免頻繁調用GameObject.SetActive

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