Unity移動(dòng)端游戲性能優(yōu)化簡譜之 以引擎模塊為劃分的CPU耗時(shí)調(diào)優(yōu)

《Unity移動(dòng)端游戲性能優(yōu)化簡譜》從Unity移動(dòng)端游戲優(yōu)化的一些基礎(chǔ)討論出發(fā),例舉和分析了近幾年基于Unity開發(fā)的移動(dòng)端游戲項(xiàng)目中最為常見的部分性能問題,并展示了如何使用UWA的性能檢測工具確定和解決這些問題。內(nèi)容包括了性能優(yōu)化的基本邏輯、UWA性能檢測工具和常見性能問題,希望能提供給Unity開發(fā)者更多高效的研發(fā)方法和實(shí)戰(zhàn)經(jīng)驗(yàn)。

今天向大家介紹文章第三部分:以引擎模塊為劃分的CPU耗時(shí)調(diào)優(yōu),共9小節(jié),包含了渲染模塊、UI模塊、物理模塊、動(dòng)畫模塊、粒子系統(tǒng)、加載模塊、邏輯代碼、Lua等多個(gè)模塊等常見的游戲CPU耗時(shí)調(diào)優(yōu)講解。

(全文長約14115字,預(yù)計(jì)閱讀時(shí)間約30分鐘)

文章第一部分《Unity移動(dòng)端游戲性能優(yōu)化簡譜之 前言》、第二部分《Unity移動(dòng)端游戲性能優(yōu)化簡譜之 常見游戲內(nèi)存控制》可戳此回顧,完整內(nèi)容可前往UWA學(xué)堂查看。

1. 總覽

1.1 模塊劃分

UWA將CPU中工作內(nèi)容明確、耗時(shí)占比一般較高的函數(shù)整理劃分為:渲染、UI、物理、動(dòng)畫、粒子、加載、邏輯等模塊。但這并不意味著模塊之間的工作互相獨(dú)立毫無關(guān)聯(lián)。舉例而言,渲染模塊的性能壓力勢必受到復(fù)雜的UI和粒子影響,而加載模塊的很多操作實(shí)際上都是在邏輯中調(diào)用并完成的。

劃分模塊有利于我們確認(rèn)問題、找到重點(diǎn)。與此同時(shí),也要建立起模塊之間的關(guān)聯(lián),有助于更高效地解決問題。

1.2 耗時(shí)瓶頸

當(dāng)一個(gè)項(xiàng)目由于CPU端性能瓶頸而產(chǎn)生幀率偏低、卡頓明顯的現(xiàn)象時(shí),如何提煉出哪個(gè)模塊的哪個(gè)問題是造成性能瓶頸的主要問題就成了關(guān)鍵。盡管我們已經(jīng)對引擎中主要模塊做了整理,各個(gè)模塊間會(huì)出現(xiàn)的問題還是會(huì)千奇百怪不可一以概之,而且它們對CPU性能壓力的貢獻(xiàn)也不盡相同。那么我們就需要對什么樣的耗時(shí)可以認(rèn)為是潛在的性能瓶頸有準(zhǔn)確的認(rèn)知。

在移動(dòng)端項(xiàng)目中,我們CPU端性能優(yōu)化的目標(biāo)是能夠在中低端機(jī)型上大部分時(shí)間跑滿30幀的流暢游戲過程。為了達(dá)成這一目標(biāo),簡單做一下除法就得到我們的CPU耗時(shí)均值應(yīng)控制在33ms以下。當(dāng)然,這并不意味著CPU均值已經(jīng)在33ms以下的項(xiàng)目就已經(jīng)把CPU耗時(shí)控制的很好了。游戲運(yùn)行過程中性能壓力點(diǎn)是不同的,可能一系列UI界面中壓力很小、但反過來游戲中最重要的戰(zhàn)斗場景中幀率很低、又或者是存在大量幾百毫秒甚至幾秒的卡頓,而最終平均下來仍然低于33ms。

為此,UWA認(rèn)為,在一次測試中,當(dāng)33ms及以上耗時(shí)的幀數(shù)占總幀數(shù)的10%以下時(shí),可以認(rèn)為項(xiàng)目CPU性能整體控制在正常范圍內(nèi)。而這個(gè)占比越高,說明當(dāng)前項(xiàng)目的CPU性能瓶頸越嚴(yán)重。

以上的討論內(nèi)容主要是圍繞著我們對CPU性能的宏觀的優(yōu)化目標(biāo),和內(nèi)存一樣,我們?nèi)砸Y(jié)合具體模塊的具體數(shù)據(jù)來排查和解決項(xiàng)目中實(shí)際存在的問題。

2. 渲染模塊

圍繞渲染模塊相關(guān)優(yōu)化更全面的內(nèi)容可以參考《Unity性能優(yōu)化系列—渲染模塊》。

2.1 多線程渲染

一般情況下,在單線程渲染的流程中,在游戲每一幀運(yùn)行過程中,主線程(CPU1)先執(zhí)行Update,在這里做大量的邏輯更新,例如游戲AI、碰撞檢測和動(dòng)畫更新等;然后執(zhí)行Render,在這里做渲染相關(guān)的指令調(diào)用。在渲染時(shí),主線程需要調(diào)用圖形API更新渲染狀態(tài),例如設(shè)置Shader、紋理、矩陣和Alpha融合等,然后再執(zhí)行DrawCall,所有的這些圖形API調(diào)用都是與驅(qū)動(dòng)層交互的,而驅(qū)動(dòng)層維護(hù)著所有的渲染狀態(tài),這些API的調(diào)用有可能會(huì)觸發(fā)驅(qū)動(dòng)層的渲染狀態(tài)地改變,從而發(fā)生卡頓。由于驅(qū)動(dòng)層的狀態(tài)對于上層調(diào)用是透明的,因此卡頓是否會(huì)發(fā)生以及卡頓發(fā)生的時(shí)間長短對于API的調(diào)用者(CPU1)來說都是未知的。而此時(shí)其它CPU有可能處于空閑等待的狀態(tài),從而造成浪費(fèi)。因此可以將渲染部分抽離出來,放到其它的CPU中,形成單獨(dú)的渲染線程,與邏輯線程同時(shí)進(jìn)行,以減少主線程卡頓。

