这次我们来看一个 UI Toolkit 实战问题:角色选择的运行时绑定。很多项目做角色选择界面,第一反应还是用 UGUI 拖一堆 Image 和 Text,再写一个滚动列表。如果只做一两个界面,UGUI 完全够用;但一旦角色数量超过 20 个、布局要微调、UI 要频繁刷新,UI Toolkit 的优势就很明显:布局样式分离、数据绑定更结构化、运行时也能用 UI Builder 直接预览。
UI Toolkit 本身不是什么新东西,Unity 2019 年就开始引入,到 2022.3 LTS 已经可以稳定承担运行时 UI。真正让不少人卡住的,是“运行时绑定”这四个字:为什么我的 TextField 无法自动显示字段值?为什么 ListView 的选中状态没有同步?为什么事件会重复触发?本文将围绕角色选择这一具体场景,把 UXML、USS、ScriptableObject、ListView、运行时 Bind 串成一条完整链路,从环境配置写到性能观察。
本文以 Unity 2022.3 LTS 为参考,如果你用的是 2021.3 或 2023.2,核心 API 基本一致,个别位置会有差异,我会在代码注释里提示。不会讲空泛的 UI Toolkit 概念,只说怎么搭、怎么绑、怎么测、怎么排错。
1. UI Toolkit 角色选择运行时绑定核心能力速览
先给一张能力速览表,帮助你快速判断这套方案是否适合你当前的项目阶段。
| 能力项 | 说明 |
|---|---|
| 方案定位 | 使用 Unity UI Toolkit 构建运行时角色选择界面,完成数据到 UI 的运行时绑定 |
| 核心功能 | UXML 面板结构、USS 样式、ScriptableObject 角色数据、ListView 列表绑定、字段自动绑定 |
| 运行时可用性 | 从 Unity 2021 LTS 开始可以作为运行时 UI 使用,2022.3 LTS 表现更稳定 |
| 绑定对象 | MonoBehaviour、ScriptableObject 的可序列化字段,均支持 SerializedObject 绑定 |
| 列表能力 | 支持 ListView 数据源驱动,动态刷新,行元素内部复用 |
| 编辑工具 | UI Builder 可视化编辑 UXML/USS,需要 Unity 2020.1 以上版本 |
| 外部依赖 | 不依赖第三方 UI 插件,使用 Unity 内置 UI Toolkit 包 |
| 适合场景 | 角色选择、背包、商城、设置页、排行榜等中小规模列表 UI |
| 不适合场景 | 极大规模虚拟列表、对既有 UGUI 复杂框架的大规模重构、低端移动设备上的频繁全量重建 |
从能力表能看出,这套方案的核心价值不是“换一套 UI 技术”,而是把“角色数据”和“界面控件”在运行时解耦。改动一条角色数据,列表能直接刷新;选中一个角色,详情区域能自动更新;点击确认按钮,业务层拿到的是一份结构化的数据对象,而不是硬编码的控件状态。
2. 适用场景与使用边界
2.1 适合的人和场景
最直接的适用者是维护中等规模 UI 的 Unity 客户端开发。如果你的项目出现了下面这些信号,就可以认真考虑把角色选择、背包、商城这类界面迁移到 UI Toolkit:
- 角色数量或道具数量在几十到几百之间,需要滚动列表展示。
- 界面结构经常调,布局、间距、字号改一次就要重新截图对齐一次。
- 需要数据驱动刷新,例如解锁新角色后列表局部更新。
- 希望用 CSS 风格的 USS 统一管理主题、配色、圆角、边距。
- 想要一个可复用的“数据绑定 + 列表渲染”模板,而不是每个界面都写重复的层级操作代码。
这类场景下,UI Toolkit 的 UXML 结构非常接近 Web 前端的语义,Label 就是文字标签,Button 就是按钮,ListView 就是数据列表。你做角色卡片模板时,可以把“角色头像、名字、简介、选中标记”写成一个独立的 UXML,再通过 VisualTreeAsset 在 ListView 中反复实例化,代码量和维护成本都比 UGUI 低很多。
2.2 不推荐的情况
不是所有界面都适合立刻切到 UI Toolkit。如果你的项目已经是成熟的 UGUI 体系,并且完全没有性能问题,那不需要为了“新”而换。尤其不适合以下情况:
- 界面极其复杂,且已经有一套完整的 UGUI 自研框架,迁移成本高于收益。
- 需要在同一 Canvas 下和 UGUI 控件做深度层级混合,两个体系的渲染排序不完全一致。
- 低端安卓机上要同时渲染大量实时变化的 UI,且对首帧时间极其敏感。
- 团队没有接触过 UXML/USS,也没有时间重新学习。
UI Toolkit 和 UGUI 在当前版本并不是“替代关系”,它们可以共存,但建议在项目里划定边界:新界面优先 UI Toolkit,老界面继续维护 UGUI,避免在一个面板里混用两套渲染体系。
2.3 合规与素材边界
角色选择界面离不开角色立绘、模型、UI 图标等美术资源。无论你是自己画、外包做,还是从网络上找测试素材,都要确认素材授权范围。尤其是用 AI 生成角色图、从公开渠道下载的立绘、临时测试用的模型,如果没有商业授权,就不能直接放进正式发布包。涉及玩家名称、头像、存档等数据时,还需要遵守隐私与数据合规要求,避免把未脱敏的用户数据写入日志或上传到未知服务器。
3. 环境准备与前置条件
3.1 Unity 版本与 Package
UI Toolkit 从 Unity 2019.1 开始以 Package 形式存在,之后逐渐并入核心模块。更稳妥的做法是直接使用 Unity 2021.3 LTS 或 2022.3 LTS。这里的推荐依据是:2021.3 之后的 UI Toolkit 运行时 API 已经相对稳定,网上能查到的踩坑记录也更多。
如果你用的是 2020.x 或更早版本,需要先在 Package Manager 里确认是否安装了 UI Toolkit 相关 Package。安装完成后,可以在项目根目录看到Packages/manifest.json中有 UI Toolkit 依赖。如果 Unity 版本过老,建议先升级到 2021.3 以上,否则本文后面的运行时绑定 API 可能对不上。
3.2 UI Builder 与 PanelSettings
打开Window > UI Toolkit > UI Builder,可以可视化编辑 UXML 和 USS。先创建一个项目文件夹,建议命名为UI/CharacterSelect,下面分别存放uxml、uss、scripts、data四个子目录。
UI Toolkit 在运行时需要一个PanelSettings资产。创建方式:
- 在 Project 窗口右键选择
Create > UI Toolkit > Panel Settings Asset。 - 或者直接在 UI Builder 右上角新建。
- 把 PanelSettings 拖到场景中 UIDocument 组件的
Panel Settings槽位。
PanelSettings 有几个关键配置项:Scale Mode控制缩放模式,Screen Match Mode控制宽高适配,Sorting Order控制多个面板的显示顺序。角色选择界面通常使用Constant Pixel Size或Scale With Screen Size,具体取决于你的 UI 基准分辨率。
3.3 EventSystem 与输入系统
这是运行时的第一个经典坑。UI Toolkit 的按钮点击和 ListView 选择依赖 EventSystem,如果你的场景里没有 EventSystem,启动后界面虽然能显示,但点击没有反应。
如果你的项目使用旧版 Input Manager,直接在场景中创建一个EventSystem,Unity 会自动添加StandaloneInputModule。如果你的项目使用新版 Input System Package,则需要在 EventSystem 上挂InputSystemUIInputModule,并确保项目设置里 Active Input Handling 选择了 Input System (Both) 或 Input System。否则 ListView 的点选、滚轮滚动都会失效。
3.4 美术资源准备
角色选择界面至少需要角色头像和角色名称。你可以先用最简单的纯色方块占位,确保功能跑通后再接入真实立绘。建议准备资源:
- 若干张角色头像或立绘 Texture2D/Sprite。
- 角色名称、简介、稀有度等配置数据。
- 用于测试的多种角色卡片模板。
4. 搭建基础界面:UXML 与 USS
4.1 创建角色选择面板 UXML
在 UI Builder 中新建一个 UXML,命名为CharacterSelectScreen.uxml。手工编辑也可以,但 XML 头部建议以 UI Builder 自动生成的内容为准,避免 Schema 缺失导致编译或预览异常。核心结构如下:
<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 name="RoleSelectionRoot"> <ui:Label text="选择角色" class="title" /> <ui:ListView name="CharacterList" class="character-list" /> <ui:Label name="SelectedName" text="未选择" class="selected-name" /> <ui:Button name="ConfirmButton" text="确认选择" class="confirm-button" /> </ui:VisualElement> </ui:UXML>这里的关键点是给控件起好名字,因为后续 C# 代码会通过root.Q<Label>("SelectedName")这种方式查找控件。名字需要大小写一致,UI Toolkit 的UQuery是区分大小写的。
4.2 创建角色卡片模板 UXML
角色列表不会直接用裸的 Label 展示,而是每个角色一张卡片。把卡片单独做成一个CharacterCard.uxml,后续在 ListView 的makeItem中直接Instantiate():
<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="character-card"> <ui:VisualElement name="CardIcon" class="card-icon" /> <ui:VisualElement class="card-info"> <ui:Label name="CardName" class="card-name" /> <ui:Label name="CardDescription" class="card-desc" /> </ui:VisualElement> <ui:VisualElement name="CardSelectedMark" class="card-selected-mark" /> </ui:VisualElement> </ui:UXML>卡片模板和主面板分离之后,你在 UI Builder 里可以直接单独预览卡片效果,也可以调整卡片布局而不用碰主面板代码。
4.3 USS 样式
创建一个CharacterSelectStyle.uss,先给出一套可运行的样式。这里不追求美观,只保证 Flex 布局正常、选中态能看清:
#RoleSelectionRoot { flex-grow: 1; padding: 16px; } .title { font-size: 24px; margin-bottom: 12px; } .character-list { flex-grow: 1; padding: 4px; } .selected-name { margin: 8px 0; font-size: 16px; } .confirm-button { width: 160px; height: 40px; } .character-card { flex-direction: row; align-items: center; padding: 8px; border-width: 1px; border-color: rgba(255, 255, 255, 0.2); border-radius: 4px; margin-bottom: 4px; } .card-icon { width: 48px; height: 48px; margin-right: 8px; background-color: rgba(255, 255, 255, 0.1); } .card-info { flex-direction: column; flex-grow: 1; } .card-name { font-size: 16px; } .card-desc { font-size: 12px; color: rgba(255, 255, 255, 0.7); } .card-selected-mark { width: 8px; height: 8px; border-radius: 4px; background-color: transparent; } .character-card.card-selected { background-color: rgba(80, 140, 200, 0.3); border-color: #5C8DCA; } .character-card.card-selected .card-selected-mark { background-color: #5C8DCA; }在 UXML 主面板的根节点上挂上 USS 引用,或者在 UIDocument 的 PanelSettings 里统一挂样式。挂样式的位置会影响样式优先级,实际项目中建议统一在 PanelSettings 的Theme Style Sheet里管理全局 USS。
4.4 挂载到场景
在场景中创建一个空物体,挂上 UIDocument 组件,把CharacterSelectScreen.uxml拖进 Source Asset,把 PanelSettings 拖进 Panel Settings。运行后如果看到界面,说明 UXML 和 USS 解析正常。
此时你看到的界面还是静态的,List 不会显示任何角色数据。下面进入核心部分:运行时绑定。
5. 运行时绑定的三种实现方式
UI Toolkit 的“绑定”在不同场景下含义不同。有人说的绑定,是指把某个 MonoBehaviour 的字段自动同步到 TextField;有人说的绑定,是指把 ListView 和角色数组关联;还有人说的绑定,是指手动监听按钮点击事件。实际项目中,这三种都会用到。下面逐一拆开讲。
5.1 方式一:bindingPath 字段绑定
适用场景:界面上有玩家名称、角色名称、数值输入框等需要和MonoBehaviour/ScriptableObject字段双向同步的控件。
先声明角色数据资产:
using UnityEngine; [CreateAssetMenu(fileName = "CharacterData", menuName = "Game/CharacterData")] public class CharacterData : ScriptableObject { public string characterId; public string displayName; public Texture2D portrait; [TextArea] public string roleDescription; public int rarity; }然后在 UXML 中给 TextField 设置binding-path:
<ui:TextField