RTS客户端-服务器网络

Stone Monarch从一开始就支持多人游戏,但随着时间的流逝,网络模型经历了多次迭代。 我最初根据这篇著名的《帝国时代》文章实现了对等锁定步骤模型。

对等锁定步骤存在一些众所周知的问题。 对等方面使玩家很难相互连接,并增加了每个新玩家的网络负载。 锁定步骤方面容易出现棘手的错误,导致游戏状态在玩家之间不同步。 我当前的体系结构引入了服务器,并且还放宽了锁定步骤的确定性要求。 它仍然使用“转弯”的锁定步骤概念来确保每个客户端运行相同的模拟,并且在未收到所有玩家的命令的情况下不会继续进行。

客户端-服务器锁定步骤

游戏分为设定持续时间的一系列“回合”。 游戏开始的第一步是确定1圈的长度。 这是通过测量从每个客户端到服务器的消息往返时间来完成的。 服务器选择最长的时间作为转弯长度。 在游戏中,转弯长度可以根据观察到的网络性能进行调整。

每当玩家想要执行某个动作时,所请求的动作就会立即发送到服务器。 服务器将汇总收到的所有动作,直到到达下一个转弯边界为止。 此时,服务器将向所有客户端发送转弯消息,并在将来转弯1步执行要执行的动作。 当客户准备执行下一个回合时,他们应该已经收到了模拟回合所需的所有信息。

在上面的示例中,转弯长度已设置为100ms。 服务器在回合1期间接收动作,并在回合2开始时发出包含回合3动作的回合消息。这应该及时到达客户端以使他们执行回合3。

处理延迟

如果其中一个客户端在准备执行该转弯时尚未收到转弯消息,则它必须暂停模拟,直到从服务器接收到转弯为止。 一旦收到转弯,便可以立即再次开始执行。 此时,客户端将稍微落后于服务器的仿真,因此它更有可能及时接收到转弯。 但是,在发出播放器的命令与执行命令之间会增加延迟。

为防止客户端与服务器的仿真滞后(并遭受高命令延迟),服务器可以尝试检测到此情况并暂停其自己的仿真。 为此,客户端可以在发送给服务器的每个动作中包括当前游戏时间。 然后,服务器在自己的模拟中将此时间与当前游戏时间进行比较。 如果差异大于1圈的长度(或任意长度),则服务器可以暂停以允许客户端追赶。 如果在模拟过程中还有其他客户端处于领先地位,这很可能导致它们也暂停,直到所有客户端彼此之间更加同步。

在我最初的实现中,我使每个暂停都等于1圈的长度,这看起来很合理,可以防止客户端在模拟过程中彼此前后滑动。 但是,当前的方法大大减少了暂停发生的时间。 通常它们甚至对玩家来说都不是很明显。 现在,可以允许慢速客户端稍微落后于其他客户端,从理论上讲,它们可能会处于不利地位,因为它们的操作将需要更长的时间才能执行。 但是,这可以由服务器设置上限,因此我可以随时对其进行调整以找到合适的平衡。

调整转弯长度

如果服务器观察到过多的停顿,它可以通过在转弯消息中包含新的转弯长度来增加转弯长度。 所有客户在开始执行新的回合时将应用此方法。 这将平均增加所有玩家的命令等待时间。

相反,如果服务器在一定时间内未观察到任何暂停,则可以减小回合长度,以使所有玩家的命令等待时间都更短。

非确定性事件

在传统的锁步网络中,每回合仅发出玩家命令,其余模拟过程将在客户端之间确定性地进行。 但是,某些游戏逻辑很难保持确定性,尤其是在Unity中,其中游戏引擎不提供任何此类保证。

在这种情况下,我确保非确定性游戏逻辑仅执行一次,并将结果作为其自己的动作与玩家要求的动作一起发布。

例如,假设有玩家袭击炸弹,导致炸弹爆炸。 播放器发出所有客户端都执行的攻击动作。 在炸弹爆炸的位置,它需要检查周围的区域以查看将要命中的单位。 这是使用不确定的Unity API完成的。 因此,只有一个客户端(可能是拥有炸弹的客户端)将进行此模拟,并将结果(应受到损坏的单位列表)作为新动作发送到服务器。 这样,每个客户端的模拟继续保持相同

如果担心作弊,则服务器可以代替客户端来计算这些动作。 这将要求服务器运行与客户端相同的模拟。 这是我前进的方向,尽管客户目前仍在做出一些不确定的决定。

这种方法的优点是,仅当预期某些特定的游戏逻辑不确定时,才需要发送其他数据。 确定性的任何内容(例如,村民继续收集直到他们的资源用完为止)不会使用任何其他网络资源。 比起重写确定性的Unity功能(尤其是在物理方面),实现起来要容易得多。

不利的一面是,如果我错了,并且认为当事情不是确定性的时,它仍然会导致游戏不同步。