高密度玩法同步:Replication Graph、Dormancy 与状态拆分

这篇回答高密度玩法场景中的网络同步问题。重点不是背 Unreal 名词,而是说明怎样从“每连接候选集合”一直治理到“状态正确性和性能验收”。

面试官:你遇到过什么高密度同步问题?

30 秒回答

我参与的多人玩法中有大量交互物、掉落物和容器。压力不只是 Actor 数量,而是每个连接每帧要筛选多少候选、多少对象需要检查属性、同步频率多高,以及拾取或开箱时是否产生瞬时 RPC 和 UI 更新。我接入并调整过 Replication Graph、距离与可见性频率、Dormancy 和 Push Model,也做过物品稳定身份与背包状态接入。核心思路是先减少相关对象,再减少同步频率,最后减少未变化属性扫描,同时保证关键交互和晚加入仍然正确。

90 秒回答

我会把问题拆成五层。第一层是相关性,用 Replication Graph 和场景规则缩小每个连接的候选集合;第二层是优先级和频率,近处、可见、正在交互的对象高频,远处或不可见对象降频;第三层是休眠,长期不变化的容器和交互物进入 Dormancy,变化前唤醒并刷新;第四层是属性复制,Push Model 和 Fast Array 只处理真正变脏的数据;第五层是语义,把需要晚加入恢复的持久状态与一次性表现事件分开。

我在项目中直接做过 RepGraph 启用与参数接入、不可见对象降频、Push Model 配置、容器 Dormancy 以及物品同步结构。引擎 RepGraph 内核和玩法中所有网络模块不是我独立设计的。现有仓库没有完整网络抓包和前后基准,所以我能讲清机制和验证方案,但不会编造节省百分比。

面试官:为什么 Actor 数量不是唯一指标?

单看世界里有多少 Actor 会误判问题。更关键的是:

每连接成本 ≈ 候选 Actor 数
           × 更新检查频率
           × 单 Actor 属性扫描与序列化成本
           + RPC / 创建销毁 / 重传峰值

十二人场景中,同一个对象可能对不同玩家有完全不同的相关性。数百个静态 Actor 如果大多不相关且已休眠,成本可能可控;几十个全连接高频复制、属性持续变化的 Actor 反而更贵。因此定位必须同时看连接数、候选数、实际复制数、字节量和服务端帧时间。

面试官:你的整体优化框架是什么?

flowchart LR
    WORLD["世界中的全部网络对象"]
    REL["相关性筛选<br/>空间 / 队伍 / 玩法上下文"]
    PRI["优先级与频率<br/>距离 / 可见 / 交互重要性"]
    DORM["Dormancy<br/>长期稳定对象跳过"]
    DIRTY["Push Model / Fast Array<br/>只处理变化数据"]
    WIRE["状态复制 / RPC<br/>序列化与发送"]
    CLIENT["客户端投影<br/>表现 / UI / 交互反馈"]

    WORLD --> REL --> PRI --> DORM --> DIRTY --> WIRE --> CLIENT

这几层不能颠倒。只压缩序列化,却让所有连接遍历全部对象,CPU 仍然高;只做空间裁剪,却让相关对象每帧全量扫描属性,成本仍然浪费;只做降频,却没有关键状态补偿,会出现交互延迟和状态错误。

面试官:Replication Graph 解决什么问题?

它把“某个连接本帧应该考虑哪些 Actor”从逐 Actor、逐连接的分散判断,组织成可复用的节点和列表。空间节点管理按位置相关的对象;全局或玩法节点管理始终相关、队伍相关或特定阶段相关的对象;连接节点再补充玩家私有对象。

这样优化的第一目标是减少候选集合,而不是直接承诺网络字节一定减少。Actor 最终是否发送、发送哪些属性,还取决于频率、优先级、Dormancy 和脏状态。

面试官:相关性、优先级和频率有什么区别?

概念回答的问题结果
相关性这个连接是否需要知道该 Actor?不相关时直接排除
优先级多个相关 Actor 争用预算时先发谁?影响发送次序和饥饿补偿
频率这个 Actor 多久进入一次更新检查?降低重复检查和发送次数

