UI物化:数据树是真相,UWidget 只是可丢弃投影
自动关联目录:UI物化:数据树是真相,UWidget 只是可丢弃投影
核心判断:创作者 UI 的权威状态在文档树,UWidget/Slate 只是当前 World 和 Viewport 的运行时投影。投影可以晚建、重建和丢弃,但不能反过来成为数据真相。
一、业务冲突:节点事件可能早于父子关系与完整属性
编辑预览直接展示源 UI,Play 时又要从运行时克隆后的树构建玩家 UI。节点创建消息可能先于 Parent 或 Name;文本框会因单双行切换而重建控件;图片异步完成时,原节点甚至已销毁或换代。
若收到 NodeAdded 就立刻显示,加载页等编辑辅助节点可能闪进正式 HUD;若把 UWidget 当状态源,重建后输入、样式和脚本可见属性都会丢失。
二、实际实现:先建拓扑,再激活;属性缓存支持重放
工程监听节点新增、属性、父子关系、销毁和重载。Play 构建时先完成根拓扑,设置活动树,再重放待处理样式并标记布局;找不到 Parent 的节点暂存,AncestryChanged 到达后再挂接和失效命中缓存。
运行时 Screen 根延迟一帧确认身份,因为 Create 事件可能早于 Name。控件类型变化导致 UWidget 重建时,缓存属性会完整重放。图片请求同时携带节点 RequestToken 与 UI Generation,回调只有在节点、请求和整棵 UI 代际都匹配时才能应用。
flowchart LR
D[版本化 UI 数据树] --> P[Document Projection]
P --> H[运行时层级]
H --> W[UWidget/Slate]
C[属性缓存] --> W
R[异步资源 + Token] --> W
W --> E[输入/编辑事件]
E --> D三、值得保留:状态重放与 Teardown 屏障
“先拓扑、后激活”避免半棵树进入屏幕;“属性缓存、表示可重建”正确适配了 UE 控件生命周期。异步图片同时校验局部 RequestToken 与全局 Generation,也避免同一节点连续改图或整场 UI 重建时旧结果覆盖新状态。
退出 Play 前先标记 TornDown、增加 Generation、停掉 Ticker、解绑 Delegate 并清除 Viewport Widget,再允许文档回滚发出新的 Create 事件,这个顺序解决了重复试玩后 Slate 残留和按钮累积。
四、结构性缺陷:单体 Manager 同时承担五种一致性
现有管理器同时处理文档事件、Widget 物化、布局、命中、输入捕获、文本输入、滚动裁剪、图片合图、资源缓存和诊断。它能集中保证顺序,但任何重载、布局或资源策略改动都可能触发全局副作用,测试也难以隔离。
只有一个全局 Dirty 标志会把局部属性变化扩大成整树工作;反向依赖不完整时,又只能靠同步全量 Flush 保证命中正确。UObject/动态委托/Slate 指针混在同一所有者中,还会让 GC、Delegate 与 Viewport 移除的清理顺序变得脆弱。
五、目标设计:版本化投影管线,而非万能 UI Manager
拆成 DocumentProjection、WidgetMaterializer、LayoutEngine、HitGraph、ResourceBinder 与 PresentationHost。每层接收带 Revision 的变更集并输出新 Revision;节点属性真相只在 Projection,Widget 保存最小缓存和 NodeId,不承担持久状态。
结构变化先更新投影与依赖图,再批量物化;资源完成只提交到匹配 Generation/RequestToken 的属性槽;控件重建从 Projection 重放。PresentationHost 独占 AddToViewport/RemoveFromParent 和生命周期屏障,其他层不直接触碰 Viewport。
void ApplyBatch(ChangeSet changes)
{
ProjectionDelta d = documentProjection.Apply(changes);
MaterializeDelta m = materializer.Reconcile(d);
layout.Invalidate(m.layoutRoots);
hitGraph.Invalidate(m.hitRoots);
resourceBinder.Refresh(m.resourceSlots);
host.CommitVisibleRoots(m.rootChanges);
}六、决策与验证:重建之后行为不变才算状态完整
| 变化 | 应重建范围 | 必须保留的事实 |
|---|---|---|
| 普通样式属性 | 当前 Widget | NodeId、输入状态、其他缓存属性 |
| 单行/多行切换 | 文本控件子树 | 文本、选择区、焦点策略 |
| 父子关系变化 | 受影响拓扑 | 稳定节点身份与层级顺序 |
| Screen 根激活 | 根与 Host | 编辑/运行模式隔离 |
| 图片请求更新 | 单个资源槽 | 新 RequestToken 胜出 |
| Play 退出 | 整个投影 Generation | 旧回调与委托全部失效 |
UI 框架的质量不应以“支持多少控件”衡量,而应看任意事件乱序、表示重建和 World 退出后,数据树、屏幕结果与输入目标能否重新收敛到同一 Revision。