百人对局(BR、大逃杀类)的复制瓶颈不在序列化,在相关性计算:ServerReplicateActors 每 tick 要回答"这 100 个连接各自 relevant 哪些 actor",默认实现是全量遍历所有 actor × 所有连接,复杂度 $O(N_{\text{actor}} \times N_{\text{conn}})$。1000 actor × 100 连接 = 每 tick 10 万次 IsNetRelevantFor 调用。ReplicationGraph 插件(Engine/Plugins/Runtime/ReplicationGraph)把这个问题重构了一遍。基于 UE5.3 源码梳理。
核心思路:把相关性变成缓存
默认模型的浪费在于相邻 tick 的相关性结果几乎不变——玩家移动是连续的,本帧 relevant 的 actor 和上帧 95% 相同,但默认实现每帧重新算。ReplicationGraph 的思路是两层缓存:
- actor → 哪些节点关心它:actor 注册时按类型/位置分到 graph 节点(
UReplicationGraphNode),节点内部维护"我管辖的 actor"列表。 - 连接 → 从哪些节点收列表:每个连接(
UReplicationGraphConnection)维护"我订阅了哪些节点",每 tick 只向订阅的节点要 actor 列表。
相关性计算被空间分桶吸收掉:UReplicationGraphNode_GridSpatialization2D 把世界切成 2D 网格,actor 进格,连接按视锥+距离查格。查格是 $O(\text{覆盖格数})$,比逐 actor 判距离便宜一个数量级,且网格列表可以跨连接共享——同格子的两个玩家查的是同一份 actor 列表。
节点类型与分工
ReplicationGraph 不是单一数据结构,是一组节点类的组合,项目里按需搭:
- GridSpatialization2D:空间分桶主节点,管"距离相关"的 actor。支持动态 actor(每 tick 重分格)和静态 actor(分一次格后休眠)。
- AlwaysRelevantNode:管
bAlwaysRelevant的 actor(GameState、PlayerState)。列表全连接共享,每 tick 零计算。 - DormancyNode:管 dormant actor。actor 休眠时移到这里,Flush 时移回空间节点。
- AdaptiveReplayNode / AlwaysRelevantStreamingLevelNode:观战回放和流送关卡的专项节点。
UReplicationGraph::RouteAddNetworkActorToNode 决定新 actor 进哪个节点,默认按 GetReplicationGraphNode 的 CDO 配置路由。自定义节点继承 UReplicationGraphNode,实现 GatherActorListsForConnection——这是整个插件的扩展点。
Gather:每 tick 到底做什么
每网络 tick,UReplicationDriver::ServerReplicateActors(ReplicationGraph 模式下的 driver 是 UReplicationGraph)对每个连接调 GatherActorListsForConnection:
- 空间节点按连接位置查格,返回本格 actor 列表;
- 各专项节点追加自己的列表;
- 去重、按优先级排序;
- 逐个走
ReplicateActor(后面就是属性同步那套协议,没变)。
成本从 $O(N_a \times N_c)$ 的逐对判断降到 $O(N_c \times N_{\text{格覆盖}})$ + $O(N_a)$ 的分格维护。1000 actor × 100 连接的场景,默认模型每 tick 10 万次距离/视锥判断;ReplicationGraph 下分格维护 1000 次,查格 100 连接 × 9 格 = 900 次,三个数量级的差距。实测 BR 项目里 ServerReplicateActors 的 gather 阶段从 3-4ms 降到 0.3ms 以内。
接入与代价
接入成本不高:.Build.cs 加 ReplicationGraph 模块,创建 UReplicationGraph 子类,在 GameMode 的 InitGame 里 SetReplicationDriverClass:
void AMyGameMode::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
{
Super::InitGame(MapName, Options, ErrorMessage);
SetReplicationDriverClass(UMyReplicationGraph::StaticClass());
}
子类的 InitGlobalActorClassSettings 里配置各类 actor 的路由规则。代价要认清楚:
- 相关性语义变了。默认模型的
IsNetRelevantFor是每对独立判定的;ReplicationGraph 是"格子里就是 relevant,格子外就不是"。自定义的 relevancy 逻辑(比如隔墙不可见)要么进ConditionalActorFilter,要么接受粒度变粗。 - 调试成本上移。默认模型的 bug 是"某个 actor 没同步";ReplicationGraph 的 bug 是"它进错格子了"或"路由规则没配"。
ReplicationGraph.Debug控制台变量族可以可视化格子归属。 - 小项目负收益。50 个 actor、4 个连接的场景,默认模型每 tick 200 次判断,ReplicationGraph 的分格开销反而更贵。百人对局是拐点。
ReplicationGraph 是 UE 复制体系里"用缓存换计算"思路的集中体现:相关性结果天然是连续的,把逐帧重算改成增量维护,再按空间维度把"哪些 actor 可能 relevant"的答案跨连接共享。理解了它,Iris 那篇里"相关性为什么被彻底重构成 fragment"就顺理成章了。