其大致的實(shí)現(xiàn)流程是,在主線程中調(diào)用的圖形API被封裝成命令,提交到渲染隊(duì)列,這樣就可以節(jié)省在主線程中調(diào)用圖形API的開銷,從而提高幀率;渲染線程從渲染隊(duì)列獲取渲染指令并執(zhí)行調(diào)用圖形API與驅(qū)動(dòng)層交互,這部分交互耗時(shí)從主線程轉(zhuǎn)到渲染線程。

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

對于正常開啟了多線程渲染的項(xiàng)目,Gfx.WaitForPresent的耗時(shí)走向也有相當(dāng)?shù)膮⒖家饬x。測試中局部的GPU壓力越大,CPU等待GPU完成工作的時(shí)間也就越長,Gfx.WaitForPresent的耗時(shí)也就越高。所以,當(dāng)Gfx.WaitForPresent存在數(shù)十甚至上百毫秒地持續(xù)耗時(shí)時(shí),說明對應(yīng)場景的GPU壓力較大。

另外,根據(jù)UWA的大量項(xiàng)目和測試經(jīng)驗(yàn),GPU壓力過大也會(huì)使得渲染模塊CPU端的主函數(shù)耗時(shí)(Camera.Render和RenderPipelineManager.DoRenderLoop_Internal)整體相應(yīng)上升。我們會(huì)在最后專門討論GPU部分的優(yōu)化。

2.2 同屏渲染面片數(shù)

影響渲染效率的兩個(gè)最基本的參數(shù)無疑就是Triangle和DrawCall。

通常情況下,Triangle面片數(shù)和GPU渲染耗時(shí)是成正比的,而對于大部分項(xiàng)目來說,不透明Triangle數(shù)量又往往遠(yuǎn)比半透明Triangle要多,尤其需要關(guān)注。UWA一般建議在低端機(jī)型上將同屏渲染面片數(shù)控制在25萬面以內(nèi),即便是高端機(jī)也不建議超過60萬面。當(dāng)使用工具發(fā)現(xiàn)局部同屏渲染面片數(shù)過高后,可以結(jié)合Frame Debugger對重點(diǎn)幀的渲染物體進(jìn)行排查。

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

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

2.3 Batch(DrawCall)

在Unity中,我們需要區(qū)分DrawCall和Batch。在一個(gè)Batch中會(huì)存在有多個(gè)DrawCall,出現(xiàn)這種情況時(shí)我們往往更關(guān)心Batch的數(shù)量,因?yàn)樗攀前唁秩緮?shù)據(jù)提交給GPU的單位,也是我們需要優(yōu)化和控制數(shù)量的真正對象。

降低Batch的方式通常有動(dòng)態(tài)合批、靜態(tài)合批、SRP Batcher和GPU Instancing這四種,圍繞Batch優(yōu)化的討論較為復(fù)雜,再寫一篇文章也不為過,所以本文不再展開來討論,但在UWA DAY 2020中我們詳細(xì)討論和分享了DrawCall與Batch的關(guān)系以及這4種Batching的使用詳解,供大家參考:《Unity移動(dòng)游戲項(xiàng)目優(yōu)化案例分析(上)》。

下面簡單總結(jié)靜態(tài)合批、SRP Batcher和GPU Instancing的合批條件和優(yōu)缺點(diǎn)。

(1)靜態(tài)合批

條件:不同Mesh,只要使用相同的材質(zhì)球即可。

優(yōu)點(diǎn):節(jié)省頂點(diǎn)信息地綁定;節(jié)省幾何信息地傳遞;相鄰材質(zhì)相同時(shí), ,節(jié)省材質(zhì)地傳遞。

缺點(diǎn):離線合并時(shí),若合并的Mesh中存在重復(fù)資源,則容易使得合并后包體變大;運(yùn)行時(shí)合并,則生成Combine Mesh的過程會(huì)造成CPU短時(shí)間峰值;同樣的,若合并的Mesh中存在重復(fù)資源,則會(huì)使得合并后內(nèi)存占用變大。

(2)SRP Batcher

條件:不同Mesh,只要使用相同的Shader且變體一樣即可。

優(yōu)點(diǎn):節(jié)省Uniform Buffer的寫入操作;按Shader分Batch,預(yù)先生成Uniform Buffer,Batch內(nèi)部無CPU Write。

缺點(diǎn):Constant Buffer(CBuffer)的顯存固定開銷;不支持MaterialPropertyBlock。

(3)GPU Instancing

條件:相同的Mesh,且使用相同的材質(zhì)球。

優(yōu)點(diǎn):適用于渲染同種大量怪物的需求,合批的同時(shí)能夠降低動(dòng)畫模塊的耗時(shí)。

缺點(diǎn):可能存在負(fù)優(yōu)化,反而使DrawCall上升;Instancing有時(shí)候被打亂,可以自己分組用API渲染。

2.4 Shader.CreateGPUProgram

該API常常在渲染模塊主函數(shù)的堆棧中出現(xiàn),并造成渲染模塊中的大多數(shù)函數(shù)峰值。它是Shader第一次渲染時(shí)產(chǎn)生的耗時(shí),其耗時(shí)與渲染Shader的復(fù)雜程度相關(guān)。當(dāng)它在游戲過程中被調(diào)用并且造成較高的耗時(shí)峰值時(shí)應(yīng)引起注意。

對此,我們可以將Shader通過ShaderVariantCollection收集要用到的變體并進(jìn)行AssetBundle打包。在將該ShaderVariantCollection資源加載進(jìn)內(nèi)存后,通過在游戲前期場景調(diào)用ShaderVariantCollection.WarmUp來觸發(fā)Shader.CreateGPUProgram,并將此SVC進(jìn)行緩存,從而避免在游戲運(yùn)行時(shí)觸發(fā)此API的調(diào)用、避免局部的CPU高耗時(shí)。

然而即便是已經(jīng)做過以上操作的項(xiàng)目也常會(huì)檢測到運(yùn)行時(shí)偶爾的該API耗時(shí)峰值,說明存在一些“漏網(wǎng)之魚”。開發(fā)者可以結(jié)合Profiler的Timeline模式,選中觸發(fā)調(diào)用Shader.CreateGPUProgram的幀來查看具體是哪些Shader觸發(fā)了該API,可以參考《一種Shader變體收集和打包編譯優(yōu)化的思路》。

2.5 Culling

絕大多數(shù)情況下,Culling本身耗時(shí)并不顯眼,它的意義在于反映一些與渲染相關(guān)的問題。

(1)相機(jī)數(shù)量多