项目中对不可见对象做过降频,例如在对应模式下把更新间隔放大到十二帧;距离阈值也分档处理。这个数字是项目参数,不是通用答案。真正设计时还要给正在交互、即将进入视野或关键任务对象设置提权通道,避免低频策略损害手感。

面试官:为什么不直接使用引擎默认距离优先级?

默认策略只看到通用网络属性,不一定理解玩法语义。一个较远的撤离点、任务目标或正在被队友操作的容器,业务重要性可能高于近处装饰物。因此项目可以保留 Actor 网络优先级,同时按自定义距离分档、可见性和玩法状态控制频率,而不是把所有决策交给单一距离函数。

代价是策略更复杂,必须防止对象长期得不到更新。通常要有最大静默时间、关键变化立即提权,以及调试工具展示对象为何被筛掉或降频。

面试官:Dormancy 怎么用?

适合 Dormancy 的是“长期存在、绝大多数时间不变、变化时可明确唤醒”的对象,例如未被打开的补给容器。对象初始化完成并同步稳定状态后进入休眠,服务端不再反复检查和复制它;发生开箱、拾取或状态变化前,先唤醒或 flush dormancy,再修改需要同步的属性。

正确顺序很重要:

服务端确认交互合法
  -> 唤醒 / FlushNetDormancy
  -> 修改权威状态并标脏
  -> 复制给相关连接
  -> 状态再次稳定后进入休眠

如果先改属性、后唤醒,更新可能不能按预期发出;如果每次 Tick 都 flush,Dormancy 就失去意义。

面试官:Dormancy 最容易出什么 bug?

  1. 服务器状态已经变化,客户端仍看到旧外观。
  2. 晚加入玩家拿不到最终快照,只看到初始状态。
  3. Actor 重新相关时没有发送最新状态。
  4. 销毁、隐藏和休眠混为一谈,客户端残留可交互碰撞或 UI。

测试不能只覆盖房主从头游玩,要覆盖晚加入、离开相关范围再返回、断线重连、连续多人交互和休眠期间状态变化。

面试官:Push Model 和 Fast Array 分别解决什么?

Push Model 的重点是跳过未标脏属性,减少每帧比较“到底有没有变化”的扫描成本。它不会自动判断业务数据何时变化,写入点必须正确标脏,否则会出现服务端正确、客户端不更新的隐蔽问题。

Fast Array 适合带稳定条目身份的集合增删改,例如背包、掉落列表或状态条目。它同步集合差异和条目变化,而不是每次发送完整数组。启用“跳过未变脏 Fast Array”后,同样依赖正确的 MarkItemDirty、MarkArrayDirty 和条目生命周期。

两者的收益主要先体现在服务端属性检查与序列化 CPU,不应简单等同于网络字节必然按同一比例下降。

面试官:物品同步结构为什么需要稳定身份?

客户端 UI、交互世界对象和服务端背包需要确认“这是同一个物品”,不能只靠数组位置。基础同步信息至少要区分:实体身份、物品类型、实例身份、耐久或状态,以及必要的扩展项。

EntityID      连接世界实体与玩法对象
UUID          标识具体物品实例
ItemTypeID    查静态配置与表现
InstanceID    关联道具或能力实例
MutableState  耐久、数量、扩展词条等动态状态

稳定身份使 Fast Array delta、UI 局部刷新、拾取确认和重连恢复都能按 ID 对齐。客户端可以乐观表现,但服务端必须验证归属、距离、占用和物品当前状态。

面试官:状态复制和 RPC 应该怎样拆?

判断标准不是“哪个写起来方便”,而是新玩家或重连玩家是否必须恢复这件事。

类型示例建议
持久状态容器已打开、物品已被拾取、当前耐久权威复制状态
瞬时意图玩家请求交互、使用技能Server RPC + 服务端校验
一次性表现音效、受击飘字、镜头反馈RPC 或由状态变化在本地派生
可重建表现开箱后的模型、UI 条目尽量由复制状态投影

