Chancy game dev notes · 全栈工程师

17.37 Mbps 是怎么被砍成能跑的

一次状态同步带宽优化的算术:删掉整条通道比省 2 bit 值钱得多,参考帧 delta 才是真正的数量级来源。

· 4 分钟读完

#netcode #performance

写网络代码的人有个坏习惯:总觉得带宽是"优化一下编码"就能解决的事。Glenn Fiedler 的 Snapshot Compression(Gaffer on Games,2015)是我见过把这件事算得最老实的一篇,所以我把里面的数字抄了一遍,因为它们比任何结论都有用。

先算出原始账单

原文给的样例场景:901 个物体、60Hz 广播裸状态,结果是

2,170,957.5 B/s  ≈  17.37 Mbps

一个房间几十个人、几十个投射物就摸到这条线上了。17 Mbps 上行不是"慢",是根本发不出去 —— 家宽上行的物理上限、加上服务器按人头的总出口,两头都不够。

单个物体的裸状态在原文里是 321 bit(位置、速度、朝向、各种标量属性全按双精度或全精度铺开)。整个优化的过程就是一路把这 321 bit 拆到几十 bit 以内。

第一步:删掉整条通道,而不是省 2 bit

最值得记住的一条结论在这儿。

速度通道:把范围限制在 32 m/s 以内之后,每轴只要 11 bit 就够 —— 这已经不错。但真正的收益出现在他注意到高刷新率下这条通道可以直接不发:60Hz 的相邻两帧之间,速度几乎没变,接收端完全可以自己差分出来。整条通道从 33 bit(3×11)变成 0。

我后来对带宽优化定的规矩就是这一条:先问这条数据能不能不算、不传、由对端推出来,再问它能不能编码得更狠。省 2 bit 的活儿很累,删 33 bit 的活儿很爽,而人总是先去做前者,因为前者有明确的技术抓手。

第二步:范围换精度

位置:把世界限制在一块固定区域里,量化步长约 2 mm(区域越小、步长越细,这两者是一笔交易)。然后利用"绝大多数物体绝大多数时间在原地"这个事实,做变长编码 —— 小位移走 5 bit,超出范围才升级到 9 bit。

朝向:四元数有 4 个分量,但单位四元数只有 3 个自由度。所以丢掉绝对值最大的那一轴,用 2 bit 记录丢了哪一轴,剩下的分量各 9 bit 量化。

到这一步,单个物体的基线从 321 bit 掉到 约 80 bit。

第三步:跟"对方已经知道的东西"做差

前面两步都是各自独立地压缩,收益是常数级的。真正改变数量级的是这个:

不要发送绝对状态,发送相对于对端已确认的参考帧的差值。

服务器记得自己上一次被确认(acked)广播给某个客户端的那份状态,新快照跟它做 delta。静止不动的物体在 delta 表示下几乎什么都不用说 —— Fiedler 给的下限低到 1 bit 一个物体。加上 delta 之后,他样例里典型物体是 约 23.3 bit。

数量级对比:321 → 80 → 23.3。前两步合起来 4 倍,第三步单独 3.4 倍。范围越窄、变化越少的场景,第三步越值。

最后 25%:算术编码

原文的实测结论是,在手工 bitpacker 之上再上算术编码(arithmetic coding),还能再多拿 约 25% 的收益 —— 因为量化之后的残差分布根本不是均匀的, Huffman 之类的定长/前缀编码吃不满这个偏斜。

25% 听着诱人,但我的判断是它排在前三步之后:那是一条需要你自己实现、自己维护、并且会让"抓包看一眼就知道发生了什么"这件事彻底消失的路。调试成本要算进收益里。

我会带走的东西

手段账单变化备注
删整条可推导的通道−33 bit优先做,收益最直接
范围量化 + 变长321 → ~80 bit精度与区域大小互换
相对 acked 参考帧的 delta~80 → ~23.3 bit静止物体可到 1 bit
算术编码再 −~25%换掉可调试性

还有一条隐性收获,接上一篇:把位置量化到网格同时修掉了浮点不确定性 —— 误差每帧被抹平,不再累积。带宽和确定性这两个八竿子打不着的问题,在量化这一步上合流了。如果你的项目正好既要压带宽又要跨平台一致,量化步长这个参数一次决策两边受益,别做两套。

参考来源