Community编程与开发github.com

MericKarabulut/unity-ui-toolkit

在Unity中使用UI Toolkit(UXML + USS + C#控制器)构建和调试游戏UI,适用于任何类型和平台——HUD、生命/弹药/体力条、交互提示、伤害晕影、菜单、暂停屏幕、计分器、过场画面。涵盖较新的PanelRenderer工作流(无UIDocument)和经典UIDocument,编写游戏专属UI风格指南、无需像素猜想的布局,以及修复UI Builder / USS / UXML问题。当用户提到Unity UI、UI Toolkit、UXML、USS、PanelRenderer、UIDocument、UI Builder、HUD、血条、弹药计数器或菜单,或希望重新设计、修复或添加任何游戏内界面时使用此技能,即使他们从未提及“UI Toolkit”。

unity-ui-toolkit 是什么?

该技能为Unity开发者提供了一套完整的UI Toolkit实战方案,专注于使用UXML(结构)、USS(样式)和轻量级C#控制器(行为)来构建游戏界面。它强调“风格指南优先”的开发理念,通过先建立一致的设计规范再逐步构建组件,有效避免界面混乱。技能核心架构要求每个UI部分独立为“一个控制器+一个UXML+一个USS”的模块,并严格遵循“推送而非轮询”的事件驱动模式——控制器仅监听游戏事件并切换USS类,所有颜色、大小和动画效果均在USS中定义,使得UI逻辑与表现分离,更易维护。 支持的平台包括PC/主机(游戏手柄)、移动端和VR/世界空间,技能文档中针对各平台提供了具体优化建议,如移动端的触摸目标尺寸和安全区域处理,以及主机的焦点管理和TV安全边距。技能还深入讲解了布局技巧(如使用弹性容器代替绝对定位)、USS状态管理与过渡动画(通过C#切换类名实现)、以及PanelRenderer工作流中`RegisterUIReloadCallback`的正确用法——这是避免空引用异常的关键。 此外,技能附带一个详尽的故障排查表,覆盖了UI Builder无法打开资产、元素样式丢失、控制器找不到元素等常见问题,并提供了完整的工作流代码模板和可复用片段(如分段显示、脉冲动画、事件闪烁和全屏晕影效果)。开发者需要准备游戏UI监听的脚本、目标平台和主题信息,但技能会主动询问缺失的部分,并以用户的语言回复。最终输出包括完整的UXML、USS、控制器代码、设置步骤和风格指南条目,显著缩短了Unity UI Toolkit的开发调试周期。

兼容平台~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 和边距流动。
  • 当布局不可跳跃时使用 opacity + 过渡隐藏;仅当空间应折叠时使用 display:none。
  • 在数字标签上设置固定 width,使 9 到 10 到 100 不会移动相邻元素。
  • 盒子是 border-box:段行宽度 = count x (width + margin) - margin。通过相同的左边缘和计算宽度对齐相关小部件。
  • Panel Settings: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: elements not found in UXML (check 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 时至少显示 1 个已填充: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 秒过渡,容器受欢迎。复古/像素:像素字体、整数缩放、点过滤纹理、精灵 9 切片边框。移动端:触摸目标约 44-48 dp、设备安全区域作为根内边距(检查用户 Unity 版本的当前 API)、无悬停状态、较大的最小字体、在真机上测试。主机/手柄:可聚焦元素带有醒目的 :focus 样式、显式的初始焦点、电视安全边距(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、隐藏字符)。修复,然后右键单击文件 > 重新导入。读取通用错误上方的控制台消息
元素无样式<Style> 缺失或路径错误。在 UI Builder 中:StyleSheets > + > Add Existing USS(写入完整的 project://database/...guid=...)
编辑后所有内容消失UXML 损坏,或 Source Asset 引用在重命名后失效。修复,重新导入,重新分配。用根元素上的 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 版本,而不是再次猜测。不要承诺设备行为或性能数据;构建结构并让用户测试。

相关技能