【转载】快节奏多人游戏(第 II 部分):客户端预测和服务器对帐

[介绍]

在本系列的[第一篇文章]( 【转载】快节奏多人游戏(第一部分):客户端-服务器游戏架构 - Creator 3.x - Cocos中文社区)中,我们探讨了一个客户端-服务器模型,该模型具有权威服务器和哑客户端,它们只是向服务器发送输入,然后在服务器发送时呈现更新的游戏状态。

此方案的天真实现会导致用户命令和屏幕上的更改之间存在延迟;例如,玩家按下向右箭头键,角色需要半秒钟才能开始移动。这是因为客户端输入必须首先传输到服务器,服务器必须处理输入并计算新的游戏状态,并且更新的游戏状态必须再次到达客户端。

1
网络延迟的影响。

在网络环境中,例如互联网,延迟可能为 10 毫秒或数百毫秒,游戏充其量可能会感觉无响应,或者在最坏的情况下,完全无法玩。在本文中,我们将找到最小化甚至消除该问题的方法。

[客户端预测]

即使有一些作弊玩家,大多数时候游戏服务器也会处理有效的请求(来自非作弊客户端和在特定时间没有作弊的作弊客户端)。这意味着收到的大部分输入都是有效的,并将按预期更新游戏状态;也就是说,如果你的角色位于 (10, 10) 处,并且按下右箭头键,则它最终将位于 (11, 10)。

我们可以利用这一点。如果游戏世界足够 确定( 即,给定一个游戏状态和一组输入,结果是完全可预测的),我们可以将输入发送到服务器并 立即 在客户端上处理它们——也就是说,我们 预测 服务器处理完输入后游戏状态会是什么;这消除了接收输入和呈现其效果之间的延迟。此外,大多数情况下,此预测是准确的,因此一旦服务器发送更新的游戏状态,就不会出现任何明显的不匹配。

假设我们有 100 毫秒的延迟,并且角色从一个方块移动到下一个方块的动画需要 100 毫秒。使用简单实现,整个操作将花费 200 毫秒:

2
网络延迟 + 动画。

由于世界是确定性的,我们可以假设我们发送到服务器的输入将成功执行。在此假设下,客户端可以预测处理输入后游戏世界的状态,并且大多数情况下这是正确的。

我们可以发送输入并开始渲染该输入的结果,就好像它们已经成功一样,而不是发送输入并等待新的游戏状态开始渲染它,同时我们等待服务器发送“真正的”游戏状态——这通常与本地计算的状态匹配:

3
在服务器确认操作时播放动画。

现在,玩家的操作和屏幕上的结果之间绝对没有延迟,而服务器仍然是权威的(如果被黑客入侵的客户端发送无效的输入,它可以在屏幕上呈现它想要的任何内容,但不会影响服务器的状态,这是其他玩家看到的)。

[同步问题]

在上面的示例中,我仔细选择了数字以使一切正常。但是,请考虑一个略微修改的场景:假设我们到服务器有 250 毫秒的延迟,从一个方块移动到下一个方块需要 100 毫秒。再假设玩家连续按右键 2 次,试图向右移动 2 个方格。

使用到目前为止的技术,将发生以下情况:

4
预测状态和权威状态不匹配。

当新的游戏状态到达时,我们在 t = 250 ms 时遇到了一个有趣的问题。客户端的预测状态为 x = 12 ,但服务器表示新游戏状态为 x = 11 。由于服务器是权威的,因此客户端必须将字符移回 x = 11 。但随后,新的服务器状态到达 t = 350 ,即 x = 12 ,因此字符再次跳跃,这次向前跳跃。

从玩家的角度来看,他们按了两次右箭头键;角色向右移动了两个方格,在那里站了 50 毫秒,向左跳了一个方格,站在那里 100 毫秒,然后向右跳了一个方格。这当然是不可接受的。

[服务器对帐]

解决此问题的关键是要意识到客户端看到 的是当前时间 的游戏世界,但由于延迟,它从服务器获取的更新实际上是 过去的 游戏状态。当服务器发送更新的游戏状态时,它还没有处理客户端发送的所有命令。

不过,这并不是很难解决。首先,客户端为每个请求添加一个序列号;在我们的示例中,第一次按键是请求 #1,第二次按键是请求 #2。然后,当服务器回复时,它包含它处理的最后一个输入的序列号:

5
客户端预测 + 服务器对账。

现在,在 t = 250 时,服务器说“ 根据我所看到的到您的请求 #1,您的位置是 x = 11 ”。由于服务器具有权威性,因此它将字符位置设置为 x = 11 。现在,我们假设客户端保留了它发送到服务器的请求的副本。根据新的游戏状态,它知道服务器已经处理了请求 #1,因此它可以丢弃该副本。但它也知道服务器仍然必须发回处理请求 #2 的结果。因此,再次应用客户端预测,客户端可以根据服务器发送的最后一个权威状态加上服务器尚未处理的输入来计算游戏的“当前”状态。

因此,在 t = 250 时,客户端得到 “ x = 11,最后处理的请求 = #1 ”。它会丢弃 #1 之前已发送输入的副本,但会保留 #2 的副本,该副本尚未被服务器确认。它使用服务器发送的内容 x = 11 更新其内部游戏状态,然后应用服务器仍然看不到的所有输入 - 在本例中,输入 #2 “向右移动”。最终结果是 x = 12 ,这是正确的。

继续我们的示例,在 t = 350 时,服务器会到达一个新的游戏状态;这次它显示“ X = 12,最后处理的请求 = #2 ”。此时,客户端丢弃 #2 之前的所有输入,并使用 x = 12 更新状态。没有要重放的未处理输入,因此处理到此结束,并获得正确的结果。

[零碎]

上面讨论的例子意味着移动,但同样的原则几乎可以应用于其他任何事情。例如,在回合制战斗游戏中,当玩家攻击另一个角色时,您可以显示血液和代表所造成伤害的数字,但在服务器表示之前,您实际上不应该更新角色的生命值。

由于游戏状态的复杂性并不总是容易逆转的,因此您可能希望避免在服务器同意之前杀死角色,即使其生命值在客户端的游戏状态中降至零以下(如果另一个角色在受到您的致命攻击之前使用了急救箱,但服务器尚未告诉您怎么办?

这给我们带来了一个有趣的点 – 即使世界是完全确定的,并且根本没有客户端作弊,客户端预测的状态和服务器发送的状态在对账后仍然有可能不匹配。如上所述,单个玩家不可能遇到这种情况,但是当多个玩家同时连接到服务器时,很容易遇到这种情况。这将是下一篇文章的主题。

[总结]

使用权威服务器时,您需要给播放器一种响应式的错觉,同时等待服务器实际处理您的输入。为此,客户端模拟输入的结果。当更新的服务器状态到达时,将根据更新的状态和客户端发送但服务器尚未确认的输入重新计算预测的客户端状态。

转载于: 客户端预测和服务器对账 - Gabriel Gambetta