Featured image of post 2016.07 (2/3) DOOM/Quake I/II/III 网络模型的演化

2016.07 (2/3) DOOM/Quake I/II/III 网络模型的演化

Views

(2/3) DOOM/Quake I/II/III 网络模型的演化

这一篇是上一篇DOOM3 技术点滴的自然延续,但内容上独立成篇,实际上描述了 id software 从 DOOM 1 开始的若干款 FPS 游戏在网络架构方面的演化。同样的,更多的细节可参考这里和 Quake III Arena 网络协议规范(非官方) (看起来在互联网上已经找不到链接了)。


本文是系列的第二篇:

  1. DOOM3 技术点滴
  2. DOOM/Quake I/II/III 网络模型的演化
  3. DOOM3 网络架构

一般性说明

游戏网络架构通常体现在四个要素的平衡上:一致性,响应性,带宽,延迟 (consistency, responsiveness, bandwidth and latency requirements)

“Multiplayer gaming is about shared reality.”

FPS 游戏的状态通常表现为一个实体列表:玩家,怪物,导弹,门等,这些实体 (entities) 与其全部作为不同的元素去区分对待,不如提供一个公共的结构和接口来简化通信。

“Networking in first person shooters is all about synchronizing the state of multiple copies of the same game entities such that all players experience the same changes and events in the virtual environment.”

为了达到即时同步这些状态的目的,有些实现方式需要参与者去管理和维护其自有的那份拷贝,通过施加一致的逻辑来推动所有的状态去同步地更新,而另一些实现则是随着时间的流逝不断地比较和发送最小的状态变化和差异。

P2P 模型 (DOOM)

DOOM (1994) 的网络模型是完全同步的 P2P 系统。该系统每秒钟对玩家的动作 (move/turn/use/fire, etc.) 采样 35 次 (得到一个 tick command) 并发送给其他所有玩家,每个玩家都接受来自所有玩家的 tick command,当某个玩家收到所有其他玩家的下一帧 tick command 后,该玩家的本地游戏状态推进到下一帧。这样的后果是全局性的延迟 (每个玩家从做出动作到收到反馈的响应时间) 由最慢网络连接的玩家决定。

这个网络模型逻辑上非常简单,但存在这些问题:

  1. 所有玩家都需要主动维护完美的状态同步,由于硬件不同(有时甚至是未初始化的变量)等引入的不一致,会让每个参与者细微的不同被累积下来,导致参与者之间显著的视觉和逻辑的差异。这种不一致的引入很难查,因为只有当它们累积起来才会有明显的效果,而等感觉到差异时,真正的问题已经发生很久了。
  2. 完全同步的网络无法跨平台。不同的硬件上,由不同编译器生成的汇编指令有时会产生轻微不同的行为 (浮点指令尤甚)。
  3. 随着玩家数量增长,延迟会迅速变得难以接受。而且只要有一个玩家的网络有波动,会影响到所有人的体验。
  4. 随着玩家数量增长,带宽需求会指数性地同步增长。
  5. 同步网络由于只发送 tick command,所有玩家必须同时启动游戏 (来保证游戏状态的一致性) 无法做到随时的加入和退出。
  6. 由于玩家本地维护了所有的状态,方便了作弊的实现。

Packet Server (包的简单中继)

这个模型在原版 DOOM 的基础上增加了一个 Packet Server,负责转发所有的 tick command。玩家不再直连其他所有玩家,而是连到这个服务器 (某个玩家机器上) 以获取最新的状态。这样改进后,同步量降低了,而且如果一个玩家很卡,只会影响到他自己的游戏体验。但上述的大多数问题依然存在。

Client Server (Quake I/II/III)

Quake I/II/III 实现了比较典型的 C/S 架构 (1996),这个模型中服务器负责所有的逻辑判断,客户端本质上只是一个渲染终端。玩家把自己的操作和输入发送给服务器,收到一个实体列表用于渲染。服务器把压缩后的快照发给客户端 (10-20Hz) 客户端使用这些快照来插值或推导出平滑连贯的体验 (interpolates between, or extrapolates from the last two snapshots)。

