因特网的坑:我从编写X翼战机VS钛战机游戏中学到的

当我们启动“X翼战机VS钛战机”这个项目的时候,我们的目标是去创造第一个多人在线的星球大战游戏。为了实现这个目标,除了通过因特网进行联网对战以外,还有很多问题需要我们解决。我将回顾我们遇到的所有问题,以及我们是如何解决的,包括最后的结果如何。我希望我的经验能够给看这篇文章的读者带来帮助。

面临的问题

“X翼战机VS钛战机”是星球大战系列的第三部作品。我们最初在开发星球X翼系列游戏引擎的时候,肯定是没有考虑因特网的。所以,这是我们第一个需要面对的难题。向一个最初没有考虑因特网的游戏引擎里添加因特网的功能是极其困难的。

我们的第二个难题是游戏设计的复杂性。我们一直认为我们游戏引擎的强大之处在于它能够模拟并创造出一种公平的对战环境。我们非常自豪的说,在我们设计的任务中,存在大量的不同的飞船,它们有各种不同的能力和行为,但是总体来说是公平的,没有那一艘飞船会影响游戏的平衡。我们开发“X翼战机VS钛战机”的目的之一就是实现一个能够既公平又富有可玩性的多人在线游戏。我们想要带给游戏玩家的多人游戏体验,比以前的”死亡匹配赛“更加激烈。为了达到这个目的,玩家所需要的数据量将会极大的增加。

我们清单上的第三个难题是我们没有专用的服务器来运行我们的游戏;我们将不得不使用P2P技术来弥补这一点。为我们预期的玩家数量,提供所需的处理能力和带宽的服务器是一笔难以想象的高额花销。由于我们游戏发行的证书的性质,不允许玩家自己架设服务器来运行游戏。但一个P2P的系统避免了这个问题,可它同时也抛出另一个技术上的难题,因为每名玩家都需要跟别的玩家进行沟通,而这些玩家不一定在同一个服务器上。因为因特网没有多播的能力,发送同样的一条消息到三个不同的目的地,相比发送到一个目的地需要3倍的带宽。

第四个难题当然就是因特网本身了。当我们启动这个项目的时候,我们假设了我们能够处理200毫秒到1秒的延迟。我们还知道我们的带宽是有限制,只有28K的调制解调器可以用。这两个限制是我们设计网络模型一开始最关注的问题,但是最后我们发现,这仅仅是我们需要解决的最简单的问题罢了。

解决办法

面对着一堆的难题,我们设计了一个我们自认为可以涵盖所有问题,并找到满意的解决方案的网络模型。第一个我们需要解决的也是最重要的一个问题,同时它也是我们后来头疼的罪魁祸首。那就是我们认为,不该让网络模型限制我们游戏关卡设计的复杂性,但是我们也知道没有办法把每一个玩家的数据压缩,然后通过这么窄的带宽进行同步。所以我们想出了三个可能的解决方案。第一个选择,也是我们所知的被其他的游戏成功运用的方法,我们只传输最重要的数据,然后通过预测的方式填充其他不重要的数据。第二个选择是只传输所需绘制的游戏世界的数据。第三个选择是只传输玩家的操作,然后在每个客户端模拟出这些操作的结果。

第一个选择需要我们有能力快速决定哪些数据是重要的,哪些不是。在我们之前的游戏中,玩家被赋予了大量的能力来探寻游戏世界中的一切。我们甚至给玩家一个能够实时显示游戏世界中所有飞船动向的地图。除此之外,玩家还能够通过使用一个叫”追踪电脑“的东西来查询任何一艘飞船的当前状态。如果说我们要解决前面提到的问题的话,我们必须去修改或者删掉这个功能。

