关于 unity-ui
面向 Unity UI 开发的参考技能,介绍 UI Toolkit、uGUI 和 IMGUI 的适用场景与实现方式。适用于构建运行时界面和编辑器扩展,并涵盖布局、事件、数据绑定及文本渲染。
主要功能
- 01比较三种 Unity UI 系统
- 02使用 UXML、USS 与 C# 构建 UI Toolkit 界面
- 03介绍事件传播、数据绑定和交互操控器
- 04覆盖 uGUI Canvas、布局与锚点
- 05提供 UI 性能与事件清理建议
- 06推荐使用 TextMeshPro 渲染文本
适用场景
- 01为新项目选择运行时 UI 框架
- 02构建 Unity 编辑器窗口和 Inspector
- 03为 uGUI 项目规划渐进迁移
- 04实现菜单、HUD 与世界空间界面
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nice-wolf-studio/ unity-claude-skills --skill unity-ui安装后在 Claude Code 中输入/unity-ui确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/textmeshpro.mdugui-legacy.mdui-data-binding.mdui-toolkit-guide.md详细介绍
Unity UI 系统
Unity 提供三种 UI 框架。对于新项目,推荐使用 UI Toolkit。 uGUI 仍受支持,适用于旧项目和某些运行时场景。IMGUI 严格限用于 Editor 工具和调试。
UI 系统对比
| 特性 | UI Toolkit | uGUI(Canvas) | IMGUI |
|---|---|---|---|
| 推荐用于新项目 | 是 | 否(旧版) | 否 |
| 运行时游戏 UI | 是 | 是 | 不推荐 |
| Editor 扩展 | 是 | 否 | 是 |
| 实现方式 | 受 Web 启发(UXML + USS + C#) | GameObject + Component | 纯代码(OnGUI) |
| 布局系统 | Flexbox(Yoga) | RectTransform + 锚点 | 即时模式 |
| 样式 | USS 样式表 | 按组件设置属性 | GUIStyle / GUISkin |
| 可视化编辑 | UI Builder | Scene View | 无 |
| 性能 | 优化的保留模式 | Canvas 批处理 | 每帧重绘 |
| 数据绑定 | SerializedObject + 运行时绑定 | 通过代码手动处理 | 通过代码手动处理 |
| 世界空间 UI | 支持 | Canvas World Space 模式 | 不支持 |
| 输入集成 | 指针/键盘事件 | EventSystem + Raycasters | Event.current |
选择指南:
- 新建运行时 UI(菜单、HUD、物品栏)--> UI Toolkit
- 新建 Editor 窗口/检查器 --> UI Toolkit
- 使用 uGUI 的现有项目 --> 继续使用 uGUI,逐步迁移
- 在 Editor 中快速创建调试叠加层 --> IMGUI
- 3D 对象上的世界空间 UI --> 可使用 UI Toolkit 或 uGUI World Space Canvas
UI Toolkit
UI Toolkit 是 Unity 的现代 UI 框架,受 Web 技术启发。它使用 UXML 定义结构、使用 USS 设置样式,并使用 C# 编写逻辑。
核心架构
UIDocument (MonoBehaviour on GameObject)
--> VisualTreeAsset (.uxml) -- defines structure
--> StyleSheet (.uss) -- defines appearance
--> C# script -- defines behavior
所有 UI 元素都继承自 VisualElement。可通过 rootVisualElement 访问根元素。
UXML 结构
UXML 以声明式方式定义 UI 层级结构:
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
<ui:Style src="MainMenu.uss" />
<ui:VisualElement name="root-container" class="container">
<ui:Label text="Game Menu" class="title" />
<ui:Button text="Play" name="play-button" class="menu-btn" />
<ui:Button text="Settings" name="settings-button" class="menu-btn" />
<ui:Toggle label="Fullscreen" name="fullscreen-toggle" />
<ui:Slider label="Volume" low-value="0" high-value="100" name="volume-slider" />
<ui:TextField label="Player Name" name="player-name" />
</ui:VisualElement>
</ui:UXML>
要点:
xmlns:ui="UnityEngine.UIElements"是标准命名空间- 使用
<ui:Style src="..." />引用 USS 文件 - 使用
name属性供 C# 查询,使用class设置 USS 样式 - 可导入模板:
<ui:Template src="other.uxml" name="other" />
USS 样式
USS 使用类似 CSS 的语法,并扩展了 Unity 专用功能。所有 USS 属性都使用前缀 -unity- 来表示 Unity 专用功能。
/* Type selector */
Button {
background-color: #2D2D2D;
border-radius: 4px;
padding: 8px 16px;
-unity-font-style: bold;
}
/* Class selector */
.menu-btn {
width: 200px;
height: 40px;
margin: 4px 0;
font-size: 16px;
color: #FFFFFF;
}
/* Name selector */
#play-button {
background-color: #4CAF50;
}
/* Pseudo-class */
.menu-btn:hover {
background-color: #555555;
scale: 1.05 1.05;
}
.menu-btn:active {
background-color: #333333;
}
.menu-btn:disabled {
opacity: 0.5;
}
/* Descendant selector */
.container > Label {
-unity-text-align: middle-center;
}
/* USS variables */
:root {
--primary-color: #4CAF50;
--font-large: 24px;
}
.title {
color: var(--primary-color);
font-size: var(--font-large);
}
选择器类型: 类型(Button)、名称(#name)、类(.class)、通用(*)、后代(A B)、子代(A > B)、多重(A.class)、伪类(:hover、:active、:focus、:disabled、:checked)。
布局基于 Flexbox: 使用 flex-direction、flex-grow、flex-shrink、justify-content、align-items、align-self、flex-wrap。默认方向为 column。
C# 设置与交互
using UnityEngine;
using UnityEngine.UIElements;
public class MainMenuController : MonoBehaviour
{
[SerializeField] private UIDocument uiDocument;
private Button playButton;
private Button settingsButton;
private Toggle fullscreenToggle;
private Slider volumeSlider;
private TextField playerNameField;
private void OnEnable()
{
var root = uiDocument.rootVisualElement;
// Query single elements by name
playButton = root.Q<Button>("play-button");
settingsButton = root.Q<Button>("settings-button");
fullscreenToggle = root.Q<Toggle>("fullscreen-toggle");
volumeSlider = root.Q<Slider>("volume-slider");
playerNameField = root.Q<TextField>("player-name");
// Register click callbacks
playButton.RegisterCallback<ClickEvent>(OnPlayClicked);
settingsButton.RegisterCallback<ClickEvent>(OnSettingsClicked);
// Register value change callbacks
fullscreenToggle.RegisterValueChangedCallback(OnFullscreenChanged);
volumeSlider.RegisterValueChangedCallback(OnVolumeChanged);
// Query multiple elements by class
var allButtons = root.Query<Button>(className: "menu-btn").ToList();
}
private void OnDisable()
{
playButton.UnregisterCallback<ClickEvent>(OnPlayClicked);
settingsButton.UnregisterCallback<ClickEvent>(OnSettingsClicked);
fullscreenToggle.UnregisterValueChangedCallback(OnFullscreenChanged);
volumeSlider.UnregisterValueChangedCallback(OnVolumeChanged);
}
private void OnPlayClicked(ClickEvent evt) => Debug.Log("Play clicked");
private void OnSettingsClicked(ClickEvent evt) => Debug.Log("Settings clicked");
private void OnFullscreenChanged(ChangeEvent<bool> evt)
{
Screen.fullScreen = evt.newValue;
}
private void OnVolumeChanged(ChangeEvent<float> evt)
{
AudioListener.volume = evt.newValue / 100f;
}
}
以编程方式创建 UI(无需 UXML):
private void CreateUIFromCode()
{
var root = uiDocument.rootVisualElement;
var container = new VisualElement();
container.AddToClassList("container");
root.Add(container);
var label = new Label("Created from C#");
container.Add(label);
var button = new Button(() => Debug.Log("Clicked")) { text = "Click Me" };
button.name = "dynamic-button";
container.Add(button);
}
事件系统
UI Toolkit 事件分两个阶段传播:
- 向下传递 -- 从根元素传递到目标元素
- 向上冒泡 -- 从目标元素传回根元素
// Default: bubble-up phase
element.RegisterCallback<PointerDownEvent>(OnPointerDown);
// Trickle-down phase (parent reacts before children)
element.RegisterCallback<PointerDownEvent>(OnPointerDown, TrickleDown.TrickleDown);
// Pass custom data to callbacks
element.RegisterCallback<ClickEvent, string>(OnClickWithData, "my-data");
// Set value without triggering ChangeEvent
myControl.SetValueWithoutNotify(newValue);
数据绑定
SerializedObject 绑定(Editor / 检查器 UI):
// In UXML: <ui:IntegerField binding-path="m_Health" label="Health" />
// In C#:
var healthField = new IntegerField("Health") { bindingPath = "m_Health" };
root.Add(healthField);
root.Bind(new SerializedObject(targetComponent));
可绑定对象:MonoBehaviour、ScriptableObject、Unity 原生类型、基元类型。
只能绑定 INotifyValueChanged 元素的 value 属性。
运行时绑定可将普通 C# 对象连接到 UI 控件,在 Editor 和运行时环境中均可使用。在元素上设置数据源,并定义绑定模式以指定同步方向。
参见:references/ui-data-binding.md
操作器
操作器封装事件处理逻辑,将交互与 UI 代码分离:
public class DragManipulator : PointerManipulator
{
private Vector3 startPosition;
private bool isDragging;
public DragManipulator(VisualElement target)
{
this.target = target;
}
protected override void RegisterCallbacksOnTarget()
{
target.RegisterCallback<PointerDownEvent>(OnPointerDown);
target.RegisterCallback<PointerMoveEvent>(OnPointerMove);
target.RegisterCallback<PointerUpEvent>(OnPointerUp);
}
protected override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback<PointerDownEvent>(OnPointerDown);
target.UnregisterCallback<PointerMoveEvent>(OnPointerMove);
target.UnregisterCallback<PointerUpEvent>(OnPointerUp);
}
private void OnPointerDown(PointerDownEvent evt)
{
startPosition = evt.position;
isDragging = true;
target.CapturePointer(evt.pointerId);
evt.StopPropagation();
}
private void OnPointerMove(PointerMoveEvent evt)
{
if (!isDragging) return;
var delta = evt.position - startPosition;
target.transform.position += (Vector3)delta;
startPosition = evt.position;
}
private void OnPointerUp(PointerUpEvent evt)
{
isDragging = false;
target.ReleasePointer(evt.pointerId);
evt.StopPropagation();
}
}
// Usage:
myElement.AddManipulator(new DragManipulator(myElement));
内置操作器类: Manipulator(基类)、PointerManipulator、MouseManipulator、Clickable、ContextualMenuManipulator、KeyboardNavigationManipulator。
自定义控件
// Unity 6+ recommended pattern: [UxmlElement] attribute (replaces deprecated UxmlFactory/UxmlTraits)
[UxmlElement]
public partial class HealthBar : VisualElement
{
[UxmlAttribute]
public float MaxHealth { get; set; } = 100f;
private VisualElement fillBar;
private float currentHealth;
public float CurrentHealth
{
get => currentHealth;
set
{
currentHealth = Mathf.Clamp(value, 0, MaxHealth);
fillBar.style.width = Length.Percent(currentHealth / MaxHealth * 100f);
}
}
public HealthBar()
{
AddToClassList("health-bar");
fillBar = new VisualElement();
fillBar.AddToClassList("health-bar__fill");
Add(fillBar);
}
}
uGUI / Canvas 系统(旧版)
uGUI 是 Unity 较早的基于 GameObject 的 UI 系统。它使用 Canvas、RectTransform 和 EventSystem。
Canvas 渲染模式
| 模式 | 描述 | 使用场景 |
|---|---|---|
| Screen Space - Overlay | 渲染在所有内容之上,并随屏幕缩放 | 标准 HUD、菜单 |
| Screen Space - Camera | 由指定摄像机渲染,受透视影响 | 带深度效果的 UI |
| World Space | 将 Canvas 作为场景中的 3D 对象 | 游戏内显示、VR UI |
核心组件
视觉: Text、Image、RawImage 交互: Button、Toggle、ToggleGroup、Slider、Scrollbar、Dropdown、InputField、ScrollRect 布局: HorizontalLayoutGroup、VerticalLayoutGroup、GridLayoutGroup、ContentSizeFitter、AspectRatioFitter、LayoutElement
RectTransform 和锚点
所有 uGUI 元素都使用 RectTransform,而不是 Transform。锚点定义元素相对于父级的位置:
- Anchor Min/Max 使用分数表示(0.0 = 左侧/底部,1.0 = 右侧/顶部)
- 锚点重合:固定位置(Pos X、Pos Y、Width、Height)
- 锚点分离:拉伸(Left、Right、Top、Bottom 内边距)
- Pivot:用于旋转和缩放的中心点
uGUI 示例
using UnityEngine;
using UnityEngine.UI;
public class MenuManager : MonoBehaviour
{
[SerializeField] private Button playButton;
[SerializeField] private Slider volumeSlider;
[SerializeField] private Toggle muteToggle;
private void OnEnable()
{
playButton.onClick.AddListener(OnPlayClicked);
volumeSlider.onValueChanged.AddListener(OnVolumeChanged);
muteToggle.onValueChanged.AddListener(OnMuteToggled);
}
private void OnDisable()
{
playButton.onClick.RemoveListener(OnPlayClicked);
volumeSlider.onValueChanged.RemoveListener(OnVolumeChanged);
muteToggle.onValueChanged.RemoveListener(OnMuteToggled);
}
private void OnPlayClicked() => Debug.Log("Play");
private void OnVolumeChanged(float value) => AudioListener.volume = value;
private void OnMuteToggled(bool muted) => AudioListener.pause = muted;
}
绘制顺序
元素按照 Hierarchy 顺序渲染:先绘制第一个子元素,最后一个子元素绘制在最上层。使用 Transform.SetAsFirstSibling()、SetAsLastSibling()、SetSiblingIndex() 调整顺序。
反模式
| 反模式 | 问题 | 正确做法 |
|---|---|---|
| 到处使用内联样式 | 每个元素都会产生内存开销 | 使用 USS 文件共享样式 |
在复杂 USS 中使用通用选择器(A * B) | 大规模使用时选择器性能较差 | 使用 BEM 类命名和子选择器 |
在包含许多后代元素的元素上大量使用 :hover | 鼠标移动会导致整个层级失效 | 仅在叶节点元素上使用 :hover |
在 CreateInspectorGUI() 中调用 Bind() | 重复绑定,返回后会自动绑定 | 交由自动绑定处理,或仅对手动创建的 UI 调用 Bind |
| 每帧重建整个 UI | 无法发挥保留模式的优势 | 仅更新发生变化的元素 |
| 在动态内容中使用多个 Canvas(uGUI) | 任一子元素发生变化都会触发 Canvas 重建批次 | 将静态和动态 UI 拆分到不同的 Canvas 中 |
| 不注销回调 | 内存泄漏、引用过期 | 始终在 OnDisable 或 OnDestroy 中注销 |
| 在运行时游戏 UI 中使用 IMGUI | 每帧都会重绘,性能较差 | 使用 UI Toolkit 或 uGUI |
| 忘记在场景中添加 EventSystem(uGUI) | 不会处理输入事件 | 确保场景中存在一个 EventSystem |
关键 API 速查
UI Toolkit
| API | 用途 |
|---|---|
UIDocument | 承载 VisualTreeAsset 的 MonoBehaviour |
rootVisualElement | 视觉树的根节点 |
Q<T>("name") | 按名称查询单个元素 |
Q<T>(className: "cls") | 按类查询单个元素 |
Query<T>().ToList() | 查询多个元素 |
RegisterCallback<TEvent>(callback) | 注册事件处理程序 |
UnregisterCallback<TEvent>(callback) | 移除事件处理程序 |
RegisterValueChangedCallback(callback) | 监听值变化 |
SetValueWithoutNotify(value) | 静默设置值 |
AddToClassList("class") | 添加 USS 类 |
RemoveFromClassList("class") | 移除 USS 类 |
AddManipulator(manipulator) | 附加事件操纵器 |
style.display = DisplayStyle.None | 隐藏元素 |
style.display = DisplayStyle.Flex | 显示元素 |
VisualTreeAsset.Instantiate() | 从 UXML 创建实例 |
element.Bind(serializedObject) | 绑定到 SerializedObject |
uGUI
| API | 用途 |
|---|---|
Canvas | 所有 uGUI 元素的根容器 |
CanvasScaler | 控制 UI 在不同分辨率下的缩放 |
GraphicRaycaster | 启用对 Canvas 的输入检测 |
EventSystem | 集中分发输入事件 |
RectTransform | 支持锚点和尺寸设置的 Transform |
Button.onClick | 用于点击的 UnityEvent |
Toggle.onValueChanged | 用于切换状态变化的 UnityEvent |
Slider.onValueChanged | 用于滑块变化的 UnityEvent |
LayoutGroup | 为子元素自动布局 |
相关技能
- unity-foundations -- GameObject、Component、MonoBehaviour 生命周期
- unity-scripting -- C# 模式、SerializeField、事件
- unity-input -- 与 UI 集成的 Input System
TextMeshPro
所有文本渲染都应使用 TextMeshPro(TMP),而非旧版 UI.Text。TMP 使用 SDF 渲染,确保文本在任意缩放比例下都清晰。Canvas UI 使用 TextMeshProUGUI,3D 世界文本使用 TextMeshPro。使用 SetText("Score: {0}", value) 实现零分配更新。完整 API、富文本标签、字体资源和模式请参阅 references/textmeshpro.md。
其他资源
- UI Toolkit | USS | UXML | 数据绑定 | 操纵器
- uGUI Canvas | uGUI 布局