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 版本,而不是再次猜测。不要承诺设备行为或性能数据;构建结构并让用户测试。