状态同步的相关问题

少于 1 分钟读完

网络延迟

问题引入:

这里主要讨论状态同步,client A发生状态变化(移动,换装,属性变化等),将变化的信息同步到client B,最简单做法就是直接通过服务器中转然后广播出去,但是由于存在网络延迟,所以client B收到的消息总是滞后的,这对于高精度高技巧性的游戏的无法容忍的。

解决方式:

首先要实现网络对时操作,所谓对时,并不是要求客户端时间跟服务器保持一致,而是通过在数据包中加入时间戳,算出数据包的网络延迟时间。首先客户端向服务器发送一个消息包,里面带上客户端的本地时间t1,服务器收到数据包打上服务器本地时间t2,服务器在内部流转该数据包可能会有时间消耗(比如一些逻辑处理,校正等),在服务器广播该数据包的时候打上服务器本地时间t3,客户端收到服务器广播的数据包时打上客户端本地时间t4。这里我们假设数据上下行的网络延迟是一样的。那么通过计算((t4-t1)-(t3-t2))/2, 就可以计算出单程一个网络包的大概延迟。

玩家移动状态同步

基于帧的状态同步

在开始设计的时候,没有考虑很多,为了简化操作,使用了基于帧的状态同步,在这种模式下,即当地图中玩家的状态发生改变,就立即往服务器发送一个状态数据包,然后通过服务器广播给当前地图中的所有玩家,其他的客户端收到该数据包后解析并设状态。这样的做的优势很明显,简单粗暴,同时状态信息准确。但是仔细分析之后,这样的做法增大了服务器的压力。

举个例子:在当前的游戏中,游戏的更新速率为 120 FPS,即一秒钟更新120次。如果玩家静止不动,则不需要发送数据包,但是如果玩家处于移动状态的时候,一秒钟会向服务器发送120个数据包,同时服务器需要广播(120*PlayerCount– 1)个数据包,

假设一个数据包的大小为10 bytes,一个地图有10个玩家(包括自己),那么服务器的带宽为12kbps ,十分消耗服务器性能。因此考虑在移动平台下,这种方式不可取。

基于预言的状态同步

由于每一次移动或者改变状态都需要发送数据包,消耗服务器性能。通过观察官方设计,使用基于预言的状态同步。

实现前提:可接受的延迟(RPG类游戏可接受200ms左右的延迟)

官方设计:官方设计实际上实现的十分精巧,将一个 玩家/怪物 的移动拆分成了一个移动片段(MovementFragments),在每个片段中,包含了当前的位置,下一次的位置和时间。即

struct MovementFragments {
    Int16_t x;
    Int16_t y;
    Int16_t nextX;
    Int16_t nextY;
    Int16_t duration;
}

在当前的客户端中,通过当前的位置(x,y),和当前的速度(speed),产生一个随机的时间duration (duration ∈(0,510)),通过这些参数算出移动过后的位置:

nextX = x + speed * duration;
nextY = y + speed * duration;

当其他客户端收到该数据包之后,通过数据包的值,算出玩家的速度,同时移动。

OtherPlayer.moveToUntil(nextX,nextY,duration);

同时在官方的设计当中,一秒钟将一次状态改变拆分成了5次片段,片段的是消耗时间为一个随机值,但是总时间为510毫秒。因此在一次更新速度为120FPS的游戏当中,一秒钟只需要发送10个数据包,大大缓解了服务器的压力。

加上网络延迟的移动同步

这里参考了云风的博客以及其他一些的资料,把目前主流的同步策略在这里做下总结。

流程如下:

  • 进入地图时做一次客户端和服务端的对时,以服务端为准,计算时间差,以后每隔一段时间(1分钟)对时一次。

  • 客户端每隔一段时间(1秒),将当前运动状态(位置、速度、加速度)和时间戳T1同步到服务端。

  • 服务端接到客户端同步后,立即推送给地图中的所有人的客户端。以下对于接收方是自己和其他人,两种情况分别讨论:

    1. 对于其他人,根据T1时刻的运动状态和当前本地时间T2,计算T2 + 1秒时刻的预测位置,反推当前的速度。
    2. 对于自己,根据T1时刻的运动状态和当前本地时间T2,计算当前时刻(T2)的预测位置,只要实际位置和预测位置相差不大,不用修正;否则直接移动到预测位置。

对于这套策略的总结如下:

  • 以客户端的操作手感为先,服务端只做校验,防止作弊。这样对于自己来说,操作体验和单机无异;对于地图上显示的其他玩家,位置还原的精准程度取决于上述步骤2中同步的频率,理论上,频率越高则越精准,不过也会带来更高的网络和服务器开销,同时也要考虑游戏类型的实际需求,各方面因素综合考虑。

  • 为了减小网络延迟的影响,采用对时和预测补偿的做法。对时是在进入地图的时候做一次,以后每隔1分钟再做一次,以防本地时间跳变带来的影响。预测补偿很重要,对于其他人的运动,我们总是预测1秒后的位置,再反推当前速度,为的是平滑处理,避免出现跳变。

  • 保持一定的同步频率,还有一个好处是保持网络流量稳定,避免玩家在频繁操作时大量发包。例如,在带摇杆的游戏里玩家有可能会旋转摇杆在原地转圈,其实这种情况下其他玩家不用关心到那么细,只用保持一定采样频率即可。另外,就算客户端需要加入物理引擎也很方便,服务端不用做强验证,只要防止穿越地图这样的明显作弊即可。

