1. 程式人生 > >關於Unity中的NGUI優化,你可能遇到這些問題

關於Unity中的NGUI優化,你可能遇到這些問題

關鍵字

介面製作
介面切換
網格重建
UICamera.Update
Draw Call
載入
字型

一、介面製作

Q1:我用的是NGUI,本來已經打包圖集了,輸出時候是不是就不用理會那些原始2D Sprite圖 ?粒子貼圖需要Packing Tag嗎?

在NGUI中使用Atlas後,原紋理是不需要進行打包或進行其他特殊處理的,因為理論上這些資源在執行時已不再需要。粒子系統所使用的紋理並不是Sprite型別的,因此不需要設定Packing Tag。

Q2:NGUI變形,如下圖走樣了,請問是不是圖片壓縮導致的?
請輸入圖片描述

當UI紋理在裝置上的顯示解析度低於原始解析度時,會因為出現aliasing現象,導致UI區域性變形。通常對於粗線條、塊狀的UI圖素,變形通常是不明顯的,但對於細線條的UI圖素,則可能非常明顯。
通常該問題可以考慮三種方式來改善:

  1. 在NGUI中將UIRoot的Scaling Style設定為Flexible,這種方式的好處在於UI紋理不會因為裝置解析度的限制而降低,而缺點在於相同的UI紋理在高解析度裝置上顯得比較小,而在低解析度裝置上顯得比較大,從而提高了UI佈局的複雜度;
  2. 將UI紋理的顯示解析度(Sprite的size屬性)設定為高於原始解析度,其缺點在於高解析度裝置上可能會產生模糊,但大多數情況下“模糊”相比於“走樣”更不易察覺;
  3. 開啟UI紋理的Mipmap,從而在低解析度裝置上自動切換到低Level,以“模糊”替換“走樣”,但缺點在於增加了紋理的大小,因此只適用於出現了明顯變形的少量UI。

Q3:能否在NGUI多解析度適應方面提供一些解決方案或者思路?

多解析度適應涉及到以下幾個方面:

  1. 佈局。通常可以通過 NGUI 中的 Anchor 元件來實現,能夠保證 UI 到螢幕上指定錨點的距離;
  2. UI 背景圖比例。通常我們建議將背景圖的長寬比放大,以適配不同長寬比的螢幕,但要注意兩邊需要留空,或者保留可被裁掉的元素;
  3. UI 的整體縮放。可以通過 UIRoot 元件的 Scaling Style 來統一配置。

需要提醒的是,不同型別的遊戲對佈局的需求通常也不同,因此還是需要結合實際開發情況來做調整。

Q4:我看到UICamera.Update()的GC呼叫特別高,只要我一移動就會產生2.8K的GC,看起來是NGUITools.FindInParents這個方法導致的,有沒有什麼可以優化的方法呢?

請輸入圖片描述
請輸入圖片描述

在 Editor 下,當呼叫GetComponent() 且 T元件並不在當前的GameObject 上時,確實會出現GC Alloc,但這在釋出後是不會出現的,因此建議在真機上做一個驗證。這是因為在Editor下,Unity的MissingComponentException實現所致,在出現以上情況時,Unity 並不是直接返回一個 NULL,而是返回一個代理 Object用來儲存一些相關資訊,在後續被訪問時可以給出更詳細的報錯資訊。

二、網格重建

Q1:我用NGUI開發,因為角色名字導致重建,使得UIPanel.LateUpdate的CPU佔用很高。如果將它們分離到多個UIPanel裡,是否這個開銷會相對小一些?

將較多的動態UI元素分組放在不同的UIPanel中確實是UWA比較推薦的方式,一方面可以降低重建的概率,某些分組中可能沒有UI元素髮生變化,從而不會進行重建;另一方面可以降低重建的開銷。通過分組,可以將每個UIPanel所產生的Mesh控制在較小的範圍內,從而控制其重建的開銷(通常重建的開銷會因Mesh的增大而明顯升高,且不是線性的關係)。雖然這種做法會產生額外的DrawCall,但DrawCall的開銷與網格重建相比通常都非常小。

Q2:我的UWA報告中看到幾乎每次切換場景都會有UIPanel.LateUpdate()這個函式的堆記憶體開銷,請問說明了什麼問題,我是否還能優化?
請輸入圖片描述

UIPanel.LateUpdate持續分配較大量的堆記憶體,說明UI介面在製作上存在以下問題:

  1. Panel中Widgets數量過多,且存在頻繁的變動,導致UIPanel需要進行大量的網格重建;
  2. 動靜態UI元素沒有分離;
  3. 建議研發團隊對UI介面的製作進行進一步檢測,儘可能將靜態UI元素和動態UI元素分開,存放於不同的Panel下。同時,對於不同頻率的動態元素也建議存放於不同的Panel中。

