tft每日頭條

 > 遊戲

 > unity代碼優化和性能優化

unity代碼優化和性能優化

遊戲 更新时间:2024-09-17 08:54:35

《Unity移動端遊戲性能優化簡譜》從Unity移動端遊戲優化的一些基礎讨論出發,例舉和分析了近幾年基于Unity開發的移動端遊戲項目中最為常見的部分性能問題,并展示了如何使用UWA的性能檢測工具确定和解決這些問題。内容包括了性能優化的基本邏輯、UWA性能檢測工具和常見性能問題,希望能提供給Unity開發者更多高效的研發方法和實戰經驗。

今天向大家介紹文章第三部分:以引擎模塊為劃分的CPU耗時調優,共9小節,包含了渲染模塊、UI模塊、物理模塊、動畫模塊、粒子系統、加載模塊、邏輯代碼、Lua等多個模塊等常見的遊戲CPU耗時調優講解。(全文長約14115字,預計閱讀時間約30分鐘)

文章第一部分《Unity移動端遊戲性能優化簡譜之 前言》、第二部分《Unity移動端遊戲性能優化簡譜之 常見遊戲内存控制》可戳此回顧,完整内容可前往UWA學堂查看。

1. 總覽

1.1 模塊劃分UWA将CPU中工作内容明确、耗時占比一般較高的函數整理劃分為:渲染、UI、物理、動畫、粒子、加載、邏輯等模塊。但這并不意味着模塊之間的工作互相獨立毫無關聯。舉例而言,渲染模塊的性能壓力勢必受到複雜的UI和粒子影響,而加載模塊的很多操作實際上都是在邏輯中調用并完成的。

劃分模塊有利于我們确認問題、找到重點。與此同時,也要建立起模塊之間的關聯,有助于更高效地解決問題。

1.2 耗時瓶頸當一個項目由于CPU端性能瓶頸而産生幀率偏低、卡頓明顯的現象時,如何提煉出哪個模塊的哪個問題是造成性能瓶頸的主要問題就成了關鍵。盡管我們已經對引擎中主要模塊做了整理,各個模塊間會出現的問題還是會千奇百怪不可一以概之,而且它們對CPU性能壓力的貢獻也不盡相同。那麼我們就需要對什麼樣的耗時可以認為是潛在的性能瓶頸有準确的認知。

在移動端項目中,我們CPU端性能優化的目标是能夠在中低端機型上大部分時間跑滿30幀的流暢遊戲過程。為了達成這一目标,簡單做一下除法就得到我們的CPU耗時均值應控制在33ms以下。當然,這并不意味着CPU均值已經在33ms以下的項目就已經把CPU耗時控制的很好了。遊戲運行過程中性能壓力點是不同的,可能一系列UI界面中壓力很小、但反過來遊戲中最重要的戰鬥場景中幀率很低、又或者是存在大量幾百毫秒甚至幾秒的卡頓,而最終平均下來仍然低于33ms。

為此,UWA認為,在一次測試中,當33ms及以上耗時的幀數占總幀數的10%以下時,可以認為項目CPU性能整體控制在正常範圍内。而這個占比越高,說明當前項目的CPU性能瓶頸越嚴重。

以上的讨論内容主要是圍繞着我們對CPU性能的宏觀的優化目标,和内存一樣,我們仍要結合具體模塊的具體數據來排查和解決項目中實際存在的問題。


2. 渲染模塊

圍繞渲染模塊相關優化更全面的内容可以參考《Unity性能優化系列—渲染模塊》。

2.1 多線程渲染一般情況下,在單線程渲染的流程中,在遊戲每一幀運行過程中,主線程(CPU1)先執行Update,在這裡做大量的邏輯更新,例如遊戲AI、碰撞檢測和動畫更新等;然後執行Render,在這裡做渲染相關的指令調用。在渲染時,主線程需要調用圖形API更新渲染狀态,例如設置Shader、紋理、矩陣和Alpha融合等,然後再執行DrawCall,所有的這些圖形API調用都是與驅動層交互的,而驅動層維護着所有的渲染狀态,這些API的調用有可能會觸發驅動層的渲染狀态地改變,從而發生卡頓。由于驅動層的狀态對于上層調用是透明的,因此卡頓是否會發生以及卡頓發生的時間長短對于API的調用者(CPU1)來說都是未知的。而此時其它CPU有可能處于空閑等待的狀态,從而造成浪費。因此可以将渲染部分抽離出來,放到其它的CPU中,形成單獨的渲染線程,與邏輯線程同時進行,以減少主線程卡頓。

其大緻的實現流程是,在主線程中調用的圖形API被封裝成命令,提交到渲染隊列,這樣就可以節省在主線程中調用圖形API的開銷,從而提高幀率;渲染線程從渲染隊列獲取渲染指令并執行調用圖形API與驅動層交互,這部分交互耗時從主線程轉到渲染線程。

而Unity在Project Settings中支持且默認開啟了Multithreaded Rendering,一般建議保持開啟。在UWA的大量測試數據中,還是發現有部分項目關閉了多線程渲染。開啟多線程渲染時,CPU等待GPU完成工作的耗時會被統計到Gfx.WaitForPresent函數中,而關閉多線程渲染時這一部分耗時則被主要統計到Graphics.PresentAndSync中。所以,項目中是否統計到Gfx.WaitForPresent函數耗時是判斷是否開啟了多線程渲染的一個依據。特别地,在項目開發和測試階段可以考慮暫時性地關閉多線程渲染并打包測試,從而更直觀地反映出渲染模塊存在的性能瓶頸。