第二个选择听起来像是一个可行方案。从玩家飞船的座舱看出去,只能看到很少的一部分物体,而且如果玩家想看到更多的物体,那么玩家必须是离的很远,那样的话就不用太精确的画出这些物体了。问题是玩家是在一个开放的空间中,并驾驶一艘机动性非常好的飞船。他们能够非常快的做一个360度回旋,在那个时候他们就能够看到游戏世界中的几乎所有的物体。我们知道这种只传输所需绘制的游戏世界的数据的方法,在那些室内游戏中能够成功,但是我们的游戏是不会用墙之类的东西把游戏世界分隔开的。我们还考虑过使用一种雾来分隔玩家的视线,这样玩家就只能看到一定距离之内的物体了,但是面对现实吧,这不是一个好主意。

第三个选择立刻吸引了我们。带宽限制了我们只能发送玩家的操作,而不用考虑游戏关卡设计的复杂性。我们以前用过一种类似的技术来让玩家记录下战斗的过程,然后能够在“VCR”里重放战斗过程。所以我们知道我们的游戏引擎是有这种功能和概念的。我们决定对这个办法做一个快速测试,几天后我们就完成了我们的第一个多人任务的关卡。

让玩家做主机

我们做出的第二个重要的决定是让玩家成为游戏中的“主机”。我们之前选择只发送玩家操作的数据的意思是,每个玩家在点对点系统中对游戏的操作,将会传送给其他的所有玩家。因为因特网没有广播和多播的能力,所以每一条消息都会被复制然后发送N-1次,N是指游戏中玩家的数量。这也就是说28K的调制解调器的带宽会被分割成和玩家数量相应的份数。

如果有一个玩家扮演“主机”的角色,那么我们就能够明显的减少其他玩家的带宽压力,同时只是稍微增加了做主机的玩家的带宽压力。每一个玩家把数据发给主机,主机把所有的数据压缩进一个大的数据包,然后发送给每一个客户端。这种方法的优势在于,如果主机有一个快速的网络连接,那么游戏就可以支持许多低速连接的玩家。在游戏正式发布之后,这一点节省了我们大量的开销,只要主机玩家的带宽能够同时支撑起8名通过调制解调器连接游戏的低速玩家就没问题。

这种方法的另一个好处是我们不需要去考虑如何在不同玩家的机器之间同步数据,相反,我们只需要关心每名玩家如何把数据同步给主机。我们希望这个方法能够更早的实现“中途加入游戏”的机制,但是可惜的是,我们的游戏没能实现这一点。

尽管我们的测试是非常轻松就通过了,但是我们不认为我们接下来的工作也能这么容易。我们知道这种方法有它自己的问题。其中最大的一个问题也是我们最为担心,就是我们之前在“VCR”功能里已经看到过的,在回放玩家操作的时候,会产生出一些和原本战斗完全不同的结果。在以前,这样的差异bug当然不会带来什么样的害处。比如说,我们也许用了一个本地的布尔变量来表示无人机可能的两种行为。如果这个变量被意外的设置过,那么结果将会是依赖于栈上这个变量的值而产生出随机的结果。这种类型的bug通常是不被注意的,除非是在放一部电影。但是在放电影的时候,即使这个bug能够引起一艘飞船采取与记录的时候不同的行为。但这个细节会被电影中的其他部分掩盖掉,就好像那艘飞船本来就应该那样。

如果这种事情发生在多人游戏中,玩家将会敏锐的发现两种截然不同的结果。我们希望用两种方法来解决这个不一致的问题。首先,我们希望能够找到这个系统尽可能多的bug,这样也许可以提前避免这种问题的出现。其次,我们开发了一种方法能够检测到这个问题的发生,然后把需要同步的数据再发送一次。

这个方法的最大优势是只需要非常小的带宽。但我们还需要处理延迟的问题。在做了一些快速的测试之后我们意识到100毫秒的延迟,我们就无法控制飞船了。当你想要射击一个目标的时候,错过了准心而你又已经开火了,会让人产生巨大的挫败感。我们没有去改变游戏的操作方式,而是设计了一种系统,让玩家在他们的操作和飞船的行为反馈上感觉不到任何延迟。这项技术的关键在于使用一种类似所谓的“航位推算”的技术。

