笃行
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
  • 引擎骨架

    • UE 引擎(客户端—服务器交互)
    • 引擎架构与一帧的时序
    • UObject / GC / 反射
    • Gameplay Framework 与端存在性矩阵
  • 复制栈底层

    • NetDriver / Channel / Bunch
    • 属性复制
    • RPC
  • 带宽与规模

    • Relevancy 与带宽预算
    • UE5 Iris 复制系统
  • 局内同步

    • 移动预测与回滚
    • 延迟补偿
    • 网络物理同步
  • 系统与运维

    • GAS 网络模型
    • 专用服务器 · Session · Travel
    • 服务器权威与反作弊

Relevancy 与带宽预算

「100 人同屏,服务器带宽怎么办?」——这是 UE 网络最硬核的规模问题。答案不是「优化一下」,而是一套可量化的账:哪些 Actor 要发给谁(Relevancy)、多久发一次(频率与休眠)、每客户端多少带宽(预算算式)、判定本身扛不扛得住(ReplicationGraph)。本篇把这套账算给你看。


一句话结论

Relevancy 决定「谁收谁」,频率/优先级/Dormancy 决定「发多勤」,带宽 ≈ 每Actor字节 × 相关Actor数 × 频率;默认判定 O(连接×Actor) 不可扩展,靠 ReplicationGraph/Iris 破。

场景问题

打个比方:这像给全城送报纸。100 户人家,如果每户都要收到另外 99 户的动态,那是 1 万份稿件——O(N²),印刷厂先破产。所以第一件事不是「把纸印薄点」(压缩),而是只送该送的:你隔着三条街看不见的人家的动态,根本不用送给你(Relevancy)。

第二件事是送多勤:重要的邻居每天一送(高 NetUpdateFrequency),远处的每周一送(NetCullDistance 外或降频),常年没动静的干脆停送、有变化再恢复(Dormancy)。

第三件事是算账:每户每天收多少字节 = 每份稿件字节 × 该送几份 × 送多少次。这笔账要能算出来,才能回答「100 人房间要多少带宽」。

最后一件容易被忽略:分拣本身也要钱。就算最后只送 20 份,如果每天都要把 100 户 × 100 户挨个比一遍「该不该送」,光分拣就压垮了印刷厂——ReplicationGraph 就是预先画好片区图,按片区直接取,不再每次全城比对。

服务器上行带宽 = 每个客户端的下行 × 客户端数。一个 100 人房间,如果每人都要收到其他 99 人的完整状态,带宽是 O(N²) 增长——100 人就是 ~1 万条 Actor 关系。所以第一步不是压缩,而是砍掉不必要的复制:你看不见的敌人根本不该发给你。这就是 Relevancy。之后才是「发多勤、怎么排、判定本身多贵」。

实现方案

1. Relevancy 判定链:谁收谁

AActor::IsNetRelevantFor(Viewer, ...) 决定某 Actor 是否要复制给某连接。判定按优先级(前面的覆盖后面的):

关键开关:

  • bAlwaysRelevant:永远发给所有人(GameState、全局管理器)。慎用,绕过所有裁剪。
  • bOnlyRelevantToOwner:只发给 owner(PlayerController 默认如此)。
  • NetCullDistanceSquared:距离剔除半径(平方,省开方)。超出就不相关。大世界最重要的裁剪手段。
  • 视线/最近可见:进一步剔除被遮挡的。

2. 节流:发多勤

相关不等于每帧发,频率由这些控制:

  • NetUpdateFrequency:每秒最多考虑复制几次(默认 100)。高频对象(玩家)设高,静态对象设低(如 2~5)。
  • MinNetUpdateFrequency:没变化时降到的最低频率(默认 2),配合自适应。
  • net.UseAdaptiveNetUpdateFrequency:开启后,没变化的 Actor 自动降频,省 CPU 和带宽。
  • NetPriority:饱和时的排序权重。高优先级先发,低的可能被饿死(见 NetDriver/Channel/Bunch 的饥饿)。
  • Dormancy(休眠):几乎不变的 Actor 可休眠,彻底退出复制考虑:
    • DORM_Awake:正常复制。
    • DORM_DormantAll:对所有连接休眠,不再比较不再发。
    • DORM_Initial:一出生就休眠(如静态道具,发完初始状态就不管了)。
    • 需要改动时调 FlushNetDormancy() 临时唤醒发一次。休眠是大世界省 CPU 的利器——一堆摆设道具发完初始值就休眠。

3. 带宽预算算式

把账算出来(数字均为数量级估算,非实测):