當(dāng)渲染模塊主函數(shù)的堆棧中Culling耗時(shí)的占比比較高(一般項(xiàng)目中在10%-20%左右)。

(2)場景中小物件多

Culling耗時(shí)與場景中的GameObject小物件數(shù)量的相關(guān)性比較大。這種情況建議研發(fā)團(tuán)隊(duì)優(yōu)化場景制作方式 ,關(guān)注場景中是否存在過多小物件,導(dǎo)致Culling耗時(shí)增高??梢钥紤]采用動(dòng)態(tài)加載、分塊顯示,或者Culling Group、Culling Distance等方法優(yōu)化Culling的耗時(shí)。

(3)Occlusion Culling

如果項(xiàng)目使用了多線程渲染且開啟了Occlusion Culling,通常會(huì)導(dǎo)致子線程的壓力過大而使整體Culling過高。

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

(4)包圍盒更新

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

(5)PostProcessingLayer.OnPreCull/WaterReflection.OnWillRenderObject

PostProcessLayer.OnPreCull這一方法和項(xiàng)目中使用的PostProcessing Stack相關(guān)。可以在PostProcessManager.cs中添加靜態(tài)變量GlobalNeedUpdateSettings,在切場景的時(shí)候通過設(shè)置PostProcessManager.GlobalNeedUpdateSettings為true來UpdateSettings。這樣就可以避免每幀都做UpdateSettings操作,從而減少一部分耗時(shí)。

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

3. UI模塊

在Unity引擎中,主流的UI框架有UGUI、NGUI以及使用越來越多的FairyGUI。本文主要從使用最多的UGUI來進(jìn)行說明。圍繞UGUI相關(guān)優(yōu)化更全面的內(nèi)容可以參考《Unity性能優(yōu)化 — UI模塊》。

3.1 UGUI EventSystem.Update

EventSystem.Update函數(shù)為UGUI的事件系統(tǒng)耗時(shí),其耗時(shí)偏高時(shí)主要關(guān)注以下兩個(gè)因素:

(1)觸發(fā)調(diào)用耗時(shí)高

作為UGUI事件系統(tǒng)的主函數(shù),該函數(shù)主要是在觸摸釋放時(shí)觸發(fā),當(dāng)本身有較高的CPU開銷時(shí),通常都是因?yàn)檎{(diào)用了其它較為耗時(shí)的函數(shù)引起。因此需要通過添加Profiler.BeginSample/EndSample打點(diǎn)或者GOT Online服務(wù)+UWA API打點(diǎn)來對所觸發(fā)的邏輯進(jìn)行進(jìn)一步地檢測,從而排查出具體是哪一個(gè)子函數(shù)或者代碼段造成的高耗時(shí)。

(2)輪詢耗時(shí)高

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

3.2 UGUI Canvas.SendWillRenderCanvases

Canvas.SendWillRenderCanvases函數(shù)的耗時(shí)代表的是UI元素自身變化帶來的更新耗時(shí),這是需要和Canvas.BuildBatch(見下文)的網(wǎng)格重建的耗時(shí)所區(qū)分的。

持續(xù)的高耗時(shí)往往是由于UI元素過于復(fù)雜且更新過于頻繁造成。UI元素的自身更新包括:替換圖片、文本或顏色發(fā)生變化等等。UI元素發(fā)生位移、旋轉(zhuǎn)或者縮放并不會(huì)引起該函數(shù)有開銷。該函數(shù)的耗時(shí)取決于UI元素發(fā)生更新的數(shù)量以及UI元素的復(fù)雜度,因此要優(yōu)化此函數(shù)的開銷通??梢詮娜缦聨c(diǎn)著手:

(1)降低頻繁更新的UI元素的頻率

比如小地圖的怪物標(biāo)記、角色或者怪物的血條等,可以控制邏輯在變動(dòng)超過某個(gè)閾值時(shí)才更新UI的顯示,再比如技能CD效果,傷害飄字等控制隔幀更新。

(2)盡量讓復(fù)雜的UI不要發(fā)生變動(dòng)

如某些字符串特別多且又使用了Rich Text、Outline或者Shadow效果的Text,Image Type為Tiled的Image等。這些UI元素因?yàn)轫旤c(diǎn)數(shù)量非常多,一旦更新便會(huì)有較高的耗時(shí)。如果某些效果需要使用Outline或者Shadowmap,但是卻又頻繁的變動(dòng),如飄動(dòng)的傷害數(shù)字,可以考慮將其做成固定的美術(shù)字,這樣頂點(diǎn)數(shù)量就不會(huì)翻N倍。

(3)關(guān)注Font.CacheFontForText

該函數(shù)往往會(huì)造成一些耗時(shí)峰值。該API主要是生成動(dòng)態(tài)字體Font Texture的開銷,在運(yùn)行時(shí)突發(fā)高耗時(shí),很有可能是一次性寫入很多新的字符,導(dǎo)致Font Texture紋理擴(kuò)容。可以從減少字體種類、減少字體字號、提前顯示常用字以擴(kuò)充動(dòng)態(tài)字體FontTexture等方式去優(yōu)化這一項(xiàng)的耗時(shí)。

3.3 UGUI Canvas.BuildBatch

Canvas.BuildBatch為UI元素合并的Mesh需要改變時(shí)所產(chǎn)生的調(diào)用。通常之前所提到的Canvas.SendWillRenderCanvases()的調(diào)用都會(huì)引起Canvas.BuildBatch的調(diào)用。另外,Canvas中的UI元素發(fā)生移動(dòng)也會(huì)引起Canvas.BuildBatch的調(diào)用。

Canvas.BuildBatch是在主線程發(fā)起UI網(wǎng)格合并,具體的合并過程是在子線程中處理的,當(dāng)子線程壓力過大,或者合并的UI網(wǎng)格過于復(fù)雜的時(shí)候,會(huì)在主線程產(chǎn)生等待,等待的耗時(shí)會(huì)被統(tǒng)計(jì)到EmitWorldScreenspaceCameraGeometry中。

這兩個(gè)函數(shù)產(chǎn)生高耗時(shí),說明發(fā)生重建的Canvas非常復(fù)雜,此時(shí)需要將Canvas進(jìn)行細(xì)分處理,通常是將靜態(tài)的元素放在一個(gè)Canvas中,將發(fā)生更新的UI元素放入一個(gè)Canvas中,這樣靜態(tài)的Canvas由于緩存不會(huì)發(fā)生網(wǎng)格更新,從而降低網(wǎng)格更新的復(fù)雜度,減少網(wǎng)格重建的耗時(shí)。