我们的解决办法是维持两份同样的游戏数据。第一份游戏数据只基于每一个玩家的操作,并且只在数据能够获取到的时候进行更新。第二份游戏数据是当前时间点的游戏状态,并且每帧都会进行更新。第二份游戏数据不能代表玩家的操作,因为玩家操作的数据因为因特网的原因造成了延迟。然而,这份游戏数据是基于对于玩家行为的预测将会产生什么样的结果。延迟越高,两份游戏数据的差距越大,预测的版本就越不准确。

我们的方法看起来解决了两个最常见的网络问题:带宽和延迟。带宽限制了我们能够发送玩家操作数据的最小值。延迟会造成游戏世界的数据异常(也就是我们常说的不同步),但是不会影响玩家的战斗操作。我们对自己的办法非常满意,而且认为我们确实做的非常聪明。

实现这个设计

我们的第一步就是去实现这个网络模型并在我们的局域网进行测试。这个过程进展的非常顺利。第一个版本是一个简单的“同步”模型,在这个模型下所有的玩家必须等待,直到接收到了所有玩家的操作,然后才开始进行游戏世界的绘制。第一份游戏世界的拷贝所需传输的数据量非常小,只要很小的带宽就行,但是在明显的网络延迟下根本就无法工作。当某个玩家帧数比较慢时,还会出现拉回现象,全同步的意思就是所有玩家都向最慢的那个玩家同步。

这个版本非常容易去编码实现,因为我们没有实现对游戏世界的预测,并且我们甚至没有尝试去解决延迟问题。此外,我们还使用DriectPlay,这样在实现一个游戏场景的绘制,并让玩家加入的工作中,就只剩下很少的工作要做了。我们让这个版本非常快的上线了,这样我们的关卡设计师就可以开始设计多人关卡了。事实上这个同步的版本我们用了很长时间。因为这个版本很容易被测试,所以在当时的开发进度来看,实现一个网络版本优先级就没有那么高了。当我们最终开始编写我们的网络模块时,我们已经拖后了进度,并且影响了我们之后的一些决定。这也意味着之后我们要完全投身于网络模型,以及用户界面的开发。

快速实现我们的第一个版本带来的另一个大的好处是我们能够做出一些非常漂亮的处理方法去应对那些不同步的bug。由于这些方法的存在以及长时间的测试,我们解决了大部分的bug。我们还能够实现了多次同步的方式,并且在局域网的情况下,多次同步的速度非常之快,以至于你根本无法注意到不同步的bug出现。

当我们开始编写网络模块的时候,我们知道,我们的首要任务是基于第一个游戏世界的拷贝,来创建第二份拷贝。不幸的是,我们的游戏引擎不支持这个特性,所以实现这个功能花费了很大的代价。但是,一旦我们实现了这个特性,我们就给我们的延迟问题带来了奇迹般的效果,在局域网测试的时候,它工作的非常之好!

我们现在拥有了一个在局域网工作的非常好的游戏的版本。它只需要非常小的带宽,而且它还能够容忍500毫秒的延迟,而你压根感觉不到。鼓起勇气,我们开始在因特网上简单的测试它。发现它居然还行!直到数周之后我们开始认真的做一些测试时,才意识到我们的错误。

学习到的东西(因特网就是个坑)

第一课:如果所有的玩家都拨打同一个电话号码,那么你就不是在测试网络,你实际上是在测试调制解调器和POP服务器,无论如何你确实不是在测试网络。如果你仔细想一下的话,就会发现这是显而易见。你的数据包通过调制解调器到达POP服务器,然后POP服务器把他们转发给其他玩家。数据包从未被POP服务器处理。

当我们最终把我们的游戏放到真实的网络环境中去,它在几秒钟之内就挂了。我们都困惑不解。它在局域网工作的如此之好,甚至能够允许500毫秒的延迟,为什么一上因特网就挂了呢。当我们进行一些检查后,发现了一些难以置信的事实,5到10秒的延迟是常见现象,并且我们发现了一些延迟甚至达到了50秒!在这种延迟的情况下,我们的游戏当然无法运行了。

