服务器提供实时游戏状态更新

4

目前我的游戏服务器很小(只有一个区域和约50个人工智能),每次发送状态更新数据包(UDP)时,都会向每个客户端发送完整的状态。这会创建一个大约1100字节的数据包大小。它所发送的几乎所有信息都是为所有实体提供以下信息:

int uid
int avatarImage
float xPos
float yPos
int direction
int attackState
24 bytes

编辑:更高效的结构

int uid
byte avatarImage
float xPos
float yPos
byte direction & attackState
14 bytes

但是我最终需要为实体发送更多的信息。例如,我正在添加以下内容:

float targetXPos
float targetYPos
float speed

随着每个实体需要发送更多的数据,我很快就接近并很可能已经超过了数据包的最大大小。因此,我正在尝试想出一些可能的解决方法:
1)将状态更新数据包累加直到没有空间为止,然后剩余部分不发送。这会给客户端带来非常糟糕的视觉效果,不是真正的选择。
2)仅向客户端发送N个最近的实体的数据。这要求在每次状态更新时计算每个客户端的最近N个实体。这可能非常耗时。
3)以某种方式设计数据包,使得可以发送同一更新的多个数据包。目前,客户端假定数据包的结构如下:
int currentMessageIndex
int numberOfPCs
N * PC Entity data
int numberOfNPCs
N * NPS Entity data

客户端然后接受到这个新数据并完全覆盖其状态的副本。由于数据包是完全自我包含的,即使客户端错过一个数据包,也不会有问题。我不确定如何实现同一更新的多个数据包的想法,因为如果我错过其中一个,那么怎么办?我无法使用更新的部分状态覆盖完整但已过时的状态。
4)仅发送实际更改的变量。例如,对于每个实体,我添加一个整数,用于每个字段的位掩码。诸如速度、目标、方向和头像图像之类的内容在每次更新时都不需要被发送。我仍然遇到了客户端错过必须更新其中一个值的数据包的问题。我不确定这有多重要。这还需要在客户端和服务器端上进行更多的计算来创建/读取数据包,但不会太多。
是否有更好的想法呢?

我喜欢你提出的第四个想法。但是,如果任何信息彼此相互关联(例如速度和方向),请确保它们一起发送。如果客户端在任一情况下错过了一个数据包...它仍将错过更新...无论数据是一次性发送还是仅在更改时发送。 - eat_a_lemon
压缩怎么样?或者使用某种自定义编码?如果您事先知道某些值的范围,您可以在每个字段上失去一些位... - Dan
1
我也喜欢数字4。除了发送update数据包外,您还可以在客户端请求时发送whole_status数据包。当您在update数据包中包含序列号时,客户端就知道何时应该请求whole_status数据包。 - Roland Illig
那么你们没有其他的想法吗?我也喜欢第4个,但如果有足够多的实体并且其中很多已经发生了变化,那么大部分数据都将被发送,并可能会导致数据包溢出。 - Nick Banks
3个回答

3
我会选择第4和第2个建议。正如您所意识到的那样,通常最好只发送更新而不是完整的游戏状态。但请确保始终发送绝对值而不是增量,以防丢包时丢失信息。在糟糕的网络条件下,您可以在客户端上使用死亡推断来使动画尽可能平滑。
您必须仔细设计,以便如果数据包丢失,它不会对游戏造成影响。
至于第2点,如果您进行了设计,它就不需要耗费时间。例如,您可以将游戏区域划分为一个方格网格,在其中每个实体始终位于一个特定的方格中,并让游戏世界跟踪这一点。在这种情况下,计算周围9个网格中的实体是O(1)操作。

这不是一个网格坐标系统,正如您可以通过float xPos,yPos看到的那样。没有网格,我允许PC重叠,所以你可以有50个人紧挨着彼此。 - Nick Banks

1

这种类型的系统通常使用死算预测合约算法来解决。你不能指望所有客户端同时获得更新,因此需要基于先前已知的值来预测位置,然后根据服务器生成的结果验证这些预测。


