从这篇开始就是我自己翻译的了,之前那一篇作者翻译完就没管了,网上也查不到其他翻译,都是转的第一篇,为了给项目上的程序猿童鞋们科普,只能忍痛客服懒癌自行翻译了,顺手发到网上来造福大家,希望大家都能有所得,翻译的不足之处还请指正!!谢谢~


《===================================华丽的分割线========================================》

客户端预演和服务器调节

介绍:

在本系列第一篇文章中,我们探索了一种客户端-服务器模型即权威服务器和只向服务器发送输入指令并在服务器下发了游戏状态后执行的傀儡客户端。

这种简易的实现方式导致玩家在输入指令后到游戏中发生反馈这期间存在着一定的延迟;例如,玩家点击了右键后,过了半秒钟的时间游戏中的角色才开始移动。这是因为客户端的输入指令必须首先被发送到服务器,而服务器必须经过输入和计算之后才能得出新的游戏状态,而后这个更新后的状态需要再次抵达客户端才行。

快节奏的多人游戏同步 - Part 2_因特网

 网络延迟效应

在一个接入了因特网的互联网环境中,延迟大概几十分之一秒,游戏体验非常卡顿,在最坏的情况下甚至无法游戏。在本篇文章中,我们将找到一个最小化以至于消除这种情况的方法。

客户端预演

尽管可能存在有很多作弊玩家,大部分情况下服务器处理的都是合法请求(来自非作弊客户端和在某些情况下不作弊的客户端)。这就意味着大多数接收到的输入信息都是合法且将如期改变游戏状态;这就是说,如果你的角色在(10,10)且右方向键被按下,他将会停在(11,10)。这可以为我们所用。如果游戏世界足够具有确定性(也就是说,赋予一个游戏状态和一套输入指令,其结果是完全可预估的)。

让我们假设我们的网络延迟为100ms,角色从一个格子走到下一个格子的动画时间要播放100ms,在上文所述的简易模式下,整个动作时间需要花费200ms:

快节奏的多人游戏同步 - Part 2_快节奏多人游戏_02

 网络延迟+动画

因为游戏世界是确定性的,我们可以假设我们向服务器发送到输入信息会被成功的执行。在这种假设下,客户端可以在输入后直接在游戏世界中进行预演,而且绝大多数情况下,这种预演都将是正确的。

不同于发送输入信息后等待服务器反馈游戏状态再执行,我们可以在客户端得到输入信息后犹如已经成功获得服务器许可般即刻进行反馈,而等服务器返回为“true”的状态时——返回“true”的情况要远多余其他,将与本地计算结果一致。

快节奏的多人游戏同步 - Part 2_互联网_03

 服务器在等待客户端确认时同时播放动画

现在玩家操作和屏幕上展现的结果毫无二致了,而服务器仍然是权威的(如果作弊客户端输入了非法指令,尽管它仍然会在其本地执行,但无法影响服务器的结果,即其他玩家所看到的结果。)

同步问题

在上述例子总,我为了保证整个流程结果良好,在数据选择上非常谨慎。然而假设一个轻微的情节变化:让我们假定服务器延迟为250ms,而从一个格子走到另一个格子仍然消耗100ms的动画播放时间。让我们仍然假定玩家连续按下了2下右方向键,尝试向右移动2格。

运用目前我们已知的技术,那么将会发生:

快节奏的多人游戏同步 - Part 2_快节奏多人游戏_04

 预演状态和权威状态出现偏差


在延迟时间为 t = 250 ms的情况下我们进入到一个有趣的情景中,当新的游戏状态抵达客户端时。客户端的预演状态已经为 x = 12,但服务器说,新的状态是 x = 11。因为服务器是权威的,客户端必须移动回 x = 11。但马上在第350ms时新的服务器状态到达客户端了,它说 x = 12,因此角色又跳了一次,这次是向前。

从玩家的角度来看,他按下了右方向键2次,角色向右走了2格,在那里站立了50ms,向左跳了一格,在那站立了100ms,然后向右跳了一格。这种情况,当然无法接受。

服务器调节

要解决这个问题的关键是要认识到,客户端所见的是现时下的游戏世界,但由于网络延迟,它从服务器所获得的游戏状态事实上是来自于过去。在服务器发送了更新状态时,它还没有处理完客户端的所有指令。

解决这个问题并不难,首先,客户端为每个请求添加序列号,在我们的例子中,第一个请求序号为#1,第二个请求序号为2#,然后服务器在回复时,也要包含这些序列号:

快节奏的多人游戏同步 - Part 2_快节奏多人游戏_05

 客户端预演+服务器调节


现在,在第250ms时,服务器说“基于看到的你的#1请求,你的位置为x=11”。因为服务器为权威它将角色设置在x=11处,现在让我们假设客户端保存了一份它发送的请求副本。基于新的游戏状态,他知道服务器已经处理了#1,那么它可以抛弃#1副本。但它也知道服务器仍然需要返回处理后的#2.因而会再次应用客户端预演,基于上一条服务器权威状态,客户端可以计算出当前状态,增加服务器还没来得及处理的新的输入信息。

因此在第250ms时,客户端得到了“x=11,最新处理请求=#1”,它抛弃了#1副本,但同时创建了#2副本,而这条请求还没有被服务器承认。它将内部游戏状态更新为服务器回复的x=11,然后应用所有服务器还没看到过的请求——在当前这个例子下即为第二条输入指令:#2“向右移动”。结果为正确的x=12。

继续我们想这个例子,在第350ms时,来自于服务器的新状态抵达了客户端,这次服务器说“x=12,最新处理请求=#2”。这时,客户端抛弃掉所有关于#2的输入指令,并更新状态为x=12.已经没有需要回复的未处理输入了,而结果是正确的。

差异和结果

上文所讨论的是关于移动的同步,但其原则可以应用于几乎所有事务上。例如,在一个回合制游戏中,当玩家攻击其他角色,你可以显示血条和表示伤害完成的数字,但在服务器许可之前你不能真正的更新玩家血量。

由于不可被轻易逆转或撤销的游戏状态的复杂性,你一定想避免在服务器许可之前杀死一个角色,即使其血量在客户端看来已经降至0以下了(万一对方角色在受到来自于你的致命攻击之前使用了急救工具而服务器还没告诉你这件事呢?)

这给我们带来有一个有趣的点,即使世界具备完全的确定性且没有客户端作弊,一样会出现调节后客户端预演的和服务器发送的状态不一致的情况。剧情不可能像上文描述的那样只有一个玩家,而是经常出现多名玩家同时接入服务器的情况。这将是下一篇文章的主题。

小结:

当使用权威服务器时,你需要在等待服务器处理后的结果时给玩家一个有响应的错觉,为了做到这一点客户端对输入指令的结果进行模拟。当更新后的服务器状态抵达时,预演的客户端状态会被更新后的状态和还没有被服务器承认的客户端发送的输入指令重新计算。