如果只广播一次“容器打开了”,晚加入玩家会永远错过;如果把每个粒子和声音都建成复制状态,又会增加状态复杂度。原则是状态保证可恢复,事件保证即时表现。

面试官:客户端预测能用在拾取或交互上吗?

可以预测低风险表现,例如高亮、进度条起播和按钮反馈,但不能预测权威归属。服务端仍要检查距离、视线或资格、对象是否被其他人占用、背包容量和请求时序。失败时客户端撤销表现并显示可理解反馈。

高竞争对象还要有幂等请求身份,避免网络重试导致重复发奖。最终背包和世界物品状态都应由服务端权威结果收敛。

面试官:如何验证优化有效并且没有破坏正确性?

性能指标

维度重点指标
DS CPURepGraph gather、属性复制、序列化耗时与 P95/P99 帧时间
每连接候选 Actor、实际复制 Actor、活跃 channel 和饥饿对象
网络每秒字节、包数、RPC 峰值、重传与 packet loss 下表现
客户端帧时间、对象创建销毁峰值、UI 更新和内存

正确性用例

  • 十二人分散、聚集、集中拾取三个压力形态。
  • 数百交互物中只有少量相关、全部进入视野、连续变更三个极端。
  • 晚加入、断线重连、离开相关区域再返回。
  • Dormancy 唤醒、对象销毁、背包满、多人同时拾取。
  • 高延迟、丢包和乱序条件下的请求幂等与最终状态。

测试需要固定地图、玩家脚本、对象数量和采样窗口,比较改造前后的中位数与尾延迟。现阶段缺少项目历史抓包,所以这些是下一步应补的验收方法,不是已经获得的结果数字。

面试官:你个人具体做了什么?

我直接做过玩法侧 Replication Graph 启用和参数接入,对不可见对象与距离档位调整同步频率,接入 Push Model 的未变脏跳过策略,并在补给容器上使用 Dormancy。物品链路中我定义和接入过包含实体 ID、UUID、类型、耐久与实例信息的同步结构。

RepGraph 引擎内部实现、整个玩法的网络架构和所有 AI/交互系统属于团队共同能力。我可以解释这些模块如何协作,也能沿源码补齐完整方案,但不会把团队系统说成个人从零设计。

面试官:如果重新做一次,你会改进什么?

第一,把网络预算变成设计输入。立项时就定义每连接候选数、字节预算、关键对象最大延迟和 DS 尾帧,而不是上线前才看总带宽。第二,增加可解释的 RepGraph 调试视图,展示对象所在节点、相关性原因、上次复制时间和 Dormancy 状态。第三,把晚加入和断线恢复纳入自动化用例,因为很多“优化后偶现不同步”本质是只验证了实时玩家。

快速背诵版

高密度同步不能只看 Actor 数。我会按每连接候选集合、相关性、优先级与频率、Dormancy、脏属性和状态语义逐层治理。项目里我接入过 Replication Graph,给不可见和远距离对象降频,对稳定容器使用 Dormancy,变化前唤醒并 flush,同时启用 Push Model 跳过未变脏属性;物品同步使用稳定 EntityID 和 UUID 对齐世界对象、背包与 UI。持久结果走复制状态,一次性意图和表现才走 RPC。优化后还必须测 DS CPU、每连接候选、网络字节和尾延迟,并覆盖晚加入、重连和休眠唤醒。现有材料缺少完整基准,所以我不会报未经验证的百分比。

责任边界

内容面试表述
RepGraph 项目启用、参数接入和频率策略可说“我直接接入和调整”
容器 Dormancy 与物品同步结构可结合代码说“我实现/参与实现”
Push Model 配置与玩法写入点配合说清自己接入范围
引擎 RepGraph 内核可解释,不说成个人原创
全玩法网络架构说“团队系统和上下游协作”
十二人、数百对象作为目标压力场景,不说成已有量化结论

关联阅读