3.4 UGUI CanvasRenderer.SyncTransform

我們常注意到有些項(xiàng)目的部分幀中CanvasRenderer.SyncTransform調(diào)用頻繁。如下圖,CanvasRenderer.SyncTransform調(diào)用次數(shù)多達(dá)1017次。當(dāng)Canvas.SyncTransform觸發(fā)次數(shù)非常頻繁時(shí),會(huì)導(dǎo)致它的父節(jié)點(diǎn)UGUI.Rendering.UpdateBathes產(chǎn)生非常高的耗時(shí)。

在Unity 2018版本及以后的版本中,Canvas下某個(gè)UI元素調(diào)用SetActive(false改成true)會(huì)導(dǎo)致該Canvas下的其它UI元素觸發(fā)SyncTransform,從而導(dǎo)致UI更新的整體開銷上升,在Unity 2017的版本中只會(huì)導(dǎo)致該UI元素本身觸發(fā)SyncTransform。

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

3.5 UGUI UI DrawCall

通常戰(zhàn)斗場景中其它模塊耗時(shí)壓力大,此時(shí)UI模塊更要仔細(xì)控制性能開銷。一般而言,戰(zhàn)斗場景中的UI DrawCall控制到40-50左右為最佳。

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

在UGUI的制作過程中,建議關(guān)注以下幾點(diǎn):

(1)同一Canvas下的UI元素才能合批。不同Canvas即使Order in Layer相同也不合批,所以UI的合理規(guī)劃和制作非常重要;

(2)盡量整合并制作圖集,從而使得不同UI元素的材質(zhì)圖集一致。圖集中的按鈕、圖標(biāo)等需要使用圖片的比較小的UI元素,完全可以整合并制作圖集。當(dāng)它們密集地同時(shí)出現(xiàn)時(shí),就有效降低了DrawCall;

(3)在同一Canvas下、且材質(zhì)和圖集一致的前提下,避免層級穿插。簡單概括就是,應(yīng)使得符合合批條件的UI元素的“層級深度”相同;

(4)將相關(guān)UI的Pos Z盡量統(tǒng)一設(shè)置為0,Z值不為0的UI元素只能與Hierarchy中相鄰元素嘗試合批,所以容易打斷合批。

(5)對于Alpha為0的Image,需要勾選其CanvasRender組件上的Cull Transparent Mesh選項(xiàng),否則依然會(huì)產(chǎn)生DrawCall且容易打斷合批。

4. 物理模塊

圍繞物理模塊相關(guān)優(yōu)化更全面的內(nèi)容可以參考《Unity性能優(yōu)化 — 物理模塊》

4.1 Auto Simulation

在Unity 2017.4版本之后,物理模擬的設(shè)置選項(xiàng)Auto Simulation被開放并且默認(rèn)開啟,即項(xiàng)目過程中總是默認(rèn)進(jìn)行著物理模擬。但在一些情況下,這部分的耗時(shí)是浪費(fèi)的。

判斷物理模擬耗時(shí)是否被浪費(fèi)的一個(gè)標(biāo)準(zhǔn)就是Contacts數(shù)量,即游戲運(yùn)行時(shí)碰撞對數(shù)量。一般來說,碰撞對的數(shù)量越多,則物理系統(tǒng)的CPU耗時(shí)越大。但在很多移動(dòng)端項(xiàng)目中,我們都檢測到在整個(gè)游戲過程中Contacts數(shù)量始終為0。

在這種情況下,開發(fā)者可以關(guān)閉物理的自動(dòng)模擬來進(jìn)行測試。如果關(guān)閉Auto Simulation并不會(huì)對游戲邏輯產(chǎn)生任何影響,在游戲過程中依然可以進(jìn)行很好地對話、戰(zhàn)斗等,則說明可以節(jié)省這方面的耗時(shí)。同時(shí)也需要說明的是,如果項(xiàng)目需要使用射線檢測,那么在關(guān)閉Auto Simulation后需要開啟Auto Sync Transforms,來保證射線檢測可以正常作用。

4.2 物理更新次數(shù)

Unity物理模擬過程的主要耗時(shí)函數(shù)是在FixedUpdate中的,也就是說,當(dāng)每幀該函數(shù)調(diào)用次數(shù)越高、物理更新次數(shù)也就越頻繁,每幀的耗時(shí)也就相應(yīng)地高。

物理更新次數(shù),或者說FixedUpdate的每幀調(diào)用次數(shù),是和Unity Project Settings的Time設(shè)置中最小更新間隔(Fixed Timestep)以及最大允許時(shí)間(Maximum Allowed Timestep)相關(guān)的。這里我們需要先知道物理系統(tǒng)本身的特性,即當(dāng)游戲上一幀卡頓時(shí),Unity會(huì)在當(dāng)前幀非??壳暗碾A段連續(xù)調(diào)用N次FixedUpdate.PhysicsFixedUpdate,Maximum Allowed Timestep的意義就在于限制物理更新的次數(shù)。它決定了單幀物理最大調(diào)用次數(shù),該值越小,單幀物理最大調(diào)用次數(shù)越少?,F(xiàn)在設(shè)置這兩個(gè)值分別為20ms和100ms,那么當(dāng)某一幀耗時(shí)30ms時(shí),物理更新只會(huì)執(zhí)行1次;耗時(shí)200ms時(shí)也只會(huì)執(zhí)行5次。

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

4.3 Contacts

就像上面提到的,如果我們確實(shí)用到物理模擬,則一般碰撞對的數(shù)量越多,物理系統(tǒng)的CPU耗時(shí)也就越大。所以,嚴(yán)格控制碰撞對數(shù)量對于降低物理模塊耗時(shí)非常重要。

首先,很多項(xiàng)目中可能存在一些不必要的Rigidbody組件,在開發(fā)者不知情的地方造成了不必要的碰撞,從而產(chǎn)生了耗時(shí)浪費(fèi);另外,可以檢查修改Project Settings的Physics設(shè)置中的Layer Collision Matrix,取消不必要的層之間的碰撞檢測,將Contacts數(shù)量盡可能降低。

5. 動(dòng)畫模塊

圍繞動(dòng)畫模塊相關(guān)優(yōu)化更全面的內(nèi)容可以參考《Unity性能優(yōu)化 — 動(dòng)畫模塊》

5.1 Mecanim動(dòng)畫系統(tǒng)