甚至有一些包出现了丢失。TCP协议规定数据包一定会被接收,并且它们会被按顺序发送。TCP协议使用一个检查系统来验证数据包是否被成功发送了,并在发生丢失的时候进行重发。按顺序发送的意思是如果前一个数据包需要重发,那么后一个数据包将会被延迟发送,直到前一个数据包被接收到。但问题是,一旦因特网连接开始丢包,那么接下来的包很大概率仍然会发生丢包。这就意味着一个数据包可能需要好几秒钟才能够到达目的地。

第二课:TCP就是恶魔。不要在游戏里用TCP协议。你将会用尽你生命剩余的时间看到满载13岁少女的泰坦尼克号的悲剧一遍遍的重演。首先,TCP在等待发送下一个数据包之前不会发送任何额外的数据包。这就是为什么我们会看到有5秒延迟的包出现。其次,如果有某个数据包没有到达目的地,TCP协议不会立刻重发这个数据包。这个方式的考虑是如果一个数据包因为阻塞而发生了丢失,那么,就没有必要去重发这个数据包,因为这样只会增加阻塞。所以这个时候TCP就会停止发包,而开始发送一些临时的非常小的检查网络通畅的包。一旦这些测试包通过了,那么TCP就会重新开始发送真正的数据包。这种重发算法解释了为什么我们发现了一些延迟达50秒的数据包。

第三课:使用UDP。应对TCP这个恶魔的办法看起来也非常简单。不用TCP,用UDP代替就可以了。不像TCP,UDP是一个不可信的协议。它不做任何事情来保证数据包会被接收到,并且不关心数据包是否按照顺序发送。换句话说,它什么都不做。所以如果你非常需要发送一个数据包,你就需要自己去控制重发和检查机制。关于UDP还有一件烦恼的事。调制解调器的连接使用一种叫做PPP的协议进行连接。当你使用TCP协议通过PPP协议的时候,PPP协议会非常聪明的压缩数据包的因特网包头内容,使它从22字节减少到3字节(甚至更小)。当你发送UDP包通过PPP连接的时候,它不会像TCP那样进行聪明的压缩,而直接发送22字节的包头。这样,你使用UDP的时候,你就不应该一次发送很少的数据,因为这样会非常浪费带宽。

我们的网络系统当然需要每一个数据包都能够被接收到。如果TCP协议能够使用,这就不是问题。但是用TCP是彻底没希望的,所以我们必须自己去写我们的协议来处理检测和重发机制。不幸的是,我们并没有马上意识到这点,我们花了很长时间才明白这点的重要性。

我们的第一步就是换掉TCP,使用UDP。对于DirectPlay,只需要传递一个标记给它,就可以换用UDP了。但是,我们的游戏在第一个数据包丢失之后就会悲剧的挂掉。所以我们实现了一个简单的重发机制去处理丢包。这样会好了一点,但是一旦发生一些意外,游戏就跟之前一样彻底悲剧了。我们的第一个猜测是DirectPlay忽略了我们的UDP标记,而依然在使用TCP协议。但是检查后发现,这个罪魁祸首比微软更加邪恶:是因特网自身的问题。

第四课:UDP比TCP要好,但是UDP也是个坑。虽然开始时我们假设了丢包会偶尔发生,但是因特网的情况更加糟糕。在某一些连接下,有五分之一通过以太网的包会被丢失。当他们说UDP是不可靠的时候,他们并没有在开玩笑啊!我们那个简单的重发系统在这种情况下工作的并不好。重发的包也非常轻易就丢失掉了,并且我们看到了在一些情况下,原包以及重发的4,5个包都一起被丢掉了。因为我们重发了太多的数据包,超出了我们的带宽限制,然后延迟就开始上升,最后所有的噩梦就开始了。

