Skip to content

fix(unreal): 修复生成蓝图的继承组件配置跨机器丢失 - #2353

Open
ZhiruiLi wants to merge 1 commit into
Tencent:masterfrom
ZhiruiLi:fix/deterministic-scs-node-guid
Open

fix(unreal): 修复生成蓝图的继承组件配置跨机器丢失#2353
ZhiruiLi wants to merge 1 commit into
Tencent:masterfrom
ZhiruiLi:fix/deterministic-scs-node-guid

Conversation

@ZhiruiLi

@ZhiruiLi ZhiruiLi commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

问题背景

PuerTS 会把组件类型的 uproperty 生成为蓝图中的 SCS 节点。开发者可以在继承该生成蓝图的子蓝图里编辑组件属性,UE 会把这些属性覆写保存到子蓝图的 UInheritableComponentHandler 中。

UInheritableComponentHandler 使用 FComponentKey{OwnerClass, AssociatedGuid} 标识继承组件。其中 AssociatedGuid 来自父蓝图 SCS 节点的 VariableGuid。匹配时只比较 GUID,不会在 GUID 不一致时按组件变量名回退匹配。

问题在于,USimpleConstructionScript::CreateNode 通过 FGuid::NewGuid() 随机生成 VariableGuid。当 TypeScript 生成蓝图被视为本地产物、没有加入版本管理时,不同用户会分别生成自己的父蓝图,得到不同的 SCS 节点 GUID:

  1. 用户 A 生成父蓝图,组件节点得到 GUID A;
  2. A 在受版本管理的子蓝图中修改该继承组件的字段,子蓝图的覆写记录引用 GUID A;
  3. 用户 B 在本地重新生成同一个父蓝图,组件节点得到 GUID B;
  4. B 同步并打开子蓝图时,UE 无法使用 GUID B 找到引用 GUID A 的覆写记录;
  5. UInheritableComponentHandler::ValidateTemplates() 会把这条记录判定为不再需要并删除;
  6. 组件字段静默回退到父模板默认值。如果 B 随后保存子蓝图,原覆写数据会从受版本管理的资产中永久消失。

因此,即使生成蓝图的路径、类名、组件变量名和属性定义完全一致,内部 GUID 不同仍会导致继承组件配置跨机器丢失。

处理方式

1. 为 SCS 节点生成确定性 GUID

新增 MakeStableSCSNodeGuid(),根据以下内容派生 SCS 节点的 GUID:

<生成蓝图对象路径>:<组件变量名>

同时包含蓝图对象路径和组件变量名,可以避免不同生成类中存在同名组件时发生冲突。

这里使用 MD5 只是为了获得稳定的 128 位结果,不涉及安全用途。实现中显式按小端顺序把 digest 组装为 FGuid 的四个 uint32,避免结果依赖主机字节序。

没有直接使用 FGuid::NewDeterministicGuid(),因为较旧的 UE 版本没有该接口,PuerTS 需要兼容这些版本。

2. 每轮生成时幂等归一已有节点

新增 NormalizeSCSNodeGuids(),在每轮生成时检查并更新已有的 SCS 节点,而不是只在首次创建节点时设置 GUID。

这样存量生成资产无需删除重建,只要重新执行一次完整生成,就会自动收敛到确定性 GUID。

处理范围包括:

  • USimpleConstructionScript::GetAllNodes() 返回的全部节点;
  • 单独处理 GetDefaultSceneRootNode(),因为默认根节点只有在实际作为根节点时才会出现在 AllNodes 中。

只有节点 GUID 确实发生变化时才返回 changed 并触发保存;已经完成归一的蓝图不会在后续生成中被重复写盘。

3. 在编译前完成归一

UPEBlueprintAsset::Save() 中、蓝图编译和保存之前执行归一。

这个顺序很重要:继承组件覆写记录的校验发生在蓝图编译阶段。如果在编译之后才修改 GUID,旧记录可能已经被 ValidateTemplates() 删除。

PIE 期间不执行归一,因为此时 UE 不允许修改类布局;退出 PIE 后的下一轮生成会自动完成收敛。

兼容性与迁移说明

这次修改会让已有生成蓝图的 SCS 节点身份发生一次性变化。已有子蓝图中的覆写记录仍然引用旧的随机 GUID,因此第一次升级时无法自动迁移这些历史记录。

升级后建议执行:

  1. 完整重新生成一次 TypeScript 蓝图;
  2. 打开受影响的子蓝图;
  3. 重新填写一次继承组件的属性覆写;
  4. 保存并提交这些子蓝图资产。

完成这次迁移后,各机器会生成相同的 GUID,后续重新生成或跨机器同步都不会再导致覆写记录失效。

组件变量改名会改变派生 GUID,因此改名后同样需要一次性重新填写对应覆写。这与把“蓝图路径 + 组件变量名”作为组件稳定身份的设计一致。

验证情况

已在一个“TypeScript 生成蓝图不加入版本管理、子蓝图加入版本管理”的 UE/PuerTS 项目中完成验证:

  • 修改后的 PuertsEditor 模块编译成功;
  • 完整生成流程处理了 61 个生成蓝图;
  • 其中 12 个包含 SCS 节点的资产都完成了一次归一并保存;
  • 所有生成的 *_GEN_VARIABLE 组件都包含预期的确定性 GUID,没有遗漏节点;
  • 再次执行完整生成时,已归一资产没有被重复写盘,确认操作幂等;
  • 在子蓝图中重新填写了继承组件的 14 个数值字段,编译并保存后全部保留;
  • 子蓝图 UInheritableComponentHandler 的键与父蓝图 SCS 节点的确定性 GUID 一致;
  • 日志中没有出现继承组件模板被判定为 invalidunnecessary 而删除的记录。

改动范围

本 PR 只修改一个 Unreal Editor 实现文件,没有新增或修改公共 API:

unreal/Puerts/Source/PuertsEditor/Private/PEBlueprintAsset.cpp

Derive generated SCS node GUIDs from the blueprint path and component variable name so inherited component overrides remain valid when generated blueprints are recreated on another machine.

Normalize existing nodes on every generation pass before compilation and saving. This makes the migration idempotent and includes the default scene root while avoiding class layout changes during PIE.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant