试玩隔离:Suppress Dirty 不能阻止运行时状态污染创作数据

自动关联目录:试玩隔离:Suppress Dirty 不能阻止运行时状态污染创作数据

核心判断:屏蔽属性广播和 Dirty 标记只能隐藏一次写入,不能把运行时状态从创作真相中隔离出去。

一、业务冲突

编辑器希望进入 Play 后让物理真正驱动物体,退出时又必须恢复创作者保存的位置。早期共享同一份对象布局最省成本:运行时每帧把物理 Transform 回写,编辑器和脚本都能立即读到最新值。但自动保存、退出 Play、场景重载和异步网格完成并不在同一个事务中。

二、实际实现

实际实现曾用“禁止广播”包裹物理回写,避免把每帧 Transform 标成待保存。问题在退出 Play 时暴露:Session 先结束,编辑场景稍后重载;两者之间若自动保存读到已被物理覆盖的内存布局,错误位置仍会写入磁盘。

修复在进入 Play 前遍历物理对象,缓存工程、关卡、返回操作身份、对象稳定身份、类型和编辑 Transform;退出后只有所有身份仍匹配才恢复脚本属性和 UE Actor,并强制关闭残留模拟,最后一次性销毁快照。它还拒绝旧返回回调消费新快照。

三、值得保留

这套补偿比“退出时把 Actor 移回去”严谨:快照同时验证对象身份、内容域和返回代次;恢复既写创作模型也写 UE 表示;物理 Body 的模拟状态被单独处理,避免 Actor 恢复后下一帧又因重力漂移。

四、结构性缺陷

快照是止损方案,不是隔离。它需要 O(N) 扫描,只覆盖已知的 Transform 和对象类型,未来运行时若修改材质、约束、动画状态或自定义属性,还会重复出现“忘了 stash”的问题。更严重的是,错误值在恢复前确实进入过创作模型,任何未受同一守卫保护的保存、插件或观察者都可能读到它。

五、目标设计

Play 应创建运行时覆盖层,创作基线保持不可变:

Value Read(PropertyKey key) {
    if (playOverlay.Contains(key)) return playOverlay[key];
    return authoringSnapshot[key];
}

void ApplyPhysicsSample(ObjectId id, Transform value) {
    playOverlay.Set({id, TransformKey}, value); // 永不写 authoringSnapshot
    projectors.UpdateActor(id, value);
}

ChangeSet CaptureRuntimeChanges(Selection s); // 需要时显式转为可审查编辑事务

自动保存只读取 Authoring Revision;退出 Play 只销毁 Overlay。若产品需要“保留试玩改动”,必须生成可预览、可撤销、可选择字段的 ChangeSet,而不是默认写穿。

六、取舍与验证

覆盖层会增加属性读取和内存成本,也要求旧代码停止直接写布局。迁移可先对物理 Transform 做 shadow overlay,并用语义对比确认 Actor 结果一致,再扩展到高风险属性。验收要在退出、自动保存、崩溃恢复、异步资源完成和重复 Play 中证明:未显式提交的运行时变化永远不会改变创作 Revision 或持久化字节。