前言

在遊戲項目中,製作UI界面,是大部分客戶端工程師需要做的一項工作。一般的工作流是,工程師捋清策劃的需求點,拼UI、寫邏輯代碼,修復邏輯bug,讓遊戲能正常運行起來,好像工作就到此完成了。但這樣的工作流其實是不完備的,整個流程中缺少了對性能的考慮,更好的習慣是,工程師從一開始著手工作時,就將任務拆分為實現功能和性能分析兩個子任務。將優化提前到一開始投入生產的階段,減少後期返工優化。

性能分析檢查點大致總結為:

  • DrawCall方面

1、合理設置UI元素的層級,減少DrawCall合併的打斷

2、分析界面操作,動靜分離,根據操作引起的變化,對界面元素進行適當分組拆分panel
  • 載入和卸載方面

1、根據UI界面出現頻次,確定載入和卸載策略

2、確定界面內UI元素是否需要延遲或分幀載入,比如大貼圖、粒子特效、大量元素載入

3、分析界面操作,對操作所需數據進行適當的前置緩存
  • OverDraw方面

1、利用Scene視圖中的OverDraw模式檢查界面OverDraw

本文將結合以上檢查點,介紹筆者使用NGUI製作UI的相關優化實踐。

這篇文章適合的讀者:

對於NGUI製作UI界面感興趣,或者正在開展相關工作。該文章會介紹一些最佳實踐,可以在實際工作中作為參考。

DrawCall方面

理解DrawCall,首先應該知道,在Unity中,每次CPU準備數據並通知GPU繪製的過程稱為一次Draw Call。當我們談DrawCall優化時,是為了減少CPU在調用圖形介面上的開銷,因為每一次的調用CPU都需要做很多工作。Widget是NGUI中負責界面顯示的基礎單位,並由Widget繼承出許多UI元素,每個Widget都必從屬於一個Panel。在NGUI下,統一使用Depth來進行渲染順序的控制,Panel之間會基於Panel的Depth進行一次排序,Panel內部也會基於Widget的Depth進行一次排序。NGUI的實際渲染流程,就是把Widget組件生成的緩存,做成UIDrawCall之後,生成mesh來渲染的過程。

優化Drawcall主要的目的是儘可能的合併指令或者讓更新時只更新必要的變化,以此降低CPU的負擔。

降低界面的渲染開銷

降低界面的渲染開銷,也就是界面的整體DrawCall,能合併的就合併。做到:

一、盡量少穿插使用不同圖集,減少DrawCall合併的打斷。因為圖集將密切相關的圖打到一起,使用同一個材質,使得存在DrawCall合併的可能,但如果沒有控制好渲染順序,依然達不到好的優化效果。

二,UITexture盡量只在大圖的情況下用,能用圖集就是用圖集。三、不同的字體都使用一個Depth,並且這個Depth最好不要與圖片Sprite有交叉,最好放在最上層。

具體的檢查操作是,結合NGUI Panel Tool工具和NGUI DrawCallTool工具,通過PanelTool找到高DrawCall的界面,再打開NGUI DrawCallTool工具,選定指定的Panel, 查看間隔的DrawCall是否使用了相同的材質球, 再查看具體的widgets,是否存在可改動的層級設置。如下圖所示,發現有部分Label層級設置問題,導致使用UI_New圖集的widget無法合併DrawCall。 通過修改Label的widget的層級,將打斷解除,使得UI_New都在一個DC中。

降低界面的更新開銷

