Unity+NGUI效能最佳化方法總結,unityngui

來源:互聯網
上載者:User

Unity+NGUI效能最佳化方法總結,unityngui

  一共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,在渲染時將第二張貼圖的紅色通道寫到第一張貼圖的透明通道裡,恢複原來的顏色:

 

fixed4 frag (v2f i) : COLOR{    fixed4 col;    col.rgb = tex2D(_MainTex, i.texcoord).rgb;    col.a = tex2D(_AlphaTex, i.texcoord).r;    return col * i.color;}

 

  這樣,一張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。如果美術對品質的降低程度實在忍不了的話,也可能不會允許採用這兩個方法。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.