Q3:UWA建議“儘可能將靜態UI元素和頻繁變化的動態UI元素分開,存放於不同的Panel下。同時,對於不同頻率的動態元素也建議存放於不同的Panel中。”那麼請問,如果把特效放在Panel裡面,需要把特效拆到動態的裡面嗎?

通常特效是指粒子系統,而粒子系統的渲染和UI是獨立的,僅能通過Render Order來改變兩者的渲染順序,而粒子系統的變化並不會引起UI部分的重建,因此特效的放置並沒有特殊的要求。

三、介面切換

Q1:我在Profiler中看到GameObject.Deactivate耗時較大,請問該如何優化?

實際上GameObject.Activate/Deactivate本身通常不會產生很高的開銷,主要都是由其上或其子節點上的元件的OnEnable/OnDisable操作引起,比如UI相關的元件在OnEnable和OnDisable中都會有較多的操作,所以較複雜的UI介面的GameObject.Activate/Deactivate會有很高的開銷。因此,針對這一問題,如果是由自定義的指令碼造成,那麼就需要考慮優化OnEnable/OnDisable的邏輯;如果是UI,那麼可以對頻繁切換啟用狀態的UI採用平移出螢幕、修改Culling Layer等方式來替換。

Q2:遊戲中出現UI介面重疊,該怎麼處理較好?比如當前有一個全屏顯示的UI介面,點其中一個按鈕會再起一個全屏介面,並把第一個UI介面蓋住。我現在的做法是把被覆蓋的介面 SetActive(False),但發現後續 SetActive(True) 的時候會有 GC.Alloc 產生。這種情況下,希望既降低 Batches 又降低 GC Alloc 的話,有什麼推薦的方案嗎?

可以嘗試通過新增一個 Layer 如 OutUI, 且在 Camera 的 Culling Mask 中將其取消勾選(即不渲染該 Layer)。從而在 UI 介面切換時,直接通過修改 UIPanel 的 Layer 來實現“隱藏”。但需要注意事件的遮蔽,禁用動態的 UI 元素等等。
這種做法的優點在於切換時基本沒有開銷,也不會產生多餘的 Draw Call,但缺點在於“隱藏時”依然還會有一定的持續開銷(通常不太大),而其對應的 Mesh 也會始終存在於記憶體中(通常也不太大)。
以上的方式可供參考,而效能影響依舊是需要視具體情況而定。

Q3:通過移動位置來隱藏UI介面,會使得被隱藏的UIPanel繼續執行更新(LateUpdate有持續開銷),那麼如果開啟的介面比較多,CPU的持續開銷是否就會超過一次SetActive所帶來的開銷?

這確實是需要注意的,通過移動的方式“隱藏”的UI介面只適用於幾個切換頻率最高的介面,另外,如果“隱藏”的介面持續開銷較高,可以考慮只把一部分Disable,這個可能就需要具體看介面的複雜度了。一般來說在沒有UI元素變化的情況下,持續的 Update 開銷是不太明顯的。

Q4:如圖,我們在UI開啟或者移動到某處的時候經常會觀測到CPU上的衝激,經過進一步觀察發現是因為Instantiate產生了大量的GC。想請問下Instantiate是否應該產生GC呢?我們能否通過資源製作上的調整來避免這樣的GC呢?如下圖,因為一次性產生若干MB的GC在直觀感受上還是很可觀的。
請輸入圖片描述

準確的說這些 GC Alloc 並不是由Instantiate 直接引起的,而是因為被例項化出來的元件會進行 OnEnable 操作,而在 OnEnable 操作中產生了 GC,比如以上圖中的函式為例:

上圖中的 UILabel.OnEnable 是在例項化一個 UI 介面時,UI 中的文字(即 UILabel 元件)進行了 OnEnable 操作,其中主要是初始化文字網格的資訊(每個文字所在的網格頂點,UV,頂點色等等屬性),而這些資訊都是儲存在陣列中(即堆記憶體中),所以文字越多,堆記憶體開銷越大。但這是不可避免的,只能儘量減少出現次數。

因此,我們不建議通過 Instantiate/Destroy 來處理切換頻繁的 UI 介面,而是通過 SetActive(true/false),甚至是直接移動 UI 的方式,以避免反覆地造成堆記憶體開銷。

四、字型

Q1:對NGUI字型錯亂有什麼好的解決方案嗎?

