UI Toolkit 架构
UI Toolkit(代码中称 UIElements)是保留模式 UI 框架,编辑器窗口和运行时游戏 UI 都使用它。与多数引擎子系统不同,它的大部分逻辑是 C# 实现:元素树、样式匹配、事件分发和渲染数据生成都在仓库中;布局计算(Yoga)、文本和 GPU 提交交给原生。本页介绍核心概念、面板更新阶段,以及编辑器和运行时分别如何驱动面板。
父页面:仓库概览 · 子页面:事件分发、样式、布局与渲染 · 相关:窗口系统、IMGUI
相关源码文件#
Modules/UIElements/Core/VisualElement.cs、Panel.csModules/UIElements/Core/VisualTreeUpdater.csModules/UIElements/Core/UIElementsUtility.cs、UIElementsRuntimeUtility.csModules/UIElements/Core/Native/UIElementsUtility.bindings.csModules/UIElements/Core/GameObjects/:UIDocument、PanelSettings、RuntimePanelModules/UIElementsEditor/RetainedMode.cs、EditorPanel.cs
1. 模块构成#
| 模块 | 程序集 | 文件数 | 内容 |
|---|---|---|---|
UIElements | UnityEngine | 665 | 核心:Core/Controls(109)、Style(106)、Renderer(72)、StyleSheets(47)、Events(34)、Layout(34)等 |
UIElementsEditor | UnityEditor | 151 | 编辑器集成:窗口后端、EditorPanel、UXML 导入器、调试器 |
UIBuilder | UnityEditor | 195 | UI Builder 可视化编辑工具 |
UIToolkitAuthoringEditor | UnityEditor | 406 | 新的创作工具(命名空间 Unity.UIToolkit.Editor) |
StyleSheetsEditor | UnityEditor | 13 | GUISkin 与样式表互相转换 |
UIElementsSamplesEditor | UnityEditor | 59 | 示例窗口 |
数据来自目录统计和 模块目录。此外,Editor/Mono/UIElements(63 个文件)包含 InspectorElement、USS 导入器等编辑器侧集成。
2. 核心概念#
| 概念 | 说明 | 证据 |
|---|---|---|
VisualElement | 元素树节点,继承 Focusable | VisualElement.cs:252 |
| 面板(Panel) | 一棵元素树的宿主,负责更新、事件和渲染。层级为 BaseVisualElementPanel → Panel → BaseRuntimePanel/EditorPanel | Panel.cs:480、Panel.cs:1217、Panel.cs:2014、EditorPanel.cs:13 |
ContextType | Player=0 或 Editor=1,决定由谁驱动面板 | Panel.cs:20-30 |
VersionChangeType | 位标志,描述元素发生了哪类变化(层级、布局、样式、变换、重绘等),与原生 VersionChangeType.h 保持同步 | Panel.cs:32-120 |
| 运行时宿主 | UIDocument : MonoBehaviour 提供 UXML 内容;PanelSettings : ScriptableObject 拥有面板;RuntimePanel : BaseRuntimePanel | UIDocument.cs:106、PanelSettings.cs:169、RuntimePanel.cs:287 |
变化通知:版本号而不是立即重算#
元素变化通常通过递增版本来通知,而不是立即重新计算:IncrementVersion(changeType) 转发给 elementPanel.OnVersionChanged VisualElement.cs:2497-2501;MarkDirtyRepaint 就是 IncrementVersion(VersionChangeType.Repaint) VisualElement.cs:2713-2716。面板递增自身版本,并把变化广播给每个已安装阶段的更新器,由各更新器记录自己关心的脏标记;层级变化还会递增层级版本 Panel.cs:1982-1994、VisualTreeUpdater.cs:150-159。元素被移出面板(elementPanel 为空)时,IncrementVersion 不做任何事。
3. 更新阶段#
VisualTreeUpdater 为每个面板维护一组按阶段排列的更新器 VisualTreeUpdater.cs:32-46、VisualTreeUpdater.cs:192-209:
| 阶段 | 默认更新器 | 职责 |
|---|---|---|
ViewData | VisualTreeViewDataUpdater | 持久化视图数据 |
Bindings | VisualTreeBindingsUpdater | SerializedObject 绑定 |
DataBinding | VisualTreeDataBindingsUpdater | 运行时数据绑定 |
Component | VisualTreeComponentUpdater | 元素组件 |
Animation | VisualElementAnimationSystem | 动画与过渡 |
Styles | VisualTreeStyleUpdater | 样式匹配与计算样式 |
Layout | VisualTreeLayoutUpdater | 布局 |
TransformClip | VisualTreeHierarchyFlagsUpdater | 变换和裁剪标志 |
Accessibility | 默认为空;只有可访问性桥接跟踪的面板才安装 | 无障碍 |
Repaint | UIRRepaintUpdater | 生成渲染数据 |
Authoring | VisualTreeAuthoringUpdater | 创作工具 |
面板不会每帧按顺序运行全部阶段,而是把它们分组到几个入口中 Panel.cs:1759-1873:
| 面板方法 | 执行内容 |
|---|---|
TickSchedulingUpdaters() | 重建脏样式表 → 调度器定时项 → 校验焦点 → 资源追踪 → Bindings → DataBinding → Component → Animation |
ValidateLayout() | 重建脏样式表 → Styles → Layout → TransformClip;用标志防止重入 |
Repaint() → UpdateForRepaint() | 如果层级版本变化后还没有 tick,先补一次 TickSchedulingUpdaters;然后执行 ViewData → Styles → Layout → TransformClip → Accessibility → Repaint → Authoring |
UpdateWithoutRepaint() | Inspector 节流创建元素时使用,只执行绑定、样式、布局和创作阶段 |
UpdateForRepaint 的“补 tick”逻辑有注释说明:新加入面板的元素可能注册调度项、绑定或动画,如果同一帧没有 tick,就会闪烁 Panel.cs:1840-1859。ValidateLayout 的防重入注释给出了实际调用链:TransformClip 阶段重新计算鼠标下的元素时会执行拾取,而拾取又会校验布局 Panel.cs:1759-1781。
4. 谁驱动面板#
图中证据:
- 编辑器:
RetainedMode.UpdateSchedulers和RequestRepaintForPanels是[RequiredByNativeCode]方法RetainedMode.cs:78-93。UIElementsUtility.UpdateSchedulers先回收布局、组件和文本管理器的资源,再复制编辑器面板列表并逐个 tick;复制是因为用户代码可能在迭代中改变面板列表UIElementsUtility.cs:268-302。RequestRepaintForPanels跳过非编辑器面板,只为 dirty 的面板请求重绘UIElementsUtility.cs:304-322。编辑器面板的Repaint由窗口重绘触发,具体路径见 窗口系统。 - 运行时:原生调用
UIElementsRuntimeUtilityNative中的四个[RequiredByNativeCode]方法,它们只是转发到可替换的回调UIElementsUtility.bindings.cs:12-60。UIElementsRuntimeUtility的静态构造函数注册更新回调UIElementsRuntimeUtility.cs:34-41,渲染回调由EnableRenderingAndInputCallbacks注册UIElementsRuntimeUtility.cs:585-588。UpdatePanels先移除无用面板并回收共享管理器,然后预更新PanelRenderer,按从后到前(世界空间先于屏幕覆盖层)调用panel.Update(),再更新PanelRenderer和默认事件系统UIElementsRuntimeUtility.cs:366-420。BaseRuntimePanel.Update就是TickSchedulingUpdaters()加ValidateLayout()Panel.cs:2117-2122;RuntimePanel会先应用PanelSettingsRuntimePanel.cs:352-360。RepaintPanels对每个未释放的面板调用Repaint;只重绘离屏面板时,只处理有targetTexture的面板UIElementsRuntimeUtility.cs:303-315。
PlayerLoop 中有名为 PreUpdate.UIElementsPreparePanelRenderers、PreLateUpdate.UIElementsUpdatePanels、PostLateUpdate.UIElementsRepaintPanels 的阶段 PlayerLoop.bindings.cs:186、PlayerLoop.bindings.cs:224、PlayerLoop.bindings.cs:253。根据名称可以推断,它们分别调用上面三个原生入口;PreparePanelRenderers 的注释也说它“在 PreUpdate 末尾运行” UIElementsRuntimeUtility.cs:353-355。其余对应关系在原生侧确认。
5. 编辑器集成要点#
- 编辑器窗口内容运行在
EditorPanel中,根部有一个运行OnGUI的IMGUIContainer(见 窗口系统 §3)。 RetainedMode在[OnCodeLoaded]中注入编辑器专用实现,例如面板资源加载、SerializedObject绑定实现和光标 ID 解析RetainedMode.cs:25-53。- UXML/USS 资源重新导入后,
RetainedModeAssetPostprocessor收集变化的资源并通知面板刷新RetainedMode.cs:95-123。
6. 局限#
Core/Controls(109 个文件)中的具体控件、Collections(ListView 等虚拟化列表)、Bindings/数据绑定、DragAndDrop、Accessibility没有单独分析。- UI Builder 和
UIToolkitAuthoringEditor只在模块目录中列出。