UGUI 性能優化系列:第一篇——基礎優化與資源管理
UGUI 性能優化系列:第二篇——Canvas 與 UI 元素管理
在 Unity 游戲中,用戶界面(UI)是玩家與游戲交互的核心。然而,不當的 UGUI 使用常常成為游戲性能的瓶頸,尤其是在移動設備上。理解 UGUI 的工作原理并掌握其優化技巧,是每個 Unity 開發者必備的技能。本系列的第一篇文章,我們將從最基礎的層面出發,深入探討 UGUI 的渲染管線,以及如何從 資源管理 的角度進行優化,為后續更深入的優化打下堅實基礎。
一、UGUI 渲染管線簡介
要優化 UGUI,首先需要了解它是如何將 UI 元素繪制到屏幕上的。UGUI 的渲染過程可以簡化為以下幾個核心步驟:
1. UI 元素層級與事件系統
當我們在 Hierarchy 窗口中創建 UI 元素時,它們會形成一個父子層級結構。UGUI 會根據這個層級結構以及它們的 Rect Transform
屬性來計算每個 UI 元素的最終位置和大小。同時,UGUI 的事件系統 (EventSystem
) 負責處理用戶的輸入(如點擊、拖拽),并將其分發給相應的 UI 元素。
2. Mesh 生成與更新
UGUI 中的每個可渲染的 UI 元素(如 Image
, Text
, RawImage
等)最終都需要被轉換為 Mesh(網格)才能被 GPU 渲染。這個 Mesh 包含了頂點的坐標、UV 坐標(用于紋理映射)和顏色信息。
- 臟標記(Dirty Flag)與重建(Rebuild):
當 UI 元素的某些屬性發生改變時(例如Text
內容變化、Image
的Sprite
變化、Rect Transform
大小位置改變),UGUI 會給這個元素打上一個“臟標記”。在一個 Canvas 上,當任何子元素的“臟標記”被激活時,UGUI 會觸發該 Canvas 的 重建 過程。
重建過程會重新計算受影響 UI 元素的 Mesh,并將新的 Mesh 數據提交給 GPU。這個過程是性能開銷的主要來源之一,因為 Mesh 的生成涉及到 CPU 計算,并且數據傳輸到 GPU 也會消耗帶寬。
3. 合批(Batching)
為了提高渲染效率,GPU 喜歡一次性接收大量數據進行處理,而不是零散地接收小批數據。因此,Unity 會嘗試將多個可以共用相同材質(Material)和紋理(Texture)的 UI 元素的 Mesh 合并成一個更大的 Mesh,然后一次性提交給 GPU 進行渲染。這個過程就是 合批(Batching)。
- Draw Call:
每一次 GPU 接收到渲染指令(渲染一批 Mesh)并進行渲染的過程,被稱為一個 Draw Call。Draw Call 的數量是衡量渲染性能的關鍵指標之一。Draw Call 越多,CPU 和 GPU 之間的通信開銷就越大,性能也就越低。
理想情況下,我們希望盡可能減少 Draw Call 的數量。UGUI 的合批機制正是為了達到這個目的。
4. 裁剪(Culling)與遮擋(Occlusion)
- 裁剪: UGUI 會自動裁剪超出 Canvas 范圍的 UI 元素,這意味著那些完全在 Canvas 外部的 UI 元素不會被渲染。
- 遮擋: 盡管 UGUI 并沒有像 3D 場景那樣的完整遮擋剔除機制,但它會根據 UI 元素的層級和
Rect Transform
的布局來確定哪些元素在 Z 軸上被其他元素完全遮擋,從而避免繪制那些完全被遮擋的像素。
5. 像素填充(Overdraw)
即使一個 UI 元素沒有被完全遮擋,它也可能與其他 UI 元素重疊。在重疊區域,GPU 可能需要多次繪制同一個像素,這個現象稱為 Overdraw(過度繪制)。Overdraw 發生在 GPU 層面,它會增加 GPU 的像素填充率(Fill Rate)壓力,尤其是在移動設備上,過高的 Overdraw 會顯著影響性能。
了解了這些基本原理,我們就可以有針對性地進行優化了。
二、Sprite Atlas(圖集)優化
圖集(Sprite Atlas) 是 UGUI 性能優化中最重要的手段之一。它的核心思想是將多個小圖片打包到一個大圖片中,從而減少 Draw Call 數量,提高渲染效率。
1. 為什么要使用圖集?
在 UGUI 中,每個 Image
組件通常引用一個 Sprite
。如果每個 Sprite
都對應一張獨立的紋理圖片,那么在渲染時,每個 Image
都可能產生一個獨立的 Draw Call。想象一下一個復雜的 UI 界面,有幾十甚至上百個 Image
組件,這將導致 Draw Call 數量飆升,嚴重拖累性能。
使用圖集后,所有打包到同一個圖集中的 Sprite
共享一張大的紋理圖片。當這些 Sprite
被渲染時,只要它們使用相同的材質,Unity 就可以將它們的 Mesh 合并,一次性渲染,從而顯著減少 Draw Call。
優勢總結:
- 減少 Draw Call: 這是最核心的優勢,直接降低 CPU 和 GPU 的通信開銷。
- 減少內存占用(可能): 某些情況下,打包后的圖集可能比散圖的總和占用更少的內存,因為它避免了每個圖片單獨加載的額外開銷。
- 提高加載速度: 加載一張大圖通常比加載多張小圖更快。
2. Unity 中的圖集制作與使用
Unity 提供了兩種主要的方式來創建和使用圖集:Sprite Packer 和 手動圖集。
a. Sprite Packer(推薦)
Sprite Packer
是 Unity 內置的自動圖集打包工具,它能自動檢測項目中符合條件的 Sprite 并將其打包成圖集。
使用步驟:
-
啟用 Sprite Packer:
打開 Unity 編輯器,選擇Edit > Project Settings > Editor
。在Sprite Packer
部分,將Mode
設置為Enabled
或Enabled For Builds
。Enabled
: 在編輯器和打包時都啟用 Sprite Packer。這會讓你在編輯器中也能看到打包后的效果,便于調試。Enabled For Builds
: 只在打包時啟用 Sprite Packer。在編輯器中會保持散圖狀態,性能消耗可能稍高,但對于一些不希望編輯器實時打包的場景可能更合適。通常建議設置為Enabled
。
-
設置 Sprite 的 Packing Tag:
選擇你想要打包的Sprite
紋理(通常是Texture Type
設置為Sprite (2D and UI)
的圖片)。在 Inspector 窗口中,找到Packing Tag
屬性。- 輸入一個相同的字符串作為
Packing Tag
,所有擁有相同Packing Tag
的Sprite
將會被打包到同一個圖集中。 - 命名規范建議:
Packing Tag
應該反映圖集的內容或用途,例如UI_Common
,UI_Icons
,UI_Battle
等。 - 你也可以在
Packing Tag
前面加上[TIGHT]
或[RECTANGLE]
來控制打包方式,但通常默認行為就足夠了。
- 輸入一個相同的字符串作為
-
打包圖集:
- 當
Sprite Packer
啟用后,Unity 會在特定時機(例如保存項目、進入 Play 模式、構建游戲)自動進行圖集打包。 - 你也可以手動觸發打包:在菜單欄選擇
Window > 2D > Sprite Packer
,然后點擊Pack
按鈕。 - 在
Sprite Packer
窗口中,你可以預覽打包后的圖集,以及各個Sprite
在圖集中的位置。
- 當
-
在 UI 中使用:
- 一旦
Sprite
被打包成圖集,你在Image
組件中引用這些Sprite
時,它們會自動使用打包后的圖集。你無需做額外的修改。
- 一旦
Sprite Packer 的優缺點:
- 優點:
- 自動化: 大大簡化了圖集管理工作,無需手動調整布局。
- 高效: Unity 內部算法會自動優化圖集尺寸和排布,最大化空間利用率。
- 易于維護: 添加、刪除或修改
Sprite
后,Unity 會自動重新打包。
- 缺點:
- 不可控性: 開發者對最終圖集的大小和布局控制較少,可能不符合特定需求(例如,要求某個圖集固定尺寸)。
- 構建時間: 項目中
Sprite
數量眾多時,每次打包都可能增加構建時間。
b. 手動圖集(舊版本或特定需求)
在較老的 Unity 版本或某些需要精確控制圖集內容的特殊情況下,開發者會選擇手動制作圖集。
使用步驟:
- 創建大圖: 使用 Photoshop、GIMP 等圖像編輯軟件,將多個小圖片拼接成一張大圖。
- 切割 Sprite: 將這張大圖導入 Unity,將其
Texture Type
設置為Sprite (2D and UI)
。在 Inspector 窗口中,將Sprite Mode
設置為Multiple
。然后點擊Sprite Editor
按鈕,手動或自動(通過Slice
功能)將大圖切割成多個Sprite
。 - 在 UI 中使用: 在
Image
組件中,直接引用這些手動切割出來的Sprite
。
手動圖集的優缺點:
- 優點:
- 完全可控: 開發者可以精確控制圖集的內容、尺寸和布局。
- 適用于復雜場景: 當需要將一些并非
Sprite
類型(如RawImage
或 3D 模型的貼圖)的圖片手動打包到一起時,這是一種靈活的方式。
- 缺點:
- 繁瑣: 手動拼接和切割工作量大,尤其是在項目后期需要頻繁修改時。
- 低效: 人工排布通常不如自動算法高效,可能造成空間浪費。
- 維護困難: 增刪改
Sprite
需要重新編輯大圖并重新切割,容易出錯。
結論: 除非有非常特殊的理由,否則強烈建議使用 Sprite Packer 來管理圖集。
3. 圖集大小、格式與壓縮
圖集的尺寸和格式直接影響內存占用和加載速度。
a. 圖集尺寸
- 推薦尺寸: 大多數情況下,圖集尺寸應為 2 的冪次方(例如 256x256, 512x512, 1024x1024, 2048x2048, 4096x4096)。這是因為 GPU 在處理 2 的冪次方的紋理時效率最高。
- 最大尺寸: 檢查目標平臺的 GPU 支持的最大紋理尺寸。通常移動設備支持 2048x2048 或 4096x4096。超過這個尺寸的紋理可能無法加載或被迫降采樣,反而浪費資源。
- 合理控制: 避免創建過大或過小的圖集。過大的圖集會增加內存占用和加載時間,過小的圖集可能導致無法有效合批。
b. 圖片格式與壓縮
圖集的格式和壓縮方式直接決定了其在內存中的大小和渲染性能。
- RGBA 32-bit: 默認格式,每個像素 32 位(R, G, B, A 各 8 位),提供最高的圖像質量,但內存占用最大。適用于需要高質量透明度的 UI 元素。
- RGBA 16-bit: 每個像素 16 位,質量略有下降但內存占用減半。在對畫質要求不是特別高的情況下,可以考慮使用。
- RGB 24-bit: 沒有透明度通道,內存占用低于 RGBA。適用于不透明的 UI 元素,但 UGUI 中帶透明度的 UI 元素占多數。
- ETC2 (Android & OpenGL ES 3.0+):
- ETC2 RGB4: 無透明度,適用于不帶 Alpha 的圖片,壓縮比高。
- ETC2 RGB4 A1: 1 位 Alpha 通道,適用于只有完全透明或完全不透明的圖片。
- ETC2 RGBA8: 8 位 Alpha 通道,支持高質量透明度,壓縮比適中。
- 優勢: 硬件解壓,GPU 直接讀取,無需 CPU 解壓,效率高,內存占用小。
- 缺點: 僅支持 Android 和部分 iOS 設備(OpenGL ES 3.0+),兼容性不如 ASTC。
- ASTC (Android & iOS):
- 自適應可伸縮紋理壓縮: 更先進的紋理壓縮格式,提供更高的壓縮比和更好的圖像質量。
- 塊大小: 可以選擇不同的塊大小(如 4x4, 6x6, 8x8, 12x12),塊越小質量越好,內存越大;塊越大質量越差,內存越小。
- 優勢: 兼容性更好(Android 和 iOS 廣泛支持),壓縮質量和效率通常優于 ETC2。
- 缺點: 壓縮時間可能較長,對硬件支持有一定要求。
- PVRTC (iOS Only):
- PowerVR 紋理壓縮: 針對 PowerVR GPU 優化的壓縮格式(早期 iOS 設備主要使用),有 2-bit 和 4-bit 兩種。
- 優勢: 在特定 iOS 設備上表現優秀。
- 缺點: 質量相對較差,且僅限 iOS 平臺。
選擇建議:
- 優先考慮平臺專屬壓縮格式:
- Android: 優先使用 ASTC,其次是 ETC2。
- iOS: 優先使用 ASTC,其次是 PVRTC(如果目標設備較舊且對內存極致敏感)。
- PC/Standalone: 通常可以使用 DXT1 (RGB) 或 DXT5 (RGBA)。
- 根據圖片內容選擇:
- 對于不需要透明度的圖片:選擇 RGB 格式或對應平臺的無 Alpha 壓縮格式(如 ETC2 RGB4, ASTC NxB 無 Alpha 塊)。
- 對于需要透明度的圖片:選擇 RGBA 格式或對應平臺的帶 Alpha 壓縮格式(如 ETC2 RGBA8, ASTC NxB 帶 Alpha 塊)。
- 權衡質量與內存: 在保證視覺效果的前提下,盡量選擇壓縮比最高的格式。
在 Unity 中,你可以通過選擇紋理圖片,在 Inspector 窗口中設置 Texture Type
為 Sprite (2D and UI)
,然后在 Platform Specific Overrides
中針對不同平臺設置不同的壓縮格式。
4. 動態圖集與靜態圖集
盡管 Sprite Packer
是自動打包,但我們仍然可以從邏輯上區分動態圖集和靜態圖集。
- 靜態圖集:
- 定義: 指那些在游戲運行過程中內容不會改變,或者變化非常少的圖集。例如,主界面的通用圖標、按鈕背景、HUD 元素等。
- 優勢: 一次加載,永久使用,內存開銷穩定。
- 管理: 將所有相關的靜態 Sprite 都放在一個或幾個大的圖集中,通過
Packing Tag
來區分。
- 動態圖集:
- 定義: 指那些內容會根據游戲進度、玩家選擇等動態加載和卸載的圖集。例如,某個特定副本的怪物頭像、裝備圖標、特定任務的 UI 元素等。
- 優勢: 按需加載,減少初次加載時間,節省內存。
- 管理: 為不同模塊、不同場景的動態 UI 元素創建獨立的圖集。當某個模塊不再使用時,可以卸載對應的圖集資源。
- 注意事項: 頻繁加載和卸載圖集本身也會有性能開銷,需要權衡。可以考慮使用 AssetBundle 或 Addressables 來管理動態圖集的加載和卸載。
規劃建議:
- 將所有在游戲中頻繁出現、通用性強的 UI 元素(如通用按鈕、通用圖標、背景、通用字體)打包到一個或幾個大的 “公共圖集” 中。
- 針對特定模塊或場景(如戰斗界面、背包界面、商店界面),將只在該模塊或場景中使用的 UI 元素打包成獨立的 “模塊圖集”。
- 避免將無關的 Sprite 打包到同一個圖集中,這可能導致圖集過大或無法有效卸載。
三、字體優化
字體在 UGUI 中也扮演著重要角色,其渲染方式也會影響性能。
1. 字體的渲染原理
當你在 UGUI 中使用 Text
或 TextMeshPro
組件時,字體字符實際上也是以紋理和 Mesh 的形式被渲染的。
- 字體紋理(Font Atlas): 每個字符都會被渲染到一張紋理上,這張紋理就是字體圖集(Font Atlas)。當需要顯示某個字符時,UGUI 會從這張字體圖集中獲取對應字符的 UV 信息,并將其繪制到屏幕上。
- Mesh 生成: 每個字符都會被轉換成四邊形 Mesh,這些 Mesh 包含了字符的形狀信息。當文本內容發生變化時,對應的 Mesh 需要重新生成。
2. 動態字體與靜態字體(TextMeshPro 的優勢)
a. 動態字體(Dynamic Font)
- 原理: Unity 默認的
Text
組件通常使用動態字體。當你導入一個.ttf
或.otf
字體文件時,Unity 會在運行時根據需要動態生成字符紋理和 Mesh。這意味著只有當某個字符被用到時,它才會被加入到字體圖集中。 - 優勢: 初始包體較小,因為不需要預生成所有字符紋理。
- 缺點:
- 運行時開銷: 第一次使用某個字符時,需要實時渲染并生成其紋理,這會產生一定的 CPU 開銷。如果文本內容頻繁變化且包含大量新字符,這種開銷會累積。
- 字體圖集擴展: 隨著使用的字符越來越多,字體圖集會不斷擴展,如果擴展次數過多,可能導致 Draw Call 增加或內存碎片。
- 渲染質量: 默認
Text
組件的渲染質量通常不如TextMeshPro
。
b. 靜態字體(Pre-generated Font Atlas / SDF Font)
- 原理:
TextMeshPro
(簡稱 TMP)是 Unity 推薦的文本解決方案,它采用 SDF(Signed Distance Field,有符號距離場) 技術。在使用 TMP 時,我們通常會預先生成一個包含所有常用字符的字體圖集(Font Atlas)。 - 優勢:
- 高質量渲染: SDF 字體在放大或縮小時依然保持清晰,沒有鋸齒感,渲染效果遠優于傳統動態字體。
- 性能穩定: 字體圖集在游戲啟動時一次性加載,運行時無需動態生成字符紋理,避免了額外的 CPU 開銷和字體圖集擴展問題。
- 更少的 Draw Call: TMP 會嘗試將所有使用相同字體和材質的文本合并成一個 Draw Call。
- 豐富的文本效果: TMP 內置了描邊、陰影、漸變等多種文本效果,且性能開銷小。
- 缺點:
- 包體增大: 預生成的字體圖集會增加游戲包體大小。
- 初次加載: 字體圖集越大,初次加載時間越長。
建議: 強烈推薦使用 TextMeshPro 來處理所有文本顯示。它的優點遠遠超過缺點。對于一些極端需要控制包體大小的場景,可以考慮只打包常用的字符集。
3. 字體 Atlas 的生成與管理 (TextMeshPro)
當你使用 TextMeshPro 時,字體 Atlas 的管理變得尤為重要。
a. 生成字體 Atlas:
- 導入字體: 將你的字體文件(.ttf 或 .otf)導入 Unity 項目。
- 創建字體 Asset: 選中字體文件,右鍵
Create > TextMeshPro > Font Asset
。 - 配置字體 Asset:
- 在生成的 Font Asset 文件上,點擊
Open Font Asset Creator
按鈕。 - Source Font: 你的字體文件。
- Font Size: 用于生成字體圖集時采樣的字體大小,越大生成的圖集質量越高,但圖集占用空間越大。通常 90-128 足夠。
- Padding: 字符之間的填充距離,用于防止字符邊緣鋸齒和裁剪。
- Atlas Resolution: 字體圖集的尺寸,通常選擇 2048x2048 或 4096x4096。
- Character Set: 選擇要包含的字符集。
ASCII
: 僅包含基本英文字符。Extended ASCII
: 包含更多歐洲語言字符。Unicode Hex Range
: 自定義 Unicode 范圍,適用于特定語言字符。Custom Characters
: 手動輸入字符。Characters From File
: 從文本文件加載字符列表。- 最常用的是
Characters From File
: 準備一個包含游戲中所有可能用到的中文字符的文本文件,然后導入。這能最大程度地壓縮字體圖集大小,同時保證所有字符可用。
- Render Mode: 通常選擇
Distance Field
(SDF) 以獲得最佳效果。 - 生成: 點擊
Generate Font Atlas
按鈕,然后保存生成的 Font Asset。
- 在生成的 Font Asset 文件上,點擊
b. 字體 Atlas 的管理:
- 字符集管理: 最重要的優化是控制字體 Atlas 中的字符數量。只包含游戲中實際會用到的字符,而不是全部字符。
- 對于中文游戲,需要收集所有文本內容,提取出唯一的字符,然后生成一個字符列表文件。
- 對于多語言游戲,為每種語言或語言組生成獨立的 Font Asset,按需加載。
- 復用 Font Asset: 確保所有使用相同字體的 TextMeshPro 組件都引用同一個 Font Asset,這樣才能最大程度地實現合批。
- 優化圖集尺寸: 在保證清晰度的情況下,選擇最小的
Atlas Resolution
。 - 多個 Font Asset: 如果游戲中有多種風格差異很大的字體,或者某種字體只在特定場景使用,可以創建多個 Font Asset,并按需加載。例如,標題字體一個 Asset,正文字體一個 Asset。
四、圖片資源優化
除了圖集,單個圖片資源的優化也至關重要,它們是構建 UI 的基本塊。
1. 圖片格式與壓縮
這部分與圖集優化中的圖片格式和壓縮原理相同,但針對的是那些不適合打包成圖集或作為 RawImage
使用的獨立圖片。
- 紋理類型 (
Texture Type
):- Sprite (2D and UI): 用于 UI
Image
組件中的Sprite
。 - Texture: 用于
RawImage
組件或 3D 模型的紋理。
- Sprite (2D and UI): 用于 UI
- Read/Write Enabled:
- 默認情況下,
Read/Write Enabled
是關閉的。這意味著 CPU 無法直接訪問紋理數據,從而節省內存。 - 除非你需要在運行時通過腳本讀寫紋理像素(例如生成截圖、進行像素級操作),否則務必保持
Read/Write Enabled
為關閉狀態。 開啟它會使紋理在內存中保留一份 CPU 可讀副本,導致內存占用翻倍。
- 默認情況下,
- Generate Mip Maps:
Mip Maps
是紋理的不同分辨率副本,用于在物體距離攝像機較遠時使用低分辨率的紋理,從而提高渲染效率和消除摩爾紋。- 對于 UGUI 紋理,通常不需要
Mip Maps
。 UI 元素通常是 2D 的,且不會因為距離變化而顯著縮小。開啟Mip Maps
會增加 33% 的內存占用。因此,在 Sprite 和 UI 紋理的 Import Settings 中,請 禁用Generate Mip Maps
。
- Filter Mode:
Point (No Filter)
:最近鄰采樣,像素化效果,用于像素藝術。Bilinear
:雙線性過濾,平滑過渡,用于大多數 UI。Trilinear
:三線性過濾,在Mip Maps
之間平滑過渡,但 UI 不開Mip Maps
,所以選擇Bilinear
即可。
2. 圖片尺寸與分辨率的合理設置
圖片尺寸是影響內存占用和渲染性能的另一個關鍵因素。
- 最小化尺寸: 圖片尺寸應該 剛好滿足 UI 元素在屏幕上顯示的最高分辨率要求。不要使用過大的圖片,然后讓 Unity 縮放。例如,如果一個按鈕圖標在 UI 中最大顯示為 64x64 像素,那么其原始圖片尺寸就應該是 64x64,而不是 256x256。
- 計算方式: 考慮 UI 在不同分辨率設備上的縮放。如果你使用的是
Canvas Scaler
的Scale With Screen Size
模式,你需要根據你設置的 Reference Resolution 和目標分辨率來計算實際渲染尺寸。
- 計算方式: 考慮 UI 在不同分辨率設備上的縮放。如果你使用的是
- 避免非 2 的冪次方: 盡管現代 GPU 對非 2 的冪次方紋理支持良好,但對于一些舊設備或特定壓縮格式,使用 2 的冪次方尺寸(如 128x128, 256x256)仍然是更安全的做法,并且可能在內部處理上更高效。
- 統一分辨率: 盡量在美術資源導出時就統一好圖片的分辨率。例如,如果你的基準分辨率是 1920x1080,那么所有 UI 元素都應該根據這個分辨率來設計其最佳顯示尺寸。
- LOD(Level of Detail)for UI? 盡管 Unity 有 LOD 系統,但它主要用于 3D 模型。對于 UGUI,通過控制圖片尺寸和圖集來達到類似的目的更為實際。例如,對于需要放大的 UI,提供更高分辨率的圖集;對于縮小或背景元素,可以使用較低分辨率的圖集。
3. 避免使用未經優化的圖片資源
- 美術規范: 與美術團隊建立良好的溝通,讓他們了解性能優化的要求。
- 導出格式: 優先導出 PNG(帶 Alpha)或 JPG(無 Alpha)。
- 裁剪透明像素: 確保圖片邊緣沒有多余的透明像素,這會增加不必要的內存占用和 Draw Call。在 Photoshop 中使用
Trim
或Crop
功能。 - 統一尺寸: 如果是系列圖標或按鈕,盡量保持其導出尺寸統一,便于圖集打包。
- 檢查圖片冗余: 項目中是否存在多余的、未使用的圖片資源?使用 Unity 的
Editor
擴展或插件來檢測并刪除它們。 - 利用
.psd
導入: Unity 可以直接導入.psd
文件,并將其切割為 Sprite。這對于美術迭代非常方便,但要確保最終導出到游戲中的圖片是經過優化的。在導入.psd
文件后,通常需要調整其Import Settings
以應用合適的壓縮。
五、Batching(合批)原理與優化
合批是 UGUI 渲染優化的核心,直接影響 Draw Call 數量。
1. 合批條件
Unity 的 UGUI 合批機制主要依賴于以下幾個條件:
- 相同 Canvas: 只有在同一個 Canvas 下的 UI 元素才可能進行合批。
- 相同 Material: 這是最核心的條件。所有參與合批的 UI 元素必須使用 完全相同的 Material 實例。
- 如果 UI 元素的
Material
屬性不同,或者即使材質文件相同但參數被修改導致生成了不同的材質實例,都無法合批。 Image
組件的Color
屬性通常不會破壞合批,因為顏色是通過頂點顏色傳遞給 Shader 的。
- 如果 UI 元素的
- 相同 Texture: 如果 Material 中引用了紋理,那么這些紋理也必須是相同的。
- 這就是為什么圖集如此重要的原因:圖集中的所有 Sprite 都共享同一個大紋理,從而滿足這個條件。
- 渲染順序: UI 元素的渲染順序也至關重要。如果兩個可以合批的元素之間插入了一個無法合批的元素,那么合批就會被中斷。
- Z 軸順序: UGUI 的渲染是基于 Z 軸(Order in Layer, Rect Transform 的 Z 坐標)和 Hierarchy 中的順序。越靠后的 UI 元素越靠前渲染。
- 透明與不透明: 透明元素和不透明元素的渲染批次是分開的。通常不透明元素先渲染,透明元素后渲染。將透明度高的 UI 元素(如半透明背景)放在不透明元素之后,可以提高合批效率。
2. 合批的種類
a. Dynamic Batching(動態合批)
- 原理: Unity 會在 CPU 上將滿足合批條件的小型 Mesh 合并成一個更大的 Mesh,然后一次性提交給 GPU。
- UGLI 中的表現: UGUI 的合批機制就是 Dynamic Batching 的一種特殊形式。
- 限制:
- 合并的頂點數量限制(通常為 300-900 左右,具體取決于 Unity 版本和平臺)。如果合并后的 Mesh 頂點數量超過這個限制,就會分成多個批次。
- Mesh 屬性:如果 Mesh 的法線、切線、UV0 以外的 UV 通道、頂點顏色等屬性不同,也可能無法合批。但 UGUI 的 Mesh 通常比較簡單,很少會遇到這些限制。
b. Static Batching(靜態合批)
- 原理: 在構建游戲時,將標記為靜態的對象合并成一個或幾個大 Mesh。
- UGUI 中適用性: 不適用于 UGUI。 UGUI 元素通常是動態的(需要響應交互、動畫等),不適合標記為
Static
。將 UI 元素標記為Static
可能導致意外行為或無法進行合批。
3. 如何通過合理組織 UI 元素來促進合批
減少 Draw Call 的關鍵在于盡可能讓更多的 UI 元素滿足合批條件。
-
統一 Material:
- 確保所有需要合批的 UI 元素使用相同的 Material。默認的 UGUI
Image
和Text
組件都使用UI/Default
Shader 和 Material。 - 如果你自定義了 UI Shader,確保使用該 Shader 的所有 UI 元素都使用同一個 Material 實例。
- 確保所有需要合批的 UI 元素使用相同的 Material。默認的 UGUI
-
使用 Sprite Atlas: 這是最關鍵的一步,保證所有
Image
組件引用來自同一個圖集的Sprite
。 -
調整 UI 層級與渲染順序:
- 將能合批的元素放在一起: 在 Hierarchy 窗口中,將那些可以合批的 UI 元素(例如,同一張圖集的不同圖標)盡量放在同一個 Canvas 下,并且在層級上盡可能靠近。
- 避免交叉: 如果 A、B、C 三個 UI 元素,A 和 C 可以合批,B 無法合批。如果層級是 A -> B -> C,那么 A 和 C 就無法合批,會產生兩個 Draw Call。理想的層級應該是 A -> C -> B,這樣 A 和 C 就可以合批,只產生一個 Draw Call。
- 透明度:
- 避免半透明與不透明 UI 元素交錯: 通常,不透明的 UI 元素先渲染,半透明的 UI 元素后渲染。如果它們交錯排列,會導致 Draw Call 頻繁切換,從而打斷合批。
- 最佳實踐: 將所有不透明的 UI 元素放在一個層級或 Canvas 下,然后將所有半透明的 UI 元素放在另一個層級或 Canvas 下。
-
Canvas 的切割與分層(下篇會詳細講解):
- 將一個大 Canvas 切割成多個小 Canvas,可以更精細地控制 UI 元素的重建范圍。
- 同時,分層后的 Canvas 也更容易進行 Draw Call 的優化,因為每個 Canvas 都可以獨立地進行合批。
-
減少 Mask 組件的使用:
Mask
組件(包括Rect Mask 2D
)會打斷合批。因為Mask
會修改渲染狀態(裁剪范圍),導致其內部和外部的元素無法合批。- 盡量減少
Mask
的使用,或者只在必要的地方使用。對于簡單的裁剪需求,可以考慮使用Image
的Type
為Filled
或Sliced
來實現。
-
善用 Unity Profiler 和 Frame Debugger:
- Profiler: 在 Profiler 的
CPU Usage
和GPU Usage
模塊中,你可以看到UI.Render
的開銷,以及 Draw Call 的數量。 - Frame Debugger: 這是分析 Draw Call 和合批情況的利器。
- 打開
Window > Analysis > Frame Debugger
。 - 在 Frame Debugger 中,你可以一步步查看每個 Draw Call 渲染了哪些對象,以及 Draw Call 為什么被中斷(例如
Material changed
,Shader changed
,Texture changed
等)。通過分析Frame Debugger
,你可以準確找出導致 Draw Call 增加的原因,并有針對性地進行優化。
- 打開
- Profiler: 在 Profiler 的
六、總結與展望
本篇文章我們深入探討了 UGUI 渲染的基礎原理,并詳細講解了如何從 資源管理 的角度進行優化,包括:
- 理解 UGUI 的 渲染管線 和 Draw Call 的概念。
- 通過 Sprite Atlas (圖集) 大幅減少 Draw Call,并學會選擇合適的打包方式(推薦
Sprite Packer
)和紋理壓縮格式。 - 強調 TextMeshPro 在字體渲染上的巨大優勢,以及如何優化其 字體 Atlas。
- 學會優化 圖片資源 的尺寸、格式和導入設置,避免不必要的內存開銷。
- 深入理解 合批(Batching) 的條件,并掌握通過合理組織 UI 元素來促進合批的方法。
- 學會使用 Unity Profiler 和 Frame Debugger 來分析和定位 Draw Call 問題。
這些基礎知識和優化策略是 UGUI 性能優化的基石。掌握它們,你就能有效地減少游戲在 UI 渲染上的性能開銷。
在下一篇文章中,我們將進一步深入,聚焦于 Canvas 的重建機制,以及如何通過 Canvas 分層 和 UI 元素管理 來實現更高級別的性能優化。
UGUI 性能優化系列:第一篇——基礎優化與資源管理
UGUI 性能優化系列:第二篇——Canvas 與 UI 元素管理