每客户端下行 ≈ Σ(每个相关 Actor 每次序列化字节) × 复制频率
服务器上行   ≈ 每客户端下行 × 客户端数

举例(示意量级):

  • 一个玩家角色每次复制约 50~100 字节(位置+动作+属性,经 NetQuantize 压缩后)。
  • 100 人房间,每人平均相关 30 个其他玩家,频率 20Hz:
    • 每客户端下行 ≈ 30 × 80 字节 × 20 ≈ 48 KB/s
    • 服务器上行 ≈ 48 KB/s × 100 ≈ 4.8 MB/s(单房间!)
  • MaxClientRate 默认约 15000 B/s 量级——上面这个已经远超默认,必须靠 Relevancy 把「相关 30 人」压下去、靠 NetQuantize 把每 Actor 字节压下去、靠降频把 20Hz 压下去。

这套算式和 极限发压工具 估算「一台机器能扛多少长连接」是同一种数量级思维——先算账,再优化。

4. ReplicationGraph:判定本身的可扩展性

默认 Relevancy 判定是 O(连接 × Actor):每帧对每个连接遍历每个 Actor 问「相关吗」。1000 Actor × 100 连接 = 每帧 10 万次判定,判定本身就吃满 CPU——这才是大房间的真瓶颈,不是带宽。

ReplicationGraph 把「谁和谁相关」预先组织成图节点,避免每次全量遍历:

  • GridSpatialization2D 节点:把世界切成网格,Actor 按格子登记。一个连接只需查它所在格子及邻近格子,把 O(Actor) 降到 O(附近 Actor)。
  • AlwaysRelevant 节点:全局相关对象(GameState)集中管理,一次算好给所有人。
  • AlwaysRelevantForConnection 节点:每连接固定相关的(自己的 Pawn/PC),直接挂连接上。

接入代价:要为项目写自定义 ReplicationGraph 类,把 Actor 分类到合适节点,改造成本不低。适合明确要做大房间(100+)的项目。

一句话记住这几组容易混的概念:

一组差别一句话类比
Relevancy vs DormancyRelevancy 是「你不该收这个 Actor」(看谁);Dormancy 是「这个 Actor 没变化,谁都别发」(看有没有变)不在你的送报片区 vs 这家根本没出新稿
NetUpdateFrequency vs NetPriority频率定「一秒最多考虑几次」;优先级定「带宽不够时谁先上车」一周送几次 vs 车装不下时先送谁
带宽瓶颈 vs CPU 瓶颈带宽是「线路塞不下」;判定 CPU 是「分拣算不完」——大房间里后者往往先炸邮车不够 vs 分拣员不够
ReplicationGraph vs IrisRepGraph 优化「谁和谁相关」的判定;Iris 重构整个复制序列化路径重画片区图 vs 整个印刷厂换设备(见 Iris)

优化顺序也就出来了:先砍相关数(Relevancy/Dormancy)→ 再降频率 → 再压每 Actor 字节(NetQuantize/FastArray)→ 判定 CPU 撑不住才上 ReplicationGraph/Iris。反过来先上 RepGraph 是把最贵的改造放在最前面。

5. 剖析工具:定位吃带宽的 Actor

排查「谁在吃带宽」的步骤:

  1. stat net:控制台看总体收发字节、包数、每秒复制的 Actor 数——先看总量是否异常。
  2. stat NetDormancy:看有多少 Actor 该休眠没休眠。
  3. NetworkProfiler:启动加 -networkprofiler=true 生成 .nprof,用 NetworkProfiler 工具打开,按 Actor/属性排序看谁占的字节最多——这是定位大户的主力工具。
  4. CSV Profiler:csvprofile start/stop 导出时间序列,看带宽随时间/场景的变化。
  5. net.DumpRelevantActors:打印某帧对某连接相关的 Actor 列表——确认「不该相关的是不是错误地相关了」。

典型排查链:stat net 发现上行超标 → NetworkProfiler 看哪个 Actor 类占字节最多 → 检查它的 NetUpdateFrequency 是否过高、是否该休眠、NetCullDistance 是否过大、属性是否该上 NetQuantize/COND_。内核侧的收发优化(epoll、零拷贝、多队列网卡)见 全栈极限低延迟,那是另一层的账。

为什么这么做

先裁剪再压缩:Relevancy 是数量级的节省(不发 = 0 字节),压缩只是常数倍。100 人房间靠 NetCullDistance 把「相关 99 人」砍到「相关 20 人」,比把每 Actor 从 100 字节压到 50 字节收益大得多。

频率/休眠分层:不同 Actor 变化频率天差地别——玩家每帧动、道具永远不动。统一高频是浪费,按需分频 + 休眠让 CPU/带宽花在真正变化的东西上。