基本思路就是这些。当然这些都是理论设想,最终效果还需要在实践中检验,也有可能部分策略需要做补充和微调,部分参数如采样频率和预测时间都需要在项目中调校,为了最终实现效果和性能的平衡。

怪物状态同步

在一个地图当中,玩家的状态同步之后,则需要同步地图中怪物的位置信息,怪物的位置信息同步方式一般有两种实现方式

基于客户端的状态同步

一个地图中的怪物状态,实际上是由地图中玩家所决定的,当玩家施加攻击、使用技能,都会改变怪物的状态。在MapleStroy的设计当中,怪物的位置计算是属于离线计算,这也就是说,服务器不参与怪物的状态,这样的好处是节约流量、减缓服务器压力,但是坏处是,会出现怪物静止、吸怪等外挂手段。在移植MapleStroy的过程中,为了同步官方和考虑移动平台流量问题,因此采用此种手段。

实现策略:

怪物的位置由第一个进入该地图的玩家决定。这也就是说,当第一个玩家进入该地图之后,控制着当前地图中所有怪物的移动状态。当第二个玩家进入该地图之后,第一个玩家会广播当前所有怪物的状态,第二个玩家根据这些数据包进行改变。当然其他玩家发生了攻击,或者激怒怪物的操作后,也会广播这个消息。

同时怪物的移动也是采用基于预言的状态同步,大体实现和玩家移动相似。

 怪物的同步在传统的端游里,是完全由服务器的怪物AI系统触发,客户端只是纯粹的接受服务器下发的怪物状态数据。对于手机游戏里,由于手机上很难出现像PC里那样的外挂,所以怪物的AI可以考虑放在客户端触发,同时减少怪物的状态同步。详细说明如下:

a) 怪物的随机移动不同步

  在地图上,怪物都会有一个固定的位置。怪物没有进入战斗状态时,就会在这个固定位置的周围走来走去,随机的移动。这个随机的移动设定由每个客户端自己控制,这样怪物的随机移动,就不用消息广播进行同步了。

  由于客户端自己控制怪物的随机走动,所以会出现不同客户端里,怪物位置不一样的问题。但由于怪物随机移动的范围较小,所以这个问题不是很明显,在手机上是可以接受的。角色打怪时,是扇形的伤害范围,所以即使怪物坐标在不同的客户端有点不一致,打怪的效果也可以接受。

b) 怪物的行为同步

  当有角色攻击被动怪物,或者进入主动怪物的视野范围内时,怪物的AI就被这个角色所在的客户端锁定了,同时怪物进入攻击状态。攻击的判断完全由锁定怪物AI的客户端进行处理,同时这个客户端会将这个怪物的行为上发到服务器,由服务器广播给周围的其他玩家。

  怪物的AI锁定,使用抢占式,即谁最先发消息给服务器申请怪物的AI锁定,谁就获得了怪物的控制权,直到怪物死亡或脱离战斗状态。

  怪物可以每进行一次攻击,客户端就发一个消息给服务器。这样做,消息还是有点多,特别是一群怪围着几个角色进行攻击时,消息广播还是有点多。所以可以将状态的概念向上扩大,只同步怪物在攻击哪个玩家,而不同步每一次的攻击,然后由每个客户端根据怪物固定的攻击速度各自去表现。这样一个怪去攻击一个玩家,就只会有一次消息广播了。

c) 精英怪和BOSS怪的AI

  精英怪和BOSS怪由于数量较少,而且比较重要,所以不能由客户端来申请AI控制权,而是服务器根据某种策略来控制。所使用的策略可以考虑角色的伤害值、防御值、角色与BOSS的距离远近等,根据这些因素,服务器计算出BOSS怪当前最适合攻击的对象(比如血量最少的玩家,最脆弱的法师等),然后将AI控制权发给那个客户端,由那个客户端控制攻击行为,同时通过消息让服务器同步给其他玩家。

  总结:怪物的同步方式的选择,就是尽量减少消息的广播,同时让游戏效果在可接受的范围内。怪物AI的这个处理方式,实际上是同时省去了游戏服务器的怪物AI模块(端游一般是专门用的一个进程或者另外一台物理服务器来进行怪物AI的计算),从而简化了MMO游戏的开发难度,同时保证了较好的游戏体验。

相关链接

  • https://www.jianshu.com/p/5dbdf81c4e69

分类:

更新时间:

留下评论