如何为联网的实时游戏实现游戏状态快照系统?


12

我想创建一个简单的客户端-服务器实时多人游戏作为我的网络课程的一个项目。

我已经阅读了很多有关实时多人网络模型的文章,并且了解了客户端和服务器之间的关系以及滞后补偿技术。

我想做的事情类似于Quake 3网络模型:基本上,服务器存储了整个游戏状态的快照;在从客户端接收到输入后,服务器将创建一个反映更改的新快照。然后,它计算新快照和最后一个快照之间的差异,并将它们发送给客户端,以便它们可以同步。

在我看来,这种方法确实很可靠-如果客户端和服务器之间具有稳定的连接,则仅发送少量必需的数据以保持它们同步。如果客户端不同步,则也可以请求完整快照。

但是,我找不到实现快照系统的好方法。我发现很难摆脱单人编程架构,并思考如何以以下方式存储游戏状态:

  • 所有数据与逻辑分离
  • 可以计算游戏状态快照之间的差异
  • 通过代码仍然可以轻松操纵游戏实体

快照类如何实现?实体及其数据如何存储?是否每个客户实体都有一个与服务器上的ID匹配的ID?

快照差异如何计算?

总的来说:游戏状态快照系统将如何实现?


4
+1。对于单个问题来说,这个范围太宽泛了,但是IMO是一个有趣的话题,可以在答案中大致涵盖。
Kromster 2014年

您为什么不只存储1个快照(实际世界),将所有传入的更改保存到此常规世界状态中,然后将更改存储在列表或其他内容中。然后,当需要将更改发送给所有客户端时,只需将列表的内容发送给所有客户端并清除列表,便从零开始(更改)。也许这不如存储2个快照那么好,但是使用这种方法,您不必担心如何快速区分2个快照的算法。
tkausl 2014年

您是否已阅读以下内容:fabiensanglard.net/quake3/network.php-地震3网络模型的回顾包括有关实现的讨论。
史蒂文

尝试构建什么样的游戏?网络设置在很大程度上取决于您制作的游戏类型。就网络而言,RTS的行为不像FPS。
AturSams 2014年

Answers:


3

您可以通过保留两个快照实例:当前一个和最后一个同步实例,来计算快照增量(更改为先前的同步状态)。

当客户端输入到达时,您将修改当前快照。然后,当需要将增量发送给客户端时,您可以逐个字段(递归地)计算当前同步的快照,并计算和序列化增量。对于序列化,您可以在其类的范围内(与全局状态范围相对)为每个字段分配唯一的ID。客户端和服务器应为全局状态共享相同的数据结构,以便客户端了解特定ID应用于什么。

然后,在计算增量后,您将克隆当前状态并使其成为最后一个同步状态,因此现在您具有相同的当前状态和上一个同步状态,但是实例不同,因此您可以修改当前状态而不影响另一个状态。

这种方法可能更容易实现,尤其是在反射的帮助下(如果您有这种奢侈的话),但是即使您高度优化了反射部分(通过构建数据架构以缓存大多数反射调用),也可能会很慢。主要是因为您需要比较两个可能较大状态的副本。当然,这取决于您如何实现比较和您的语言。在带有硬编码比较器的C ++中,它可以很快完成,但不够灵活:全局状态结构的任何更改都需要修改该比较器,并且这些更改在项目的初始阶段就如此频繁。

另一种方法是使用脏标志。每次客户端输入到达时,您都将其应用于全局状态的单个副本,并将相应字段标记为脏。然后,当需要同步客户端时,您可以使用相同的唯一ID(递归地)对脏字段进行序列化。(次要的)缺点是有时您发送的数据超出了严格要求:例如int field1最初为0,然后分配为1(并标记为脏),然后又分配为0(但仍为脏)。好处是拥有巨大的分层数据结构,您无需完全分析它即可计算增量,而只需计算脏路径即可。

通常,此任务可能非常复杂,取决于最终解决方案的灵活性。例如,Unity3D 5(即将发布)将使用属性来指定应自动同步至客户端的数据(非常灵活的方法,除了向您的字段添加属性外,您无需执行任何其他操作)然后生成代码构建后步骤。此处有更多详细信息。


2

首先,您需要知道如何以协议兼容的方式表示您的相关数据。这取决于与游戏相关的数据。我将以RTS游戏为例。

出于联网目的,列举了游戏中的所有实体(例如,皮卡,单位,建筑物,自然资源,破坏性物品)。

玩家需要拥有与其相关的数据(例如所有可见的单位):

  • 他们活着还是死了?
  • 他们是什么类型的?
  • 他们还剩下多少健康?
  • 当前位置,旋转,速度(速度+方向),不久的将来的路径...
  • 活动:进攻,散步,建筑,固定,复原等。
  • 增益/减益状态效果
  • 以及其他属性,例如法力,盾牌,还有什么不可以?

首先,玩家必须先进入完整状态,然后才能进入游戏(或选择与该玩家相关的所有信息)。

每个单元都有一个整数ID。属性是枚举的,因此也具有整数标识符。单元ID不必长32位(如果我们不节俭,则可能是32位)。它很可能是20位(为属性保留10位)。单位的ID必须是唯一的,当实例化和/或添加到游戏世界时,它很可能由计数器分配(建筑物和资源被认为是不可移动的单位,在地图上可以为资源分配ID)已加载)。

服务器存储当前的全局状态。每个玩家最近的更新状态由指向a list的最近更改的指针表示(指针尚未发送到该玩家之后的所有更改)。更改会list在发生时添加到。一旦服务器完成发送最后一个更新,它就可以开始遍历列表:服务器将玩家的指针沿列表移至其尾部,一路收集所有更改并将其放置在缓冲区中,然后发送给播放器(即协议的格式可以是这样的:unit_id; attr_id; new_value)新单元也被视为更改,并将其所有属性值发送给接收播放器。

如果您不使用带有垃圾收集器的语言,则需要设置一个滞后指针,该指针将滞后,然后追上列表中最陈旧的播放器指针,从而释放对象。您可以记住哪个玩家是优先堆中最陈旧的玩家,或者只是简单地进行迭代和释放,直到惰性指针等于(即与玩家指针之一指向同一项目)。

您没有提出的一些问题,我认为很有趣:

  1. 客户是否应该首先接收所有数据的快照?视野之外的物品呢?RTS游戏中的战争迷雾怎么样?如果发送所有数据,则客户端可能会被黑客入侵,以显示播放器不可用的数据(取决于您采取的其他安全措施)。如果仅发送相关数据,则此问题已解决。
  2. 什么时候发送变更而不是发送所有信息至关重要?考虑到现代机器上的可用带宽,如果发送“增量”而不是发送所有信息,我们是否有任何收获?
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.