性能優化是游戲專案開發程序中一個永恒的話題,專案的性能優化主要圍繞CPU、GPU和記憶體三大方面進行,但是如此的總結我感覺太繁雜不能成系統,例如:影響記憶體的主要三個部分1.資源記憶體占用;2.引擎模塊自身記憶體占用;3.托管堆記憶體占用,你得資源和代碼質量都會影響記憶體,這里主要從五個方面對專案進行優化,分別是資源記憶體,圖形和GPU,編程和代碼框架,專案中各種資源組件的配置,
目錄
資源記憶體
正確匯入紋理
調整網格匯入設定
檢查多邊形面數
圖形和GPU
批處理執行繪制呼叫
燈光
相機
編程和代碼框架
Update
字串
不要使用LINQ
注意裝箱程序
確保外部代碼庫不產生垃圾
小心回圈代碼
僅在需要時運行代碼
使用Array陣列代替List串列
使用Float運算替代Vector運算
Camera.main
使用LocalPosition替代Position
物件池
協程
AssetBundle的卸載
避免在運行時添加組件
洗掉除錯日志陳述句
使用ScriptableObject
使用NonAlloc函式
重構代碼來減小GC的影響
專案中各種資源組件的配置
UI
音頻
影片
物理
總結
資源記憶體
資源管線可以大幅影回應用程式的性能,在一個較為復雜的大中型專案中,資源的記憶體占用往往占據了總體記憶體的70%以上,
正確匯入紋理
紋理會占用大部分記憶體,因此,匯入設定非常重要,通常,請遵循以下指導原則 :
-
減小 Max Size :使用能生成視覺上可接受的結果的最低設定,這種非破壞性方式,可以快速降低紋理記憶體,
-
使用 2 的冪 (POT) :Unity 要求移動端紋理壓縮格式 (PVRCT 或 ETC)采用 POT 紋理尺寸,
-
制作紋理圖集 :將多個紋理放置到單個紋理中,可以減少繪制呼叫和加快渲染速度,使用 或Unity 精靈圖集第三方Texture Packer可以制作紋理圖集,
-
關閉 Read/Write Enabled 選項 :如果啟用,此選項在 CPU 和 GPU 可尋址記憶體中都會創建副本,紋理會占用雙倍記憶體,大多數情況下,應保持此選項為禁用狀態,如果要在運行時生成紋理,請通過 Texture2D.Apply 強制執行,并且傳入設定為 true 的 makeNoLongerReadable,
-
禁用不必要的 Mip Map :對于在螢屏上大小保持不變的紋理(如 2D 精靈和 UI 圖形),Mip Map 不是必需的,對于與攝像機的距離會變化的 3D 模型,請保留 Mip Map為啟用狀態,
-
壓縮紋理,
?
?
盡可能減少過度繪制和Alpha 混合
避免繪制不必要的透明或半透明影像,這種方式導致的過度繪制和 Alpha 混合會極大影響移動平臺,
調整網格匯入設定
與紋理很像,如果匯入時不小心,網格可能占用過多記憶體,要盡可能減少網格占用的記憶體,請執行以下操作 :
-
壓縮網格 :高性能壓縮可以減少占用磁盤空間(但不會影響運行時的記憶體),請注意,網格量化可能造成不準確,因此應試驗不同的壓縮級別,從而找到適合模型的壓縮級別,
-
禁用 Read/Write :如果啟用此選項,記憶體中會有重復網格,網格的一個副本在系統記憶體中,另一個在 GPU 記憶體中,大多數情況下,應將其禁用(在 Unity 2019.2 以及更早版本中,此選項默認為選中狀態),
-
禁用骨骼和 BlendShape :如果網格不需要骨架或 BlendShape 影片,請盡可能禁用這些選項,
-
盡可能禁用法線和切線 :如果確信網格的材質不需要法線或切線,請取消選中這些選項,以節省更多記憶體,
?
檢查多邊形面數
解析度越高的模型,需要的記憶體使用量越大,并且可能占用更長的 GPU 時間,
使用細節級別 (lOD)
隨著物件移向遠處,細節級別可以將它們切換為使用更簡單的網格,以及更簡單的材質和著色器,從而幫助提高 GPU 性能,
?
使用遮擋剔除來移除隱藏的物件
隱藏在其他物件之后的物件仍然可能渲染和使用資源,使用遮擋剔除可以將它們丟棄,攝像機之外的視錐體剔除 (frustum culling) 是自動執行的,遮擋剔除 (occlusion culling) 是則要經過烘焙程序,只需將物件標記為靜態遮擋物或被遮擋物,然后通過 Window > Rendering > Occlusion Culling 對話框進行烘焙,盡管不是所有場景都適合,剔除在很多情況下都能改善性能,
圖形和GPU
每一幀,Unity 都需要確定必須渲染哪些物件,然后創建繪制呼叫,繪制呼叫是呼叫圖形 API 來繪制物件(如三角形),而批處理是要一起執行的一組繪制呼叫,
批處理執行繪制呼叫
將要繪制的物件組合為批次,可以盡可能減少在批次中繪制每個物件所需的狀態更改,這種方式通過減少渲染物件的 CPU 開銷,可以改善性能,Unity 可以使用以下幾種方法將多個物件組合為較少的批次 :
-
動態批處理 :對于小網格,Unity 在 CPU 上分組和轉換頂點,然后一次性繪制它們,注意 :只在有足夠低復雜度網格(少于 900 個頂點屬性和不超過 300 個頂點)時使用這一方法,動態批處理程式不會對更大的網格進行批處理,如果啟用會浪費 CPU 時間在每一幀都去查找要批處理的小網格,
-
靜態批處理 :對于不移動的幾何體,Unity 可以減少所有共享相同材質的網格的繪制呼叫,它比動態批處理更有效,但使用更多記憶體,
-
GPU 實體化 :如果有大量相同的物件,這種方法通過影像硬體對它們進行更有效地批處理?
-
SRP 批處理 :在 Advanced 下面的 Universal Render Pipeline Asset 中啟用 ,這樣可以SRP Batcher大幅提高 CPU 渲染速度,具體取決于場景,

