Community程式設計與開發github.com

MericKarabulut/unity-ui-toolkit

使用 Unity UI Toolkit (UXML + USS + C# 控制器) 為任何類型與平台建置及除錯遊戲 UI,包含 HUD、生命/彈藥/體力條、互動提示、傷害暗角、選單、暫停畫面、分數計數器、過場畫面等。

unity-ui-toolkit 是什麼?

此技能專注於以 Unity UI Toolkit 開發遊戲使用者介面,無論是 HUD、選單或各種遊戲內元素。其核心方法是將結構 (UXML)、樣式 (USS) 與行為 (C# 控制器) 明確分離,每個 UI 元件均由單一控制器、UXML 與 USS 組成,避免大型控制器。它同時支援較新的 PanelRenderer 工作流程與傳統的 UIDocument 方式,並提供明確的架構規則,例如透過事件驅動而非輪詢、將動畫與顏色保留在 USS 中,以及使用 CSS 類別切換狀態。此技能涵蓋了從初始設定(包含風格指南建立、佈局避坑與面板設定)到進階技巧(如區段元件、脈衝動畫與暗角效果)的完整流程。它還包含了針對不同平台(街機、復古、手機、主機/遊戲手把、VR/世界空間)的建議,以及一個實用的疑難排解章節,列舉常見錯誤及其解決方案,尤其能幫助開發者節省時間。通用於任何提及 Unity UI、UI Toolkit、UXML、USS、PanelRenderer、UIDocument、UI Builder、HUD、血條、彈藥計數器或選單等情境。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/MericKarabulut/claude-skills/tree/HEAD/skills/unity-ui-toolkit

Installed? Explore more 程式設計與開發 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

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 版本,而不是再次猜測。不要承諾裝置行為或效能數據;建立結構並要求使用者進行測試。

相關技能