1. UI Toolkit 的整体设计思路
1.1 为什么 UI Toolkit 值得重新学一遍
这两年做 Unity 客户端,凡是碰 UI 的,基本都会遇到 UGUI 老项目迁移、性能瓶颈、复杂度失控这些问题。UI Toolkit(早期叫 UIElements)其实是 Unity 官方从编辑器内部拿出来的那套 UI 系统,后来开放给运行时使用。从 2019 年开始,它就在编辑器扩展里被大量使用,但真正作为游戏运行时 UI 方案成熟起来,是 2021 LTS 之后的事。
我刚开始接触 UI Toolkit 时,最大的感受就是:这套东西跟 UGUI 完全不是一个思路。UGUI 是组件式、层级嵌套式的 Gameobject 结构,UI 元素挂在 Canvas 下面,依赖 RectTransform 做布局;而 UI Toolkit 核心是 VisualElement 和一套类似 Web 前端的技术栈——UXML 描述结构、USS 描述样式、C# 控制数据和行为。用下来之后,我发现它在复杂界面、动态数据、风格统一这三个方向的优势非常明显,尤其适合做背包、商城、任务系统、设置面板这类以数据和状态驱动的界面。
这篇内容主要围绕 UI Toolkit 的 UIElement 体系展开,讲清核心概念、组件结构、基本写法和实测中容易踩的坑。适合两类人看:一类是之前只写过 UGUI、想了解新方案的客户端开发;另一类是已经在编辑器里用过 UI Toolkit、准备把运行时 UI 也迁过来的朋友。
1.2 三种 UI 方案的对比:IMGUI、UGUI、UI Toolkit
Unity 走到今天,UI 方案基本经历过三代:
| 方案 | 定位 | 优点 | 缺点 |
|---|---|---|---|
| IMGUI | 编辑器扩展、调试面板 | 代码即 UI,直接绘制 | 不适合做游戏 HUD、性能差 |
| UGUI | 传统运行时 UI | 组件化、可视化编辑、生态成熟 | 复杂界面层级深、性能开销大 |
| UI Toolkit | 编辑器 + 运行时 | 结构样式分离、灵活、数据驱动友好 | 运行时文档少、迁移成本高 |
IMGUI 其实没有完全退出历史舞台,如果你想从零给 Unity 编辑器写一个简单窗口,它依然是快捷方案。UGUI 在多数商业项目中依然是主力,因为很多旧代码、第三方插件(比如对话系统、背包系统)都基于它。但 UGUI 的痛点很明确:层级一多,Canvas 重建的代价就不小,再加上 RectTransform 做复杂自适应布局时,代码会写得很难受。
UI Toolkit 最吸引我的一点是它的 UI 结构不再是一堆物体节点,而是一棵 VisualElement 树。开发者只需要告诉它“界面长什么样”,布局、渲染、交互都是框架层面的能力。再加上 USS 样式,样式的复用性和可维护性完全碾压 UGUI。对于中大型项目来说,方案选型早晚会往这个方向靠。当然,它本身也在持续迭代,运行时 API 在 2022 LTS 里已经比较稳定了,至少我在一个轻量战斗界面项目里实测下来,效率和数据刷新都非常干净。
1.3 UI Toolkit vs UGUI:核心差异到底在哪里
很多第一次接触 UI Toolkit 的人会问:这跟 UGUI 的 Canvas + Image + Button 有多大区别?我直接用实战角度讲三个关键差异。
第一,对象模型不同。UGUI 里一切 UI 都是 GameObject,需要在场景里组织、挂组件、处理层级和 Renderer。UI Toolkit 则通过 UIDocument 承载,内部维护一棵 VisualElement 树。节点不在场景里,而是在内存里,通过 PanelSettings 渲染到屏幕或 UI Camera。这意味着你不需要频繁地在场景和预制体之间切来切去,纯代码也能动态生成整个界面。
第二,布局机制不同。UGUI 的布局系统靠 LayoutElement、LayoutGroup 这些组件,一旦界面复杂度上来,布局刷新容易出问题。UI Toolkit 天然支持 Flexbox 风格的布局,这是从 CSS 借鉴来的——通过 flex-direction、justify-content、align-items 这些属性做排列和自适应。写起来感觉就像在调网页,手机、PC、不同分辨率下的适配方式也跟 Web 保持一致。
第三,样式管理不同。UGUI 的样式分散在各个 Inspector 面板里,改个按钮配色需要手动找一遍。UI Toolkit 用 USS 文件集中管理样式,一个类名可以同时影响几十个按钮的状态。改主题时,直接换一份 USS 就完事,这在做白名单测试、跨渠道换皮时非常省力。
如果你之前主要写 UGUI,这三条差异建议先充分理解,后面写 UI Toolkit 代码的时候,思路才不会拧着来。不要把 VisualElement 硬套成 RectTransform,也不要总想着用代码遍历找子物体,这些习惯在 UI Toolkit 里都会变成反模式。
2. UIElement 核心概念拆解
2.1 视觉树:VisualElement 家族到底长什么样
UI Toolkit 的本质是维护一棵视觉树(Visual Tree)。所有可见元素都是 VisualElement 的子类。刚开始你只需要认识这几种基础元素:
- VisualElement:最基础的元素,相当于一个容器。
- Label:显示文本。
- Button:按钮,可绑定点击事件。
- TextField / Slider / Toggle:输入、拖动、开关。
- Image:显示图片。
这些元素不是 GameObject,不需要放在场景里。它们挂在 UIDocument 的 rootVisualElement 下面,通过代码动态 Add 或 Remove。单看这个特性,就能理解为什么 UI Toolkit 做动态列表很方便——运行时生成一百个列表项,在 UGUI 里要疯狂 Instantiate、销毁、管理对象池,而在 UI Toolkit 里就是循环 new VisualElement,Add 到容器里,再配合对象池控制数量即可。
视觉树本身是一个树结构,父节点控制子节点的布局和显示,子节点可以继续嵌套。UI Toolkit 的坐标体系也是基于父节点的相对坐标,但不用手动计算,Flexbox 会自动帮你排布。
2.2 UXML 与 USS:结构、样式和逻辑三分离
UI Toolkit 最核心的“三件套”是 UXML、USS、C#。理解这三者的分工,基本就理解了 UI Toolkit 的全貌。
UXML 是界面结构的描述文件,本质是 XML。它定义了界面上有哪些元素、元素的层级关系、每个元素的 name、class、属性值。你可以类比成 UGUI 里的 Prefab 结构。
USS 是样式表文件,定义元素长什么样:背景色、字体、边框、间距、对齐方式等。一个样式类可以在多个元素上复用。这是 UGUI 最缺失的能力。
C# 负责“行为”部分——数据填充、事件监听、界面动态变化。比如按钮点击后打开哪个面板、血量变化后血量条宽度改成多少,这些都是 C# 代码来写。
三分离的好处是团队协作更顺畅:策划或者 UI 美术可以改 UXML/USS 调整界面结构和样式,不碰逻辑代码;程序则可以把注意力放在数据绑定和交互逻辑上,不需要反复通过 Inspector 手动查找元素。
2.3 布局与渲染:UIElement 是怎么被画出来的
视觉树搭好之后,UI Toolkit 会经历布局(Layout)和绘制(Painting)两个阶段。
布局阶段,UI Toolkit 会计算每个 VisualElement 的尺寸和位置。这套布局引擎参考了 Web 的 Flexbox,支持相对定位、绝对定位、比例尺寸、自动尺寸,还有 flex-grow / flex-shrink 控制伸缩。布局结束后,每个元素都会有一个最终尺寸和位置,类似 CSS Layout 后的结果。
绘制阶段,UI Toolkit 会把所有可见元素按层级顺序渲染出来。默认情况下,它可以直接渲染到 UI Document 的 Panel 上,也可以绑定到指定 Camera 上,跟 3D 场景做遮挡关系。
运行时 UI 的渲染其实走的是 UI Toolkit 自己的渲染管线,不是 Canvas 的 Batcher 逻辑。实测下来,相同复杂度界面上,UI Toolkit 的 draw call 控制得比 UGUI 更干净,尤其在大量重复样式的列表场景里,性能优势明显。
不过也要注意,UI Toolkit 的渲染流程和 UGUI 不完全一样。面板切换、样式大规模修改时,如果触发整棵树重绘,开销依然不小。后面我会专门讲性能优化部分。
3. 从零搭建第一个 UIElement 界面
3.1 环境准备与基础配置
实操之前,先确认 Unity 版本。UI Toolkit 在 2021.3 之后算是可用的稳定版本,推荐使用 2022.3 LTS 或更高版本,因为很多运行时 API 和编辑器工具都已经补齐了。
创建项目后,打开 Package Manager,确认是否已经安装了 UI Toolkit 相关包。2021 及以上版本通常会自带 UnityEngine.UIElements 和 UnityEditor.UIElements 模块,不需要额外安装。
接着创建两个基础资产:
右键 Project 窗口,选择 Create -> UI Toolkit -> Panel Settings Asset,命名为 PSPanelSettings。
然后右键 Create -> UI Toolkit -> UI Document,这里会生成一个带默认 UXML 引用的资产。在场景里创建空物体,添加 UIDocument 组件,把 Panel Settings 和 Source Asset 拖进去。这样运行时 UI 的基础架子就搭好了。
另外推荐装上 UI Toolkit 的调试工具。Window -> UI Toolkit -> Debugger 可以实时查看视觉树、样式和层级,排查问题和调整样式非常方便。
3.2 用 UXML 搭出界面骨架
下面写一个用户信息面板的例子:包含一个 Label 显示名字、一个 HP 进度条、一个按钮和一个输入框。
创建 UXML 文件(Create -> UI Toolkit -> UI Document,或者手动新建 UXML 文件),内容如下:
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd"> <ui:VisualElement class="root-container"> <ui:Label text="PlayerInfo" name="titleLabel" class="main-title" /> <ui:ProgressBar name="hpBar" class="hp-bar" high-value="100" low-value="0" /> <ui:TextField label="Name" name="nameInput" class="name-input" /> <ui:Button text="Save" name="saveButton" class="primary-button" /> </ui:VisualElement> </ui:UXML>这个 UXML 定义了一个根容器,里面依次放了标题、进度条、输入框和按钮。name 属性用来在 C# 里精准查找元素,class 属性用来匹配 USS 样式。ProgressBar 是内置控件,可以直接显示进度。
需要注意:UXML 文件如果从零手写,命名空间声明很容易漏。建议先通过 Unity 菜单生成一次,再在这个基础上改,避免各种 IDE 报红。
3.3 用 USS 完成样式设计
现在给这个面板加上样式。创建 USS 文件(Create -> UI Toolkit -> Style Sheet),命名 PlayerPanelStyle.uss。
.root-container { background-color: #1E1E1E; padding: 20px; flex-direction: column; } .main-title { font-size: 24px; color: #FFFFFF; margin-bottom: 10px; } .hp-bar { height: 20px; margin-bottom: 15px; } .name-input { margin-bottom: 15px; } .primary-button { background-color: #4C8BF5; color: #FFFFFF; border-radius: 6px; padding: 10px 20px; align-self: flex-start; } .primary-button:hover { background-color: #6FA3FF; }然后把 USS 文件挂到 UXML 上:
<ui:UXML ...> <Style src="PlayerPanelStyle.uss" /> ... </ui:UXML>USS 的使用方式和 CSS 很像,类选择器以点开头,id 选择器以井号开头,元素选择器直接写元素名。优先级的规则也比较直观:内联样式 > id 选择器 > 类选择器 > 元素选择器。这个优先级规则在实际调整样式时非常常用。
我实际使用中发现:UI Toolkit 的样式是运行时通过 StyleSheet 动态加载的,所以改 USS 之后,在编辑器模式下很多时候只需要重新进入播放模式就能看到效果,不需要改场景。
3.4 用 C# 控制界面行为
接着写 C# 逻辑。在 UIDocument 所在物体上挂一个脚本,比如 PlayerPanelController.cs。
using UnityEngine; using UnityEngine.UIElements; public class PlayerPanelController : MonoBehaviour { private UIDocument uiDoc; private void Awake() { uiDoc = GetComponent<UIDocument>(); } private void Start() { VisualElement root = uiDoc.rootVisualElement; Label titleLabel = root.Q<Label>("titleLabel"); titleLabel.text = "Warrior"; ProgressBar hpBar = root.Q<ProgressBar>("hpBar"); SetHP(72f); Button saveButton = root.Q<Button>("saveButton"); saveButton.RegisterCallback<ClickEvent>(evt => { TextField nameField = root.Q<TextField>("nameInput"); Debug.Log($"Save name: {nameField.value}"); }); } public void SetHP(float hpPercent) { ProgressBar hpBar = uiDoc.rootVisualElement.Q<ProgressBar>("hpBar"); hpBar.value = hpPercent; hpBar.title = $"{hpPercent:F0} / 100"; } }root.Q ("name") 是 UQuery API,后缀可以传 name,也可以传 class。这里 Q 是 Query 的缩写。当元素比较多、结构比较复杂时,建议用 UQueryBuilder 一次筛选,避免频繁 Q 掉性能。
事件绑定方面,UI Toolkit 并不用 UGUI 里那套 Button.onClick.AddListener,而是用 RegisterCallback ,支持 ClickEvent、ChangeEvent 、PointerDownEvent 等。实际写起来更接近前端的事件监听模式,也更容易记忆。
在运行时,如果要临时创建元素,直接用代码 new 一个 VisualElement 并 Add 到父节点就行,比如:
var newItem = new Label("New Item"); newItem.AddToClassList("list-item"); root.Q<VisualElement>("itemContainer").Add(newItem);3.5 一个小延伸:用 UI Toolkit 扩展编辑器
UI Toolkit 不仅能做运行时 UI,更适合做编辑器工具面板。如果你需要给团队写一些内部工具窗口,用 UI Toolkit 会比 IMGUI 舒服很多。
创建一个继承 EditorWindow 的类,直接设置视觉树:
using UnityEditor; using UnityEngine; using UnityEngine.UIElements; public class MyToolWindow : EditorWindow { [MenuItem("Tools/My Tool Window")] public static void ShowWindow() { MyToolWindow wnd = GetWindow<MyToolWindow>(); wnd.titleContent = new GUIContent("My Tool Window"); } public void CreateGUI() { VisualElement root = rootVisualElement; Label label = new Label("这是一个自定义工具窗口"); label.AddToClassList("tool-title"); root.Add(label); Button refresh = new Button(() => RefreshData()) { text = "刷新数据" }; root.Add(refresh); } private void RefreshData() { Debug.Log("数据刷新逻辑"); } }这段代码放在 Editor 文件夹下即可。编辑器窗口里的 UI 开发体验和运行时基本一致,缺点是需要额外处理一些编辑器状态数据,不过整体比 IMGUI 可维护得多。
4. 常见问题与排坑实录
4.1 运行时界面不显示
这是新手上手时最常碰见的问题。检查顺序有三步:
第一,场景里是否有 UIDocument 组件,并且 Panel Settings 资产是否已经绑定到组件上。PanelSettings 是决定 UI 渲染到哪个 Panel、哪个排序层的核心资产,漏掉它 UI 就不会显示。
第二,UIDocument 的 Source Asset 是否已经指定 UXML 文件。如果这里为空,界面上什么都不会出现。
第三,如果确认了前两步仍然不显示,检查 UIDocument 的 Sorting Order。多个 UIDocument 同时存在时,数值大的会覆盖在前面。如果 UI 显示在场景里但被遮挡,检查 Camera 设置。建议在 PanelSettings 里把 Target Texture、Render Mode 等参数按实际需求调一下,确保 UI 的渲染层级正确。
4.2 USS 样式不生效
样式不生效,通常是因为选择器写错了,或者样式文件没有挂载。
首先检查 UXML 里是否引用了 USS:
<Style src="PlayerPanelStyle.uss" />这里路径是相对 UXML 所在目录的,如果你移动了文件,路径可能失效。
其次,确认选择器的 class 名和元素上的 class 是否完全一致,大小写也要小心。UI Toolkit 的选择器是区分大小写的。
有一个很实用的调试技巧:打开 UI Toolkit Debugger(Window -> UI Toolkit -> Debugger),选中界面元素,右侧可以看到它匹配到的所有样式规则,以及哪些规则被覆盖了。这个窗口比猜代码高效得多。
4.3 UGUI 和 UI Toolkit 混用时的问题
因为大部分项目不可能一次性完成迁移,UGUI 和 UI Toolkit 混用很常见。混用时的经验是:UI Toolkit 面板默认渲染在单独的 Panel 上,不会天然跟 UGUI 的 Canvas 做排序。如果你需要在 UI Toolkit 面板和 UGUI 面板之间切换显示,可以给 PanelSettings 增加 Render Mode 并配合 Camera Tag 指定渲染层级,或者直接通过代码控制 UIDocument 的 enabled。
我个人的经验是:尽量把 UI Toolkit 面板做成独立的“页面”,比如战斗结算界面、主菜单,不要在同一个翻页系统里让 UGUI 和 UI Toolkit 交替出现,排序逻辑会非常混乱。长期看,还是应该逐步把核心界面统一到 UI Toolkit 一侧。
4.4 性能优化:列表和样式高频刷新
最后谈性能。UI Toolkit 虽然没有 Canvas 重建问题,但也不是完全无代价的。大量动态创建、销毁 VisualElement 依然会产生 GC 和布局开销。
列表类界面建议使用 IListView 或 ListView,官方已经提供了懒加载和复用机制,直接绑定数据源即可。如果自己手动生成列表项,要额外做对象池。
样式刷新方面,尽量减少对 VisualElement 的 style 属性反复赋值。需要整体换主题时,直接切换 styleSheet 更高效。另外,视觉树越深,布局计算越复杂。设计界面层级时,能用扁平结构尽量扁平。
事件系统上,RegisterCallback 的匿名 lambda 会在触发时分配额外内存,在频繁触发的事件(比如 Slider 拖动)里,建议注册命名方法。
我在一个 HUD 战斗界面里实测:同时显示 20 个动态文本、10 个图标、2 个血量条,开启 Profiler 后,UI Toolkit 的布局开销远低于 UGUI 的 Canvas 重建开销,而且没有 Canvas 的 batch 断裂问题。整体来说,只要控制好视觉树深度和节点数量,运行时性能是足够看。
4.5 数据绑定和数据刷新经验
UI Toolkit 没有像生产级前端框架那样的响应式数据绑定。数据变化后,你需要主动更新 UI,比如上文的 SetHP 方法,就是手动给 ProgressBar 赋值。这个设计初期看起来麻烦,但好处是逻辑更透明——每个界面值的变化都发生在明确的方法里,不像某些插件那样在暗地里刷新。
频繁刷新时,建议在方法里做值判断,只有数据真的变化才去更新 UI,避免不必要的绘制。例如:
if (Mathf.Approximately(hpBar.value, hpPercent)) return; hpBar.value = hpPercent;这行代码在帧率敏感的战斗 HUD 里,能省下不少无意义的布局计算。
4.6 兼容性注意事项
UI Toolkit 的运行时 API 在 2019-2022 之间变化很大。如果你查资料时看到很老的代码,比如使用 VisualElement.RegisterCallback 之前要调用 visualElement.AddManipulator 的做法,那基本都是早期方案。2021 LTS 以后,事件系统已经趋于稳定,直接写 RegisterCallback 即可。
还有一点,UI Toolkit 不支持 shader 自定义的 UGUI Mask 之类的东西。遮罩用 VisualElement 的 overflow: hidden 来实现,这个属性可以让子节点被安全裁剪。如果你之前习惯用 RectMask2D,这里需要调整一下思路。
我目前的主力项目已经完整迁移了一个大型背包界面到 UI Toolkit,团队整体反馈是:界面开发速度比 UGUI 略快,调试效率更高,但学习曲线确实存在,前两周需要适应 UXML/USS 的写法。整体来说,如果项目是新立项,趁早切到 UI Toolkit,会比后期从 UGUI 迁出来省很多时间。
最后再分享一个小技巧:刚开始学 UI Toolkit 时,拿 UI Toolkit Debugger 把 Unity 自带的编辑器界面元素拆一遍,看它们的类名和样式,比任何教程都管用。看几遍,再来写自己的 UXML 和 USS,思路会通透明亮很多。