對于正常開啟了多線程渲染的項目,Gfx.WaitForPresent的耗時走向也有相當的參考意義。測試中局部的GPU壓力越大,CPU等待GPU完成工作的時間也就越長,Gfx.WaitForPresent的耗時也就越高。所以,當Gfx.WaitForPresent存在數十甚至上百毫秒地持續耗時時,說明對應場景的GPU壓力較大。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)1

另外,根據UWA的大量項目和測試經驗,GPU壓力過大也會使得渲染模塊CPU端的主函數耗時(Camera.Render和RenderPipelineManager.DoRenderLoop_Internal)整體相應上升。我們會在最後專門讨論GPU部分的優化。

2.2 同屏渲染面片數影響渲染效率的兩個最基本的參數無疑就是Triangle和DrawCall。

通常情況下,Triangle面片數和GPU渲染耗時是成正比的,而對于大部分項目來說,不透明Triangle數量又往往遠比半透明Triangle要多,尤其需要關注。UWA一般建議在低端機型上将同屏渲染面片數控制在25萬面以内,即便是高端機也不建議超過60萬面。當使用工具發現局部同屏渲染面片數過高後,可以結合Frame Debugger對重點幀的渲染物體進行排查。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)2

常見的優化方案是,在制作上需要嚴格控制網格資源的面片數,尤其是一些角色和地形的模型,應嚴格警惕數萬面及以上的網格;另外,一個很好的方法是一通過LOD工具減少場景中的面片數——比如在低端機上使用低模、減少場景中相對不重要的小物件的展示——進而降低渲染的開銷。

需要指出的是,UWA工具所關注和統計的面片數量并不是當前幀場景模型的面片數,而是當前幀所渲染的面片數,其數值不僅與模型面片數有關,也和渲染次數相關,更加直觀地反映出同屏渲染面片數造成的渲染壓力。例如:場景中的網格模型面片數為1萬,而其使用的Shader擁有2個渲染Pass,或者有2個相機對其同時渲染;又或者使用了SSAO、Reflection等後處理效果中的一個,那麼此處所顯示的Triangle數值将為2萬。所以,在低端機上應嚴格警惕這些一下就會使同屏渲染面片數加倍的操作,即便對于高端機也應做好權衡,三思而後用。

2.3 Batch(DrawCall)在Unity中,我們需要區分DrawCall和Batch。在一個Batch中會存在有多個DrawCall,出現這種情況時我們往往更關心Batch的數量,因為它才是把渲染數據提交給GPU的單位,也是我們需要優化和控制數量的真正對象。

降低Batch的方式通常有動态合批、靜态合批、SRP Batcher和GPU Instancing這四種,圍繞Batch優化的讨論較為複雜,再寫一篇文章也不為過,所以本文不再展開來讨論,但在UWA DAY 2020中我們詳細讨論和分享了DrawCall與Batch的關系以及這4種Batching的使用詳解,供大家參考:《Unity移動遊戲項目優化案例分析(上)》。

下面簡單總結靜态合批、SRP Batcher和GPU Instancing的合批條件和優缺點。

(1)靜态合批條件:不同Mesh,隻要使用相同的材質球即可。優點:節省頂點信息地綁定;節省幾何信息地傳遞;相鄰材質相同時, ,節省材質地傳遞。缺點:離線合并時,若合并的Mesh中存在重複資源,則容易使得合并後包體變大;運行時合并,則生成Combine Mesh的過程會造成CPU短時間峰值;同樣的,若合并的Mesh中存在重複資源,則會使得合并後内存占用變大。

(2)SRP Batcher條件:不同Mesh,隻要使用相同的Shader且變體一樣即可。優點:節省Uniform Buffer的寫入操作;按Shader分Batch,預先生成Uniform Buffer,Batch内部無CPU Write。缺點:Constant Buffer(CBuffer)的顯存固定開銷;不支持MaterialPropertyBlock。

(3)GPU Instancing條件:相同的Mesh,且使用相同的材質球。優點:适用于渲染同種大量怪物的需求,合批的同時能夠降低動畫模塊的耗時。缺點:可能存在負優化,反而使DrawCall上升;Instancing有時候被打亂,可以自己分組用API渲染。

2.4 Shader.CreateGPUProgram該API常常在渲染模塊主函數的堆棧中出現,并造成渲染模塊中的大多數函數峰值。它是Shader第一次渲染時産生的耗時,其耗時與渲染Shader的複雜程度相關。當它在遊戲過程中被調用并且造成較高的耗時峰值時應引起注意。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)3

對此,我們可以将Shader通過ShaderVariantCollection收集要用到的變體并進行AssetBundle打包。在将該ShaderVariantCollection資源加載進内存後,通過在遊戲前期場景調用ShaderVariantCollection.WarmUp來觸發Shader.CreateGPUProgram,并将此SVC進行緩存,從而避免在遊戲運行時觸發此API的調用、避免局部的CPU高耗時。

然而即便是已經做過以上操作的項目也常會檢測到運行時偶爾的該API耗時峰值,說明存在一些“漏網之魚”。開發者可以結合Profiler的Timeline模式,選中觸發調用Shader.CreateGPUProgram的幀來查看具體是哪些Shader觸發了該API,可以參考《一種Shader變體收集和打包編譯優化的思路》。

2.5 Culling絕大多數情況下,Culling本身耗時并不顯眼,它的意義在于反映一些與渲染相關的問題。

(1)相機數量多當渲染模塊主函數的堆棧中Culling耗時的占比比較高(一般項目中在10%-20%左右)。

(2)場景中小物件多Culling耗時與場景中的GameObject小物件數量的相關性比較大。這種情況建議研發團隊優化場景制作方式 ,關注場景中是否存在過多小物件,導緻Culling耗時增高。可以考慮采用動态加載、分塊顯示,或者Culling Group、Culling Distance等方法優化Culling的耗時。

