本文是系列的第三篇:
本文绝大部分为较简短的记录,进一步的描述请 参考原文 。
客户端把输入采样等玩家动作发给服务器,服务器回之以 PVS 内的压缩后的状态快照。
Doom3 做到了 同样的玩家输入序列总是能产生同样的结果 ,因为以下两点得到了保证:
服务器以 10-20 Hz 的频率向客户端发状态快照。由于快照是一个 rtt 之前的状态,客户端需要回到那个时间点上去处理这个“过去的”状态,然后再基于这个状态重新预测并刷新所有物体在当下的状态,如下图:
预测示意图(Prediction at the client with a snapshot rate at 20Hz and a ping of around 80 milliseconds)
由于玩家输入的频率 (input per second) 远低于逻辑处理的频率 (60 Hz),一个合理的推论是,最接近当下的几个逻辑帧,继续沿用与之前同样的输入一般是安全的。客户端使用服务器同步过来的其他玩家的输入来预测其接下来的运动,这些物理响应的机制与服务器上的真实逻辑是一致的。
与 Quake 3 不同的是,玩家在屏幕上看到的渲染结果与真实的逻辑状态是无时差的 (注意是无时差而不是 100% 绝对准确),因此不需要像 Q3 那样在本地延时比较大时需要充分考虑提前量,因为系统把下发同步的预测也完全实现了。系统的确定性保证了服务器和客户端可以运行完全一致的逻辑 (dead reckoning),因此得到至少与服务器上一样好的行为预测结果。Quake 3 的 bot 已经展示了通过算法来预测玩家移动可以达到什么样的程度,即使用慢速导弹武器 (火箭筒 RL) 也可以非常精确地命中。(Q3 bot 使用考虑碰撞检测的简化物理逻辑来预测玩家在之后的位置)
与 Quake 3 不同,Doom 3 的服务器和客户端使用同一份代码来更新/预测实体的状态,这样不用担心早先提到的互相干扰,开发新的单人模式 (并兼容多人) 也变得更简单了。
对于大多数状态同步而言,像 TCP 那样重发价值不大,因为被重发的状态十有八九因为过期已经不再有意义。
Doom 3 实现了下面这样一个基于 UDP 特性的 FPS 通信架构
上行和下行均为单连接,同时可发送 reliable & unreliable 的消息 (前者确保抵达),后者用于输入 (c2s) 和状态 (s2c) 的同步,只有非常特定和关键的消息使用可靠方式发送。
这个网络系统被设计为 不间断地生成一个不可靠消息流 (unreliable stream) (包括 10-20Hz 的状态同步和更高频的输入同步),可靠消息被驼运 (piggy back) 在这个不可靠消息流上 (蚂蚁搬家)。具体实现上,可靠消息被先缓存在队列里,每一个都由一个不可靠消息搭载着发出,ack 后再发下一个 (ack 直接借用了对面过来的 unreliable stream) 这样整个信道实现了最重要的保证:(通过1:1的驼载) 任何一条可靠消息总是能在首个紧接着的不可靠消息之前抵达。 (the message channel guarantees that a reliable message arrives before the first next unreliable messages comes through)
此外,对于不可靠的信息流,客户端的发送频率比服务器高3-4倍 (可靠消息的运输和响应能力),这样的话来自服务器的可靠消息是不需要 timeout 机制的,因为接下来的几个客户端消息没有 ack 的话,服务器就可以直接重发了。
整个系统的大部分信息是来自服务器的状态快照 (Snapshots) 和来自客户端的玩家输入 (User Commands),这些业务数据都通过 unreliable message 传递。(message header 如下图所示)
服务器:
客户端:
下图是下发快照的构成和完整的操作序列:
快照包含的几项关键信息:
实际的业务数据信息 (以下信息均做了差异压缩):
下图是上行的用户指令构成和完整的操作序列:
实体级的差异压缩。
pvs 差异压缩。
客户端随着 User Commands 上报的 ack 频率远高于下发快照的频率,所以丢包也没关系。服务器一旦收到 ack 就可以更新公共基并用 reliable message 通知客户端做同样的改动,驼运机制保证了 reliable message 总是先于新快照抵达客户端,这样被 ack 的快照总是能在处理新快照前被用于更新客户端的公共基。这样,公共基的状态维护就可以保证是整体上同步的
上面的差异压缩会产生大量的 0 (没有变化),所以开销最小也最有效的压缩是针对 0 的特殊处理。
每次处理 3 位,如果中间有一位不为 0 就保持不变,否则继续读,直到遇到不为零的情况,此时写下三个零 (3 bits) 和重复次数 (3 bits)
最大压缩比为 4:1,这里可以用不同的位数但 3 被验证为实际压缩比最高的。
举个例子:
000'000'000'010'000'000'000'000'110'000'000'000'000'000
会被压缩为
000'011'010'000'100'110'000'101
这个例子里压缩比为 14:8。
反过来也可以针对这种压缩方式对快照中的变量排列进行优化。把变量按照改变频率分组放在一起,以促使产生更多的连续 0。
客户端的预测可以改得更加细粒度
可以把所有的实体以同样的频率更新加以改进,让那些不那么重要的实体以较低的频率更新 (LOD-syncing)
对于不重要的实体,客户端的多帧预测往往可以合并为较少的较大帧 (降低运算量)
[系列完]
Gu Lu
[2016-08-11]