Mechanic動(dòng)畫系統(tǒng)是Unity公司從Unity 4.0之后開始引入的新版動(dòng)畫系統(tǒng)(使用Animator控制動(dòng)畫),相比于Legacy的Animation控制系統(tǒng),在功能上,Mecanim動(dòng)畫系統(tǒng)主要有以下幾點(diǎn)優(yōu)勢:

(1)針對人形角色提供了一套特殊的工作流,包括Avatar的創(chuàng)建以及Muscles肌肉的調(diào)節(jié);

(2)動(dòng)畫重定向(Retarting)的能力,可以非常方便地把一個(gè)動(dòng)畫從一個(gè)角色模型應(yīng)用到其他角色模型上;

(3)提供了可視化的Animator編輯器,可以快捷預(yù)覽和創(chuàng)建動(dòng)畫片段;

(4)更加方便地創(chuàng)建狀態(tài)機(jī)以及狀態(tài)之間Transition的轉(zhuǎn)換;

(5)便于操作的混合樹功能。

在性能上,對于骨骼動(dòng)畫且曲線較多的動(dòng)畫,使用Animator的性能是要比Animation要好的,因?yàn)锳nimator是支持多線程計(jì)算的,而且Animator可以通過開啟Optimized GameObjects進(jìn)行優(yōu)化,具體細(xì)節(jié)可以參考UWA學(xué)堂的課程《Unity移動(dòng)游戲中動(dòng)畫系統(tǒng)的性能優(yōu)化》。相反,對于比較簡單的類似于移動(dòng)旋轉(zhuǎn)這樣的動(dòng)畫,使用Animation控制則比Animator要高效一些。

5.2 BakeMesh

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

5.3 Active Animator數(shù)量

Active狀態(tài)的Animator個(gè)數(shù)會(huì)極大地影響動(dòng)畫模塊的耗時(shí),而且是一個(gè)可量化的重要標(biāo)準(zhǔn),控制其數(shù)量到一個(gè)相對合理的值是我們優(yōu)化動(dòng)畫模塊的重要手段。需要開發(fā)者結(jié)合畫面排查對應(yīng)的數(shù)量是否合理。

(1)Animator Culling Mode

控制Active Animator的一個(gè)方法是針對每個(gè)動(dòng)畫組件調(diào)整合理的Animator.CullingMode設(shè)置。該項(xiàng)設(shè)置一共有三個(gè)選項(xiàng):AlwaysAnimate、CullUpdateTransforms和CullComplete。

默認(rèn)的AlwaysAnimate使得當(dāng)前物體不管是不是在視域體內(nèi),或者在視域體被LOD Culling掉了,Animator的所有東西都仍然更新;其中,UI動(dòng)畫一定要選AlwaysAnimate,不然會(huì)出現(xiàn)異常表現(xiàn)。

而設(shè)置為CullUpdateTransforms時(shí),當(dāng)物體不在視域體內(nèi),或者被LOD Culling掉后,邏輯繼續(xù)更新,就表示狀態(tài)機(jī)是更新的,動(dòng)畫資源中連線的條件等等也都是會(huì)更新和判斷的;但是Retarget、IK和從C++回傳Transform這些顯示層的更新就不做了。所以,在不影響表現(xiàn)的前提下把部分動(dòng)畫組件嘗試設(shè)置成CullUpdateTransforms可以節(jié)省物體不可見時(shí)動(dòng)畫模塊的顯示層耗時(shí)。

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

(2)DOTween插件

很多時(shí)候,UI動(dòng)畫也會(huì)貢獻(xiàn)大量的Active Animator。針對一些簡單的UI動(dòng)畫,如改變顏色、縮放、移動(dòng)等效果,UWA建議改用DOTween制作。經(jīng)測試,性能比原生的UI動(dòng)畫要好得多。

5.4 開啟Apply Root Motion的Animator數(shù)量

在Animators.Update的堆棧中,有時(shí)會(huì)看到Animator.ApplyBuiltinRootMotion占比過高,這一項(xiàng)通常和項(xiàng)目中開啟了Apply Root Motion的模型動(dòng)畫相關(guān)。如果其動(dòng)畫不需要產(chǎn)生位移,則不必開啟此選項(xiàng)。

5.5 Animator.Initialize

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

5.6 Meshskinning.Update和Animators.WriteJob

網(wǎng)格資源對于動(dòng)畫模塊耗時(shí)的影響是十分顯著的。

一方面,Meshskinning.Update耗時(shí)較高時(shí)。主要因素為蒙皮網(wǎng)格的骨骼數(shù)和面片數(shù)偏高,所以可以針對網(wǎng)格資源進(jìn)行減面和LOD分級。

另一方面,默認(rèn)設(shè)置下,我們經(jīng)常發(fā)現(xiàn)很多項(xiàng)目中角色的骨骼節(jié)點(diǎn)的Transform一直都是在場景中存在的,這樣在Native層計(jì)算完它們的Transform后,會(huì)回傳給C#層,從而產(chǎn)生一定的耗時(shí)。

在場景中角色數(shù)量較多,骨骼節(jié)點(diǎn)的回傳會(huì)產(chǎn)生一定的開銷,體現(xiàn)在動(dòng)畫模塊的主函數(shù)之一PreLateUpdate.DirectorUpdateAnimationEnd的Animators.WriteJob子函數(shù)上。

對此開發(fā)者可以考慮勾選FBX資源中Rig頁簽下的Optimize Game Objects設(shè)置項(xiàng),將骨骼節(jié)點(diǎn)“隱藏”,從而減少這部分的耗時(shí)。

5.7 GPU Skinning/Compute Skinning

特別地,對于Unity引擎原生的GPU Skinning設(shè)置項(xiàng)(新版Unity中為Compute Skinning),理論上會(huì)在一定程度上改變網(wǎng)格和動(dòng)畫的更新方法以優(yōu)化對骨骼動(dòng)畫的處理,但從針對移動(dòng)平臺(tái)的多項(xiàng)測試結(jié)果來看,無論是在iOS還是安卓平臺(tái)上,多個(gè)Unity版本提供的GPU Skinning對性能的提升效果都不明顯,甚至存在負(fù)優(yōu)化的現(xiàn)象。在Unity的迭代中已對其逐步優(yōu)化,將相關(guān)操作放到渲染線程中進(jìn)行,但其實(shí)用性還需要進(jìn)一步考察。

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

6. 粒子系統(tǒng)