(3)Occlusion Culling如果項目使用了多線程渲染且開啟了Occlusion Culling,通常會導緻子線程的壓力過大而使整體Culling過高。

由于Occlusion Culling需要根據場景中的物體計算遮擋關系,因此開啟Occlusion Culling雖然降低了渲染消耗,其本身的性能開銷卻也是值得注意的,并不一定适用于所有場景。這種情況建議開發者選擇性地關閉一部分Occlusion Culling去測試一下渲染數據的整體消耗進行對比,再決定是否需要開啟這個功能。

(4)包圍盒更新Culling的堆棧中有時出現的FinalizeUpdateRendererBoundingVolumes為包圍盒更新耗時。一般常見于Skinned Mesh和粒子系統的包圍盒更新上。如果該API出現很頻繁,則要通過截圖去排查此時是否有較大量的Skinned Mesh更新,或者較為複雜的粒子系統更新。

(5)PostProcessingLayer.OnPreCull/WaterReflection.OnWillRenderObjectPostProcessLayer.OnPreCull這一方法和項目中使用的PostProcessing Stack相關。可以在PostProcessManager.cs中添加靜态變量GlobalNeedUpdateSettings,在切場景的時候通過設置PostProcessManager.GlobalNeedUpdateSettings為true來UpdateSettings。這樣就可以避免每幀都做UpdateSettings操作,從而減少一部分耗時。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)4

WaterReflection.OnWillRenderObject則是項目中使用到的水面反射效果的相關耗時,若該項耗時較高,可以關注一下實現方式上是否有可優化的空間,比如去除一些不必要的粒子、小物件等的反射渲染。


3. UI模塊

在Unity引擎中,主流的UI框架有UGUI、NGUI以及使用越來越多的FairyGUI。本文主要從使用最多的UGUI來進行說明。圍繞UGUI相關優化更全面的内容可以參考《Unity性能優化 — UI模塊》。

3.1 UGUI EventSystem.UpdateEventSystem.Update函數為UGUI的事件系統耗時,其耗時偏高時主要關注以下兩個因素:

(1)觸發調用耗時高作為UGUI事件系統的主函數,該函數主要是在觸摸釋放時觸發,當本身有較高的CPU開銷時,通常都是因為調用了其它較為耗時的函數引起。因此需要通過添加Profiler.BeginSample/EndSample打點或者GOT Online服務 UWA API打點來對所觸發的邏輯進行進一步地檢測,從而排查出具體是哪一個子函數或者代碼段造成的高耗時。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)5

(2)輪詢耗時高所有UGUI組件在創建時都默認開啟了Raycast Target這一選項,實際上是為接受事件響應做好了準備。事實上,大部分比如Image、Text類型的UI組件是不會參與事件響應的,但仍然會在鼠标/手指劃過或懸停時參與輪詢,所以通過模拟射線檢測判斷UI組件是否被劃過或懸停,造成不必要的耗時。尤其在項目中UI組件比較多時,關閉不參與事件響應的組件的Raycast Target設置,可以有效降低EventSystem.Update()耗時。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)6

3.2 UGUI Canvas.SendWillRenderCanvasesCanvas.SendWillRenderCanvases函數的耗時代表的是UI元素自身變化帶來的更新耗時,這是需要和Canvas.BuildBatch(見下文)的網格重建的耗時所區分的。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)7

持續的高耗時往往是由于UI元素過于複雜且更新過于頻繁造成。UI元素的自身更新包括:替換圖片、文本或顔色發生變化等等。UI元素發生位移、旋轉或者縮放并不會引起該函數有開銷。該函數的耗時取決于UI元素發生更新的數量以及UI元素的複雜度,因此要優化此函數的開銷通常可以從如下幾點着手:

(1)降低頻繁更新的UI元素的頻率比如小地圖的怪物标記、角色或者怪物的血條等,可以控制邏輯在變動超過某個阈值時才更新UI的顯示,再比如技能CD效果,傷害飄字等控制隔幀更新。

(2)盡量讓複雜的UI不要發生變動如某些字符串特别多且又使用了Rich Text、Outline或者Shadow效果的Text,Image Type為Tiled的Image等。這些UI元素因為頂點數量非常多,一旦更新便會有較高的耗時。如果某些效果需要使用Outline或者Shadowmap,但是卻又頻繁的變動,如飄動的傷害數字,可以考慮将其做成固定的美術字,這樣頂點數量就不會翻N倍。

(3)關注Font.CacheFontForText該函數往往會造成一些耗時峰值。該API主要是生成動态字體Font Texture的開銷,在運行時突發高耗時,很有可能是一次性寫入很多新的字符,導緻Font Texture紋理擴容。可以從減少字體種類、減少字體字号、提前顯示常用字以擴充動态字體FontTexture等方式去優化這一項的耗時。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)8

3.3 UGUI Canvas.BuildBatchCanvas.BuildBatch為UI元素合并的Mesh需要改變時所産生的調用。通常之前所提到的Canvas.SendWillRenderCanvases()的調用都會引起Canvas.BuildBatch的調用。另外,Canvas中的UI元素發生移動也會引起Canvas.BuildBatch的調用。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)9

Canvas.BuildBatch是在主線程發起UI網格合并,具體的合并過程是在子線程中處理的,當子線程壓力過大,或者合并的UI網格過于複雜的時候,會在主線程産生等待,等待的耗時會被統計到EmitWorldScreenspaceCameraGeometry中。

這兩個函數産生高耗時,說明發生重建的Canvas非常複雜,此時需要将Canvas進行細分處理,通常是将靜态的元素放在一個Canvas中,将發生更新的UI元素放入一個Canvas中,這樣靜态的Canvas由于緩存不會發生網格更新,從而降低網格更新的複雜度,減少網格重建的耗時。

