Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
3d02426
docs: add UITKModule design spec
vvgool Jun 11, 2026
55b3437
docs: add UITKModule Plan 1 - Core Framework implementation plan
vvgool Jun 11, 2026
9338ed3
docs: add UITKModule Plan 2 - Binding + MVVM + ListView + Source Gene…
vvgool Jun 11, 2026
9bc19f6
feat(uitk): add UITKAnimationType enum and IUITKAnimationDriver inter…
vvgool Jun 11, 2026
cc3753e
feat(uitk): implement USSAnimationDriver with USS transition presets
vvgool Jun 11, 2026
e15af43
feat(uitk): add IUITKResourceLoader interface and YooAsset implementa…
vvgool Jun 11, 2026
234686a
feat(uitk): add UIWindowAttribute and UILayer enum
vvgool Jun 11, 2026
bce6444
feat(uitk): add UITKBase with lifecycle hooks, events, and widget fac…
vvgool Jun 11, 2026
58a9b52
feat(uitk): add UITKWindow with full lifecycle, visibility, and anima…
vvgool Jun 11, 2026
fd73ce5
feat(uitk): add UITKWidget with independent UXML, lifecycle, and list…
vvgool Jun 11, 2026
9abdcf3
feat(uitk): add UITKModule singleton with panel, stack, and animation…
vvgool Jun 11, 2026
a326449
feat(uitk): add USS variables foundation and common styles
vvgool Jun 11, 2026
d302622
feat(uitk): register UITKModule in GameModule
vvgool Jun 11, 2026
8552fb4
test(uitk): add smoke test window to verify UITKModule basic function…
vvgool Jun 11, 2026
9f4b942
feat(uitk): add binding attributes - Q, OnClick, OnChange, Bind, Bind…
vvgool Jun 11, 2026
f269561
feat(uitk): add MVVM core - BindableProperty, Command, List, ViewMode…
vvgool Jun 11, 2026
b1f6c96
feat(uitk): add UITKListView with virtualization and Widget lifecycle
vvgool Jun 11, 2026
62640f1
feat(uitk): add BindContext/UnbindContext to UITKBase for MVVM support
vvgool Jun 11, 2026
6af47ac
feat(uitk): add Source Generator with compile-time UXML validation
vvgool Jun 11, 2026
c5c73a4
test(uitk): add integration test for MVVM binding (manual binding, pr…
vvgool Jun 11, 2026
0b0b8b9
fix(uitk): remove duplicate UILayer enum, reuse existing from UIModule
vvgool Jun 11, 2026
2b5631e
fix(uitk): move misplaced files to correct UnityProject/Assets path +…
vvgool Jun 11, 2026
ea89710
fix(uitk): fix access modifiers - _hasOverrideUpdate to internal, OnS…
vvgool Jun 11, 2026
4aeb58b
feat(uitk): add UITK resource group to YooAsset collector config
vvgool Jun 11, 2026
5a1706b
fix(uitk): load PanelSettings asset for UIDocument panels
vvgool Jun 11, 2026
2635d3c
test(uitk): add comprehensive full-feature test window covering MVVM,…
vvgool Jun 11, 2026
26991e5
feat(uitk): build and integrate Source Generator DLL + add auto-bind …
vvgool Jun 11, 2026
7bcf211
docs: update design spec and Plan 2 - replace Source Generator with E…
vvgool Jun 11, 2026
ab30146
feat(uitk): replace Source Generator with Editor pre-generation tool …
vvgool Jun 11, 2026
d35267b
fix(uitk): change binding info structs to classes to fix null comparison
vvgool Jun 11, 2026
f869c9e
refactor(uitk): auto-call __UITKAutoBind in framework lifecycle, deve…
vvgool Jun 11, 2026
17be662
fix(uitk): skip .bindgen.cs files in generator to prevent recursive g…
vvgool Jun 11, 2026
8371e7b
fix(uitk): remove erroneously generated .bindgen.bindgen.cs file
vvgool Jun 11, 2026
a015049
fix(uitk): remove stale bindgen file, needs re-generate in Unity
vvgool Jun 11, 2026
8a347d3
test(uitk): add navigation, layer, popup, and stack test windows
vvgool Jun 11, 2026
0b2c7f0
fix(uitk): add missing using Cysharp.Threading.Tasks in TestSubPage
vvgool Jun 11, 2026
0f6b534
feat(uitk): add IUITKClickSoundHandler with panel-level click interce…
vvgool Jun 11, 2026
6d3d444
feat(uitk): add sound parameter to OnClickAttribute, integrate with g…
vvgool Jun 11, 2026
a0d1b9a
fix(uitk): use cached delegates for click unbinding, fix anonymous la…
vvgool Jun 11, 2026
d1c27ba
refactor(uitk): simplify click sound - remove sound from OnClick attr…
vvgool Jun 11, 2026
ce3b7da
docs: update all design and plan documents to reflect final implement…
vvgool Jun 11, 2026
3472d0f
docs: update CLAUDE.md with UITKModule guidelines, principles, and re…
vvgool Jun 11, 2026
d6055aa
docs: add UITKModule reference docs (uitk-module-design, uitk-lifecyc…
vvgool Jun 11, 2026
2d92fb7
feat(uitk): 重构 MVVM 自动绑定,生成器产出 Bind* helper 调用
vvgool Jun 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 207 additions & 0 deletions Books/3-5-2-UI-Toolkit-AssetBundle-热更新研究-2026-06-11.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
# TEngine UI Toolkit + AssetBundle / YooAsset 热更新研究报告

**日期**: 2026-06-11
**研究目标**: Unity UI Toolkit (UXML/USS) 从 AssetBundle/YooAsset 运行时加载的可行性、限制、性能与设计建议(服务于 HybridCLR + YooAsset 热更新移动游戏)。
**来源**: Unity 官方文档 (Unity 6 / 6000.x)、论坛讨论、GitHub 真实代码示例、Addressables/YooAsset 集成模式。

## 1. 可行性结论

**完全可行**。UI Toolkit 的 `VisualTreeAsset` (UXML) 和 `StyleSheet` (USS) 是普通 Unity 资产,可通过 AssetBundle、Addressables 或 YooAsset 正常加载和运行时使用。

- 官方推荐路径:**Addressables**(Unity 文档明确提供完整示例)。
- TEngine 实际路径:**YooAsset**(与 Addressables 底层同为 AssetBundle + 清单机制),将 UXML/USS 作为普通资产打进资源包即可。
- 无版本硬性阻断:Unity 2021.3+(TEngine 最低要求)至 Unity 6 均支持。不能在运行时从原始文本动态生成 VisualTreeAsset(Importer 是 Editor-only),但预构建的资产加载完全没问题。

**核心证据**:
- Unity 官方文档《Load UI assets with Addressables》:https://docs.unity3d.com/6000.6/Documentation/Manual/ui-systems/load-ui-assets-with-addressables.html (提供 MonoBehaviour + PanelRenderer 完整异步加载 + 应用代码)。
- Unity 官方文档《Load UXML and USS in C# scripts》:https://docs.unity3d.com/6000.6/Documentation/Manual/UIE-manage-asset-reference.html (明确列出 Addressables、序列化引用、Resources 三种运行时加载方式)。
- 性能最佳实践文档强调:“Use Asset Bundles or Addressables: When possible, only load the UI documents and style sheets required... Unload assets when not needed”。

## 2. 加载 VisualTreeAsset (UXML) 和 StyleSheet (USS) 的标准模式

### 2.1 Addressables 官方推荐模式(可直接映射到 YooAsset)

```csharp
// 伪代码,Addressables 示例(YooAsset 几乎等价)
private AsyncOperationHandle<VisualTreeAsset> uxmlHandle;
private AsyncOperationHandle<StyleSheet> ussHandle;
private VisualTreeAsset loadedUxml;
private StyleSheet loadedUss;

void LoadUI()
{
uxmlHandle = Addressables.LoadAssetAsync<VisualTreeAsset>("uxmlexample"); // 或自定义 key
uxmlHandle.Completed += OnUxmlLoaded;

ussHandle = Addressables.LoadAssetAsync<StyleSheet>(ussAssetReference);
ussHandle.Completed += OnUssLoaded;
}

void OnUxmlLoaded(AsyncOperationHandle<VisualTreeAsset> handle)
{
if (handle.Status == AsyncOperationStatus.Succeeded)
{
loadedUxml = handle.Result;
// 方式A:赋值给 PanelRenderer / UIDocument
panelRenderer.visualTreeAsset = loadedUxml;
// 方式B:手动 CloneTree
// loadedUxml.CloneTree(rootVisualElement);
}
}

void OnUssLoaded(AsyncOperationHandle<StyleSheet> handle)
{
if (handle.Status == AsyncOperationStatus.Succeeded)
{
loadedUss = handle.Result;
// 必须检查重复添加(reload 回调常见问题)
if (!rootElement.styleSheets.Contains(loadedUss))
rootElement.styleSheets.Add(loadedUss);
}
}

void Unload()
{
if (uxmlHandle.IsValid()) Addressables.Release(uxmlHandle);
if (ussHandle.IsValid()) Addressables.Release(ussHandle);
}
```

**YooAsset 映射**(TEngine 实际使用):
- 使用 `YooAssets.GetPackage(packageName).LoadAssetAsync<VisualTreeAsset>(location)`。
- `handle.AssetObject as VisualTreeAsset` 或泛型结果。
- 释放使用 `handle.Release()` 或框架上层 `GameModule.Resource.UnloadAsset` / `AssetsReference` 机制。
- 与 TEngine 现有 `IUIResourceLoader` + `UIResourceLoader` 集成点:新增 `LoadVisualTreeAssetAsync` / `LoadStyleSheetAsync`。

### 2.2 CloneTree() / Instantiate() 运行时用法

- `VisualTreeAsset.CloneTree(VisualElement target)` —— 最常用,把树克隆到已有根节点。
- `VisualTreeAsset.Instantiate()` —— 返回新的 `TemplateContainer` 根。
- 两者在运行时加载的 VisualTreeAsset 上**完全可用**,无特殊限制。
- 新版还支持带 `VisualElementAssetReferenceTable` 的重载,用于按 AuthoringId 事后查找元素。

性能提示(来自官方最佳实践):
- CloneTree 本身成本不高,**主要成本在它引用的纹理/字体等资产被拉入内存** + 后续布局/样式解析。
- 大型 UXML 建议拆成多个小模板,按需动态加载。
- 列表类 UI 必须用 `ListView` 虚拟化,不要为每个条目都 CloneTree。

## 3. PanelSettings 管理

- `PanelSettings` 是 **ScriptableObject 资产**,可以像普通资产一样打进 AB / 放到 YooAsset 组 / 标记 Addressable,然后运行时加载。
- 它是 UIDocument / PanelRenderer 的渲染配置载体(缩放模式、排序、主题等)。
- 多个 UIDocument 可共享同一个 PanelSettings 以优化性能。
- **重要限制/坑**(论坛实测):
- 纯运行时 `new PanelSettings()` 创建的实例,在构建后可能拿不到默认样式表(因为默认样式表是 Editor 资产,只有通过序列化引用打包进去才会被包含)。
- 推荐做法:**在 Editor 里提前创建好 PanelSettings 资产**,配置好 Theme Style Sheet,然后通过资源系统加载该资产实例并赋值。
- 热更新主题:可以运行时加载不同的 PanelSettings,或修改已加载 PanelSettings 的 `themeStyleSheet` 引用(需测试具体版本行为)。

## 4. USS 引用解析、@import、Theme 文件的运行时坑

这是最容易踩的区域:

- **@import 链在运行时不可靠**:
- UI Builder 和 Editor 里预览正常,构建后或运行时加载后样式丢失/部分生效。
- 论坛多帖反馈:“Unreferenced theme file seems to change USS load order”、“How to use Themes (TSS) properly for runtime UI”。
- 根因:运行时样式表加载顺序与 Editor 不同,@import 解析依赖 AssetDatabase 行为。

- **推荐的运行时稳妥做法**(社区 + 实测总结):
1. **UXML 里显式声明**:`<Style src="theme.uss" />`、`<Style src="buttons.uss" />`(顺序重要,后面的覆盖前面的)。
2. **C# 运行时添加**:`root.styleSheets.Add(loadedStyleSheet);`(务必先 Contains 检查,避免 reload 重复添加)。
3. **应用级主题用 TSS + PanelSettings**:在 PanelSettings 的 Theme Style Sheet 槽位挂 TSS,TSS 内部可以用 @import,运行时表现更好。
4. 不要完全依赖纯 @import 链来做运行时主题切换。

- 变量(USS variables)和 override 机制本身没问题,关键是**加载顺序**。

## 5. 性能与内存管理

来自官方《Optimizing performance》最佳实践:

- **内存**:加载 UXML/USS 会连带拉入它引用的所有纹理/字体。必须在使用完后 `RemoveFromHierarchy()` + 释放句柄(Addressables.Release / YooAsset 对应释放)。
- **按需加载**:把大型 UI 拆成模块化小 VisualTreeAsset,按需加载。
- **动态图集**:UI Toolkit 有 Dynamic Atlas(Panel Settings 可配),配合 2D SpriteAtlas 使用可减少纹理切换导致的 batch break。
- **Batching & 8 纹理限制**:同一 batch 最多 8 张纹理,超了就断 batch。图集化 + 合理组织元素很重要。
- **隐藏元素**:
- `style.display = DisplayStyle.None`:停止布局和渲染,适合不常出现的面板。
- `RemoveFromHierarchy()` + 释放:彻底去掉内存开销,重新显示时成本较高。
- `visible = false` 或 `opacity = 0`:仍在布局/渲染管线中,成本较高。
- **动画**:优先用 `transform`(translate/scale/rotate)+ `usageHints = DynamicTransform / GroupTransform`,避免改 layout 属性触发昂贵重算。
- **数据绑定**:用 `INotifyBindablePropertyChanged` + `IDataSourceViewHashProvider` 避免无谓更新;编译时代码生成 property bag 比反射好。

**CloneTree 性能实测/建议**:没有发现“CloneTree 本身很慢”的硬伤报告,瓶颈通常在:
- 引用的重资源(大图、字体)。
- 极深层级 + 频繁样式/布局变更。
- 没有虚拟化的长列表。

## 6. HybridCLR + YooAsset + UI Toolkit 热更新特殊考虑

- UI 资产(UXML/USS/纹理/PanelSettings)属于**数据侧**,与 HybridCLR 热更代码完全正交。
- YooAsset 已经负责 AB 依赖、清单、远程更新、LRU/ARC 缓存,与 TEngine 现有资源模块深度集成。
- 设计时只需把 UXML/USS 正常打进 YooAsset 资源包(可单独分组,便于小更新)。
- 加载/释放必须走框架上层 API(`GameModule.Resource` / `IUIResourceLoader`),以便 AssetsReference 自动追踪或 YooAsset 句柄池管理。
- 异步优先:UI 打开流程已经是 UniTask + 异步,加载 UXML/USS 必须 await,不能阻塞。
- 事件/生命周期:UI 关闭时必须释放对应 VisualTreeAsset/StyleSheet 句柄,防止内存泄漏(与现有 UIWindow/Widget 销毁流程对齐)。

## 7. TEngine UIToolkit 模块管道设计建议(下游输入)

1. **资源加载层**:
- 在 `UIModule` 或新建 `UIToolkitModule` 中扩展 `IUIResourceLoader`,增加:
```csharp
UniTask<VisualTreeAsset> LoadVisualTreeAssetAsync(string location);
UniTask<StyleSheet> LoadStyleSheetAsync(string location);
```
- 内部用 `GameModule.Resource.LoadAssetAsync<T>` 实现,复用现有 YooAsset 封装和释放策略。

2. **UI 文档管理**:
- 提供 `UIToolkitWindow` 基类(或扩展现有 `UIWindow`),持有:
- `VisualTreeAsset` 句柄
- `StyleSheet` 列表
- `UIDocument` 或 `PanelRenderer` + `PanelSettings` 引用
- 打开流程:`await LoadUxml + LoadUss` → `CloneTree(root)` 或赋值 → `styleSheets.Add`(去重)→ 绑定事件/数据。
- 关闭流程:`RemoveFromHierarchy` → 释放句柄 → 走 TEngine 现有资源释放路径。

3. **PanelSettings 策略**:
- Editor 预创建 1~N 个 PanelSettings 资产(不同分辨率/主题需求)。
- 运行时加载对应 PanelSettings 资产,赋值给 UIDocument。
- 支持运行时切换 Theme(加载新 TSS 或新 PanelSettings)。

4. **主题与样式加载策略**(避坑):
- 优先 UXML 内 `<Style src="..."/>` + C# `styleSheets.Add`。
- 应用级主题走 PanelSettings.themeStyleSheet + TSS。
- 提供工具或规范,禁止在运行时热更的 USS 里使用复杂 @import 链。

5. **内存与性能规范**(写入开发文档):
- 所有 UIToolkit 资产必须通过框架加载器加载,便于追踪释放。
- 大 UI 必须模块化拆 UXML。
- 动态列表必须用 ListView。
- 动画元素加 usageHints。
- 关闭 UI 必须释放对应资源(可做自动化检查)。

6. **与现有 UI 模块关系**:
- 当前 TEngine UI 模块是基于 UGUI + 纯 C# UIWindow/UIWidget 的成熟商业方案。
- UIToolkit 可作为**并行/可选**的第二 UI 技术栈引入(例如新功能、Editor 工具扩展、或逐步迁移)。
- 不要破坏现有 UGUI 流程,先做成独立模块或条件编译开关。

## 8. 风险与未决问题(需后续验证)

- PanelSettings 运行时创建 + 默认样式表的构建包含问题(已知,建议永远 Editor 预建资产)。
- 不同 Unity 版本(2021.3 vs 2022.3 vs 6000.x)TSS/@import 运行时行为的细微差异(建议在目标版本上实测)。
- 极高频打开/关闭 UI 时的句柄池与释放延迟(YooAsset 已有池化,需确认 UI 资产是否命中良好)。
- 热更单个 USS 后是否需要刷新已实例化的 VisualElement 样式(通常重新 Add 或重建树可解决,需验证)。

## 9. 参考资料(可直接点击)

- 官方 Addressables 加载 UI 示例:https://docs.unity3d.com/6000.6/Documentation/Manual/ui-systems/load-ui-assets-with-addressables.html
- 官方 UXML/USS 运行时加载:https://docs.unity3d.com/6000.6/Documentation/Manual/UIE-manage-asset-reference.html
- 性能优化最佳实践(内存、batch、隐藏策略):https://docs.unity3d.com/Manual/best-practice-guides/ui-toolkit-for-advanced-unity-developers/optimizing-performance.html
- 论坛关键帖:Addressables + Custom VisualElement、Theme/TSS 运行时问题、从字符串加载 USS 的限制讨论。
- YooAsset 文档与 TEngine 资源模块文档(3-1-资源模块.md)。

---

**下一步行动建议**:
1. 在 TEngine 分支上建一个小 Demo,把一个简单 UXML + USS 打进 YooAsset 包,用现有资源加载器异步加载 + CloneTree + 添加 StyleSheet,验证全流程。
2. 设计 `UIToolkitResourceLoader` 和基础 `UIToolkitWindow` 骨架(遵循现有 UIWindow 生命周期 + 异步 + 事件)。
3. 把本报告关键结论写入 `Books/` 或 `.claude/skills/tengine-dev/references/` 新增 `ui-toolkit-hotupdate.md`。
4. 针对 PanelSettings + Theme 做专项小测试,确认热更新主题的可行路径。

报告完成。可作为 UIToolkit 模块资源管道设计的直接输入。
143 changes: 143 additions & 0 deletions UnityProject/.claude/skills/tengine-dev/references/uitk-binding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# UITKModule 自动绑定

## 机制

Editor 预生成 `.bindgen.cs` 文件,运行时零反射。

**触发方式**:Unity 菜单 `TEngine → UITK → Generate All Bindings`

## Attribute

### [Q] — 元素查询绑定

```csharp
[Q] Button btnLogin; // → 查询 name="btn-login"
[Q] Label lblTitle; // → 查询 name="lbl-title"
[Q("explicit-name")] DropdownField serverList; // → 查询 name="explicit-name"
```

**命名规则**: camelCase → kebab-case
```
btnLogin → btn-login
lblPlayerHP → lbl-player-hp
inputAccount → input-account
imgAvatarBg → img-avatar-bg
```

### [OnClick] — 点击事件绑定

```csharp
[OnClick] void OnBtnLogin() { } // → 绑定 btn-login.clicked
[OnClick] void OnBtnClose() { } // → 绑定 btn-close.clicked
[OnClick("custom-btn")] void HandleClick() { } // → 显式指定目标
```

**推导规则**: 去掉 "On" 前缀,剩余部分转 kebab-case
```
OnBtnLogin → btn-login
OnBtnClose → btn-close
```

### [OnChange] — 值变化事件

```csharp
[OnChange("input-name")]
void OnNameChanged(ChangeEvent<string> evt) { }

[OnChange("slider-value")]
void OnSliderChanged(ChangeEvent<float> evt) { }
```

### [Bind] / [BindCommand] — MVVM 自动绑定

将 UI 控件直接绑定到 ViewModel 的 `BindableProperty<T>` / `BindableCommand`,**无需手动 += / -=**:

```csharp
[Bind("Gold", format: "{0:N0}")] Label lblGold; // OneWay + 格式化
[Bind("PlayerName", BindingMode.TwoWay)] TextField inputName; // 双向
[Bind("Hp", BindingMode.TwoWay, converter: typeof(IntToStringConverter))] TextField inputHp; // 异类型经转换器
[BindCommand("BuyCommand")] Button btnBuy; // 命令 + CanExecute

private ShopViewModel _vm; // 类内恰一个 ViewModelBase 字段,生成器据此解析
```

- `[Bind]/[BindCommand]` 字段会被自动查询赋值(隐含 `[Q]`,字段名 → kebab-case),无需再加 `[Q]`。
- 框架在 `OnCreate` 之后自动绑定、`OnDestroy` 之前自动解绑(同一委托实例,无泄漏)。
- 详见 [uitk-mvvm.md](uitk-mvvm.md)「自动绑定」。

## 完整示例

```csharp
[UIWindow(UILayer.UI)]
public partial class ShopWindow : UITKWindow
{
[Q] Label lblGold;
[Q] Label lblTitle;
[Q] Button btnBuy;
[Q] Button btnClose;
[Q] TextField inputSearch;

[OnClick] void OnBtnBuy()
{
// 购买逻辑
}

[OnClick] void OnBtnClose()
{
UITKModule.Instance.CloseUI<ShopWindow>();
}

[OnChange("input-search")]
void OnSearchChanged(ChangeEvent<string> evt)
{
// 搜索过滤
}

protected override void OnCreate()
{
// [Q] 字段已自动填充,直接使用
lblTitle.text = "商店";
}
}
```

## 生成文件

输入: `ShopWindow.cs` → 输出: `ShopWindow.bindgen.cs`(同目录)

生成内容:
```csharp
partial class ShopWindow
{
protected override void __UITKAutoBind(VisualElement root)
{
lblGold = root.Q<Label>("lbl-gold");
lblTitle = root.Q<Label>("lbl-title");
btnBuy = root.Q<Button>("btn-buy");
btnClose = root.Q<Button>("btn-close");
inputSearch = root.Q<TextField>("input-search");
}

protected override void __UITKAutoBindEvents()
{
btnBuy.clicked += OnBtnBuy;
btnClose.clicked += OnBtnClose;
inputSearch.RegisterValueChangedCallback(OnSearchChanged);
}

protected override void __UITKAutoUnbindEvents()
{
btnBuy.clicked -= OnBtnBuy;
btnClose.clicked -= OnBtnClose;
inputSearch.UnregisterValueChangedCallback(OnSearchChanged);
}
}
```

## 注意事项

- 类必须声明为 `partial class`
- 修改 [Q]/[OnClick]/[OnChange] 后需重新 Generate
- UXML 中对应元素的 `name` 属性必须匹配
- `.bindgen.cs` 文件不要手动编辑(会被覆盖)
- 生成的代码参与 HybridCLR 热更新
Loading