复制依赖:Owner变化不该靠每帧全表巡检
自动关联目录:复制依赖:Owner变化不该靠每帧全表巡检
核心判断:子 Actor 跟随父 Actor 的相关性是一条有生命周期的图边。注册时读取一次 Owner 不够,但用每帧扫描全部子 Actor 修复,也只是把正确性债务搬进复制准备阶段。
一、业务冲突:附件应跟随宿主,脱离后又必须独立同步
挂载物、载具附件和临时子实体通常不需要自己做距离相关性判断;父 Actor 对某连接相关时,子 Actor 才有意义。但父对象销毁、Owner 清空或换宿主后,子对象可能继续存在,必须恢复自己的相关性路径。
二、实际实现:依赖边加独立回退,正确性意识是对的
Actor 加入节点时,若有有效 Owner,就把它登记为父 Actor 的依赖;没有 Owner 则进入普通 Actor 列表。每个复制准备阶段遍历“已有父”和“无父”两个集合:Owner 改变时删除旧父关系并重新挂接,失去 Owner 时回到独立节点,后来获得 Owner 时再转成依赖。
这比“有过 Owner 就永远从属”可靠,也明确处理了宿主销毁后附件继续存在的业务情况。问题不在回退语义,而在变化检测方式和边的所有权。
三、结构性缺陷:全表扫描、残留项和多父误删
每帧两次集合遍历使成本按全部依赖 Actor 增长,与真正发生 Owner 变化的数量无关。当前无效 Actor 分支只跳过,没有当场移除,残留项可能长期占据容器。重新挂接前使用“删除该子 Actor 的全部父关系”,还可能误删其他系统建立的合法依赖边。
更深的问题是 Owner 同时参与 RPC 所有权、连接归属和 Gameplay 语义,它不天然等于唯一复制父。把所有依赖都隐式绑定到 Owner,会让未来的多父、临时观察或组合实体无法表达。
四、目标设计:显式边事件,低频审计兜底
struct RepEdge {
ActorId parent;
ActorId child;
EdgeKind kind; // OwnerFollow / Attachment / GameplayDependency
uint32 generation;
};
EdgeToken SetReplicationParent(Actor& child, Actor* next, EdgeKind kind) {
EdgeToken old = Edges.FindOwned(child, kind);
if (old) Edges.Remove(old); // 只删除本系统拥有的边
if (!next) return RouteIndependently(child);
return Edges.Add(*next, child, kind);
}所有会改变语义 Owner 的服务端入口必须同时更新复制边;Attachment 与 Gameplay 依赖使用不同 EdgeKind。Actor 销毁时按 Token 清边。为防止遗漏,每帧只审计一个分片,发现不一致则修复并记录调用栈或原因,而不是把审计当主更新通道。
五、验证不变量
- 每条边只有一个明确拥有者,删除 Token 不影响其他父关系。
- 无父但仍存活的 Actor 必须拥有独立路由,不能静默消失。
- 父销毁、换 Owner、Detach、对象池复用和 Travel 后不存在跨代边。
- 热路径成本与本帧边变化数相关;审计成本有固定预算。
应记录依赖边数、每帧增删数、独立回退数、审计不一致数、残留无效项和一次父变化到子 Actor 首次正确复制的延迟。