在一般情况下(比如在古代的引擎Quake 1中),客户端收集到用户命令后发送给服务器,此后就在等待服务器返回新的游戏状态。这是很笨的。在Quake 3中,客户端不会傻等,而会预测可能的游戏状态,其实预测状态所用的代码跟服务器端的代码是一样的,所以服务器端的状态和客户端的状态往往是一致的。如果确实不一致,则“服务器为准原则”将生效。

  • “Quake III Arena 网络协议规范(非官方)”

响应性和预判

这个模型同样有响应性问题,从输入的采样和发送到屏幕反馈同样需要一个 roundtrip 延时。为了克服延时客户端预测了玩家的下一步行动 (在之前的 blog中有提到)。玩家的输入在发出去的同时,本地立刻处理,而环境状态做了上文说到的 interpolate/extrapolate,也就是说玩家看到的自身是 (可预计的) 操作结果,而其他人是过去的状态。(这一点与魔兽世界是一致的) 这个 C/S 架构是异步的。对任何一个玩家而言,服务器的全局模拟落后于该玩家在本地的实际操作快照,而环境的状态同步更是落后于全局模拟。

这个模型允许中途加入和退出 (除了做 server 的玩家,如果不是 dedicated 的话)。由于玩家的判断基于的是其他玩家过去的状态,实际的击中检测发生在晚些时候的服务器上,在延时较高的情况下,玩家需要不断考虑延时状况并打提前量才能在未来的实际判断中击中对方。

延迟补偿的潜在问题

半条命在这个基础上引入了一种特定的延迟补偿 (lag compensation),当玩家向某个目标 (若干毫秒前的状态) 射击时,做实际检测的服务器会采用该目标若干毫秒前的状态来检验是否击中。这么做需要服务器把之前一小段时间的状态持续地保存下来,这样不仅增加了实现复杂度,而且导致了某种程度的不一致性。延时高的玩家反而更容易因为补偿获得更有利的判断,严重影响游戏体验 (实例见这里第六页末尾,值得一读)。这种补偿只能对目标的位置回滚,而所有其他环境状态的改变却已无法倒退,这也会影响实际的体验。

工程问题:逻辑和预测代码分离

Q3 里服务器上跑的逻辑代码 (“game code”) 跟客户端跑的渲染和预测代码 (“client game code”) 实现在物理上不同的模块里,但却需要对彼此的内部细节非常清楚 (才能保证预测和实际行为的一致性)。这个强耦合使得扩展游戏变得很困难,这也是难以实现单人游戏模式的原因之一。有时使用 Q3 引擎的游戏得为多人模式和单人模式发布两个不同的 exe,其中单人模式直接使用 game code 来简化逻辑流程。

插值/推导的局限性

由于快照的接收频率往往低于实际渲染的帧速,就需要上文提到的 interpolate/extrapolate,考虑物理模拟和交互的话,(为了跟服务器逻辑一致) 推导会增加额外的实现复杂度。这些插值对位置数据很有效,但其他一些状态很难插值,有时性能也是问题,比如四元数的 slerp 就挺费的 (上一篇末尾提到了相关的优化)

压缩、状态同步冗余、固定字长

Quake III 里只有在 PVS 内的实体才会被同步状态,而且被同步的是压缩后的与上一次同步比较的差值 (delta compressed relative to the entity states from a previous snapshot) 这导致的结果是如果一个物体频繁进出 PVS 就没法做 delta 比较,总是发送完整状态,会导致不少冗余的同步量。

为了提高网络通讯速度,降低带宽,Quake 3中采用了压缩的技术。这并不是指用一些压缩算法来直接压缩数据。而是指,在传送游戏状态数据时,只传送改变了的游戏状态,而不是全部发送过来。一般来说,这个叫做Delta技术。

  • “Quake III Arena 网络协议规范(非官方)”

出于简化,Q3 使用了固定长度的同步结构,导致不少字段被不同的功能各种复用,一晦涩复杂度就上去了。


[本文完,系列待续]

(全文完)


comments powered by Disqus
Built with Hugo