最近一直在学习网络游戏有关的内容。无意中发现一个极好的系列文章,由浅入深的讲解了网络游戏的很多基础知识和具体实现。

这个系列文章原文发表在作者的个人网站上 http://gafferongames.com/networking-for-game-programmers/ 。作者 Glenn Fiedler 在网络游戏领域是一位公认的专家,在游戏行业具有超过 15 年的经验。

由于精力有限,我不可能全文翻译这个系列文章。所以我只选择一些重点来说明,相当于原文的读书笔记。

这个系列文章分为几个部分:

  • UDP 和 TCP 传输协议的区别,以及为什么应该选择 UDP
  • 如何在 UDP 之上实现自己的游戏通讯协议
  • 一些和网络游戏有关的扩展内容,例如浮点数的确定性和状态同步等

我按照自己的理解重新组织了一下文章的顺序,并介绍了每篇文章的重点。

~

为什么应该选择 UDP

文章 UDP vs. TCP 详细阐述了 UDP 和 TCP 传输协议的区别,以及为什么应该在游戏里选择 UDP 作为传输协议。

作者在文章里解释了 TCP/IP 的基本原理,以及 TCP 和 UDP 两种传输协议的主要区别:

  • TCP
    • 基于连接
    • 保证数据到达和数据到达的顺序
    • 自动将数据切分为包
    • 确保数据发送速度不会超过网络连接速度(堵塞控制)
    • 容易使用,读取和写入数据就像操作一个文件
  • UDP
    • 没有连接的概念,必须自己编码实现
    • 无法保证数据确实能够到达,也无法保证数据到达的顺序,甚至可能出现重复数据
    • 需要自己切分数据为包来发送
    • 需要自己实现堵塞控制
    • 需要自己确定数据包是否丢失,并决定是否需要重新发送

相比 UDP,TCP 的使用更简单,开发者需要操心的事情更少。但对于需要快速发送数据的网络游戏来说,UDP 却是更好的选择,为什么呢?

TCP 在传输数据时,如果某个数据包没能到达接收方。TCP 会强制重新发送数据,而重新发送前会有一个等待时间,并且此时新的数据也无法发送到服务器。对于快节奏的网游来说,这种等待是个障碍。因为对于服务器或者参与游戏的其他玩家来说,通常只有最新的数据有意义。所以这种发送数据失败重试的机制反倒会阻碍客户端将最新状态发送到服务器。

文章里还阐述了为什么不应该混合使用 TCP 和 UDP,以及一些基础知识。

接下来作者在文章 Sending and Receiving Packets 里以简洁的代码展示了如何用 UDP 发送和接收数据包。

~

实现自己的游戏通讯协议

由于 UDP 是无连接状态的,所以首先需要解决连接状态的确认问题。文章 Virtual Connection over UDP 前面部分阐述了数据包在网络上是如何传输的,让读者明白为什么 UDP 传输时无法保证数据包到达的可靠性和到达顺序。

接下来作者从最简单的 Peer-to-Peer 开始,讲解如何设计一个自己的数据包结构:

  • 在数据包中插入协议标识符(protocol id),让接收方可以过滤掉无关的 UDP 数据包
  • 分析数据包的来源 IP,确定什么时候创建一个虚拟连接的内部状态
  • 利用超时来确定虚拟连接的断开

然后就是 UDP 最复杂的部分了。文章 Reliability, Ordering and Congestion Avoidance over UDP 阐述使用 UDP 传输数据时如何处理可靠性、包顺序和堵塞控制。

通过自己设计的数据包格式来解决可靠性、数据顺序:

  • 在数据包中添加序列号(sequence)。发送方每次发送一个新数据包时,就增加 seq。接收方通过比较收到数据包的 seq 和记录到的最大 seq,就知道一个数据包是不是最新的。
  • 在数据包中添加确认序列号(acknowledge)。发送方每次发送一个数据包时,ack 会保存发送方收到的最新数据包的 seq

利用 seqack,发送方和接受方都可以检查收到的数据包是不是最新的,也可以知道自己发送的数据包是不是已经被发送收到。

