UE5.8 带宽优化与网络分析
1. 上线第一晚的告警
我们负责的多人战术射击项目《哨兵行动》在 UE5.8 上线后,第一个周末就收到了服务器带宽告警。32 人对战地图,单台服务器出口带宽飙到 18 Mbps,峰值丢包率 3%。玩家反馈集中在两点:远处敌人位置偶尔瞬移,近战交火时开枪有延迟。
运维同学把 Network Profiler 抓到的数据发过来。我一看,光是每个客户端每秒接收的 Replication 数据就占了 12 Mbps,其中 70% 来自与当前战局无关的远端 Actor。地图是一张 2km × 2km 的开放世界,交战半径平均只有 80 米,可服务端还在把山另一头载具的详细状态发给每个玩家。
这就是带宽优化的起点。服务端不是故意浪费流量,而是默认配置没有根据玩法收紧。UE5.8 提供了一整套工具链,从裁剪距离、复制频率、休眠机制,到 Network Profiler 和 Unreal Insights,能把 Replication 流量压到合理区间。本文把这次排查和改造过程整理出来,希望给同类项目一个参考。
2. 网络复制在 UE5.8 中的基本结构
理解带宽问题之前,先看一下数据由服务端发往客户端的流动方式。UE5.8 的网络复制可以分成几个层次:World 负责管理所有 Actor;NetDriver 维护连接;每个 Actor 根据 NetUpdateFrequency 决定多久向客户端发送一次属性更新;服务器最后把包序列化后发送出去。
flowchart TD
subgraph api ["服务端世界"]
db["Actor 集合"]
disk1["NetDriver"]
server["Replication 调度器"]
disk2["属性序列化"]
internet["网络包发送"]
end
db --> server
server --> disk2
disk2 --> internet
server --> disk1上面的架构图描述了服务端一侧的主要组件。客户端收到包后由 NetDriver 反序列化,再应用属性变化。我们的问题出在前面三层:太多 Actor 被纳入复制范围,更新频率也没有区分远近。接下来几节会逐一处理。
3. Net Cull Distance:先砍掉看不见的东西
Net Cull Distance 决定了一个 Actor 距离客户端观察者多远之后,服务端就不再向该客户端复制它。UE 默认给很多 Actor 设置了 150 米甚至全图的 Cull Distance。战术射击游戏里,玩家隔着两栋建筑和一条河,不可能看到对岸一把手枪的状态变化,服务端却还在同步它。
我们按 Actor 类型建立了一张 Cull Distance 表:
| Actor 类型 | 旧 Cull Distance | 新 Cull Distance | 理由 |
|---|---|---|---|
| 玩家角色 | 全图 | 500 米 | 超出后只保留队伍标记 |
| 普通武器 | 150 米 | 80 米 | 地面武器超过 80 米无意义 |
| 投掷物 | 全图 | 300 米 | 手雷抛物线远端仍需可见 |
| 载具 | 800 米 | 400 米 | 载具移动快,保留中距离 |
| 可破坏掩体 | 200 米 | 120 米 | 破坏事件只影响附近玩家 |
| 环境小物件 | 100 米 | 0 米 | 花瓶、纸袋不做网络同步 |
Cull Distance 不是越远越好,也不是一刀切到 0。过短会导致远处爆炸或载具突然出现在视野里,过长又会浪费带宽。我们的策略是:影响战局的保留合理距离,纯装饰性的直接剔除同步。
在 UE5.8 中,可以通过 NetCullDistanceSquared 直接设置平方距离,避免每帧开方。假设视觉距离为 ,则写入的值为 。两个玩家之间的同步距离用平方比较,计算量更小:
行内我们经常写判断:如果 ,就跳过该连接。代码层面可以在 Actor 构造函数里设置:
AStaticWeaponPickup::AStaticWeaponPickup()
{
NetCullDistanceSquared = FMath::Square(8000.0f); // 80 米
NetUpdateFrequency = 10.0f;
MinNetUpdateFrequency = 2.0f;
}这张表上线后,Replication Actor 数量从平均每个玩家 1400 个降到 380 个,带宽立刻少了 35%。Net Cull Distance 是最低成本的优化手段,前提是你对地图和玩法有清晰分层。
4. Replication Frequency:该快的快,该慢的慢
Net Cull Distance 解决的是同步哪些 Actor。Replication Frequency 解决的是同步得多勤。UE5.8 默认 NetUpdateFrequency = 100 次每秒,对静态箱子每秒发 100 次包显然浪费。
我们的做法是给 Actor 打标签,按动态程度分档:
- 高频:玩家角色、正在飞行的子弹、快速移动的载具,30 到 60 Hz。
- 中频:开门状态、陷阱触发、远处玩家,5 到 10 Hz。
- 低频:环境破坏结果、仓库库存、任务进度,1 到 2 Hz。
- 事件驱动:血量变化、死亡事件,只在变化时复制。
单连接带宽可以粗略估算为:
其中 是总带宽, 是第 个 Actor 的更新频率, 是平均包大小, 是连接数。这个公式不是精确模型,但告诉我们一个方向:想降低 ,要么减少 ,要么降低 ,要么压缩 。Net Cull Distance 减少 ,Replication Frequency 降低 。
UE5.8 有两个关键参数:NetUpdateFrequency 是目标频率,MinNetUpdateFrequency 是保底频率。当属性没有变化时,UE 会自动把更新间隔拉长,但不会低于 MinNetUpdateFrequency。如果某个 Actor 在某段时间内完全没变,还可以进入 Dormancy,后面会讲到。
下面这张图展示了我们是如何根据距离和重要性决定更新频率的。
flowchart TD
A["Actor 进入复制列表"] --> B{距离是否小于
Net Cull Distance?}
B -->|"否"| C["跳过该连接"]
B -->|"是"| D{是否玩家控制角色?}
D -->|"是"| E["60 Hz, 全属性"]
D -->|"否"| F{是否移动载具?}
F -->|"是"| G["30 Hz, 位置和旋转"]
F -->|"否"| H{是否在交战半径内?}
H -->|"是"| I["20 Hz, 关键属性"]
H -->|"否"| J["5 Hz, 位置和生命状态"]这个流程图去掉了很多分支判断,只保留我们最常用的几条路径。实际项目中不必为了复杂而复杂,能让团队一眼看懂最重要。
5. Dormancy:让静止的 Actor 闭嘴
有些 Actor 创建后就很少变化,比如占点模式里的旗帜、解谜关卡里的机关。它们虽然还在 Cull Distance 内,也设置了较低更新频率,但只要活着,服务端每隔一段时间还是会检查是否有属性变化。量大了以后,这种检查本身也会消耗 CPU 和少量带宽。
Dormancy 机制允许 Actor 进入休眠状态。休眠期间,服务端不会主动复制它,除非满足某个唤醒条件。这个机制和 Net Cull Distance 不同:Cull 是按距离把 Actor 移出客户端视野,Dormancy 是 Actor 还在视野里但暂时不需要更新。UE5.8 提供了几种 Dormancy 模式:
DORM_Never:不休眠,保持默认行为。DORM_Awake:当前清醒,正常工作。DORM_DormantAll:对所有客户端休眠。DORM_DormantPartial:对部分客户端休眠,对另一些保持清醒。DORM_Initial:初始状态,等待第一次复制。
我们的旗帜 Actor 采用 DORM_DormantAll。当占领进度变化时,调用 FlushNetDormancy() 强制唤醒并发送一次更新,随后再次休眠。代码很短:
void ACaptureFlag::SetCaptureProgress(float NewProgress)
{
if (!FMath::IsNearlyEqual(NewProgress, CaptureProgress))
{
CaptureProgress = NewProgress;
FlushNetDormancy();
ForceNetUpdate();
}
}注意 FlushNetDormancy() 只是唤醒,真正触发复制还需要属性变化或手动 ForceNetUpdate()。如果只唤醒但属性没变,包不会发出去。
对于占领点这种所有玩家共同关注的对象,我们用 DORM_DormantAll。对于玩家自己建造但别人看不到的掩体,则用 DORM_DormantPartial,只向拥有者保持清醒,向其他玩家休眠。Partial Dormancy 在多人在线建造玩法里特别有用,能避免每个玩家的小改动都广播给全图。
Dormancy 的另一面是客户端预测。旗帜被占领时,客户端不能干等下一次复制,否则进度条会卡顿。我们在 UI 层做了本地插值,收到服务器确认后再对齐真实值。这样玩家看到的是平滑推进,实际网络流量只发生在关键点。
6. Network Profiler:找到带宽大头
优化之前必须测量。UE 自带的 Network Profiler 是定位带宽问题的首选工具。我们在开发包启动参数里加上 -tracehost=127.0.0.1 和 -trace=net,然后在编辑器里打开 Session Frontend,连接目标进程,开始录制。
Network Profiler 主界面会列出所有复制的 Actor 类型,按每秒字节数排序。我们当时的 Top 3 是:
- 玩家角色状态:4.2 Mbps
- 物理驱动的可破坏箱子:2.1 Mbps
- 粒子系统同步:1.8 Mbps
sequenceDiagram
participant 运维 as "运维同学"
participant 客户端 as "测试客户端"
participant 服务端 as "游戏服务端"
participant Profiler as "Network Profiler"
运维 ->> 客户端: 启动并连接服务器
运维 ->> 服务端: 添加 -trace=net 参数
运维 ->> Profiler: 打开 Session Frontend
服务端 ->> Profiler: 实时发送网络事件流
客户端 ->> 服务端: 开始一局 32 人对战
运维 ->> Profiler: 录制 120 秒
Profiler ->> 运维: 显示 Actor 级带宽排行
运维 ->> 开发: 导出 .nprof 文件并标注 Top 3这张序列图记录了我们当晚的真实排查流程。Network Profiler 的好处是粒度细到每个 Actor 类型、每个属性、每个 RPC。点击某一行,能看到它占总带宽的百分比,以及主要消耗在哪几个属性上。
物理箱子占 2.1 Mbps 是我们之前没想到的。原因是关卡设计师把大量装饰性木箱勾选了 Replicate Movement,玩家开枪打飞时,每个箱子的刚体状态都在同步。解决办法分两步:一是把装饰箱改为客户端物理模拟,只在被命中时发一次 RPC 通知;二是减少物理 Actor 数量,把十几个小箱子合并成一个破坏组。
Network Profiler 还有一个隐藏技巧:按连接过滤。不同玩家的视角不同,带宽分布也不同。 spectating 玩家的带宽通常最高,因为他能同时看到多个交战区域。我们只分析普通玩家连接,避免被观战视角带偏。
另一个细节是看属性列。我们曾有段时间怀疑角色带宽过高,点开后发现 40% 来自一个已经弃用的 AnimState 结构体,里面还包含了一堆不再使用的索引。删除这个属性后,角色包大小从 180 字节降到 110 字节。Network Profiler 的粒度足以暴露这种历史遗留问题。
7. 带宽预算与压缩
经过前面几步,单连接带宽从 12 Mbps 降到 6 Mbps。但运维要求我们控制在 4 Mbps 以内,因为海外节点还要过普通家用宽带。于是我们引入带宽预算机制。
预算分配思路是:先给每类数据封顶,再预留 20% 缓冲。公式如下:
我们把总目标设为 ,缓冲占 0.8 Mbps,剩下 3.2 Mbps 分给各类数据。玩家状态 1.5 Mbps,载具 0.6 Mbps,环境 0.5 Mbps,RPC 和控制指令 0.6 Mbps。
有了预算,就可以反推每个 Actor 的允许更新频率。假设某个载具 Actor 平均包大小 ,分配给它的预算是 ,那么最大频率:
行内我们也会用这个思路快速估算: 除以 得到每秒最多多少次。结果不是整数时向下取整,再设置 NetUpdateFrequency。
压缩方面,UE5.8 默认会对浮点属性做一定压缩,但仍有优化空间。例如角色血量 0 到 100 用 uint8 就够了,没必要用 float。位置同步如果精度要求不高,可以把坐标缩放系数从 100 降到 10。我们还在项目设置里启用了 net.UseFixedBruteForceReplication 相关选项的精简版本,把同一帧内多个小属性合并到一个 Bunch 里发送。
下面这张图展示了带宽、频率、包大小和 Actor 数量之间的关系。
graph LR
A["Actor 数量 n"] --> B["总带宽 B"]
C["更新频率 f"] --> B
D["包大小 s"] --> B
E["连接数 c"] --> B
F["Net Cull Distance"] --> A
G["Replication Frequency"] --> C
H["属性压缩"] --> D
I["服务器分片"] --> E8. Unreal Insights:不只是渲染和 CPU
Unreal Insights 在 UE5.8 里继续扩展了网络分析能力。我们在客户端和服务端都启动 Insights:
UE5Editor-Cmd.exe Project.uproject -game -tracehost=127.0.0.1 -trace=net,cpu,memory抓取到的 trace 文件用 Unreal Insights 打开,切换到 Networking 标签页,可以看到帧级别的 Outgoing Replication 时间、序列化耗时、每个 Channel 的等待时间。这些信息比 Network Profiler 更偏性能剖面,能发现复制本身是否卡了主线程。
我们遇到过一个典型问题:服务端主线程每 10 帧会卡顿 8 毫秒,玩家感觉子弹判定延迟。Insights 显示卡顿来自一个自定义的 GetLifetimeReplicatedProps 实现,里面每次都在遍历一个动态数组。修复方式是把条件判断提前,只在必要时重建属性列表。8 毫秒卡顿消失,服务端帧时间从 22 毫秒降到 14 毫秒。
Insights 还能和 Network Profiler 的数据对齐。Profiler 告诉你哪个 Actor 占带宽,Insights 告诉你序列化它花了多少 CPU。两者结合,才能判断是高频率导致的网络问题,还是复杂属性导致的 CPU 问题。
我们常用的 trace 组合是 -trace=net,cpu,memory。net 负责网络事件,cpu 看主线程耗时,memory 用来检查序列化过程中有没有临时分配过多。三者一起开对性能有一定开销,建议只在调试专用服务器或本地多开测试时使用,正式对局不要长期开启。
9. Actor 优先级:先把有限的带宽给重要对象
当服务端带宽吃紧时,UE 会根据 Actor 的 Net Priority 决定谁先被复制。默认所有 Actor 的优先级都是 1.0。我们可以根据玩法调整:玩家角色 2.5,敌方玩家 3.0,任务目标 2.0,普通环境 0.3。
优先级影响的是复制调度顺序,不是复制与否。优先级高的 Actor 会更频繁地获得 Channel 资源,优先级低的会被推迟。计算公式大致是:
越大,越优先复制。LastUpdateTime 是上次更新到现在的时间,DistanceFactor 通常与距离相关。近处敌人优先级自然更高,远处木箱优先级自然更低。
我们给不同类别设置了不同的 NetPriority:
| Actor 类型 | Net Priority |
|---|---|
| 玩家角色 | 2.5 |
| 敌方玩家 | 3.0 |
| 任务目标 | 2.0 |
| 载具 | 1.5 |
| 环境破坏 | 0.8 |
| 装饰物件 | 0.3 |
调整优先级时要注意相对关系。如果所有 Actor 都调成 3.0,等于都没调。优先级的价值在于区分,而不是绝对数值。
10. 优化检查清单
最后把这次改造中沉淀下来的检查项列出来。每次新增玩法或地图时,我们都会过一遍。
| 检查项 | 工具 | 目标 |
|---|---|---|
| 为每类 Actor 设置合理的 Net Cull Distance | 关卡编辑器 + 运行时统计 | 远处无关 Actor 不复制 |
| 按动态程度设置 NetUpdateFrequency 和 MinNetUpdateFrequency | Network Profiler | 静态 Actor 不抢高频资源 |
| 对变化少的 Actor 启用 Dormancy | 代码审查 | 降低空闲检查开销 |
| 用 Network Profiler 抓 5 分钟对局 | Network Profiler | Top 10 Actor 占总带宽不超过 60% |
| 建立并维护带宽预算表 | Excel / 内部文档 | 单连接目标 ≤ 4 Mbps |
| 审查浮点属性精度,能用整型就用整型 | 代码审查 | 减少每个包大小 |
| 用 Unreal Insights 检查序列化耗时 | Unreal Insights | 主线程无异常卡顿 |
| 调整 Net Priority,保证关键对象优先 | 运行时统计 | 玩家和任务目标优先复制 |
| 上线前做 spectating 和普通玩家对比测试 | 本地多开 | 观战视角不污染优化结论 |
| 保留 20% 带宽缓冲,应对突发流量 | 带宽预算表 | 峰值不触发丢包 |
这张清单不是一次性工作。随着地图扩大、玩家数增加、玩法变复杂,Cull Distance 和 Frequency 需要反复调整。建议把 Network Profiler 抓取加入 CI 的自动化性能测试,每次提交后都跑一次小地图对战,生成带宽基线报告。
11. 最终效果与持续改进
《哨兵行动》经过这轮优化后,32 人对战单连接带宽稳定在 3.8 Mbps,峰值不超过 4.5 Mbps。这个结果是多轮测量和调整后的产物,不是一次性改出来的。服务器出口带宽从 18 Mbps 降到 9 Mbps,丢包率从 3% 降到 0.4%。玩家反馈的远端瞬移和开枪延迟问题基本消失。
UE5.8 的 Replication 系统非常灵活,灵活也意味着默认值偏向通用场景,不会为你的玩法量身定制。带宽优化没有银弹,核心思路就是三句话:看不见的不发,不动的少发,重要的先发。剩下的工作交给 Network Profiler 和 Unreal Insights 去验证。
这次改造也让我意识到,网络优化不能只做一次。每次新增武器、载具或地图时,都要重新评估 Cull Distance 和更新频率。把优化检查清单纳入版本发布流程,比事后再救火要省钱得多。团队里最好指定一位网络负责人,专门维护带宽预算表和 Profiler 基线。
如果你的项目也遇到了类似问题,建议从 Net Cull Distance 开始改起,这是收益最明显的一步。改完再用 Network Profiler 看 Top 带宽消耗,按图索骥调整 Replication Frequency 和 Dormancy。最后把预算和优先级固化成流程,避免每次靠个人经验救火。