有這麼幾種可能:

  1. 一次展開文字太多了。這種情況在部分高通機型和Unity早期版本上都經常出現,現在也偶爾有,究其原理是FontTexture的擴容操作做得不夠快或者收到了硬體驅動的限制。
    一般來說有兩種方法可以解決:(1)減少面板中的字型內容;(2)一開始就用超大量的字型去擴容,將動態字型的FontTexture擴大到足夠大;
  2. 文字渲染與開發團隊編寫的多執行緒渲染髮生了衝突。這種情況也常有發生,特別是通過GL.IssuePluginEvent方式來開啟多執行緒渲染的專案,就會容易出現問題。
    就我們的優化經驗來看,第一種情況發生的可能性比較大。

Q2:我在用Profiler真機檢視iPhone App時,發現第一次開啟某些UI時,Font.CacheFontForText佔用時間超過2s,這塊主要是由什麼影響的?若iPhone5在這個介面消耗2s多,是不是問題很大?這個消耗和已經生成的RenderTexture的大小有關嗎?

Font.CacheFontForText主要是指生成動態字型Font Texture的開銷, 一次性開啟UI介面中的文字越多,其開銷越大。如果該項佔用時間超過2s,那麼確實是挺大的,這個消耗也與已經生成的Font Texture有關係。簡單來說,它主要是看目前Font Texture中是否有地方可以容下接下來的文字,如果容不下才會進行一步擴大Font Texture,從而造成了效能開銷。

五、載入相關

Q1:載入UI預製的時候,如果把特效放到預製裡,會導致載入非常耗時。怎麼優化這個載入時間呢?

UI和特效(粒子系統)的載入開銷在多數專案中都佔據較高的CPU耗時。UI介面的例項化和載入耗時主要由以下幾個方面構成:

1. 紋理資源載入耗時
UI介面載入的主要耗時開銷,因為在其資源載入過程中,時常伴有大量較大解析度的Atlas紋理載入,我們在之前的Unity載入模組深度分析之紋理篇有詳細講解。對此,我們建議研發團隊在美術質量允許的情況下,儘可能對UI紋理進行簡化,從而加快UI介面的載入效率。
請輸入圖片描述

2. UI網格重建耗時
UI介面在例項化或Active時,往往會造成Canvas(UGUI)或Panel(NGUI)中UIDrawCall的變化,進而觸發網格重建操作。當Canvas或Panel中網格量較大時,其重建開銷也會隨之較大。

3. UI相關建構函式和初始化操作開銷
這部分是指UI底層類在例項化時的ctor開銷,以及OnEnable和OnDisable的自身開銷。

上述2和3主要為引擎或外掛的自身邏輯開銷,因此,我們應該儘可能避免或降低這兩個操作的發生頻率。我們的建議如下:

  1. 在記憶體允許的情況下,對於UI介面進行快取。儘可能減少UI介面相關資源的重複載入以及相關類的重複初始化;
  2. 根據UI介面的使用頻率,使用更為合適的切換方式。比如移進移出或使用Culling Layer來實現UI介面的切換效果等,從而降低UI介面的載入耗時,提升切換的流暢度。
  3. 對於特效(特別是粒子特效)來說,我們暫時並沒有發現將UI介面和特效耦合在一起,其載入耗時會大於二者分別載入的耗時總和。因此,我們僅從優化粒子系統載入效率的角度來回答這個問題。粒子系統的載入開銷,就目前來看,主要和其本身元件的反序列化耗時和載入數量相關。對於反序列化耗時而言,這是Unity引擎負責粒子系統的自身載入開銷,開發者可以控制的空間並不大。對於載入數量,則是開發者需要密切關注的,因為在我們目前看到的專案中,不少都存在大量的粒子系統載入,有些專案的數量甚至超過1000個,如下圖所示。因此,建議研發團隊密切關注自身專案中粒子系統的數量使用情況。一般來說,建議我們建議粒子系統使用數量的峰值控制在400以下。
    請輸入圖片描述

Q2:我有一個UI預設,它使用了一個圖集, 我在打包的時候把圖集和UI一起打成了AssetBundle。我在載入生成了GameObject後立刻解除安裝了AssetBundle物件, 但是當我後面再銷燬GameObject的時候發現圖集依然存在,這是什麼情況呢?

這是很可能出現的。unload(false)解除安裝AssetBundle並不會銷燬其載入的資源 ,是必須呼叫 Resources.UnloadUnusedAssets才行。關於AssetBundle載入的詳細解釋可以參考我們之前的文章:你應該知道的AssetBundle管理機制。