3.4 UGUI CanvasRenderer.SyncTransform我們常注意到有些項目的部分幀中CanvasRenderer.SyncTransform調用頻繁。如下圖,CanvasRenderer.SyncTransform調用次數多達1017次。當Canvas.SyncTransform觸發次數非常頻繁時,會導緻它的父節點UGUI.Rendering.UpdateBathes産生非常高的耗時。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)10

在Unity 2018版本及以後的版本中,Canvas下某個UI元素調用SetActive(false改成true)會導緻該Canvas下的其它UI元素觸發SyncTransform,從而導緻UI更新的整體開銷上升,在Unity 2017的版本中隻會導緻該UI元素本身觸發SyncTransform。

所以,針對UI元素(如Image、Text)特别多的Canvas,需要注意是否存在一些UI元素在頻繁地SetActive,對于這種情況建議使用SetScale(0或者1)來代替SetActive(false或者true)。或者,也可以将Canvas适當拆分,讓需要進行SetActive(true)操作的元素和其它元素不在一個Canvas下,就不會頻繁調用SyncTransform了。

3.5 UGUI UI DrawCall通常戰鬥場景中其它模塊耗時壓力大,此時UI模塊更要仔細控制性能開銷。一般而言,戰鬥場景中的UI DrawCall控制到40-50左右為最佳。

在不減少UI元素的前提下,控制DrawCall的問題,其實也就是如何使得UI元素盡量合批的問題。一般的合批要求材質相同,而在UI中卻常常會發生明明是使用同一材質、同一圖集制作的UI元素卻無法合批的現象。這其實和UGUI DrawCall的計算原理有關。詳細的原理介紹可以參考UWA學堂的這篇課程《詳解UGUI DrawCall計算和Rebuild操作優化》。

在UGUI的制作過程中,建議關注以下幾點:(1)同一Canvas下的UI元素才能合批。不同Canvas即使Order in Layer相同也不合批,所以UI的合理規劃和制作非常重要;(2)盡量整合并制作圖集,從而使得不同UI元素的材質圖集一緻。圖集中的按鈕、圖标等需要使用圖片的比較小的UI元素,完全可以整合并制作圖集。當它們密集地同時出現時,就有效降低了DrawCall;(3)在同一Canvas下、且材質和圖集一緻的前提下,避免層級穿插。簡單概括就是,應使得符合合批條件的UI元素的“層級深度”相同;(4)将相關UI的Pos Z盡量統一設置為0,Z值不為0的UI元素隻能與Hierarchy中相鄰元素嘗試合批,所以容易打斷合批。(5)對于Alpha為0的Image,需要勾選其CanvasRender組件上的Cull Transparent Mesh選項,否則依然會産生DrawCall且容易打斷合批。


4. 物理模塊

圍繞物理模塊相關優化更全面的内容可以參考《Unity性能優化 — 物理模塊》。

4.1 Auto Simulation在Unity 2017.4版本之後,物理模拟的設置選項Auto Simulation被開放并且默認開啟,即項目過程中總是默認進行着物理模拟。但在一些情況下,這部分的耗時是浪費的。

判斷物理模拟耗時是否被浪費的一個标準就是Contacts數量,即遊戲運行時碰撞對數量。一般來說,碰撞對的數量越多,則物理系統的CPU耗時越大。但在很多移動端項目中,我們都檢測到在整個遊戲過程中Contacts數量始終為0。

在這種情況下,開發者可以關閉物理的自動模拟來進行測試。如果關閉Auto Simulation并不會對遊戲邏輯産生任何影響,在遊戲過程中依然可以進行很好地對話、戰鬥等,則說明可以節省這方面的耗時。同時也需要說明的是,如果項目需要使用射線檢測,那麼在關閉Auto Simulation後需要開啟Auto Sync Transforms,來保證射線檢測可以正常作用。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)11

4.2 物理更新次數Unity物理模拟過程的主要耗時函數是在FixedUpdate中的,也就是說,當每幀該函數調用次數越高、物理更新次數也就越頻繁,每幀的耗時也就相應地高。

物理更新次數,或者說FixedUpdate的每幀調用次數,是和Unity Project Settings的Time設置中最小更新間隔(Fixed Timestep)以及最大允許時間(Maximum Allowed Timestep)相關的。這裡我們需要先知道物理系統本身的特性,即當遊戲上一幀卡頓時,Unity會在當前幀非常靠前的階段連續調用N次FixedUpdate.PhysicsFixedUpdate,Maximum Allowed Timestep的意義就在于限制物理更新的次數。它決定了單幀物理最大調用次數,該值越小,單幀物理最大調用次數越少。現在設置這兩個值分别為20ms和100ms,那麼當某一幀耗時30ms時,物理更新隻會執行1次;耗時200ms時也隻會執行5次。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)12

所以一個行之有效的方法是調整這兩個參數的設置,尤其是控制更新次數的上限(默認為17次,最好控制到5次以下),物理模塊的耗時就不會過高;另一方面則是先優化其它模塊的CPU耗時,當項目運行過程中耗時過高的幀很少,則FixedUpdate也不會總是達到每幀更新次數的上限。這對于其它FixedUpdate中的函數是同理的,也是基于這種原因,我們一般不建議在FixedUpdate中寫過多遊戲邏輯。

4.3 Contacts就像上面提到的,如果我們确實用到物理模拟,則一般碰撞對的數量越多,物理系統的CPU耗時也就越大。所以,嚴格控制碰撞對數量對于降低物理模塊耗時非常重要。

