多人项目的带宽问题几乎从不以"带宽不够"的面目出现——它表现为远端玩家瞬移、命中判定漂移、偶发的整体卡顿。等玩家投诉再查就晚了,正确姿势是把上行带宽当预算管:先算账,再上杠杆,最后用数据验证。这篇把三个杠杆(频率、量化、休眠)各拆一遍,附定位热点的完整流程。
先算账:带宽公式
一个 actor 占用的带宽近似为
$$B_{\text{actor}} = f_{\text{update}} \times S_{\text{update}} \times (1 - p_{\text{dormant}})$$
其中 $f_{\text{update}}$ 是实际复制频率(由 NetUpdateFrequency 和 MinNetUpdateFrequency 上下夹住),$S_{\text{update}}$ 是单次复制的序列化体积,$p_{\text{dormant}}$ 是处于休眠的时间占比。优化就是分别压这三个因子。
经验数字:一条 ReplicatedMovement(开启组件复制时的 transform)序列化后大约 40~70 字节;一个血条组件(一个 float 属性)几字节。100 个高频 actor 在 30Hz 下就是 $100 \times 30 \times 60\text{B} \approx 172\text{KB/s}$——家用宽带上行就满了。频率是第一杠杆,因为所有属性共享一次属性块打包。
杠杆一:NetUpdateFrequency
每个 AActor 有 NetUpdateFrequency(复制间隔下限的倒数)和 MinNetUpdateFrequency(相关性变差时的降频下限)。NetDriver 每个连接每 tick 遍历 relevant 的 channel,检查 NextUpdateTime,到期才调度。把远景 NPC 从 30Hz 降到 10Hz,带宽直接砍 2/3,玩家看不出差别——十米外的人每 100ms 才插值一次,动捕精度远超需求。
两个细节常被忽略:
- 降频不省 CPU。属性脏检查和 bunch 打包的固定开销还在,只是发得少。GAS 项目里 ASC 属性多的角色,瓶颈经常是这个遍历。
- 别用
MinNetUpdateFrequency兜底NetUpdateFrequency设得太高的错误。正确做法是先想清楚"这个对象多久变一次值得同步",直接用SetNetUpdateFrequency按游戏状态动态切(战斗中 20Hz,站岗 2Hz)。
杠杆二:量化——用精度换比特
浮点属性默认按 32 位发,但游戏里 99% 的数值不需要 IEEE 精度。量化的数学很直白:把范围 $R$ 切成步长 $\Delta$ 的格子,需要的比特数是
$$N = \left\lceil \log_2 \frac{R}{\Delta} \right\rceil$$
血量 0–1000、步长 1:$R/\Delta = 1000$,只要 10 bit,比 float 省 22 bit。引擎内置的量化原语直接覆盖常见场景:
FVector_NetQuantize系列:位置/向量按固定精度压缩(序列化时按值域自适应位宽,SerializePackedVector),角色移动 RPC 和ReplicatedMovement默认就走这个。FFloat_NetQuantize/FFloat_NetQuantize8/FFloat_NetQuantize16:把 float 压成 8/16 位定点,自己声明UPROPERTY(ReplicatedUsing=...)的手写属性首选。- 枚举、bool 组、
uint8:能小则小。一个 bool 数组打包进 bitfield 比 8 个 bool 属性省一个数量级。
量化的红线是语义边界:位置量化误差会进碰撞和命中判定(步长 1cm 在爆头判定里就是大事);血量量化误差会进死亡判定。凡是喂给"离散判决"的数值,$\Delta$ 必须小于判决阈值的 1/2,否则边界情况会随机翻转。这篇的取舍口诀:连续体验类数值大胆量化,离散判决类数值保守量化。
杠杆三:DORM 休眠
dormancy 那篇细讲过状态机,这里只算带宽账:DORM_Awake → DORM_DormantAll 切换后,该 actor 完全退出复制调度(直到被 WakeUp 或接收客户端 RPC)。配合 FlushDormancy 的"睡之前把最终状态发干净",休眠是免费的零带宽。箱子、尸体、建造物——凡是不动的都该睡。上面公式里的 $p_{\text{dormant}}$ 在静态场景里能压到 0.9 以上。
定位:谁花了我的带宽
先 stat net 看总量和 channel 数(连接级:in/out bytes per second、bunches、 actors)。总量超了再用 Network Profiler 出明细:控制台 netprofile enable 跑一段有代表性的战斗,netprofile disable 后在 Saved/Profiling/Network/ 得到 .nprof,用 UnrealFrontend 或 NetProfileAnalyzer 打开,按 actor class 排序看每个类的 bytes/sec——高频低信息量的 class 会立刻浮出来(常见嫌疑:特效 actor 忘了销毁、ASC 属性默认值也在同步、Tick 里每帧改一个 replicate 变量)。
落地顺序与一个反面教训
推荐顺序就是上图从左到右:频率最无感、收益最大;量化要逐属性验证;休眠适合静态世界。反面教训:我见过一个项目反过来做——先给所有属性加量化,结果命中判定用的血量被量化成 4 位,判决翻转导致"明明打死了没死"的 bug 查了一周。杠杆顺序错了,省下的带宽会用调试时间还回去。
最后,stat net 的数字要和真实网络条件一起看:本地回环测出来的带宽没有 UDP 重传和 MTU 分片的损耗,上线前务必在加包(net pktloss/net pktlag 控制台命令)下复测一次。