游戏中的网络同步

网络对时

大部分的强交互网游会做对时, 使 client 和 server 保持时间基本一致.

  1. client 带上本地时间 t0 向 server 发送对时请求.
  2. server 回复当前自己的系统时间 t1.
  3. client ack 收到 server 回复时的本地时间 t2.

如果只有 client 关心对时结果, 这时可以优化到只做1,2步甚至只做第2步, 相当于 client 主动发起 ping;

同样, 如果只有 server 关心对时结果, sever 主动发起 ping (第2,3步)即可.

单次对时往往会受到网络波动的影响, 所以一般还需要做多次统计一个比较合理的时间偏移.

网络对时, 一般在进游戏之初做. 如果在游戏过程中发现有网络波动过大的情况(在下行协议包中带上 server 的时间, 很容易检查到提前或者滞后), 也需要重新发起对时.

考虑到反外挂(比如加速齿轮, 或者减速器)的因素, server 端需要对主动发起的client对时请求做一些安全校验, 例如时间递增, 是否满足阈值等等.

光从对时的角度来看, UDP 协议比 TCP 更合适一些, 不过取决于项目需要.

Lock-Step

Lock-Step 算法是一种应用比较广泛的 P2P 同步算法, 典型的帧同步. 简化的算法流程如下:

在每一帧, client 发起指令 (step) 到对端的 client, 并收到确认. 在下一帧执行上一帧内收到的所有 step, 包括自己.

扩展到多 client 的情况, 就需要一个网状的网络拓扑, 每个 client 之间都能两两通信, 网络内流量暴涨的同时还会带来可靠性降低. 这时可以采取 master 策略, 从 client 中选取一个作为 master, 由 master 来收集 steps 并确认分发.

严格的帧同步, 受网络延迟的影响比较大. 一般局域网内的 rts 或者格斗类游戏多用这种算法; 广域网上对网络延迟要求不高的对战游戏, 也可以考虑.

缺点也很明显: 游戏过程中卡不卡, 完全取决于网络环境最差的那个 client, 一个玩家的高延迟会导致一起玩的所有玩家都卡, 恶意玩家甚至还可以模拟差网络, 破坏游戏环境.

Bucket Synchronization

Bucket Synchronization 是 Lock-Step 的改良算法. 算法流程可以参考下图:

Bucket Synchronization 算法应用于网状网络, 网络中有一个 master 节点(也是 client).

master 在启动之初, 会对所有 client 做网络对时, 计算网络包的超时时间.

master 会设置一个 bucket 时间, 在每个 bucket 时间节点, master 执行收集到的所有 step 指令, 并将更新推送到所有的 client 上. (上图的例子是一个简化流程, 只有俩 client, 没有 master 推送)

master 对收集到的 step 包做超时校验机制, 如果收到的 step 指令包的时间戳, 延迟超过了预设的阈值, 就当作超时包丢弃.

与 Lock-Step 相比, Bucket Synchronization 改进的是: 设置了 bucket 的概念, 执行每一帧的时间是固定的 bucket 时间节点, 而不必等到收到所有的 client step 指令, 从而网络不再受最差的 client 限制.

位置同步

位置同步是一种典型的状态同步.

需要提前做好对时, 国内的公网通信, 非跨网时一般在 120ms 左右, 移动端的波动很大.

考虑到防外挂的需求, 即便以 client 的通知为主, 也要做校验, 如果 CPU 压力比较大, 可以按照一定策略抽查.

为了保证表现平滑, 用户体验好, client 一般会应用 DR 算法做优化.

为了平衡流量和效果, 需要控制同步包的帧率, 这一点在手游上需要特别重视.

以 client 为主, server 只做简单校验, 不做逻辑运算, 优点是比较平滑, 但是仍然有安全性的风险.

  • 位置包 PKG 数据包括: 当前位置 P, 速度 V, 时间戳 T
  • client 发送 PKG 到 server, 发包的触发条件有两个: 超过了 DR 的阈值, 或者定时 tick.
  • server 收到 PKG, 校验合法性, 不合法则拉回合法位置. 根据对时时差, 预测 client 当前位置, 并把计算结果广播出去, 包括 client 自己.
  • client A 收到 PKG, 根据对时视察, 计算实际位置 P.
  • 如果 A 收到的是 client B 的位置, 需要把 B 修正到最新的位置 P. 常见的算法是 DR. 一个简单的方法是预测一段时间(例如 1S)之后 B 的位置, 用一条直线运动去修正. 如果 B 的网络特别差的情况下, A 看到的画面上 B 的速度可能会突然加快一下, 但是相对瞬移来说好很多.
  • 如果 A 收到的是自己的位置, 即 server 认可得自己的状态(并广播给了别人). 如果有误差, 是由于 server 得预测补偿和网络延迟的误差造成得. 如果误差比较大超过了预设的阈值, 直接跳转过去, 否则不处理.

以 server 为主的处理流程:

以 server 得计算和模拟为主, client 只在变化速度或者方向的时候, 通知 server, 特点是安全性高, 逻辑简单, 代价是 server 的 CPU 消耗大.

与 client 为主的处理流程不一样得是第2步, server 的计算也是两个触发条件:

  • 时间触发, server 定时做 server move, 来模拟计算并更新所有 client 的位置信息.
  • 消息触发, 收到 client 的位置 PDU.

Dead-Reckoning

Dead Reckoning, 中文翻译叫导航推测, 简称 DR, 它提供了隐藏延时和减少带宽的方法.

DR 在本地模拟其他联网 client 状态, 主要是位置信息, 减少网络带宽消耗得同时, 尽可能的还原真实情况.

举个例子:

  1. 飞机的运动速度固定, 两台游戏机联网玩, 分别是 A 和 B.
  2. 对于 A 和 B 来说, 是否发送 PDU (protocol data unit, 协议数据), 是通过阈值决定的.
  3. 实际运动位置 P1, DR 算法模拟得到的位置是 P2, 如果 P2 - P1 超过阈值就该发送 PDU 通知对方.
  4. 每次收到对方的 PDU, 就更新对方的位置.
  5. 没有收到对方的 PDU 时, DR 提供了集中经典算法来模拟对方的运动:
  • 位置1 = 位置0, 即位置不变.
  • 位置1 = 位置0 + 速度 * (T1 – T0) 相当于根据PDU中的数据, 做匀速运动
  • 位置1 = 位置0 + 速度 (T1 – T0) + 1/2 加速度 * (T1 - T0)\^2, 多了加速度

DR 的阈值是一个需要测试衡量的关键参数, 阈值设置的越低, 游戏画面会越平滑, 但是网络带宽也会消耗的越快.

DR 可以忍受一定程度上的丢包, 所以可以用 UDP 来提高时效性, 避免 TCP 重传的消耗.

网络游戏的位置同步比较复杂, 在应用 DR 算法的基础上, 还需要在细节处进一步完善, 才能提升网络波动下的用户体验.

参考文章

  1. 《网络游戏的对时以及同步问题》

  2. 《MMOG网络同步算法揭秘(QQ幻想)》

  3. 《网络游戏实时动作同步方案手记》

  4. 《位置同步策略》

  5. 《网络游戏得对时以及同步问题》

  6. 《网络游戏南北互通问题分析》

  7. 《多人游戏位置同步》

  8. 《Dead Reckoning: 在网络游戏中消除延时影响 》

  9. 《Dead Reckoning: Latency Hiding for Networked Games》