Unity UI Toolkit 遊戲 UI
結構在 UXML 中、外觀在 USS 中、行為在小型 C# 控制器中,這些控制器會對遊戲事件做出反應。從一個真實專案(恐怖射擊遊戲 HUD)中提煉而成;其中的疑難排解清單最有可能節省時間。
1. 開始撰寫程式碼之前
僅詢問缺少的部分,一次簡短批次完成:
- UI API。
[RequireComponent(typeof(PanelRenderer))]+RegisterUIReloadCallback= PanelRenderer 工作流程(無UIDocument、無rootVisualElement)。UIDocument+rootVisualElement= 傳統方式。根據貼上的程式碼推斷;如果不清楚,請要求一個可運作的控制器及其 UXML。切勿混用兩者(會導致 NullReferenceException)。使用者提供的可運作檔案勝過你對 API 的記憶。 - 類型/主題(附螢幕截圖或調色盤)、平台與輸入、參考解析度/長寬比。
- UI 要監聽的遊戲腳本。 要求這些腳本,閱讀真正的公開事件/屬性,並綁定到這些內容。如果事件缺失,請提出對遊戲腳本的最小新增。
- 以使用者的語言回覆;以現有腳本使用的語言撰寫程式碼註解。
2. 先建立風格指南
一致性是使用者最先注意到的。在建立許多部分之前:提出一個簡短的指南,先建立一個小部分,讓使用者反應,然後將他們的反饋轉化為規則(「不要有背景框」、「更緊湊」)。將其儲存在專案中(例如 Assets/UI/UI_STYLE_GUIDE.md)並告訴使用者將其貼到未來的 UI 請求中。當部分或規則變更時更新它。內容:設計語言(色調、容器、密度、動態)、調色盤表格(文字、低調、警告、關鍵、陰影)、排版(每種角色的尺寸、陰影規則)、佈局(區域、邊距、面板設定)、USS 命名與狀態類別、程式碼架構規則、UXML 注意事項、現有部分清單、新部分的檢查清單。
3. 架構
- 一個部分 = 一個控制器 + 一個 UXML + 一個 USS,並有一個資料來源。沒有大型控制器。
- 必須放在一起的部分共用一個 UXML/flex 容器(兩個控制器可以共用一個 GameObject/PanelRenderer;每個控制器透過名稱找到自己的元素)。
- 控制器僅負責尋找元素、設定文字/數值以及切換 USS 類別。顏色、尺寸和動畫都放在 USS 中。
- PanelRenderer:根節點透過
RegisterUIReloadCallback(OnUIReload)到達(在 OnEnable 中註冊,在 OnDisable 中取消註冊)。它在 UI 準備就緒時以及每次重新載入時觸發,因此所有Q<T>("name")查找、快取、事件綁定和狀態重置都在此進行。在Update()中對元素存在與否進行空值防護。 - 遊戲事件可能在 UI 存在之前觸發:將最新值快取在欄位中,並在
OnUIReload結束時重新套用。 - 玩家/遊戲物件可能較晚生成:使用序列化參考,否則使用
FindFirstObjectByType(約 0.5 秒節流),然後綁定;在 OnDisable 中取消訂閱。 - 推送,不要輪詢。訂閱事件並僅在變更時寫入。僅輪詢沒有事件的值(持續變化的體力、重新裝填的布林值),在變更時切換類別。不要每幀進行字串建構。
- 在程式碼中一次建立重複元素(點狀指示器、區段);僅在其來源變更時重建;在重新載入時清除快取的列表。
- 計時器/脈衝:
element.schedule.Execute(...).Every(ms)在建立時暫停,僅在需要時恢復。一次性閃爍:.StartingIn(ms)。 - HUD 元素:所有元素都設定
picking-mode="Ignore",沒有可聚焦的控制項。選單是例外。 - 檔案名稱 = MonoBehaviour 類別名稱。
4. 佈局(大部分視覺錯誤的來源)
- 永遠不要使用個別猜測的像素偏移來放置相關部分:過去曾因盲目估計
bottom而導致兩個絕對定位面板重疊。將它們放在一個 flex 容器中,並僅定位該容器。 - 每個螢幕區域使用一個絕對定位的容器;子元素使用 flex 和邊距流動。
- 當佈局不應跳動時,使用
opacity+ 過渡來隱藏;僅在需要空間摺疊時才使用display:none。 - 在數字標籤上使用固定
width,以防止 9 到 10 到 100 時移動相鄰元素。 - 盒子為 border-box:區段行寬度 =
數量 x (寬度 + 邊距) - 邊距。透過相同的左邊緣和計算寬度來對齊相關小部件。 - 面板設定:Scale With Screen Size(例如 1920x1080,匹配 0.5);對於 HUD,關閉 Clear Color 和 Clear Depth Stencil。使用 PanelRenderer 的 Sort Order 來分層部分(全螢幕暗角在文字下方)。
5. USS 與狀態
- 狀態 = 從 C# 切換的類別(
EnableInClassList);動畫 = USS 過渡。永遠不要在程式碼中設定顏色或動畫。 - 根據意義命名狀態:
--low、--critical、--hidden、--hit、--pulse、--unavailable。對於新部分,偏好使用 BEM。 - 覆寫規則(已消耗的區段、閃爍)放在它們所覆蓋的規則之後;同等特定性,最後一個勝出。
- 過渡使用目標狀態的持續時間/延遲:
.x--hidden { opacity:0; transition-duration:.5s; transition-delay:.9s; }延遲淡出並立即淡入;.x--flash { transition-duration:.05s; }快速閃現,以基本速度淡出。 - 沒有背景的浮動文字需要
text-shadow。不使用text-transform(在 C# 中使用大寫);不使用徑向漸層(參見暗角)。 - 對於可重新換膚的 UI,將顏色/尺寸放在根類別上的 USS 變數中(
--c-text,使用方式為var(--c-text));檢查使用者的 Unity 版本是否支援此功能。
6. 控制器骨架 (PanelRenderer)
[RequireComponent(typeof(PanelRenderer))]
public class PartController : MonoBehaviour
{
[SerializeField] private SourceComponent source; // 空白 = 運行時尋找
private PanelRenderer _pr; private VisualElement _block; private Label _value;
private bool _bound; private float _v = -1f; private int _shown = -1; private float _nextFind;
private void Awake() => _pr = GetComponent<PanelRenderer>();
private void OnEnable() { _pr.RegisterUIReloadCallback(OnUIReload); TryBind(); }
private void OnDisable() { _pr.UnregisterUIReloadCallback(OnUIReload); Unbind(); _block = null; _value = null; }
private void Update()
{
if (_bound || Time.unscaledTime < _nextFind) return;
_nextFind = Time.unscaledTime + 0.5f; TryBind();
}
private void TryBind()
{
if (_bound) return;
if (source == null) source = FindFirstObjectByType<SourceComponent>();
if (source == null) return;
source.OnChanged += OnChanged; _bound = true;
_v = source.Current; Apply(); // 拉取一次當前狀態
}
private void Unbind() { if (_bound && source != null) source.OnChanged -= OnChanged; _bound = false; }
private void OnUIReload(PanelRenderer r, VisualElement root)
{
_block = root.Q<VisualElement>("part"); _value = root.Q<Label>("part-value");
if (_block == null || _value == null) { Debug.LogError("Part: 在 UXML 中找不到元素(檢查 Source Asset)。", this); _block = null; return; }
_shown = -1; Apply(); // 新樹:從快取強制重寫
}
private void OnChanged(float v) { _v = v; Apply(); }
private void Apply()
{
if (_block == null || _v < 0f) return;
int n = Mathf.CeilToInt(_v);
if (n != _shown) { _shown = n; _value.text = n.ToString(); } // 僅在變更時寫入
_block.EnableInClassList("part--low", _v <= 0.3f);
}
}
7. 可重複使用的程式碼片段
- 區段/點狀指示器: 在
OnUIReload中,row.Clear(); list.Clear();然後新增 N 個VisualElement{pickingMode=Ignore}並帶有類別seg;變更時list[i].EnableInClassList("seg--spent", i >= filled)。當值大於 0 時至少顯示一個已填充:filled = v<=0 ? 0 : Clamp(CeilToInt(v*N-0.001f),1,N)。 - 脈衝:
_pulse = _block.schedule.Execute(() => _block.ToggleInClassList("part--pulse")).Every(400); _pulse.Pause();進入狀態時恢復,離開時暫停 + 移除類別;追蹤_pulsing布林值並在重新載入時重置。 - 事件觸發閃爍:
var b=_block; b.AddToClassList("part--hit"); b.schedule.Execute(()=>b.RemoveFromClassList("part--hit")).StartingIn(150); - 暗角: 在 Awake 中一次生成 128x128
Texture2D(RGBA32, Clamp, Bilinear,HideFlags.HideAndDontSave),其中 alpha = 從中心計算的SmoothStep(0,1,InverseLerp(inner,outer,dist/1.4142)),RGB 為白色;在 OnUIReload 中設定_el.style.backgroundImage = new StyleBackground(tex);在 OnDestroy 中銷毀。USS:全螢幕絕對定位元素,opacity:0,-unity-background-scale-mode: stretch-to-fill,-unity-background-image-tint-color用於顏色,類別--low/--critical/--pulse/--flash設定透明度(flash 最後,持續時間短)。Sort Order 低於文字部分。
8. 類型與平台
架構在不同類型間相同;風格指南和輸入模型會改變。街機:大而粗的數字、向上計數、縮放彈跳、連擊光暈、強而有力的 0.08-0.25 秒過渡、歡迎使用容器。復古/像素:像素字體、整數縮放、點過濾紋理、精靈九宮格邊框。手機:觸控目標約 44-48 dp、裝置安全區域作為根節點內邊距(檢查當前 Unity 版本的 API)、沒有懸停狀態、較大的最小字體、在真實裝置上測試。主機/遊戲手把:具有強 :focus 樣式的可聚焦元素、明確的初始焦點、TV 安全邊距(3-5%)、較大的文字、可隨活動裝置切換的字形。VR/世界空間:取決於 Unity 版本,先查詢。這些平台要點是合理的通用做法,但未在真實專案中測試:請說明並要求使用者在裝置上驗證。將輸入標籤(按鍵名稱或字形)保留在控制器讀取的資料中,而不是硬編碼在 UXML 中。
9. 交付一個部分
按順序:完整的 UXML、完整的 USS、完整的控制器、設定步驟(GameObject、PanelRenderer + 共享 Panel Settings + Source Asset、Sort Order、要填寫的參考、在 Play 模式下會看到什麼)、調整旋鈕(哪個 USS 值改變尺寸/位置/顏色/閾值)以及一個簡短的風格指南條目。變更現有檔案時,如果大部分檔案變更,請提供整個檔案(部分貼上曾導致真實錯誤)。
10. 疑難排解
| 症狀 | 原因 / 修復 |
|---|---|
rootVisualElement / GetComponent<UIDocument>() 的 NullReference | 程式碼使用 UIDocument 但物件具有 PanelRenderer。使用 RegisterUIReloadCallback,在 OnUIReload 中綁定 |
| 控制器記錄「找不到元素」 | 錯誤的 Source Asset、與 Q<>() 的名稱不符、或無效的 UXML |
| UI Builder「無法開啟資產」 | 無效的 UXML(重複的 </ui:UXML>、錯誤的 <Style>、佔位符 GUID、隱藏字元)。修復,然後在檔案上按右鍵 > Reimport。閱讀泛型錯誤訊息上方的控制台訊息 |
| 未套用樣式的元素 | <Style> 缺失或路徑錯誤。在 UI Builder 中:StyleSheets > + > Add Existing USS(寫入完整的 project://database/...guid=...) |
| 編輯後所有東西都消失了 | UXML 損壞,或 Source Asset 參考在重新命名後失效。修復,Reimport,重新指派。使用 background-color: rgba(255,0,0,.3) 在根節點上測試 |
| 部分錯位或與另一個部分重疊 | 陳舊的 GameObject 仍在繪製舊的 UXML,或使用了分開的絕對偏移。刪除舊物件,將相鄰部分合併到一個容器中 |
| Game 視圖中出現佔位符文字 | 編輯模式會繪製 UXML;按下 Play |
| 無法新增腳本 | 檔案名稱 != 類別名稱。在 Unity Project 視窗內重新命名,以便 .meta 跟隨 |
無效的修復:樣式表標籤是 <Style src="..."/>,沒有 ui: 前綴(<ui:Style> 會靜默忽略);永遠不要發明或留下佔位符 GUID;使用標準標頭 <ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="False">。
11. 在未看到編輯器的情況下工作
對於佈局問題,要求提供螢幕截圖並仔細閱讀;這解決了猜測無法解決的錯誤。簡短承認自己的錯誤並提供完整的檔案來修復。對於編譯錯誤,要求提供確切的文字和 Unity 版本,而不是再次猜測。不要承諾裝置行為或效能數據;建立結構並要求使用者進行測試。