一般游戲的性能指標(biāo)有:幀率、穩(wěn)定性、流暢性、加載時間(loading)、內(nèi)存占用(這一項在移動設(shè)備上比較重要,有很多閃退原因就是由此造成的)、安裝包大小、網(wǎng)絡(luò)延遲、耗電量等。
程序:代碼優(yōu)化
美術(shù):資源優(yōu)化
策劃:合理和設(shè)計方案以避免性能開銷
性能優(yōu)化主要從以下幾個方面展開: CPU、GPU、內(nèi)存
一、CPU
CPU的性能開銷主要來自于以下兩個方面:
1.引擎模塊自身的性能開銷:渲染(Draw Call)、動畫、物理引擎、UI模塊、粒子系統(tǒng)、資源加載、GC(Garbage Collection)。
2.游戲自身代碼的性能開銷:代碼結(jié)構(gòu)、循環(huán)、數(shù)據(jù)結(jié)構(gòu)等。
渲染:
1.降低Draw Call
CPU每次在準(zhǔn)備數(shù)據(jù)(頂點位置、法線、顏色、坐標(biāo)紋理等)并通知GPU渲染的過程稱為一次Draw Call,其實就是CPU對底層圖形程序接口的調(diào)用。
Draw Call的消耗來源:如果每次Draw Call只提交少量的數(shù)據(jù)將導(dǎo)致CPU瓶頸,CPU無法將GPU填滿。Draw Call對GPU的耗費在于硬件一直等待CPU提交數(shù)據(jù),而無法得到有效利用。GPU大量的時間耗費在不斷切換狀態(tài)和正確性檢測上。
降低Draw Call有以下幾種方法:
①減少所渲染物體的材質(zhì)種類 → 通過把紋理打包成圖集來盡量減少材質(zhì)的使用。
②通過Draw Call Batching來減少其數(shù)量 → 靜態(tài)批處理和動態(tài)批處理。
批處理的思想:在每次調(diào)用Draw Call時盡可能多地處理多個物體(多個物體最好一起渲染,將批處理之前需要很多次調(diào)用的物體合并,之后只需要調(diào)用一次底層圖形的接口就行),減少每一幀需要的Draw Call數(shù)目。
靜態(tài)批處理的優(yōu)點:自由度高,限制很少。
靜態(tài)批處理的缺點:可能會占用更多的內(nèi)存(額外的內(nèi)存開銷來存儲合并后的幾何數(shù)據(jù)),而經(jīng)過靜態(tài)批處理后的所有物體都不可以再移動了(即使在腳本中嘗試改變物體的位置也是無效的)。
靜態(tài)批處理的時間點:⑴在游戲?qū)С龅臅r候,在player setting中勾選static batching,這樣導(dǎo)出包的時候就進(jìn)行批處理,包體較大。⑵在游戲場景中勾選物體的static選項,在加載該場景的時候,會進(jìn)行一次靜態(tài)批處理的合并,這樣導(dǎo)出來的包不大,但是在加載的時候會使得內(nèi)存變大。
動態(tài)批處理的優(yōu)點:Unity自動完成,實現(xiàn)方便,經(jīng)過批處理的物體仍然可以移動,這是由于在處理每一幀時Unity都會重新合并一次網(wǎng)格。
動態(tài)批處理的缺點:限制有很多,可能一不小心就破壞了這種機(jī)制,導(dǎo)致Unity無法批處理一些使用了相同材質(zhì)的物體。
③盡量少使用反光、陰影等,因為這樣會使得物體多次渲染。
2.簡化資源
3.LOD
Levels Of Detail是一種優(yōu)化游戲效率的常用方法,它是根據(jù)物體在游戲畫面中所占視圖的百分比來調(diào)用不同復(fù)雜度的模型的,其缺點在于會占用大量內(nèi)存,使用這個技術(shù)一般是在解決運行時流暢度的問題,以空間交換時間。
4.剔除Culling
UI模塊:
1.動靜分離
2.預(yù)加載、常駐、即時釋放
3.圖集
4.內(nèi)存池
5.Active/Deactive
6.UISprite、Texture
7.不移動、不可見UI不更新
8.對于不交互的UI元素,關(guān)閉Raycast Target
9.資源預(yù)加載
10.shader預(yù)加載
在使用UGUI的過程中,有以下幾點可能會成為影響游戲性能的原因:
⑴圖集整理不規(guī)范
在UI界面設(shè)計的時候要考慮到重用性,比如一些公用的UI資源或使用頻率較高的資源可列為公共資源,放在若干張大圖集當(dāng)中(1~3張)作為重用圖集,在游戲開始時進(jìn)行加載,必要的情況考慮常駐內(nèi)存使用;對于一些特殊的、使用情況不多的UI,可對其按照使用功能劃分為功能圖集,在特定的時間進(jìn)行加載;對于一些同時使用到重用圖集與功能圖集的UI,在功能圖集的“留白”部分較多的情況下,可以考慮將部分在重用圖集中出現(xiàn)的元素單獨提取出來,合并到功能圖集中,從而做到讓UI只依賴功能圖集,這樣可以省去部分加載重用圖集的資源消耗(可接受的冗余換取性能)。
UGUI自動打包圖集時,有時候同一個Tag會自己打出多個Group圖集,導(dǎo)致Draw Call增加,產(chǎn)生Group的主要原因有兩種:
①紋理的格式不同
②紋理量太大,一個Group放不進(jìn)
⑵UI的層級深度
有相同材質(zhì)和紋理的UI元素是可以Batch的,可以Batch的UI(注意UI的層級)上下疊在一起不會影響性能,但是不能Batch的UI元素疊在一起就會增加Draw Call。要注意UI元素之間的層疊關(guān)系,有一些UI會有透明的部分,在設(shè)計和開發(fā)的過程中可能會在透明的部分上面疊加了其他的UI元素,這樣就有可能造成Draw Call的增加,比如排列、列表、背包等情況。
有些情況可以考慮人為增加層級從而減少Draw Call,比如一個Text的層級為0,另一個可以Batch的Text疊在了圖片A上,層級為1,那此時有兩個Text因為層級不同會安排2個Draw Call,但如果在第一個Text下放一張透明圖片(可以和圖片A進(jìn)行Batch),那兩個Text的層級就一致了,Draw Call就可以減少一個。
⑶圖文交叉
Image和Text組件,當(dāng)Text疊在Image上面(比如Button),然后Text上又疊了一張圖片,就會至少多2個Draw Call,這種情況可以考慮將字體直接印在下面的圖片上。
⑷Mask
應(yīng)避免使用Mask,其實Mask組件功能有時候可以變通,比如設(shè)計一個邊框,讓這個邊框疊在最上面,底下的UI移動時,就會被這個邊框給遮住,Mask以內(nèi)的和Mask以外的UI無法Batch
如果需要使用Mask,需要評估一下Mask會帶來的性能消耗,如果Mask內(nèi)的UI是動態(tài)生成的話,需要注意UI之間是否有重疊 → 可能某些透明部分存在重疊,需要細(xì)致觀察。
⑸Raycast Target屬性
用不上的UI盡量關(guān)閉這個屬性
⑹Alpha = 0
在某些情況下可能會使用到將Canva Group組件中的Alpha設(shè)置為0來隱藏UI,雖然不會增加draw call,但在引擎處理的時候?qū)嶋H上還是畫了一個透明度為0的面片,這種隱藏方法依然會觸發(fā)GPU渲染。
加載模塊
主要出現(xiàn)于場景切換處,且CPU占用峰值均較高 → 前一場景的場景卸載和下一場景的場景加載。
⑴場景卸載
調(diào)用SceneManger.LoadScene時,引擎即會對上一場景進(jìn)行處理
其主要開銷如下:
--Destroy
引擎在切換場景時會收集未標(biāo)識成"DontDestroyOnload"的GameObject,然后進(jìn)行Destroy。同時,代碼中的OnDestroy被觸發(fā)執(zhí)行,這里的性能開銷主要取決于OnDestroy回調(diào)函數(shù)中的代碼邏輯。
--Resource.UnloadUnusedAssets
一般情況下,場景切換過程中,該API會被調(diào)用兩次,一次為引擎在切換場景時自動調(diào)用,另一次則為用戶手動調(diào)用(一般出現(xiàn)在場景加載后,用戶調(diào)用他拉確保上一場景中的資源被卸載干凈),其耗時開銷主要取決于場景中Asset和Object的數(shù)量,數(shù)量越多、耗時越長。
⑵場景加載
--資源加載(90%以上)
其加載效率主要取決于資源的加載方式(R.L和AB加載)、加載量(紋理、網(wǎng)格、材質(zhì)等資源數(shù)據(jù)的大小)和資源的格式(紋理格式、音頻格式等)。
--Instantiate實例化
①資源加載,在Instantiate實例化時,引擎底層會查看其相關(guān)的資源是否已經(jīng)被加載,如果沒有,則會先加載其相關(guān)資源,再進(jìn)行實例化 → Instantiate耗時的根本原因。
②除此之外,Instantiate實例化的性能開銷還體現(xiàn)在腳本代碼的序列化(當(dāng)GameObject上Component數(shù)目比較多時,其Instantiate實例化性能會受到影響)和構(gòu)造函數(shù)的執(zhí)行上。(Awake和Start函數(shù)中的代碼邏輯,其產(chǎn)生的開銷也會被計算在Instantiate實例化內(nèi))
資源加載是加載模塊中最為耗時的部分,其CPU開銷在Unity中主要體現(xiàn)在Loading.UpdatePreloading和Loading.ReadObject
Loading.UpdatePreloading
Loading.UpdatePreloading這一項僅在調(diào)用類似LoadLevel(Async)的接口處出現(xiàn),主要負(fù)責(zé)卸載當(dāng)前場景的資源并且加載下一場景中的相關(guān)資源和序列化信息等。下一場景中,自身所擁有的GameObject和資源越多,其加載開銷越大。(在很多項目中,存在另外一種加載方式,及場景為空場景,絕大部分資源和GameObject都是通過OnLevelwasloaded回調(diào)函數(shù)進(jìn)行加載、實例化和拼合的,對于這種情況,Loading.UpdatePreloading的開銷會很小)
Loading.ReadObject
Loading.ReadObject這一項記錄的則是資源加載時的真正資源讀取性能開銷,基本上引擎的主流資源(紋理、資源、網(wǎng)絡(luò)資源、動畫片段等)讀取均是通過該項來進(jìn)行體現(xiàn)的??梢哉f這一項很大程度上決定了項目場景的切換效率。
另外,在使用Resources.UnloadUnusedAssets()時有可能造成一定的卡頓,盡量不要主動使用,在切換場景時會自動調(diào)用該API。
從點擊應(yīng)用到出現(xiàn)游戲畫面,加載時間受哪些方面的影響?
①Resource文件夾中的資源數(shù)量。在游戲啟動時,Unity引起會為Resource文件夾下的資源建立一個查找樹來存放與其對應(yīng)的索引,便于后續(xù)資源的加載。一般來說,Resource文件夾下資源數(shù)量越多,其構(gòu)建時間越長,應(yīng)用啟動也就越慢。
②首場景的資源加載和相關(guān)代碼的初始化工作。如果首場景的資源量越多,其腳本初始化的任務(wù)越繁重,則應(yīng)用的啟動時間也會越慢。
物理:
1.少用或不用mesh colider(網(wǎng)格碰撞器)
太復(fù)雜,網(wǎng)格碰撞器利用一個網(wǎng)格資源并在其上構(gòu)建碰撞器。對于復(fù)雜網(wǎng)狀模型上的碰撞檢測,它要比應(yīng)用原型碰撞器精確的多。
2.設(shè)置一個合適的Fixed Timestep
Fixed Timestep和物理計算有關(guān),若計算的頻率太高,增加CPU的開銷
3.優(yōu)化物理算法
GC(Garbage Collection)
GC:主要作用在于從已用內(nèi)存中找出那些不再使用的內(nèi)存,并進(jìn)行釋放
GC是Mono運行時的機(jī)制,而非Unity3D游戲引擎的機(jī)制,故GC不是用來處理引擎的Assets的內(nèi)存釋放的。
什么東西會被分配到托管堆上? → 引用類型(類的實例、字符串、數(shù)組等)
而值類型是分配到堆棧上而非堆上。
所以GC的優(yōu)化其實就相當(dāng)于是代碼的優(yōu)化。
下列兩種情況會觸發(fā)GC:
⑴當(dāng)空閑內(nèi)存不足時會觸發(fā)GC。(PS:GC釋放的內(nèi)存只會留給Mono使用,并不會交還給,因此Mono堆內(nèi)存是只增不減的)
⑵在代碼中通過調(diào)用GC.Collect()手動進(jìn)行GC,但是GC本身是比較耗時的操作,而且由于GC會暫停那些需要Mono內(nèi)存分配的線程(C#代碼創(chuàng)建的線程和主線程),因此無論是否在主線程中調(diào)用,GC都會導(dǎo)致游戲一定程度的卡頓,需要謹(jǐn)慎處理。
Mono內(nèi)存泄漏:對象不再使用卻沒有被GC回收的情況 → 會導(dǎo)致空閑內(nèi)存減少,GC頻繁,Mono堆不斷擴(kuò)充,最終導(dǎo)致游戲占用的內(nèi)存升高。
※游戲中大部分Mono內(nèi)存泄漏的情況都是由于靜態(tài)對象的引用而引起的,因此對于靜態(tài)對象盡量少用,對于不再使用的靜態(tài)對象將其引用設(shè)置為null,使其可以即使被GC回收。
GC使用注意點:
⑴字符串連接的處理。因為將兩個字符串連接的過程,其實是生成一個新的字符串的過程。而作為引用類型的字符串,其空間是在堆上分配的,被棄置的舊的字符串的空間會被GC當(dāng)做垃圾回收。
⑵盡量不用使用foreach,foreach循環(huán)將會在每一次迭代中創(chuàng)建一個enumerator對象,這時候GC會對enumerator對象進(jìn)行回收處理,消耗資源。
⑶不要直接訪問gameobject的tag屬性。比如if (MyGameObject.tag == “Player”)最好換成if (MyGameObject.CompareTag (“Player”))。因為訪問物體的tag屬性會在堆上額外的分配空間。
⑷使用對象池 → 實現(xiàn)空間的復(fù)用
⑸最好不用LINQ的命令,因為它們會分配臨時的空間,同樣也是GC收集的目標(biāo)。
⑹盡量減少代碼堆內(nèi)存分配,應(yīng)避免頻繁創(chuàng)建和開辟空間,防止頻繁觸發(fā)GC,同時在Loading或者對性能不敏感的時候主動GC。
二、GPU
GPU的性能瓶頸主要存在于以下幾個方面:
1.Fill Rate(填充率),是指顯卡每幀或者說每秒能夠渲染的像素數(shù)。在每幀的繪制中,如果一個像素被反復(fù)繪制(overdraw)的次數(shù)越多,那么它占用的資源也必然越多。目前在移動設(shè)備上,F(xiàn)illRate的壓力主要來自半透明物體。因為多數(shù)情況下,半透明物體需要開啟Alpha Blend并且關(guān)閉ZTest和ZWrite(shader中的渲染隊列),同時如果我們繪制像alpha = 0這種實際上不會產(chǎn)生效果的顏色上去,也會有Blend操作,這是一種極大的浪費。
2.像素的復(fù)雜度,比如動態(tài)陰影、光照、復(fù)雜的shader等。
3.幾何體的復(fù)雜度(頂點數(shù)量)。
4.GPU的顯存帶寬。
降低填充率
在開發(fā)過程中應(yīng)注意避免overdraw和盡量降低overdraw,降低overdraw有以下幾種方法:
⑴盡量減少alpha = 0的資源的使用,因為這種資源也會參與繪制,占用一定的GPU。
⑵制作圖集的時候,盡量使小圖排布緊湊,盡量圖集中大面積留白,理由同上。
⑶避免無用對象及組件的過度使用(比如新手教學(xué)部分用了很多“不可見”的Image作為交互響應(yīng)的控件;但這些東西雖然畫上去沒有效果,依然占用了顯卡資源,特別是有很多大塊的區(qū)域),這種情況下可以實現(xiàn)一個只在邏輯上響應(yīng)Raycast但是不參與繪制的組件即可。
具體可參考以下文章:https://blog.uwa4d.com/archives/fillrate.html
⑷通過修改渲染隊列也能降低overdraw。
⑸注意UI文本的空白區(qū)域引起的overdraw。UI文本字形是作為獨立的面片(quad)進(jìn)行渲染的,每個字符都是一個面片。這些面片通常含有大量的空白區(qū)域圍繞著字體,空白區(qū)域的大小取決于字形的形狀,在放置文本時很容易就會忽略(破壞其他UI的批處理,所以對字體盡可能預(yù)留一定的空間。
⑹Image Sliced模式(九宮格拉伸),情況允許下取消勾選Fill Center(中心鏤空,以其他UI元素覆蓋),可以減少overdraw。
減少頂點數(shù)量
⑴保持材質(zhì)的數(shù)目盡可能少,使Unity容易批處理 → 盡可能減少模型中三角形的數(shù)目,盡可能重用頂點。(“軟邊”→減少頂點數(shù)目,并且可以使得渲染效果更加平滑。)
⑵使用紋理圖集(一張大貼圖里面包含了很多子貼圖)來代替一系列單獨的小貼圖。它們可以更快地被加載,具有很少的狀態(tài)轉(zhuǎn)換,而且對批處理更友好。
⑶如果使用了紋理圖集和共享材質(zhì),如果使用了紋理圖集和共享材質(zhì),使用Renderer.sharedMaterial 來代替Renderer.material。
⑷使用光照紋理(LightMap)而非實時燈光。(LightMap是一種很常見的優(yōu)化策略,它主要用于場景中整體的光照效果。這種技術(shù)主要是提前把場景中的光照信息存儲在一張光照紋理中,然后在運行時刻只需要根據(jù)紋理采樣得到光照信息即可。)
⑸使用LOD,好處就是對那些離得遠(yuǎn),看不清的物體的細(xì)節(jié)可以忽略。(但如果沒有調(diào)整好距離的話可能會造成模型的突變,使用時需要注意)
⑹遮擋剔除(Occlusion culling),這里需要注意的是,合并方式也會影響Culling,例如把整個游戲所有的樹都合并成一個DC,DC是下降了,但是只要有一棵樹在攝像機(jī)里,所有合并的樹模型都會被渲染,增大了渲染帶寬和負(fù)載,需要權(quán)衡使用。
⑺使用mobile版的shader。因為簡單。
優(yōu)化顯存帶寬
壓縮圖片,減小顯存帶寬的壓力。
顯存帶寬的瓶頸:
①尺寸很大且未壓縮的紋理。(理解為一個通道,太大難過去)
②分辨率過高的framebuffer
優(yōu)化方法:
⑴設(shè)置格式壓縮
Android → ETC1
IOS → PVRTC
但對于透明紋理,ETC1不支持,而PVRTC則可能會有較大失真,因此更推薦使用RGBA16(要注意“色階問題”,即色彩過渡不均勻,應(yīng)避免大量的過渡色使用),RGBA32比較占內(nèi)存,不推薦使用。
另外,針對Android上帶alpha通道的圖片,還有一種比較常見的做法:即把a(bǔ)lpha通道獨立出來作為另外一張紋理,從而將RGB部分和alpha部分分別采用ETC1來壓縮,但渲染時就需要自定義的shader來處理。
⑵Mipmap
Mipmap中每一個層級的小圖都是主圖的一個特定比例的縮小細(xì)節(jié)的復(fù)制品。因為存了主圖和它的那些縮小的復(fù)制品,所以內(nèi)存占用會比之前大。但是為何又優(yōu)化了顯存帶寬呢?因為可以根據(jù)實際情況,選擇適合的小圖來渲染。所以,雖然會消耗一些內(nèi)存(大概增加30%),但是為了圖片渲染的質(zhì)量(比壓縮要好),這種方式也是推薦的。(一般UI沒有必要開啟Mipmap)
三、內(nèi)存
Unity內(nèi)存占用主要來自一下三個方面:
1.資源內(nèi)存占用
2.引擎模塊自身內(nèi)存占用
3.托管堆內(nèi)存占用,高頻率地New Class/Container/Array等,注意盡量不要在Update等高頻函數(shù)中開辟新內(nèi)存。
PS:Log輸出也會占用少量內(nèi)存,當(dāng)有大量Log輸出時需要注意。
資源內(nèi)存占用
⑴紋理(Texture)
①盡可能根據(jù)硬件的種類選擇硬件支持的紋理格式
②紋理尺寸越大,則占用內(nèi)存越大,必要時可以使用九宮格拉伸
③Mipmap,此項開啟后內(nèi)存消耗會增加1/3,UI不開啟,2D游戲所有圖片不開啟,3D場景內(nèi)貼圖開啟,角色和特效根據(jù)實際情況開啟
④Read & Write,此項開啟后內(nèi)存消耗會增大一倍(開啟后可以在運行時進(jìn)行貼圖合并操作)
⑵網(wǎng)格(Mesh)
Color數(shù)據(jù)、Normal數(shù)據(jù)、Tangent數(shù)據(jù),面數(shù)越多加載越慢,LOD
⑶動畫片段(AnimationClip)
①壓縮格式
②數(shù)據(jù)精度(衡量概率曲線會隨著精度的上升增加)
③動畫的類型:Generic OR? Humanoid
⑷音頻片段(AudioClip)
受LoadType和Compression Format影響
首推Mp3
默認(rèn)情況下Load in Background不開啟
非及時音效建議開啟,如bgm
大量頻繁使用的音效不開啟,如音效資源,選擇Decompressed On Load來降低CPU的開銷
對于Quality,建議選擇50%(此效果是非線性的),在某些極端情況下或?qū)σ糍|(zhì)要求不高的情況下可以選擇1%
⑸材質(zhì)(Material)
⑹著色器(Shader)
⑺字體資源(Font)
⑻文本資源(TextAsset)
另外,盡量減少在Hierarchy對資源的直接引用,使用動態(tài)有效的管理方式去管理當(dāng)前場景用到的資源文件。
Unity官方給出的一些優(yōu)化建議:
1.PC平臺的話保持場景中顯示的頂點數(shù)少于200K~3M,移動設(shè)備的話少于10W,一切取決于你的目標(biāo)GPU與CPU。
2.如果你用U3D自帶的SHADER,在表現(xiàn)不差的情況下選擇Mobile或Unlit目錄下的。它們更高效。
3.盡可能共用材質(zhì)。
4.將不需要移動的物體設(shè)為Static,讓引擎可以進(jìn)行其批處理。
5.盡可能不用燈光。
6.動態(tài)燈光更加不要了。
7.嘗試用壓縮貼圖格式,或用16位代替32位。
8.如果不需要別用霧效(fog)
9.嘗試用OcclusionCulling,在房間過道多遮擋物體多的場景非常有用。若不當(dāng)反而會增加負(fù)擔(dān)。
10.用天空盒去“褪去”遠(yuǎn)處的物體。
11.shader中用貼圖混合的方式去代替多重通道計算。
12.shader中注意float/half/fixed的使用。
13.shader中不要用復(fù)雜的計算pow,sin,cos,tan,log等。
14.shader中越少Fragment越好。
15.注意是否有多余的動畫腳本,模型自動導(dǎo)入到U3D會有動畫腳本,大量的話會嚴(yán)重影響消耗CPU計算。
16.注意碰撞體的碰撞層,不必要的碰撞檢測請舍去。