燈光
避免使用過多動態光線
避免向移動端應用程式添加過多動態光線,考慮采用其他方式,如對動態網格使用自定義著色器效果和光照探針,以及對靜態網格使用烘焙光照,
禁用陰影
陰影投射可按 MeshRenderer 和光線禁用,盡可能禁用陰影可以減少繪制呼叫,
您也可以通過向簡單網格應用模糊紋理或在角色下面應用四邊形來創建偽陰影,另外,可以使用自定義著色器創建模糊陰影,

將光照烘焙到光照貼圖中
烘焙陰影和光照的渲染不會影響運行時性能,
光照探針
光照探針存盤場景中的空白空間的烘焙光照資訊并且提供高質量的光照(直接和間接),它們使用球諧函式,這種函式的計算速度比動態光照快很多,
相機
限制攝像機的使用
每個攝像機都會產生開銷,無論它是否在做有意義的作業,只在有必要渲染時才使用攝像機組件,在低端移動平臺,每個攝像機最多可以使用 1 ms CPU 時間,
限制后期處理效果
全螢屏后期處理效果(如發光)會極大降低性能,請在游戲的美術設計中謹慎使用這些效果,
限制使用一些相機特效,
編程和代碼框架
因為我是程式出身就仔細說一下代碼的問題吧,
每個 Unity 腳本都將按預定順序運行多個事件函式,您應該了解 Awake、Start、Update 及其他創建腳本生命周期的函式之間的區別,
Update
盡可能減少每幀運行的代碼,
考慮代碼是否必須每一幀都運行,將不必要的邏輯移出 Update、 LateUpdate 和 FixedUpdate,可在這些事件函式中方便地放置必須每幀更新的代碼,但應提取出任何不需要以這種頻率更新的邏輯,盡可能只在情況發生改變時才執行邏輯,
如果確實 需要使用 Update,可以考慮每 n 幀運行一次代碼,這是一種應用時間切片 (將繁重的作業負載分布到多個幀的常用技術)的方法,邏輯層和表現層分離,分層限幀和動態負載均衡,
private int interval = 3;
void Update()
{
if (Time.frameCount % interval == 0)
Function();
}
void Function()
{
}
不在Update方法中創建新物件
理想情況下,開發者在Update、FixedUpdate或LateUpdate方法中不應該使用New關鍵字,而是應該使用已有物件,
一次創建,多次重用
這條規則的意思是:要在Start方法和Awake方法中分配所有內容,這條規則和第一條類似,其實只是從Update方法移除new關鍵字的另一種方式,
開發者應該從Update方法移除有以下行為的代碼:
-
創建新實體
-
尋找任意游戲物件
然后,將這些代碼移動到Start方法或Awake方法中,
//未優化的代碼
private List<GameObject> objectsList;void Update()
{
objectsList = new List<GameObject>();
objectsList.Add(......)
}
//優化后的代碼
private List<GameObject> objectsList;void Start()
{
objectsList = new List<GameObject>();
}
void Update()
{
objectsList.Clear();
objectsList.Add(......)
}
GameObject.Find、GameObject.GetComponent 和 Camera.main( 在 2020.2之前的版本中)可能開銷較大,應避免在 Update 方法中呼叫它們,而應在 Start 中呼叫它們,并且快取相應結果,
//未優化的代碼
void Update()
{
var levelObstacles = FindObjectsOfType<Obstacle>();
foreach(var obstacle in levelObstacles) { ....... }
}
//優化后的代碼
private Object[] levelObstacles;
void Start()
{
levelObstacles = FindObjectsOfType<Obstacle>();
}
void Update()
{
foreach(var obstacle in levelObstacles) { ....... }
}
嘗試避免在Update方法中使用訪問器,只在Start方法中呼叫一次訪問器,并快取回傳的數值,
//未優化的代碼
void Update()
{
//分配包含所有touches的新陣列
Input.touches[0];
}
//優化后的代碼
void Update()
{
Input.GetTouch(0);
}
//未優化的代碼
void Update()
{
//回傳新的字串(垃圾),然后對比2個字串
gameObject.Tag == "MyTag";
}
//優化后的代碼
void Update()
{
gameObject.CompareTag("MyTag");
}
避免空Unity 事件
即使是空的 MonoBehaviour 也需要資源,因此應洗掉空的 Update 或 LateUpdate 方法,
字串
使用哈希值而不是字串引數
Unity 不使用字串名稱對 Animator、Material 和 Shader 屬性進行內部尋址,為了加快速度,所有屬性名稱都經過哈希處理為屬性 ID,實際上是這些 ID 用于尋址屬性,
每當在 Animator、Material 或 Shader 上使用 Set 或 Get 方法時,請使用整數值方法而非字串值方法,字串方法只執行字串哈希處理,然后將經過哈希處理的 ID 轉發給整數值方法,
避免字串連接
在涉及到垃圾分配的時候,字串要特別注意,即使是基本的字串操作,也可能產生大量垃圾,這是為什么呢?
因為字串是無法改變的陣列,這意味著,如果要把兩個字串連接起來,我們會創建新陣列,而舊陣列會成為垃圾,所以我們可以使用StringBuilder避免或最小化這類垃圾分配,
//未優化的代碼
void Start()
{
text = GetComponent<Text>();
}
void Update()
{
text.text = "Player " + name + " has score " + score.toString();
}
//優化后的代碼
void Start()
{
text = GetComponent<Text>();
builder = new StringBuilder(50);
}
void Update()
{
//StringBuilder為所有型別多載了Append方法
builder.Length = 0;
builder.Append("Player ");
builder.Append(name);
builder.Append(" has score ");
builder.Append(score);
text.text = builder.ToString();
}
不要使用LINQ
盡可能不要使用LINQ,也就是說,不要在任何經常執行的代碼中使用LINQ,
雖然使用LINQ可以使代碼更容易閱讀,但在很多情況下,這類代碼的性能和記憶體分配都非常糟糕,
注意裝箱程序
裝箱程序會生成垃圾,什么是裝箱程序呢?
最常見的裝箱程序是將數值型別,例如int,float,bool等傳遞到需要Object型別引數的函式時,所發生的程序,
確保外部代碼庫不產生垃圾
如果發現部分垃圾由Asset Store資源商店下載的代碼產生,我們有多個解決方法,但在我們進行逆向工程并除錯前,請再次查看Asset Store資源商店的相應頁面,代碼庫是否有進行更新,
在我們的專案中,我們使用的所有資源一直由資源的開發者進行維護,他們一直在進行性能更新,從而解決了我們的所有問題,
所以,一定要讓專案使用的依賴保持更新,如果遇到沒有維護的代碼庫,建議放棄這類代碼庫,
小心回圈代碼
同Update一樣,
僅在需要時運行代碼
建議通過使用C#事件實作觀察者設計模式,
使用Array陣列代替List串列
在代碼中,我們發現大多數串列有固定的長度,或是可以計算出最大成員數量,因此我們使用陣列重新實作了這些串列,在特定情況下,可以在迭代資料的時候得到原先2倍的速度,
在某些情況下,我們無法避免使用串列或其它復雜的資料結構,常見的情況是:需要經常添加或移除元素的時候,使用串列的效果更好的時候,通常來說,我們會對固定大小的串列使用陣列,
在堆記憶體上進行鏈表的分配的時候,如果該鏈表需要多次反復的分配,我們可以采用鏈表的clear函式來清空鏈表從而替代反復多次的創建分配鏈表,
//優化前
void Update()
{
List myList = new List();
PopulateList(myList);
}
//優化后
private List myList = new List();
void Update()
{
myList.Clear();
PopulateList(myList);
}
使用Float運算替代Vector運算
Float運算和Vector運算的區別不是很明顯,除非像我們一樣進行上千次運算,因此對我們來說,這項改動的性能提升效果非常明顯,
Camera.main
使用Camera.main很簡單,但是這種操作的性能非常糟糕,這是因為在每個Camera.main呼叫背后,Unity其實會執行FindGameObjectsWithTag()來獲取結果,因此頻繁呼叫Camera.main并不好,
最好的解決方法是在Start或Awake方法中快取Camera.main的參考,
使用LocalPosition替代Position
在代碼允許的位置,為獲取函式(Getter)和設定函式(Setter)使用Transform.LocalPosition替代Transform.Position,
這樣的原因是:每次Transform.Position呼叫的背后,都會有更多操作要執行,包括在呼叫獲取函式時計算全域位置,或是在呼叫設定函式時從全域位置計算出本地位置,
在專案中,我們發現在出現Transform.Position的幾乎所有情況中都可以用LocalPosition替代Transform.Position,無需在代碼中做其它改動,
物件池
物件池用于減少記憶體開銷,其原理就是把可能用到到的物件,先存在一個地方(池),要用的時候就調出來,不用就放回去,而不是要用的時候創建,不用的時候銷毀,
協程
呼叫 StartCoroutine()會產生少量的記憶體垃圾,因為unity會生成物體來管理協程,所以在游戲的關鍵時刻應該限制該函式的呼叫,基于此,任何在游戲關鍵時刻呼叫的協程都需要特別的注意,特別是包含延遲回呼的協程,另外還要注意不要同時開啟多個攜程,必要的時候可以手動關閉協程StopCoroutine();
AssetBundle的卸載
不用的資源記得卸載,
避免在運行時添加組件
在運行時呼叫 AddComponent 需要一些開銷,每當在運行時添加組件時,Unity 都必須檢查是否有重復項或是否需要其他組件,
洗掉除錯日志陳述句
日志陳述句(尤其是在 Update、LateUpdate 或 FixedUpdate 中)可能會降低性能,在進行構建之前,請禁用日志陳述句,
使用ScriptableObject
在 ScriptableObject 中而不是 MonoBehaviour 中存盤不變的值或設定,ScriptableObject 這種資源只需設定一次就可以在專案中一直使用,它不能直接附加到游戲物件,
在 ScriptableObject 中創建欄位來存盤值或設定,然后在 Monobehaviour 中參考該 ScriptableObject,
使用 ScriptableObject 的這些欄位可以防止每次使用該 Monobehaviour 實體化物件時出現不必要的資料重復,