我已经知道了,但问题仍然是如何将所有数据传输到客户端,以便它可以使用它来进行死区推算。如果没有某种速度和方向,或者在这种情况下是它正在寻路的目标,它就无法完成。 - Nick Banks
你无法保证每个客户端都能同时得到更新;因此使用“死推算法”。你的服务器应该是实体位置的权威来源;但是每个客户端可以根据你已经提供的数据 - 位置、方向、速度和目标 - 预测它自己和所有其他实体的位置。每隔 x 毫秒,你的服务器应该发送最新的位置增量,这些增量将用于客户端纠正其预测。这个数据包可以简单地使用 {uid,deltax,deltay} 的格式。你将会看到分别针对速度、方向和目标变更的不同数据包。 - Aaron Saarela
你的原始数据包结构浪费了比特。你可能没有INT_MAX个头像图像或攻击状态。你可以使用位掩码而不是整数来减少它们。你也可以尝试对你的uid和方向做同样的处理。 - Aaron Saarela
你说得对,关于我的当前数据包结构。我将会更新成我认为最有效的形式,同时也满足我的需求。 - Nick Banks

0

我曾遇到一个问题,即发送增量更新(基本上是第4个选项)时,数据可能会按顺序排列或过时,具体取决于服务器的并行性,即多个客户端同时更新服务器的竞争条件。

我的解决方案是向所有客户端发送更新通知,并使用位掩码设置每个已更新项的位。

然后客户端根据位掩码请求特定数据的当前值,这也允许客户端只请求感兴趣的数据。

其中的优点是它避免了竞态条件,客户端始终获取最新值。

缺点是需要往返获取实际值。

更新以证明我要表达的观点。

假设有4个客户端A、B、C、D。A和B同时向服务器上的可变状态X发送更新Xa和Xb。由于B稍晚进入,因此服务器上X的最终状态为X=Xb。

服务器在获取更新状态后向所有客户端发送更新状态,因此C和D都会得到X的更新状态。由于传递顺序不确定,C会先收到Xa然后是Xb,而D则会先收到Xb然后是Xa。因此,在这一点上,客户端C和D对X的状态有不同的看法,一个反映了服务器的状态,另一个则没有,它具有过时(或旧)数据。
另一方面,如果服务器只向所有客户端发送通知,表示X已更改,那么C和D将会收到两个关于X的状态更改通知。他们都会请求当前X的状态,并且他们最终都会得到服务器上的最终状态Xb。
由于状态通知的顺序无关紧要,因为其中没有数据,而且客户端在每个通知上发出请求以获取更新状态,所以他们最终都会得到一致的数据,
我希望这更清楚地说明了我试图表达的观点。
是的,这确实会增加延迟,但设计师必须决定什么更重要,延迟还是让所有客户端反映可变数据的相同状态。这将取决于数据和游戏。

客户端数据总是旧的,因此强制客户端请求并不能解决这个问题。唯一真正的问题是数据的老化程度。如果服务器正确地管理世界状态,那么它应该能够向任何感兴趣的客户端发送更新,而不会出现问题。 - Kylotan
旧含义已被弃用。如果多个客户端同时更新相同的数据,则无法保证一个客户端不会获得过时的数据。并发意味着消息以非确定性顺序接收。我的方法意味着您始终可以获得服务器知道的最新数据,并且永远不会获得错误的数据,尽管可能需要连续进行两个请求。想一想,这很复杂。 - Jim Morris
1
在实时更新的情况下,应该在状态发生改变时立即将其发送给感兴趣的客户端。这是一种经典的网络观察者模式。因此,获取最新值时就不需要往返传输,因为你已经拥有它或正在接收中。无论哪种方式,你都必须始终假设可能存在旧数据,因为信息需要时间从服务器到达客户端,并且数据可能自上次通知以来再次更改。额外的往返传输只意味着你获取的旧数据略微陈旧了一点。 - Kylotan
@kylotan 对不起,我表达得不够清楚,我不是在谈论数据的年龄,很明显它会有延迟。我指出了一个相对难以理解的可变数据并发更新的概念。我将更新我的答案以示例说明我所说的内容。 - Jim Morris
1
由于交付顺序不确定,啊,所以您担心数据重新排序。鉴于正在使用UDP,数据可能根本不会到达!因此,您可以假设客户端对此具有强大的适应性。至于重新请求数据,我认为这没有任何区别-无论如何,您都会得到过去某个时刻绝对正确的值,并且由于它们在不同时间接收响应,它始终可能因客户端而异。跨多个客户端和服务器完美同步状态需要锁定该状态,否则您始终处于时间的摆布之下。 - Kylotan
@kylotan 对不起,我无法用一种易于理解的方式表达我想说的话。这不仅仅是数据重排的问题,如果你考虑一下,服务器上的任何锁定都无法帮助客户端。在客户端获取不一致的状态数据可能会在许多情况下产生问题,而我正在提出一种避免这种情况的方法,它有效,我已经使用了多年。 - Jim Morris

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接