我们的解决办法非常简单而且有效。每一个包都包含上一个包的拷贝,这样如果有一个包丢失了,那么下一个到达的包就会送来上一个包的信息。我们就又可以愉快的玩耍了:)。这需要大约之前带宽的两倍,幸运的是,我们本来所需要的带宽就非常小,所以增加一倍我们还能够接受。这个方法在连续的两个包同时丢失的时候也会失败,但是看起来不会发生这样的事。如果它真的发生了,我们就用重发的机制。

这个办法看起来非常奏效!我们最终让游戏在因特网上成功的跑起来了!当然因特网的情况比我们想象中更糟,但是我们还能够处理它。

第五课:当你认为因特网的情况不会更坏时,它就真的变的更坏了。更加广泛的测试显示我们还有很多严重的问题。显然,我们的重发机制代码里还有一些bug,因为偶尔有一些玩家会出现丢失连接而且任何数据都无法被发送的情况。在花费了无数个小时想从我们的代码里找到bug的时候,最后发现我们的代码是没问题的,反而是因为因特网断开连接了。

有的时候因特网变得非常差,根本无法发送任何数据包!我们记录下在10秒到20秒之内只有3或4个包能够被收到。难怪TCP这时会不再发送数据,而是发送检查包!你在这种情况下怎么玩游戏?现在我们确实有一个大问题了。断开连接这种问题我们确实没有准备。

幸运的是,这种情况通常非常短,大约几秒钟左右。通过调整重发机制的代码就能够处理这种情况。当玩家出现这种情况时,游戏就停止下来直到重新连接了游戏,一旦这种情况过去了,他就可以继续游戏了。

不幸的是,这种失去连接的状态可能会持续很长世间,如果真的那样,我们就无能为力了,最后我们只能把玩家踢出游戏。这不算是一个真的解决办法,但是至少一个坏的连接不会毁了所有玩家。

对于我们游戏的最后一个改良是处理因特网带来的对游戏预测不准确的问题。由于延迟能够变得非常高,需要一种方法来处理预测的游戏世界拷贝与真实不符的情况。

我们的第一条线索就是之前为了提高性能,做出的存在主服务器的设计。我们意识到如果每一个玩家都无法顺畅的把数据发送给主服务器,那么所有玩家都会延迟,因为主服务器在没有收到所有玩家的数据之前是不会向所有玩家发送压缩的数据包的。我们最终决定如果一个玩家超过一段时间还没有把数据发给主服务器,那么主服务器就丢弃掉这个玩家的数据,而把已经获得的数据通过压缩发送给所有玩家。

如果你仔细考虑这个办法的每一步,你将会意识到这使得游戏变得非常糟糕。玩家总是非常精确的知道他们自己飞船的位置。毕竟,他们知道他们实际上进行了哪些操作,所以他们准确的知道他们应该飞到哪里去。但是如果主服务器的官方游戏世界拷贝里丢失了他们的输入操作,而那些操作又关乎他们在游戏世界位置的准确性,最后,为了保持和其他玩家的同步,服务器不得不改变这些丢失了操作的玩家的位置。最后的结果就是玩家本地的位置被改变了,这使得他们游戏世界里的所有东西的位置都发生了变化,包括星空也会变化位置。

这个被服务器位移了的效果,被我们戏称为“星空跃迁”,它非常的令人不安,因为它使得游戏完全无法进行下去了。最终我们只能妥协,除非一个玩家的数据确实有非常高的延迟,否则我们不会丢掉它,这使得“星空跃迁”非常少见了。但是,后来我们发现,如果这里也使用我们后来对别的玩家的处理方式的话,也许会更好。

这种位置的瞬间变化,或者叫做“星空跃迁”,在绘制别的玩家身时也经常出现,因为他们的位置总出现预测偏差。在延迟非常低的时候(比如少于200毫秒)这种变化就看不出来,但是随着延迟的提高,游戏世界预测的就越不准确,然后这种“跃迁”就变得非常明显。

为了解决这个问题,我们实现了一种“平滑”的效果。这个平滑算法记录我们上一次预测的每一个玩家的位置。它把当前预测的位置更加靠近上一次预测的位置。这使玩家飞船的动作非常平滑,然后看起来非常好,即使是有点不准确也没关系。

