我对探索实时多人客户端服务器游戏开发和相关算法感兴趣。许多著名的多人游戏,例如Quake 3或Half-Life 2,都使用增量压缩技术来节省带宽。
服务器必须不断将最新的游戏状态快照发送给所有客户端。始终发送完整的快照非常昂贵,因此服务器仅发送最后一个快照与当前快照之间的差异。
...容易,对吗?好吧,我觉得很难思考的部分是如何实际计算两个游戏状态之间的差异。
游戏状态可能非常复杂,并且具有通过指针相互引用在堆上分配的实体,可以具有其表示形式随体系结构而变化的数字值等等。
我很难相信每个游戏对象类型都有手写的序列化/反序列化/差异计算功能。
让我们回到基础。假设我有两种状态,用位表示,我想计算它们之间的差异:
state0: 00001000100 // state at time=0
state1: 10000000101 // state at time=1
-----------
added: 10000000001 // bits that were 0 in state0 and are 1 in state1
removed: 00001000000 // bits that were 1 in state0 and are 1 in state1
太好了,我现在有了added
和removed
diff位集-但是...
...差异的大小仍然与州的大小完全相同。实际上,我必须通过网络发送两个差异!
一个有效的策略实际上是根据那些差异位集构建某种稀疏数据结构吗?例子:
// (bit index, added/removed)
// added = 0
// removed 1
(0,0)(4,1)(10,0)
// ^
// bit 0 was added, bit 4 was removed, bit 10 was added
这可能是有效的方法吗?
假设我设法为JSON中的所有游戏对象类型编写了序列化/反序列化函数。
我能否以某种方式拥有两个JSON值,就位自动地计算它们之间的差异?
例子:
// state0
{
"hp": 10,
"atk": 5
}
// state1
{
"hp": 4,
"atk": 5
}
// diff
{
"hp": -6
}
// state0 as bits (example, random bits)
010001000110001
// state1 as bits (example, random bits)
000001011110000
// desired diff bits (example, random bits)
100101
如果有这种可能,那么避免与体系结构有关的问题和手写差异计算功能将相当容易。
给定两个串甲和乙彼此相似,是可以计算的字符串Ç其在尺寸上大于较小甲和乙,即表示之间的差甲和乙可以和施加到甲得到乙在结果?
由于您已经以Quake3为例,所以我将重点介绍那里的工作方式。您需要了解的第一件事是,与客户端-服务器游戏相关的“游戏状态”并不涉及对象的整个内部状态,包括AI的当前状态,碰撞功能,计时器等。游戏的服务器实际上减少了客户很多。只是对象的位置,方向,模型,模型的动画,速度和物理类型中的框架。后两个用于通过允许客户模拟弹道运动来使运动更平滑,但仅此而已。
每个游戏帧大约每秒发生10次,服务器为游戏中的所有对象运行物理,逻辑和计时器。然后,每个对象调用API函数以更新其新位置,框架等,并更新是否在此框架中添加或删除了该对象(例如,由于击中墙壁而被删除的镜头)。实际上,《雷神之锤3》在这方面有一个有趣的错误-如果镜头在物理阶段发生移动并撞到墙壁上,它将被删除,并且客户端接收到的唯一更新也将被删除,而不是之前朝向墙壁的飞行,因此,客户看到镜头在实际撞墙之前会在空中1/10秒内消失。
有了很少的每个对象的信息,就很容易区分新信息与旧信息。另外,只有实际更改的对象才调用更新API,因此保持不变的对象(例如Walls或不活动的平台)甚至不需要执行这种差异。另外,服务器可以通过不向客户端发送直到客户端看不到的对象才能进一步节省发送的信息。例如,在Quake2中,一个关卡被划分为多个视图区域,并且如果一个区域(及其内部的所有对象)都被关闭,则该区域被视为“不可见”。
请记住,服务器不需要客户端具有完整的游戏状态,仅需要一个场景图,并且需要更简单的序列化,并且绝对不需要指针(在Quake中,它实际上保存在一个静态大小的数组中,该数组也限制游戏中对象的最大数量)。
除此之外,还有用于玩家健康状况,弹药等内容的UI数据。同样,每个玩家只会获得自己的健康状况和弹药,而不是服务器上每个人的健康状况。服务器没有理由共享该数据。
更新:为确保获得最准确的信息,我仔细检查了代码。这基于Quake3,而不是Quake Live,因此某些情况可能有所不同。发送给客户端的信息基本上封装在称为的单个结构中snapshot_t
。它包含playerState_t
当前玩家的单个,entityState_t
可见游戏实体的256个数组以及一些额外的整数,以及代表“可见区域”的位掩码的字节数组。
entityState_t
依次由22个整数,4个向量和2个轨迹组成。“轨迹”是一种数据结构,用于表示物体未发生任何变化时在空间中的运动,例如弹道运动或直线飞行。它是2个整数,2个向量和一个枚举,在概念上可以存储为一个小整数。
playerState_t
是一个更大的数组,包含大约34个整数,大约8个整数数组,每个数组的大小范围从2到16,以及4个向量。它包含从武器的当前动画帧到玩家的清单到玩家发出的声音的所有内容。
由于所使用的结构具有预设的大小以及结构,因此制作一个简单的手动“ diff”功能,将每个成员进行比较非常简单。然而,据我所知道的,entityState_t
并且playerState_t
仅在全部发送,而不是在部分。唯一被“增量”处理的是作为实体数组的一部分发送的实体snapshot_t
。
快照最多可以包含256个实体,而游戏本身最多可以包含1024个实体。从客户端的角度来看,这意味着只能在单个帧中更新25%的对象(任何其他对象都会导致臭名昭著的“数据包溢出”错误)。服务器仅跟踪哪些对象进行了有意义的移动,然后将其发送。它比执行实际的差异要快得多-只需发送任何本身称为“更新”的对象,并且该对象位于播放器的可见区域位掩码中即可。但是从理论上讲,手写的每个结构的差异不会变得那么难。
为了团队健康,虽然Quake3似乎没有做到这一点,所以我只能推测在Quake Live中是如何完成的。有两种选择:要么发送所有playerState_t
结构(因为最多64个结构),要么添加另一个数组playerState_t
以保持团队HP(因为它只有64个整数)。后者的可能性更大。
为了使对象数组在客户端和服务器之间保持同步,每个实体都有一个从0到1023的实体索引,并将其作为消息的一部分从服务器发送到客户端。当客户端接收到一个由256个实体组成的数组时,它将遍历该数组,从每个实体中读取索引字段,并在其本地存储的实体数组中的读取索引处更新该实体。
本文收集自互联网,转载请注明来源。
如有侵权,请联系[email protected] 删除。
我来说两句