ReplicationGraph 破 O(连接×Actor):带宽能靠裁剪压下来,但判定次数压不下来——只要还是「每连接遍历每 Actor」,Actor 一多 CPU 就崩。空间网格把判定局部化,是大房间唯一的扩展路径(或上 Iris)。

为什么别的选择不行

  • 全 bAlwaysRelevant:绕过所有裁剪,带宽 O(N²) 爆炸,100 人必崩。
  • 统一高 NetUpdateFrequency:静态道具也每秒考虑 100 次,CPU 浪费在没变化的对象上。
  • 不用 ReplicationGraph 硬扛大房间:判定 O(连接×Actor) 每帧十万次,GameThread 直接打满,带宽再省也没用。
  • 凭感觉优化不测量:吃带宽的往往是意料之外的 Actor(某个忘了降频的特效管理器)。不用 NetworkProfiler 定位就是瞎调。

沉淀结论

规模问题的账:Relevancy 决定谁收谁(bAlwaysRelevant/bOnlyRelevantToOwner/NetCullDistanceSquared/视线),频率/优先级/Dormancy 决定发多勤。带宽 ≈ 每Actor字节 × 相关Actor数 × 频率,服务器上行再 × 客户端数。默认判定 O(连接×Actor) 不可扩展,大房间靠 ReplicationGraph(空间网格)或 Iris。定位大户用 NetworkProfiler。

记忆口诀

谁收谁:AlwaysRelevant 全发 / OnlyRelevantToOwner 只给主 / NetCullDistance 距离剔除 / 视线遮挡剔除
发多勤:NetUpdateFrequency 频率 / 自适应降频 / NetPriority 排序防饥饿 / Dormancy 休眠退出复制
算账:每Actor字节 × 相关数 × 频率 = 每客户端下行 / × 客户端数 = 服务器上行
扩展:默认 O(连接×Actor) / ReplicationGraph 空间网格局部化 / Iris 全局脏状态
剖析:stat net 看总量 / NetworkProfiler 定位大户 / DumpRelevantActors 查相关性

内容来源

综合整理。主要参考:Epic 官方文档 Network Relevancy、Actor Dormancy、Replication Graph、UE 源码 ReplicationGraph 插件、Epic 官方演讲《Replication Graph》(Fortnite 大规模实践)。数字为引擎默认值或业界数量级估算,非实测。

相关专题:饥饿与带宽配额的底层见 NetDriver/Channel/Bunch;属性字节压缩(NetQuantize/COND_)见 属性复制;下一代复制系统见 UE5 Iris;单机承载压测方法见 极限发压工具;内核侧收发优化见 全栈极限低延迟。

自测:合上资料能说清楚吗?

100 人房间服务器带宽怎么算?怎么压下来?

参考答案

每客户端下行 ≈ 每相关 Actor 字节 × 相关 Actor 数 × 复制频率;服务器上行 ≈ 每客户端下行 × 客户端数(O(N²) 增长)。三个方向压:① Relevancy(NetCullDistanceSquared 距离剔除)把「相关 Actor 数」砍下来——收益最大;② NetQuantize/COND_ 把「每 Actor 字节」压下来;③ 降频 + Dormancy 把「频率」压下来。

大房间真正的瓶颈是带宽还是 CPU?为什么?

参考答案

往往是 CPU(Relevancy 判定)。默认判定是 O(连接×Actor):每帧对每个连接遍历每个 Actor 问是否相关,1000 Actor × 100 连接 = 每帧 10 万次判定,GameThread 打满。带宽可以靠裁剪压,但判定次数压不下来。解法是 ReplicationGraph 用空间网格把判定局部化,或上 Iris。

Dormancy 三态是什么?什么时候用?

参考答案

DORM_Awake(正常复制)、DORM_DormantAll(对所有连接休眠,退出复制比较)、DORM_Initial(出生即休眠,发完初始状态就不管)。几乎不变的 Actor(摆设道具、静态建筑)用休眠彻底退出复制考虑,省 CPU。需要改动时调 FlushNetDormancy() 临时唤醒发一次。

怎么定位是哪个 Actor 最吃带宽?

参考答案

stat net 先看总收发是否异常 → 启动加 -networkprofiler=true 生成 .nprof,用 NetworkProfiler 工具按 Actor/属性排序看谁占字节最多 → 用 net.DumpRelevantActors 确认相关性是否错误。找到大户后检查它的 NetUpdateFrequency、是否该休眠、NetCullDistance、属性是否该压缩。

最近更新: 2026/9/10 11:38
Next
UE5 Iris 复制系统