首先,很多項目中可能存在一些不必要的Rigidbody組件,在開發者不知情的地方造成了不必要的碰撞,從而産生了耗時浪費;另外,可以檢查修改Project Settings的Physics設置中的Layer Collision Matrix,取消不必要的層之間的碰撞檢測,将Contacts數量盡可能降低。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)13


5. 動畫模塊

圍繞動畫模塊相關優化更全面的内容可以參考《Unity性能優化 — 動畫模塊》。

5.1 Mecanim動畫系統Mechanic動畫系統是Unity公司從Unity 4.0之後開始引入的新版動畫系統(使用Animator控制動畫),相比于Legacy的Animation控制系統,在功能上,Mecanim動畫系統主要有以下幾點優勢:(1)針對人形角色提供了一套特殊的工作流,包括Avatar的創建以及Muscles肌肉的調節;(2)動畫重定向(Retarting)的能力,可以非常方便地把一個動畫從一個角色模型應用到其他角色模型上;(3)提供了可視化的Animator編輯器,可以快捷預覽和創建動畫片段;(4)更加方便地創建狀态機以及狀态之間Transition的轉換;(5)便于操作的混合樹功能。

在性能上,對于骨骼動畫且曲線較多的動畫,使用Animator的性能是要比Animation要好的,因為Animator是支持多線程計算的,而且Animator可以通過開啟Optimized GameObjects進行優化,具體細節可以參考UWA學堂的課程《Unity移動遊戲中動畫系統的性能優化》。相反,對于比較簡單的類似于移動旋轉這樣的動畫,使用Animation控制則比Animator要高效一些。

5.2 BakeMesh對于一兩千面這樣面數較少且動畫時長較短的對象,如MOBA、SLG中的小兵等,可考慮用SkinnedMeshRenderer.BakeMesh的方案,用内存換CPU耗時。其原理是将一個蒙皮動畫的某個時間點上的動作,Bake成一個不帶蒙皮的Mesh,從而可以通過自定義的采樣間隔,将一段動畫轉成一組Mesh序列幀。而後在播放動畫時隻需選擇最近的采樣點(即一個Mesh)進行賦值即可,從而省去了骨骼更新與蒙皮計算的時間(幾乎沒有動畫,隻是賦值的動作)。整個操作比較适合于面片數小的人物,因為此舉省去了蒙皮計算。其作用在于:用内存換取計算時間,在場景中大量出現同一個帶動畫的模型時,效果會非常明顯。該方法的缺點是内存的占用極大地受到模型頂點數、動畫總時長及采樣間隔的限制。因此,該方法隻适用于頂點數較少,且動畫總時長較短的模型。同時,Bake的時間較長,需要在加載場景時完成。

5.3 Active Animator數量Active狀态的Animator個數會極大地影響動畫模塊的耗時,而且是一個可量化的重要标準,控制其數量到一個相對合理的值是我們優化動畫模塊的重要手段。需要開發者結合畫面排查對應的數量是否合理。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)14

(1)Animator Culling Mode控制Active Animator的一個方法是針對每個動畫組件調整合理的Animator.CullingMode設置。該項設置一共有三個選項:AlwaysAnimate、CullUpdateTransforms和CullComplete。

默認的AlwaysAnimate使得當前物體不管是不是在視域體内,或者在視域體被LOD Culling掉了,Animator的所有東西都仍然更新;其中,UI動畫一定要選AlwaysAnimate,不然會出現異常表現。

而設置為CullUpdateTransforms時,當物體不在視域體内,或者被LOD Culling掉後,邏輯繼續更新,就表示狀态機是更新的,動畫資源中連線的條件等等也都是會更新和判斷的;但是Retarget、IK和從C 回傳Transform這些顯示層的更新就不做了。所以,在不影響表現的前提下把部分動畫組件嘗試設置成CullUpdateTransforms可以節省物體不可見時動畫模塊的顯示層耗時。

最後,CullComplete就是完全不更新了,适用于場景中相對不重要的動畫效果,在低端機上需要保留顯示但可以考慮讓其靜止的物體,分級地選用該設置。

(2)DOTween插件很多時候,UI動畫也會貢獻大量的Active Animator。針對一些簡單的UI動畫,如改變顔色、縮放、移動等效果,UWA建議改用DOTween制作。經測試,性能比原生的UI動畫要好得多。

5.4 開啟Apply Root Motion的Animator數量在Animators.Update的堆棧中,有時會看到Animator.ApplyBuiltinRootMotion占比過高,這一項通常和項目中開啟了Apply Root Motion的模型動畫相關。如果其動畫不需要産生位移,則不必開啟此選項。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)15

5.5 Animator.InitializeAnimator.Initialize API會在含有Animator組件的GameObject被Active和Instantiate時觸發,耗時較高。因此尤其是在戰鬥場景中不建議過于頻繁地對含有Animator的GameObject進行Deactive/Active GameObject操作。對于頻繁實例化的角色和UI,可嘗試通過緩沖池的方式進行處理,在需要隐藏角色時,不直接Deactive角色的GameObject,而是Disable Animator組件,并把GameObject移到屏幕外;在需要隐藏UI時,不直接Deactive UI對象,而是将其SetScale=0并且移出屏幕的方式,也不會觸發Animator.Initialize。

5.6 Meshskinning.Update和Animators.WriteJob網格資源對于動畫模塊耗時的影響是十分顯著的。

一方面,Meshskinning.Update耗時較高時。主要因素為蒙皮網格的骨骼數和面片數偏高,所以可以針對網格資源進行減面和LOD分級。

另一方面,默認設置下,我們經常發現很多項目中角色的骨骼節點的Transform一直都是在場景中存在的,這樣在Native層計算完它們的Transform後,會回傳給C#層,從而産生一定的耗時。