圍繞粒子系統(tǒng)相關(guān)優(yōu)化更全面的內(nèi)容可以參考《粒子系統(tǒng)優(yōu)化——如何優(yōu)化你的技能特效》。

6.1 Playing粒子系統(tǒng)數(shù)量

UWA統(tǒng)計(jì)了粒子系統(tǒng)數(shù)量和Playing狀態(tài)的粒子系統(tǒng)數(shù)量。前者是指內(nèi)存中所有的ParticleSystem的總數(shù)量,包含正在播放的和處于緩存池中的;后者指的是正在播放的ParticleSystem組件的數(shù)量,這個(gè)包含了屏幕內(nèi)和屏幕外的,我們建議在一幀中出現(xiàn)的數(shù)量峰值不超過50(1GB機(jī)型)。

針對這兩個(gè)數(shù)值,我們一方面關(guān)注粒子系統(tǒng)數(shù)量峰值是否偏高,可選中某一峰值幀查看到底是哪些粒子系統(tǒng)緩存著、是否都合理、是否有過度緩存的現(xiàn)象;另一方面關(guān)注Playing數(shù)量峰值是否偏高,可選中某一峰值幀查看到底是哪些粒子系統(tǒng)在播放、是否都合理、是否能做些制作上的優(yōu)化(具體見下文GPU部分中的討論)。

6.2 Prewarm

ParticleSystem.Prewarm的耗時(shí)有時(shí)也需要關(guān)注。當(dāng)有粒子系統(tǒng)開啟了Prewarm選項(xiàng),其在場景中實(shí)例化或者由Deactive轉(zhuǎn)為Active時(shí),會(huì)立即執(zhí)行一次完整的模擬。

但Prewarm的操作通常都有一定的耗時(shí),經(jīng)測試,大量開啟Prewarm的粒子系統(tǒng)同時(shí)SetActive時(shí)會(huì)造成耗時(shí)峰值。建議在不必要的情況下,將其關(guān)閉。

7. 加載模塊

圍繞加載模塊相關(guān)優(yōu)化更全面的內(nèi)容可以參考《Unity性能優(yōu)化系列—加載與資源管理》。

7.1 Shader加載

(1)Shader.Parse

Shader.Parse是指Shader加載進(jìn)行解析的操作,如果此操作較為頻繁,通常是由于Shader的重復(fù)加載導(dǎo)致的,這里的重復(fù)可以理解為2層意思。

第一層是由于Shader的冗余導(dǎo)致的,通常是因?yàn)榇虬麬ssetBundle的時(shí)候,Shader被被動(dòng)打進(jìn)了多個(gè)不同的AssetBundle中而沒有進(jìn)行依賴打包,這樣當(dāng)這些AssetBundle中的資源進(jìn)行加載的時(shí)候,會(huì)被動(dòng)加載這些Shader,就進(jìn)行了多次“重復(fù)的”Shader.Parse,所以同一種Shader就在內(nèi)存中有多份了,這就是冗余了。

要去除這種冗余的方法也很簡單,就是把這些會(huì)冗余的Shader依賴打包進(jìn)一個(gè)公共的AssetBundle包。這樣就會(huì)主動(dòng)打包了,而不是被動(dòng)進(jìn)入某些使用了這個(gè)Shader的包體中。如果對這個(gè)Shader進(jìn)行了主動(dòng)打包,那么其它使用了這個(gè)Shader的AssetBundle中就只會(huì)對這個(gè)Shader打出來的公共AssetBundle進(jìn)行引用,這樣在內(nèi)存中就只有一份Shader,其它用到這個(gè)Shader的時(shí)候就直接引用它,而不需要多次進(jìn)行Shader.Parse了。

第二層意思是同一個(gè)Shader多次地加載卸載,沒有緩存住導(dǎo)致的。假設(shè)AssetBundle進(jìn)行了主動(dòng)打包,生成了公共的AssetBundle,這樣在內(nèi)存中只有這一份Shader,但是因?yàn)檫@個(gè)Shader加載完后(也就是Shader.Parse)沒有進(jìn)行緩存,用完馬上被卸載了。下次再用到這個(gè)Shader的時(shí)候,內(nèi)存里沒有這個(gè)Shader了,那就必須再重新加載進(jìn)來,這樣同樣的一個(gè)Shader加載解析了多次,就造成了多次的Shader.Parse。一般而言,經(jīng)過變體優(yōu)化以后的開發(fā)者自己寫的Shader內(nèi)存占用都不高,可以統(tǒng)一在游戲開始時(shí)加載并緩存。

特別地,對于Unity內(nèi)置的Shader,只要是變體數(shù)量不多的,可以放進(jìn)Project Settings中的Always Included中去,從而避免這一類Shader的冗余和重復(fù)解析。

(2)Shader.CreateGPUProgram

該API也會(huì)在加載模塊主函數(shù)甚至UI模塊、邏輯代碼的堆棧中出現(xiàn)。相關(guān)的討論上文已經(jīng)涉及,優(yōu)化方法相同,不再贅述。

7.2 Resources.UnloadUnusedAssets

該API會(huì)在場景切換時(shí)被Unity自動(dòng)調(diào)用,一般單次調(diào)用耗時(shí)較高,通常情況下不建議手動(dòng)調(diào)用。

但在部分不進(jìn)行場景切換或用Additive加載場景的項(xiàng)目中,不會(huì)調(diào)用該API,從而使得項(xiàng)目整體資源數(shù)量和內(nèi)存有上升趨勢。對于這種情況則可以考慮每5-10min手動(dòng)調(diào)用一次。

Resources.UnloadUnusedAssets的底層運(yùn)作機(jī)理是,對于每個(gè)資源,遍歷所有Hierarchy Tree中的GameObject結(jié)點(diǎn),以及堆內(nèi)存中的對象,檢測該資源是否被某個(gè)GameObject或?qū)ο螅ńM件)所使用,如果全部都沒有使用,則引擎才會(huì)認(rèn)定其為Unused資源,進(jìn)而進(jìn)行卸載操作。簡單來講,Resources.UnloadUnusedAssets的單次耗時(shí)大致隨著((GameObject數(shù)量+Mono對象數(shù)量)*Asset數(shù)量)的乘積變大而變大。

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

(1)Resources.UnloadAsset/AssetBundle.Unload(True)