除了通過合併DC來降低界面的渲染開銷之外,還應該分析界面操作,減少界面重建的出現,降低界面的更新開銷。做到動靜分離,將頻繁變化的動態UI元素存放於獨立的Panel下,這雖然會多造成DrawCall,但相比更新的開銷,渲染的開銷可以忽略不計。NGUI更新開銷最主要是UIPanel.LateUpdate的開銷,一般認為UIPanel.LateUpdate超過3ms到4ms,可以認為這部分的更新開銷是比較高的,而UIPanel的更新機制分為更新panel中的某單個UIDrawCall(UIPanel.FillDrawCall)和更新panel的所有UIDrawCall(UIPanel.FillAllDrawCalls),要避免觸發第二種情況,一出現就通常有峰值。FillAllDrawCalls一般是在UI重建時觸發,往往一些很不起眼的代碼,也會導致FillAllDrawCalls的出現,比如元素的隱藏顯示,新增元素穿插到panel中,NGUI沒有把DrawCall合併起來,就很可能會觸發Panel重建的啟動。關於FillAllDrawCalls的優化稍顯複雜,而動靜分離是降低其出現的一個折衷策略,盡量保證局部重建而不影響整個Panel,而拆分比較細可以提高容錯率,即使做了一些敏感操作,導致整個UIPanel或者Canvas發生了重建,也對整個界面的影響相對小些。更細緻的優化,可以藉助UWA的GOT Online和Unity的Profiler進行分析。

具體檢查操作是,利用GOT,觀察UI模塊中,找到高堆內存分配的幀。發現是由一個點擊操作造成的,打開profiler,在相應界面停留,找到對應幀,查看UICamera.Update和UIPanel.LateUpdate函數相關數值。

復現點擊操作,從profiler中觀察,看到一個點擊操作後,觀察DrawCall,發現點擊前為25,點擊後變為26;同時UIPanel.LateUpdate中出現了GameObject.Deactivate和Active,這是UIPanel完全重建的表現。因此猜測,點擊操作導致界面UI元素髮生了變化,並且新增的UI元素,由於depth穿插問題,打破了原有panel中DrawCall。

優化結果是點擊操作後,在profiler選擇指定幀,觀察產生的GameObject.Activate,都沒有出現大量的GameObject.Active和GameObject.Deactive。具體的改動,是將新增的UI元素合入到現有DrawCall中,並優化了NGUI代碼新增和刪除Widget的操作,不在此展開。

載入和卸載方面

在製作界面時,需要考慮到界面的使用頻率,對於不常用界面需要注意緩存級別。

一、日常經常使用的UI,預先載入,不卸載;二、一旦載入就可能會在後面經常用到的UI,使用時載入,不卸載; 三、很少使用的UI,使用時載入,用完卸載。此外,界面內部元素的載入和卸載也應該注意, 載入的時候,如果不是一定需要繪製的重資源,盡量動態載入(使用時再載入)。卸載的時候,對於大貼圖是否應該清空引用,是常常會被忽略的問題。比如第一時間不會繪製的大貼圖、第一時間不會播放的特效,製作界面的占點陣圖,都不要直接掛載在界面預製體上。而對於打開會造成卡頓明顯的UI元素,可以考慮用分幀的方式去載入,比如,大貼圖和背包道具,如果策劃同意視覺上會有一個依次出現的過程,那麼對打開時的性能壓力會有所減少。最後,對於數據的緩存,應該更多的關注到,如果是一個頻繁的操作,計算能夠前置的就盡量前置,將結果提前緩存,代替多次的頻繁計算,因為往往這樣的計算可能會有new的操作,造成不必要的浪費。

OverDraw方面

以上部分談到的,主要是針對CPU端,而針對UI界面的GPU端的優化,需要關注的OverDraw。

所有被Canvas繪製的圖形都會被放到透明渲染隊列中,圖形上的每一個像素都會被採樣,即使它被另一個不透明的圖形完全覆蓋。Overdraw表示單位像素的重新繪製次數,Unity提供了查看Overdraw的視圖,在Scene視圖中選擇RenderMode→Overdraw。直觀上看,越亮的區域表示Overdraw的程度越高,也就越消耗GPU。

一般來說,同一時刻只有一個全屏顯示,當打開全屏的時候,關閉上一個全屏頁面,這樣帶來的好處是降低了OverDraw。

如上圖,可以觀察到,如果打開全屏界面B時,沒有關閉界面A,則overdraw模式下,界面變得更亮,overdraw變高了。

另外, 對於可九宮切圖的底圖,如果上方有其他圖片遮擋住中間區域,可以根據情況設置該圖片的fill center屬性為invisible,這樣中心區域就鏤空不渲染,重合面積也會小。

推薦閱讀:

相关文章