使用NonAlloc函式
對于特定Unity函式,我們可以找到不分配任何記憶體的替代函式,在我們的專案中,這些函式都和物理功能有關,我們在碰撞檢測使用的函式如下,
Physics2D. CircleCast();
對于該函式,我們可以找到不分配任何記憶體的版本,
Physics2D. CircleCastNonAlloc();
許多其它函式都有類似的替代函式,因此請記得查看檔案,了解函式是否有相應的NonAlloc版本,
重構代碼來減小GC的影響
即使我們減小了代碼在堆記憶體上的分配操作,代碼也會增加GC的作業量,最常見的增加GC作業量的方式是讓其檢查它不必檢查的物件,struct是值型別的變數,但是如果struct中包含有參考型別的變數,那么GC就必須檢測整個struct,如果這樣的操作很多,那么GC的作業量就大大增加,在下面的例子中struct包含一個string,那么整個struct都必須在GC中被檢查:
//優化前
public struct ItemData
{
public string name;
public int cost;
public Vector3 position;
}
private ItemData[] itemData;
//優化后
private string[] itemNames;
private int[] itemCosts;
private Vector3[] itemPositions;
如果我們知道堆記憶體在被分配后并沒有被使用,我們希望可以主動地呼叫GC操作,或者在GC操作并不影響游戲體驗的時候(例如場景切換的時候),我們可以主動的呼叫GC操作:
System.GC.Collect()
總之代碼的優化還有很多,平時要多注意這些細節養成良好的書寫代碼的習慣,
專案中各種資源組件的配置
這一塊的內容主要是開發者對Unity引擎的熟悉程度,如果你作業時間長經歷的專案多自然會知道這些組件的優化點,
UI
UGUI)常常是性能問題的來源,Canvas 組件生成和更新 UI 組件的網格并向 GPU 發出繪制呼叫,它的運行開銷很大,因此,在使用 UGUI 時,請注意以下因素,
隱藏不可見的UI 元素
可能有些 UI 元素(如僅當角色收到傷害時才出現的生命值血條)只偶爾在游戲中出現,如果不可見的 UI 元素是活動的,它仍然可能使用繪制呼叫,顯式禁用所有不可見的 UI 組件,在需要時再重新啟用,
如果只需要關倍訓布的可見性,請禁用 Canvas 組件而不是游戲物件,這樣就不必重新構建網格和頂點,
限制GraphicRaycaster 和禁用Raycast Target
輸入事件(如屏上觸摸或單擊)需要 GraphicRaycaster 組件,它只是回圈處理螢屏上的每個輸入點,檢查它是否在 UI 的 RectTransform 之內,
從層級視圖的頂層畫布中移除默認的 GraphicRaycaster,只向需要互動的各元素(按鈕、滾動矩形等)添加 GraphicRaycaster,