考虑到 UDP 可能丢失多个数据包,所以还需要添加 ack bitfield 到数据包中。通过设置 unsigned int 位的方式,让接收方知道上一次发送的数据包有哪些已经收到。配合超时策略,发送方就知道哪些数据包没有到达接收方,从而可以决定是否要重新发送丢失的数据包(但发送时仍然使用最新的 seq)。

最后,文章阐述了一种简单的度量方法。利用发送 seq 和收到相同 ack 之间的时间差来计算网络的速率。并在速率下降时,降低发送的频率,而在速率正常后恢复到正常的发送频率。

整篇文章利用一些简单的机制,实现了一个初步的解决方案。虽然离实际应用还有不少距离,但对于理解和学习却非常有价值。

要完整学习如何设计网络游戏的数据协议,可以阅读系列文章 Building a Game Network Protocol

~

和网络游戏有关的扩展内容

网络游戏的调试是个很复杂的问题。文章 Debugging Multiplayer Games 里告诉你很多游戏集成多人玩法之所以失败是因为团队抱着 focus on the singleplayer experience 的错误想法来做,所以在需要集成多人玩法时就懵逼了。作者以他多年的经验告诉我们:debugging multiplayer games is hard。最后,看起来作者开了一个新坑,不过这个新的系列还没任何内容。

Floating Point Determinism 这篇文章也是我过去曾经反复探寻过的一个问题,如何在不同平台间保证浮点数的确定性。

当时我想做一个 1v1 的双人对战物理游戏。很显然,不可能让两个玩家互相发送所有物理世界的数据,只能是发送玩家的操作和时间等数据,然后在两个客户端里分别模拟物理效果。但我在网上找了一大圈,没找到合适的解决方案,最后放弃了这个游戏的想法。

由于物理引擎依赖于大量的浮点计算,所以这个问题是实现网络物理游戏的关键。这篇文章,作者也没提出具体的解决方案,但他提供了大量他搜集和整理的资料供我们参考。

总结而言,跨平台的网络物理游戏,需要考虑处理器(AMD、Intel、ARM)、编译器,第三方库等各种问题,才能保证在不同平台(客户端和服务端肯定是不同平台)之间的浮点数确定性。

~

最后一篇,What Every Programmer Needs To Know About Game Networking 介绍了一些网络游戏常用的同步策略,以及优化用户体验的方法。

文章里提到了两种基本的策略:

  • Lockstep:在每一次游戏状态同步期间,都同步所有玩家的状态。这样做的好处是实现简单,但坏处则是任何一个玩家和服务器之间的网络连接不好,就会导致整个游戏世界停顿。

  • Client/Server:客户端只管把自己的操作发给服务端。服务端负责更新所有玩家的状态,然后广播给所有客户端。这样做的好处是客户端相当于一个只有输入和显示的哑终端,所有状态更新操作都在服务端进行。坏处就是由于客户端和服务端之间必然存在的网络延迟,玩家会觉得自己的操作总是要慢一拍才能反映在画面上。

不过 Client/Server 策略结合客户端预测(Client-Side Prediction)技术,就可以很大程度上解决 Client/Server 策略的体验问题。

当玩家在客户端做出操作时,对于一些确定性的操作,例如前进、转身,客户端可以立即更新画面状态。当服务端返回的数据到达客户端时,通过计算时间差和比较状态,再来决定是维持客户端已经更新的状态,还是修正客户端的状态。

例如在 WOW 里,如果网速很差,我们经常会发现跑了一段路以后,却突然瞬移了一下位置。就是客户端根据服务端传回的数据对客户端状态进行了修正。

这篇文章虽然没有提到具体的方法,但为我们介绍了当今主流网游采用的数据同步策略。

~

总的来说,这个作者不但经验丰富,更是能把复杂的问题分解为相对容易理解的小块写成文章让读者看明白。

作者网站上还有大量与网络游戏相关的内容,还有一些相关的开源项目,建议对网络游戏有兴趣的同学深入发掘一下。

-EOF-