研發(fā)團(tuán)隊(duì)可嘗試在游戲運(yùn)行時(shí),通過Resources.UnloadAsset/AssetBundle.Unload(True)來去除已經(jīng)確定不再使用的某一資源,這兩個(gè)API的效率很高,同時(shí)也可以降低Resources.UnloadUnusedAssets統(tǒng)一處理時(shí)的壓力,進(jìn)而減少切換場景時(shí)該API的耗時(shí);

(2)嚴(yán)格控制場景中材質(zhì)資源和粒子系統(tǒng)的使用數(shù)量。

專門提到這兩種資源,因?yàn)樵诖蠖鄶?shù)項(xiàng)目中,雖然它們的內(nèi)存占用一般不是大頭,但往往資源數(shù)量遠(yuǎn)高于其他類型的資源,很容易達(dá)到數(shù)千的數(shù)量級,從而對單次Resources.UnloadUnusedAssets耗時(shí)有較大貢獻(xiàn)。

(3)降低駐留的堆內(nèi)存。

堆內(nèi)存中的對象數(shù)量同樣會(huì)顯著影響Resources.UnloadUnusedAssets的耗時(shí),這在上文也已經(jīng)討論過。

7.3 加載AssetBundle

使用AssetBundle加載資源是目前移動(dòng)端項(xiàng)目中比較普遍的做法。

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

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

在桌面獨(dú)立平臺(tái)、控制臺(tái)和移動(dòng)平臺(tái)上,API將只加載AssetBundle的頭部,并將剩余的數(shù)據(jù)留在磁盤上。AssetBundle的Objects會(huì)按需加載,比如:加載方法(例如:AssetBundle.Load)被調(diào)用或其InstanceID被間接引用的時(shí)候。在這種情況下,不會(huì)消耗過多的內(nèi)存。

但在Editor環(huán)境下,API還是會(huì)把整個(gè)AssetBundle加載到內(nèi)存中,就像讀取磁盤上的字節(jié)和使用AssetBundle.LoadFromMemoryAsync一樣。如果在Editor中對項(xiàng)目進(jìn)行了分析,此API可能會(huì)導(dǎo)致在AssetBundle加載期間出現(xiàn)內(nèi)存尖峰。但這不應(yīng)影響設(shè)備上的性能,在做優(yōu)化之前,這些尖峰應(yīng)該在設(shè)備上重新再測試一遍。

要注意,這個(gè)API只針對未壓縮或LZ4壓縮格式,因?yàn)槿绻褂肔ZMA壓縮,它是針對整個(gè)生成后的數(shù)據(jù)包進(jìn)行壓縮的,所以在未解壓之前是無法拿到AssetBundle的頭信息的。

由于LoadFromMemory的加載效率相較其他的接口而言,耗時(shí)明顯增大,因此我們不建議大規(guī)模使用,而且堆內(nèi)存會(huì)變大。如果確實(shí)有對AssetBundle文件加密的需求,可以考慮僅對重要的配置文件、代碼等進(jìn)行加密,對紋理、網(wǎng)格等資源文件則無需進(jìn)行加密。因?yàn)槟壳笆忻嫔弦呀?jīng)存在一些工具可以從更底層的方式來獲取和導(dǎo)出渲染相關(guān)的資源,如紋理、網(wǎng)格等,因此,對于這部分的資源加密并不是十分的必要性。

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

7.4 加載資源

有關(guān)加載資源所造成的耗時(shí),若加載策略比較合理,則一般發(fā)生在游戲一開始和場景切換時(shí),往往不會(huì)造成嚴(yán)重的性能瓶頸。但不排除一些情況需要予以關(guān)注,那么可以把資源加載耗時(shí)的排序作為依據(jù)進(jìn)行排查。

對于單次加載耗時(shí)過高的資源,比如達(dá)到數(shù)百毫秒甚至幾秒時(shí),就應(yīng)考察這類資源是否過于復(fù)雜,從制作上考慮予以精簡。

對于反復(fù)頻繁加載且耗時(shí)不低的資源,則應(yīng)該在第一次加載后予以緩存,避免重復(fù)加載造成的開銷。

值得一提的是,在Unity的異步加載中有時(shí)會(huì)出現(xiàn)每幀進(jìn)行加載所能占用的最高耗時(shí)被限制,但主線程中卻在空轉(zhuǎn)的現(xiàn)象。尤其是在切場景的時(shí)候集中進(jìn)行異步加載,有時(shí)會(huì)耗費(fèi)幾十甚至數(shù)十秒的時(shí)間,但其中大部分時(shí)間是被空轉(zhuǎn)浪費(fèi)的。這是因?yàn)榭刂飘惒郊虞d每幀最高耗時(shí)的API Application.backgroundLoadingPriority默認(rèn)值為BelowNormal,每幀最多只加載4ms。此時(shí)一般建議把該值調(diào)為High,即最多50ms每幀。

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

7.5 實(shí)例化和銷毀

實(shí)例化同樣主要存在單個(gè)資源實(shí)例化耗時(shí)過高或某個(gè)資源反復(fù)頻繁實(shí)例化的現(xiàn)象。根據(jù)耗時(shí)多少排列后,針對疑似有問題的資源,前者考慮簡化,或者可以考慮分幀操作,比如對于一個(gè)較為復(fù)雜的UI Prefab,可以考慮改為先實(shí)例化顯眼的、重要的界面和按鈕,而翻頁后的內(nèi)容、裝飾圖標(biāo)等再進(jìn)行實(shí)例化;后者則建立緩存池,使用顯隱操作來代替頻繁的實(shí)例化。

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

7.6 激活和隱藏

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

在UWA GOT Online Resource模式下的資源管理頁面中可以排查激活隱藏操作較頻繁的資源,從而排查和優(yōu)化相關(guān)邏輯和調(diào)用。

8. 邏輯代碼

邏輯代碼的CPU耗時(shí)優(yōu)化更多是結(jié)合項(xiàng)目實(shí)際需求、考驗(yàn)程序員本人的過程,很難定量定性進(jìn)行討論。不過UWA SDK中提供了方便開發(fā)者在邏輯代碼中進(jìn)行打點(diǎn)的API&UWA GOT Online,從而將復(fù)雜的函數(shù)拆解開,在報(bào)告中排查堆棧耗時(shí)、更快速地驗(yàn)證優(yōu)化效果。

我們發(fā)現(xiàn)有越來越的團(tuán)隊(duì)在使用JobSystem將主線程中的部分邏輯代碼放入子線程中來進(jìn)行處理,對于可以并行運(yùn)算的邏輯,非常推薦將其放入到子線程中來處理,這樣可以有效降低主線程CPU處理邏輯運(yùn)算的壓力。

