17.37 Mbps 是怎么被砍成能跑的
一次状态同步带宽优化的算术:删掉整条通道比省 2 bit 值钱得多,参考帧 delta 才是真正的数量级来源。
写网络代码的人有个坏习惯:总觉得带宽是"优化一下编码"就能解决的事。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% | 换掉可调试性 |
还有一条隐性收获,接上一篇:把位置量化到网格同时修掉了浮点不确定性 —— 误差每帧被抹平,不再累积。带宽和确定性这两个八竿子打不着的问题,在量化这一步上合流了。如果你的项目正好既要压带宽又要跨平台一致,量化步长这个参数一次决策两边受益,别做两套。
参考来源
- Snapshot Compression — Glenn Fiedler, Gaffer on Games(2015,本文全部数字出自此文)
- Floating Point Determinism — Glenn Fiedler, Gaffer on Games