到达数据:跨 World 载荷不能用一次性事件交付

自动关联目录:到达数据:跨 World 载荷不能用一次性事件交付

核心判断:跨世界数据必须先于目标 World 到达,并在目标运行时完成首帧读取前持续有效;因此它是带迁移身份的快照,不是一次广播事件。

一、业务冲突

跨世界时,数据由源端或协调端先产生,目标 World、脚本 VM 和玩家 Actor 后创建。若把载荷挂在旧 World、旧 Actor 或 Travel 回调上,它会随旧对象销毁;若只广播一次,晚一帧创建的消费者就永久错过。UI 已经结束加载,也不代表玩法首帧已经读取完到达数据。

二、实际实现

实际方案把到达载荷存放在 GameInstance 生命周期的持有者中,在 Travel 前预装,目标 World 中可重复读取。读取不消费数据,脚本 VM、角色初始化和表现层不需要争夺同一事件。

载荷与加载界面分开管理:界面可以在视觉条件满足后关闭,而到达数据继续存活到本次到达流程结束。World teardown 不清理它,只在会话结束或 GameInstance 退出时兜底释放。服务端与客户端使用相同的值状态表达,避免两端各自解释一份临时参数。

三、值得保留

把“跨 World 保存”和“目标 World 使用”拆开,解决了 UE 对象生命周期与业务生命周期错位的问题。可重复读取也比消费式事件更稳健:消费者的初始化顺序不再构成协议的一部分。

四、结构性缺陷

单槽位、后写覆盖虽然简单,却无法证明当前 World 读到的是哪次迁移。第一次 Travel 失败、第二次请求已经开始或旧回调迟到时,仅靠“槽里有数据”可能把旧快照交给新世界。只在会话结束清理又过于宽松;载荷若缺少 Schema、完整性摘要和大小预算,还会把兼容与内存风险带进首帧。

五、目标设计

到达数据应有准备、激活、读取和过期四个阶段:

struct ArrivalSnapshot {
    TransferId transfer;
    uint32 attempt;
    WorldKey target;
    uint32 schemaVersion;
    Hash digest;
    ValueState values;       // 不携带 UObject、Actor 或脚本闭包
    MonotonicTime expiresAt;
};

Prepare(snapshot);                           // Travel 前写入
Activate(snapshot.transfer, worldGeneration); // 目标 World 身份确认后激活

Result<ValueState> TryRead(ArrivalKey key) {
    return key.Matches(activeTransfer, activeWorldGeneration)
        ? VerifySchemaAndDigest(snapshot)
        : Error::StaleArrival;
}

激活令牌把迁移记录与目标 World 世代绑定。失败回源、迁移被替换或过期时撤销提交权,而不是等待进程退出才清空。

六、取舍与验证

快照协议增加身份和校验成本,但换来了可重放、可诊断和与初始化顺序无关的交付。测试应覆盖 Travel 前后失败、同一迁移重复回调、两次迁移交错、UI 与玩法并发读取、目标 World 重建、载荷过期、Schema 不兼容和摘要错误。验收不看“事件是否触发”,只看正确 World 能否重复读到唯一匹配的快照。