9. Lua

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

重申:Lua報(bào)告中出現(xiàn)的函數(shù)名稱格式為:函數(shù)名稱@文件名:行號。

可以通過報(bào)告提供的Lua文件名/行號/函數(shù)名來定位CPU耗時(shí)的瓶頸函數(shù)和CPU耗時(shí)峰值的具體原因。Lua函數(shù)的命名格式為X@Y:Z,其中X是其函數(shù)名,在無法獲取時(shí),X會(huì)變?yōu)槟J(rèn)的unknown;Y是該函數(shù)定義的文件位置;Z則是該函數(shù)被定義的行號。需要注意的是,當(dāng)Lua腳本以字節(jié)碼運(yùn)行時(shí),該值將始終為0,因此建議在測試時(shí)盡可能使用Lua源碼來運(yùn)行。

(1)正序調(diào)用分析——總表(曲線圖+列表)

曲線圖:

曲線選取了選取總體Lua代碼耗時(shí)和按照耗時(shí)均值正向排序的前五個(gè)函數(shù)耗時(shí)組成耗時(shí)曲線圖,每一個(gè)數(shù)據(jù)點(diǎn)代表了該函數(shù)在當(dāng)前幀(橫坐標(biāo))的耗時(shí)(縱坐標(biāo)),有助于定位耗時(shí)瓶頸函數(shù)。

列表:

列表默認(rèn)按照耗時(shí)均值從高到低對Lua函數(shù)進(jìn)行了排序,粗略展示了函數(shù)名、總CPU耗時(shí)、場景CPU耗時(shí)、耗時(shí)均值等數(shù)據(jù)。通過點(diǎn)擊函數(shù),可以進(jìn)入對應(yīng)的單個(gè)函數(shù)分析頁面。

(2)正序調(diào)用分析——單個(gè)函數(shù)頁(截圖+曲線圖+堆棧信息)

截圖:

項(xiàng)目運(yùn)行時(shí)截圖與使用者選中的幀大致對應(yīng),有助于定位問題。

曲線圖:

曲線圖包括了CPU耗時(shí)曲線圖和調(diào)用次數(shù)曲線圖;也可以使用下方條縮放曲線觀察局部耗時(shí)情況。

從曲線圖中可以觀察到:函數(shù)是否存在持續(xù)性高耗時(shí);函數(shù)是否存在短暫的大量耗時(shí),導(dǎo)致卡頓;某些函數(shù)單次耗時(shí)并不高,但因?yàn)楸淮罅康恼{(diào)用,導(dǎo)致函數(shù)總耗時(shí)較高。

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

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

列表中各項(xiàng)指標(biāo)含義是:總體占比,以根節(jié)點(diǎn)函數(shù)的總耗時(shí)為100%,當(dāng)前節(jié)點(diǎn)函數(shù)總耗時(shí)相對根節(jié)點(diǎn)函數(shù)的總耗時(shí)占比;自身占比,以根節(jié)點(diǎn)函數(shù)的總耗時(shí)為100%,當(dāng)前節(jié)點(diǎn)函數(shù)自身耗時(shí)相對根節(jié)點(diǎn)函數(shù)的總耗時(shí)占比;總耗時(shí),時(shí)間范圍內(nèi)執(zhí)行該函數(shù)的耗時(shí);自身耗時(shí),時(shí)間范圍內(nèi)去除子節(jié)點(diǎn)函數(shù)(該函數(shù)調(diào)用的函數(shù))耗時(shí)剩余的耗時(shí);調(diào)用次數(shù),時(shí)間范圍內(nèi)該函數(shù)被調(diào)用的次數(shù);單次耗時(shí),總耗時(shí)/調(diào)用次數(shù),表示每次執(zhí)行該函數(shù)的平均耗時(shí);顯著調(diào)用幀數(shù),該函數(shù)自身耗時(shí)大于3ms的幀數(shù)。

(3)倒序調(diào)用分析——總表(曲線圖+列表)

曲線圖:與正序調(diào)用分析不同的是,選取了自身耗時(shí)正向排序的前五個(gè)函數(shù),每一個(gè)數(shù)據(jù)點(diǎn)代表了該函數(shù)在當(dāng)前幀(橫坐標(biāo))的自身耗時(shí)(縱坐標(biāo))。

列表:與上同理。

(4)倒序調(diào)用分析——單個(gè)函數(shù)頁(截圖+曲線圖+堆棧信息)

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

各項(xiàng)指標(biāo)含義(與正序相比有所不同)變?yōu)榱耍鹤陨碚急龋赃x定函數(shù)的自身耗時(shí)總和為100%,這條調(diào)用路徑下選定函數(shù)的自身耗時(shí)相對選定節(jié)點(diǎn)函數(shù)總自身耗時(shí)的占比;自身耗時(shí),時(shí)間范圍內(nèi),這條調(diào)用路徑下,選定函數(shù)自身耗時(shí)的總和;調(diào)用次數(shù),這條調(diào)用路徑的調(diào)用次數(shù);單次耗時(shí),代表這條路調(diào)用路徑下,選定函數(shù)的平均耗時(shí)。

在通過以上界面定位到自身耗時(shí)較高的函數(shù)后,常見的優(yōu)化手段有:優(yōu)化該函數(shù)的函數(shù)體,減少該函數(shù)自身的耗時(shí);定位調(diào)用次數(shù)較多的調(diào)用路徑,減少調(diào)用次數(shù)。

(5)注意事項(xiàng)

Lua CPU耗時(shí)中暫不包括GC耗時(shí);Lua 函數(shù)耗時(shí)相當(dāng)于在進(jìn)出函數(shù)時(shí)打點(diǎn),統(tǒng)計(jì)耗時(shí)。所以如果Lua腳本運(yùn)行時(shí)調(diào)用了C#函數(shù),這部分C#函數(shù)是會(huì)被統(tǒng)計(jì)進(jìn)去的,所以需要關(guān)注和C#穿插調(diào)用的情況,盡量控制在50次以內(nèi)。


本文內(nèi)容就介紹到這里啦,更多內(nèi)容可以前往UWA學(xué)堂進(jìn)行閱讀。課程將從內(nèi)存、CPU、GPU三個(gè)維度討論當(dāng)前游戲項(xiàng)目中經(jīng)常出現(xiàn)的一些性能問題。


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容