在場景中角色數量較多,骨骼節點的回傳會産生一定的開銷,體現在動畫模塊的主函數之一PreLateUpdate.DirectorUpdateAnimationEnd的Animators.WriteJob子函數上。

對此開發者可以考慮勾選FBX資源中Rig頁簽下的Optimize Game Objects設置項,将骨骼節點“隐藏”,從而減少這部分的耗時。

5.7 GPU Skinning/Compute Skinning特别地,對于Unity引擎原生的GPU Skinning設置項(新版Unity中為Compute Skinning),理論上會在一定程度上改變網格和動畫的更新方法以優化對骨骼動畫的處理,但從針對移動平台的多項測試結果來看,無論是在iOS還是安卓平台上,多個Unity版本提供的GPU Skinning對性能的提升效果都不明顯,甚至存在負優化的現象。在Unity的疊代中已對其逐步優化,将相關操作放到渲染線程中進行,但其實用性還需要進一步考察。

對于大量同種怪物的需求,可以考慮使用自己實現的《GPU Skinning 加速骨骼動畫》,和UWA開源庫中的GPU Instancing來進行渲染,這樣既可以降低Animator.Update耗時,又能達到合批的效果。


6. 粒子系統

圍繞粒子系統相關優化更全面的内容可以參考《粒子系統優化——如何優化你的技能特效》。

6.1 Playing粒子系統數量UWA統計了粒子系統數量和Playing狀态的粒子系統數量。前者是指内存中所有的ParticleSystem的總數量,包含正在播放的和處于緩存池中的;後者指的是正在播放的ParticleSystem組件的數量,這個包含了屏幕内和屏幕外的,我們建議在一幀中出現的數量峰值不超過50(1GB機型)。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)16

針對這兩個數值,我們一方面關注粒子系統數量峰值是否偏高,可選中某一峰值幀查看到底是哪些粒子系統緩存着、是否都合理、是否有過度緩存的現象;另一方面關注Playing數量峰值是否偏高,可選中某一峰值幀查看到底是哪些粒子系統在播放、是否都合理、是否能做些制作上的優化(具體見下文GPU部分中的讨論)。

6.2 PrewarmParticleSystem.Prewarm的耗時有時也需要關注。當有粒子系統開啟了Prewarm選項,其在場景中實例化或者由Deactive轉為Active時,會立即執行一次完整的模拟。

但Prewarm的操作通常都有一定的耗時,經測試,大量開啟Prewarm的粒子系統同時SetActive時會造成耗時峰值。建議在不必要的情況下,将其關閉。


7. 加載模塊

圍繞加載模塊相關優化更全面的内容可以參考《Unity性能優化系列—加載與資源管理》。

7.1 Shader加載(1)Shader.ParseShader.Parse是指Shader加載進行解析的操作,如果此操作較為頻繁,通常是由于Shader的重複加載導緻的,這裡的重複可以理解為2層意思。

第一層是由于Shader的冗餘導緻的,通常是因為打包AssetBundle的時候,Shader被被動打進了多個不同的AssetBundle中而沒有進行依賴打包,這樣當這些AssetBundle中的資源進行加載的時候,會被動加載這些Shader,就進行了多次“重複的”Shader.Parse,所以同一種Shader就在内存中有多份了,這就是冗餘了。

要去除這種冗餘的方法也很簡單,就是把這些會冗餘的Shader依賴打包進一個公共的AssetBundle包。這樣就會主動打包了,而不是被動進入某些使用了這個Shader的包體中。如果對這個Shader進行了主動打包,那麼其它使用了這個Shader的AssetBundle中就隻會對這個Shader打出來的公共AssetBundle進行引用,這樣在内存中就隻有一份Shader,其它用到這個Shader的時候就直接引用它,而不需要多次進行Shader.Parse了。

第二層意思是同一個Shader多次地加載卸載,沒有緩存住導緻的。假設AssetBundle進行了主動打包,生成了公共的AssetBundle,這樣在内存中隻有這一份Shader,但是因為這個Shader加載完後(也就是Shader.Parse)沒有進行緩存,用完馬上被卸載了。下次再用到這個Shader的時候,内存裡沒有這個Shader了,那就必須再重新加載進來,這樣同樣的一個Shader加載解析了多次,就造成了多次的Shader.Parse。一般而言,經過變體優化以後的開發者自己寫的Shader内存占用都不高,可以統一在遊戲開始時加載并緩存。

特别地,對于Unity内置的Shader,隻要是變體數量不多的,可以放進Project Settings中的Always Included中去,從而避免這一類Shader的冗餘和重複解析。

(2)Shader.CreateGPUProgram該API也會在加載模塊主函數甚至UI模塊、邏輯代碼的堆棧中出現。相關的讨論上文已經涉及,優化方法相同,不再贅述。

7.2 Resources.UnloadUnusedAssets該API會在場景切換時被Unity自動調用,一般單次調用耗時較高,通常情況下不建議手動調用。

但在部分不進行場景切換或用Additive加載場景的項目中,不會調用該API,從而使得項目整體資源數量和内存有上升趨勢。對于這種情況則可以考慮每5-10min手動調用一次。

Resources.UnloadUnusedAssets的底層運作機理是,對于每個資源,遍曆所有Hierarchy Tree中的GameObject結點,以及堆内存中的對象,檢測該資源是否被某個GameObject或對象(組件)所使用,如果全部都沒有使用,則引擎才會認定其為Unused資源,進而進行卸載操作。簡單來講,Resources.UnloadUnusedAssets的單次耗時大緻随着((GameObject數量 Mono對象數量)*Asset數量)的乘積變大而變大。

因此,該過程極為耗時,并且場景中GameObject/Asset數量越高,堆内存中的對象數越高,其開銷也就越大。對此,我們的建議如下:

(1)Resources.UnloadAsset/AssetBundle.Unload(True)研發團隊可嘗試在遊戲運行時,通過Resources.UnloadAsset/AssetBundle.Unload(True)來去除已經确定不再使用的某一資源,這兩個API的效率很高,同時也可以降低Resources.UnloadUnusedAssets統一處理時的壓力,進而減少切換場景時該API的耗時;

(2)嚴格控制場景中材質資源和粒子系統的使用數量。專門提到這兩種資源,因為在大多數項目中,雖然它們的内存占用一般不是大頭,但往往資源數量遠高于其他類型的資源,很容易達到數千的數量級,從而對單次Resources.UnloadUnusedAssets耗時有較大貢獻。

(3)降低駐留的堆内存。堆内存中的對象數量同樣會顯著影響Resources.UnloadUnusedAssets的耗時,這在上文也已經讨論過。

7.3 加載AssetBundle使用AssetBundle加載資源是目前移動端項目中比較普遍的做法。

而其中,應盡量用LZ4壓縮格式打包AssetBundle,并用LoadFromFile的方式加載。經測試,這種組合下即便是較大的AssetBundle包(包含10張1024*1024的紋理),其加載耗時也僅零點幾毫秒。而使用其他加載方式,如LoadFromMemory,加載耗時則上升到了數十毫秒;而使用WebRequest加載則會造成AssetBundle包的駐留内存顯著上升。

這是因為,LoadFromFile是一種高效的API,用于從本地存儲(如硬盤或SD卡)加載未壓縮或LZ4壓縮格式的AssetBundle。

在桌面獨立平台、控制台和移動平台上,API将隻加載AssetBundle的頭部,并将剩餘的數據留在磁盤上。AssetBundle的Objects會按需加載,比如:加載方法(例如:AssetBundle.Load)被調用或其InstanceID被間接引用的時候。在這種情況下,不會消耗過多的内存。

但在Editor環境下,API還是會把整個AssetBundle加載到内存中,就像讀取磁盤上的字節和使用AssetBundle.LoadFromMemoryAsync一樣。如果在Editor中對項目進行了分析,此API可能會導緻在AssetBundle加載期間出現内存尖峰。但這不應影響設備上的性能,在做優化之前,這些尖峰應該在設備上重新再測試一遍。

要注意,這個API隻針對未壓縮或LZ4壓縮格式,因為如果使用LZMA壓縮,它是針對整個生成後的數據包進行壓縮的,所以在未解壓之前是無法拿到AssetBundle的頭信息的。

由于LoadFromMemory的加載效率相較其他的接口而言,耗時明顯增大,因此我們不建議大規模使用,而且堆内存會變大。如果确實有對AssetBundle文件加密的需求,可以考慮僅對重要的配置文件、代碼等進行加密,對紋理、網格等資源文件則無需進行加密。因為目前市面上已經存在一些工具可以從更底層的方式來獲取和導出渲染相關的資源,如紋理、網格等,因此,對于這部分的資源加密并不是十分的必要性。

在UWA GOT Online Resource模式下的資源管理頁面中可以排查加載耗時較高的AssetBundle,從而排查和優化加載方式、壓縮格式、包體過大等問題,或者對反複加載的AssetBundle考慮予以緩存。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)17

7.4 加載資源有關加載資源所造成的耗時,若加載策略比較合理,則一般發生在遊戲一開始和場景切換時,往往不會造成嚴重的性能瓶頸。但不排除一些情況需要予以關注,那麼可以把資源加載耗時的排序作為依據進行排查。

對于單次加載耗時過高的資源,比如達到數百毫秒甚至幾秒時,就應考察這類資源是否過于複雜,從制作上考慮予以精簡。

對于反複頻繁加載且耗時不低的資源,則應該在第一次加載後予以緩存,避免重複加載造成的開銷。

值得一提的是,在Unity的異步加載中有時會出現每幀進行加載所能占用的最高耗時被限制,但主線程中卻在空轉的現象。尤其是在切場景的時候集中進行異步加載,有時會耗費幾十甚至數十秒的時間,但其中大部分時間是被空轉浪費的。這是因為控制異步加載每幀最高耗時的API Application.backgroundLoadingPriority默認值為BelowNormal,每幀最多隻加載4ms。此時一般建議把該值調為High,即最多50ms每幀。

在UWA GOT Online Resource模式下的資源管理頁面中可以排查加載耗時較高的資源,從而排查和優化加載方式、資源過于複雜等問題,或者對反複加載的資源考慮予以緩存。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)18

7.5 實例化和銷毀實例化同樣主要存在單個資源實例化耗時過高或某個資源反複頻繁實例化的現象。根據耗時多少排列後,針對疑似有問題的資源,前者考慮簡化,或者可以考慮分幀操作,比如對于一個較為複雜的UI Prefab,可以考慮改為先實例化顯眼的、重要的界面和按鈕,而翻頁後的内容、裝飾圖标等再進行實例化;後者則建立緩存池,使用顯隐操作來代替頻繁的實例化。

在UWA GOT Online Resource模式下的資源管理頁面中可以排查實例化耗時較高的資源,從而排查和優化資源過于複雜的問題,或者對反複實例化的資源考慮予以緩存。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)19

7.6 激活和隐藏激活和隐藏的耗時本身不高,但如果單幀的操作次數過多就需要予以關注。可能出于遊戲邏輯中的一些判斷和條件不夠合理,很多項目中往往會出現某一種資源的顯隐操作次數過多,且其中SetActive(True)遠比SetActive(False)次數多得多、或者反之的現象,亦即存在大量不必要的SetActive調用。由于SetActive API會産生C#和Native的跨層調用,所以一旦數量一多,其耗時仍然是很可觀的。針對這種情況,除了應該檢查邏輯上是否可以優化外,還可以考慮在邏輯中建立狀态緩存,在調用該API之前先判斷資源當前的激活狀态。相當于使用邏輯的開銷代替該API的開銷,相對耗時更低一些。