总结

总结非常明显了:因特网就是个坑。我们对于我们游戏在糟糕的因特网连接下的表现非常失望。但是回过头来看,我们就像其他人做的一样好,在各种限制之下我们努力奠定了游戏的风格。

缺乏一个专门的服务器最终变成了一个巨大的问题。在丢失连接的情况持续一定的时间之后,直接发送整个游戏世界的状态要比重发所有丢失的数据包要容易一些。但这个办法不实用,因为需要这么做的只有一个玩家,并且无法节省带宽。一个专门的服务器可以解决这个问题,并可以提高让一个玩家加入一场已经开始的游戏中来的能力。“中途加入”是我们非常想要的一个游戏特性,但是在没有一个专门的服务器时,我们还是感觉它不太实用。

一个专门的服务器能够支持更多的玩家。延迟也能够减少一半左右,因为消息在重发给别的玩家时不需要再通过调制解调器了,而在有主机玩家的情况下却是需要。除此之外,一台专门的服务器能够更加容易的认证连接进游戏的玩家,因为他们只需要关心和服务器之间的连接。当一个玩家扮演主机时,其他玩家必须关心主机玩家和因特网的连接速度,同时还有他们自己的连接速度。

我们网络模型面对的一个最大问题在于数据包需要按顺序进行处理。那些接收到的乱序的数据包,本来能够用来提高游戏世界的预测精确度的,但是在“X翼战机对战钛战机”中是不行的。甚至它们的存在,也会带来明显的性能问题。问题在于当按照顺序到达的数据包到达的时候,我们需要立刻处理它们,而同时也会有乱序的数据包到达。因为处理器会遍历每一个数据包,所以这会带来处理时间上的额外开销。

如果我们一开始就考虑到上面提及的问题的话,所有的难题都会简单许多。但是我们是在修改一个已经存在的游戏引擎,我们被它的特性所限制。如果游戏引擎对高延迟的处理能够更加高效,那么事情就容易许多了。事实上我们需要使用一种灵活的处理时间间隔,这使得在处理高延迟的时候非常低效。除此之外,如果引擎能够利用乱序的数据来提高游戏世界预测的精确度的话,一个高延迟,不停重发数据包的过程,就不会被注意到。

我们解决问题的办法一个优势是它完全独立于游戏逻辑之外。我们发送的数据包只包含玩家的输入操作,并且这种技术能够不做修改就用在任何一种实时游戏中。这个模型实现中最佳的一块是我们不需要去担心游戏内容发生变化,而引起我们需要去修改网络模块的代码。事实上数据包中不包含游戏的任何特定数据使得玩家在使用机器人去作弊上更加困难。为了获得一种优势,一个机器人必须要能够比人更快的创建一串数据的输入,在我看来这个在我们的游戏中非常难做到。

目前Peter Lincroft是Ansible Software公司的主程序和董事长。他毕业于California大学计算机科学专业。他第一次成名于游戏Pipe Dream,那是他在几周内独立完成的游戏。他在Lawrence Holland公司的Secret Weapons of the Luftwaffe项目组里继续编写他的第一款3D图形引擎。后来他继续和Lawrence合作,帮助他们创建了游戏软件开发公司,最终成立了Totally Games公司。他在1998年4月之前一直是该公司的首席技术官。后来他离开去创办了自己的公司,Ansible Software。他开发的游戏包括Pipe Dream,Secret Weapons of the Luftwaffe,X-Wing:Star Wars Space Combat Simulator, X-Wing CD, TIE Fighter, TIE Fighter CD, and X-Wing vs. TIE Fighter。这些游戏卖出了3百多万份,并且获得了无数的年度游戏大奖。TIE Fighter CD最近被PC Gamer杂志冠以“PC史上最佳的游戏”的殊荣。

收藏 评论

关于作者:菜鸟浮出水

a tiny programmer 个人主页 · 我的文章 · 13 ·     

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部