另外,在所有不需要 Raycast Target 的 UI 文本和影像上將其禁用,如果是包含很多元素的復雜 UI,所有這些小更改都可以減少不必要的計算,

避免使用布局組
布局組的更新很低效,應少量使用,如果內容是動態的,應完全避免不用,而是使用錨點進行比例布局,或者,創建自定義代碼,在Layout Group 組件設定 UI 之后,將該組件禁用,
如果動態元素確實需要使用布局組(水平、垂直、網格),應避免嵌套它們,從而改善性能,
?
使用全屏UI 時,隱藏其他全部內容
如果暫停螢屏或者啟動螢屏遮住場景中的其他全部內容,則禁用攝像機對 3D 場景的渲染,同樣,禁用隱藏在頂層畫布之后的所有背景畫布元素,
由于不需要以 60 fps 的幀率進行更新,可以考慮在全屏 UI 程序中降低 Application.targetFrameRate,
合并圖集
減少DrawCall,多張圖片需要多次DrawCall,合并成一張大圖只需要呼叫一次DrawCall
減少對記憶體的占用,
音頻
盡管音頻通常不會造成性能瓶頸,還是可以進行優化以節省記憶體,
盡量使用單聲道聲音剪輯
如果要使用 3D 空間音頻, 請以單聲道 (single channel) 的形式創作聲音剪輯,或者啟用 Force To Mono 設定,在運行時定位使用的多聲道聲音會扁平化為單聲道源,因此會增加 CPU 開銷和浪費記憶體,
盡可能使用原始未壓縮WAV 檔案作為源資源
如果使用任何壓縮格式(如 MP3 或 Vorbis),Unity 會將其解壓并在構建時重新壓縮,這樣會導致兩個有損通道,從而降低最終質量,
壓縮剪輯并降低壓縮位元率
通過壓縮減小剪輯的大小和記憶體使用量 :
-
對大多數聲音使用 Vorbis(或者對不回圈的聲音使用 MP3),
-
對常用的短聲音使用 ADPCM(如腳步聲、槍聲),相比于未壓縮的 PCM,這樣可以減小檔案大小,在播放時又可以很快解碼,
移動設備上的音效最高為 22,050 Hz,使用較低設定通常對最終質量影響很小,當然,請使用您自己的耳朵來判斷
?,
選擇正確的加載型別
每個剪輯大小的設定都不同,
-
小剪輯 (< 200 kb) 應采用 Decompress on Load,將聲音解壓縮為原始 16 位 PCM 音頻資料,會導致 CPU 開銷和記憶體占用,因此,這僅適用于短聲音,
-
中等剪輯 (>= 200 kb) 應保持為 Compressed in Memory,
-
大檔案(背景音樂)應設定為 Streaming,否則,整個資源會一次性加載到記憶體中,
從記憶體中卸載靜音的音頻源 (AudioSources)
實作靜音按鈕時,不要只是將音量設定為 0,可以銷毀 AudioSource 組件,從而將其從記憶體中卸載,這樣,播放器不需要過于頻繁地切換開關,
影片
Unity的影片相當復雜,盡可能限制在移動設備上使用下面的設定,
使用通用還是人形骨架
默認情況下,Unity 通過通用骨架匯入影片模型,但在影片化角色時,開發人員常常切換為人形骨架,
人形骨架每一幀(即使未使用)都計算反向動力學和影片重定向,占用的 CPU 時間比等效的通用骨架多 30-50%,如果不需要這些特定人形骨架功能,請使用通用骨架,
避免過多使用 Animator
Animator 主要用于人形角色,但也常用于影片化單個值(如 UI 元素的 Alpha 通道),避免過多使用 Animator,尤其是與 UI 元素結合使用,只要可能,對移動設備使用舊版 Animation 組件,
考慮創建補間函式或者使用第三方庫來實作簡單影片(如 DOTween),
?
物理
Unity 的內置物理系統 (Nvidia PhysX) 在移動設備上開銷較大,下面的提示可以幫助您每秒減少更多幀,
優化設定
在 PlayerSettings 中,盡可能選中 Prebake Collision Meshes,
?
請務必同時編輯 Physics 設定 (Project Settings > Physics),盡可能簡化 Layer Collision Matrix,
禁用 Auto Sync Transforms 并啟用 Reuse Collision Callbacks,
?
簡化碰撞體
網格碰撞體開銷較大,用簡單的原始碰撞體或網格碰撞體代替更復雜的網格碰撞體來近似原始形狀,
使用物理方法移動剛體
使用類方法(如 MovePosition 或 AddForce)來移動 Rigidbody 物件,直接轉換其 Transform 組件可能導致重新計算物理世界,在復雜場景中,這樣需要較大開銷,
在 FixedUpdate 中而不是 Update 中移動物理體,
修改固定時間間隔
Project Settings 中的默認 Fixed Timestep 是 0.02 (50 Hz),根據目標幀率對此進行更改(例如,對 30 fps 設定為 0.03),
否則,如果幀率在運行時下降,也就是說 Unity 每幀都多次呼叫 FixedUpdate,可能會因物理內容過多而造成 CPU 性能問題,
Maximum Allowed Timestep 對幀率下降時物理計算和 FixedUpdate 事件可以使用的時間進行限制,降低該值意味著在性能頓挫程序中,物理系統和影片會緩慢下來,但也會減小其對幀率的影響,
?
總結
性能優化是一個廣泛的話題,理解移動硬體的運行方式及其限制,要找到符合您的設計要求的有效解決方案,您需要掌握 Unity 的類和組件、演算法和資料結構,以及平臺的性能分析工具,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/300824.html
標籤:其他
上一篇:Unity 異步