在UWA GOT Online Resource模式下的資源管理頁面中可以排查激活隐藏操作較頻繁的資源,從而排查和優化相關邏輯和調用。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)20


8. 邏輯代碼

邏輯代碼的CPU耗時優化更多是結合項目實際需求、考驗程序員本人的過程,很難定量定性進行讨論。不過UWA SDK中提供了方便開發者在邏輯代碼中進行打點的API&UWA GOT Online,從而将複雜的函數拆解開,在報告中排查堆棧耗時、更快速地驗證優化效果。

我們發現有越來越的團隊在使用JobSystem将主線程中的部分邏輯代碼放入子線程中來進行處理,對于可以并行運算的邏輯,非常推薦将其放入到子線程中來處理,這樣可以有效降低主線程CPU處理邏輯運算的壓力。


9. Lua

GOT Online Lua模式提供的分析Lua造成的CPU耗時工具可視化程度高,堆棧清晰明了,還提供了實用且特色的倒序調用分析功能。以下結合一個Lua報告Demo簡單介紹使用該工具分析Lua耗時的方法。

重申:Lua報告中出現的函數名稱格式為:函數名稱@文件名:行号。

可以通過報告提供的Lua文件名/行号/函數名來定位CPU耗時的瓶頸函數和CPU耗時峰值的具體原因。Lua函數的命名格式為X@Y:Z,其中X是其函數名,在無法獲取時,X會變為默認的unknown;Y是該函數定義的文件位置;Z則是該函數被定義的行号。需要注意的是,當Lua腳本以字節碼運行時,該值将始終為0,因此建議在測試時盡可能使用Lua源碼來運行。

(1)正序調用分析——總表(曲線圖 列表)曲線圖:

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)21

曲線選取了選取總體Lua代碼耗時和按照耗時均值正向排序的前五個函數耗時組成耗時曲線圖,每一個數據點代表了該函數在當前幀(橫坐标)的耗時(縱坐标),有助于定位耗時瓶頸函數。

列表:

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)22

列表默認按照耗時均值從高到低對Lua函數進行了排序,粗略展示了函數名、總CPU耗時、場景CPU耗時、耗時均值等數據。通過點擊函數,可以進入對應的單個函數分析頁面。

(2)正序調用分析——單個函數頁(截圖 曲線圖 堆棧信息)截圖:

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)23

項目運行時截圖與使用者選中的幀大緻對應,有助于定位問題。

曲線圖:

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)24

曲線圖包括了CPU耗時曲線圖和調用次數曲線圖;也可以使用下方條縮放曲線觀察局部耗時情況。

從曲線圖中可以觀察到:函數是否存在持續性高耗時;函數是否存在短暫的大量耗時,導緻卡頓;某些函數單次耗時并不高,但因為被大量的調用,導緻函數總耗時較高。

函數XXXX堆棧信息 (列表):

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)25

其中,可以在右上角選定列表數據的時間範圍:總體堆棧信息時,時間範圍為全部測試時間;指定場景堆棧信息時,時間範圍為指定場景的開啟時間;指定幀堆棧信息時,時間範圍為當前在曲線圖中選中的指定幀。

列表中各項指标含義是:總體占比,以根節點函數的總耗時為100%,當前節點函數總耗時相對根節點函數的總耗時占比;自身占比,以根節點函數的總耗時為100%,當前節點函數自身耗時相對根節點函數的總耗時占比;總耗時,時間範圍内執行該函數的耗時;自身耗時,時間範圍内去除子節點函數(該函數調用的函數)耗時剩餘的耗時;調用次數,時間範圍内該函數被調用的次數;單次耗時,總耗時/調用次數,表示每次執行該函數的平均耗時;顯著調用幀數,該函數自身耗時大于3ms的幀數。

(3)倒序調用分析——總表(曲線圖 列表)曲線圖:與正序調用分析不同的是,選取了自身耗時正向排序的前五個函數,每一個數據點代表了該函數在當前幀(橫坐标)的自身耗時(縱坐标)。

列表:與上同理。

(4)倒序調用分析——單個函數頁(截圖 曲線圖 堆棧信息)

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)26

函數XXXX堆棧信息 (列表):各項指标含義(與正序相比有所不同)變為了:自身占比,以選定函數的自身耗時總和為100%,這條調用路徑下選定函數的自身耗時相對選定節點函數總自身耗時的占比;自身耗時,時間範圍内,這條調用路徑下,選定函數自身耗時的總和;調用次數,這條調用路徑的調用次數;單次耗時,代表這條路調用路徑下,選定函數的平均耗時。

在通過以上界面定位到自身耗時較高的函數後,常見的優化手段有:優化該函數的函數體,減少該函數自身的耗時;定位調用次數較多的調用路徑,減少調用次數。

(5)注意事項Lua CPU耗時中暫不包括GC耗時;Lua 函數耗時相當于在進出函數時打點,統計耗時。所以如果Lua腳本運行時調用了C#函數,這部分C#函數是會被統計進去的,所以需要關注和C#穿插調用的情況,盡量控制在50次以内。


本文内容就介紹到這裡啦,更多内容可以前往UWA學堂進行閱讀。課程将從内存、CPU、GPU三個維度讨論當前遊戲項目中經常出現的一些性能問題。

unity代碼優化和性能優化(Unity移動端遊戲性能優化簡譜之)27

,

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关遊戲资讯推荐

热门遊戲资讯推荐

网友关注

Copyright 2023-2024 - www.tftnews.com All Rights Reserved