在CS架构中,无论采用什么样的同步方法,都始终遵循着客户端向服务器发送消息,服务器收到消息后稍作处理,再把这条消息发给需要的客户端们。所传递的消息可以是玩家的位置、旋转这样的状态值,可以是“向后走“这样的指令值。前者称为状态同步,后者称为指令同步。
难点:由于存在网络延迟和抖动,先发送的包可能后到,后发的包可能先到,往往很难做到精确的同步。
网络延迟参考:
局域网:0.1毫秒
PC宽带-外网服务器:12毫秒
手机WiFi-外网服务器:25毫秒
手机4G-外网服务器:50毫秒
假设客户端每隔0.033秒向服务器发送玩家自身的位置信息,服务器转发给其他的客户端,其他客户端并不能保证每隔0.033秒就更新一次位置,可能1秒才更新一次。最后导致玩家之间不同步。
网络延迟问题基本无解,只能权衡。比如,尽量发送更少的数据,数据越少,发生数据丢失并重传的概率就越小,平均速度越快。又比如,在客户端做些手脚,让玩家感觉不到延迟
状态同步:
状态同步指的是同步状态信息,服务器收集客户端的信息,处理后给客户端一个结果信息。比如,发送位置,旋转信息
直接状态同步
最直接的同步方案莫过于客户端定时向服务端报告位置,其他玩家收到消息后,直接将对方的人物移动到指定位置。但是由于网络延迟的问题,会导致同步的玩家会产生瞬移的效果
跟随算法
为了解决瞬移问题,引入了“跟随算法”,客户端不直接将玩家拉到目的地,而是让玩家以一定的速度移动到目的地。当同步频率够高,误差可以忽略。
预测算法
跟随算法的一大缺陷就是误差会变得很大,为了减少误差,在有规律可循的条件下,比如:匀速运动,匀加速运动,我们可以预测玩家在下一个时间点的位置,让玩家提前走到预测的位置上,这就是预测算法。
帧同步:
帧同步是指令同步的一种,即同步操作信息。基本上所有指令同步方法都结合了帧同步,两者可以视为一体。这里的帧跟每秒多少帧有所不同。称为同步帧
指令同步
状态同步所同步的是状态信息,如果要同步玩家的位置和旋转,需要同步6个值。如果减少传输的数据量,只传输玩家的操作指令(向上,向下走),数据量会减少很多。
比如:玩家按下键盘的上键,玩家向服务器发送“向前走”的消息给服务端,经由转发,让其他玩家收到后,让同步玩家向前移动。
缺点:这个过程的一大缺点就是误差的累积。有些电脑速度快,有些电脑慢,只要电脑运行速度不同,有些玩家看到走了很远,而有些人却只能看到移动一点点距离,为了解决这个问题,引入了“同步帧”的概念
同步帧:
假设我们自己实现一个类似Update的方法,称为FrameUpdate,程序会固定每隔0.1秒就调用它一次。每一次调用FrameUpdate称为一帧,第一次调用称为一帧,第二次为2帧。相当于0.1秒的时候执行了第一帧。
然而,现实很残酷,比如在执行第2帧的时候,系统卡顿了1秒,导致第3帧无法按时执行。为了保证后面的帧能够按时执行,程序需要做出调整,即减少后面3-12帧之间的时间间隔,以保证程序在1.2秒时,执行到第13帧
实现:
void Update(){
while(Time.time<frame*interval){
FrameUpdate();
frame++;
}
}
指令的执行:
为了保证所有客户端有一样的表现,往往要做一些妥协,常见有两种妥协
1,有的客户端快,有的慢,如果要让他们表现一致,只能让快的等慢的,才有可能表现一致。例如:客户端1在第3帧发出向前的指令,由于网络延迟的原因,客户端2在第5帧才收到,所以客户端1的玩家只能在第5帧才开始前进
2,对于慢的客户端所发送的,丢弃那些过时的帧,直至它赶上来。
在方案一种,每帧客户端都要向服务端发送指令,没有操作也要发送一个代表没有操作的指令。服务端收集各个客户端的指令,收集完,在接下来的某一帧广播出去。而客户端只有收到消息时,才会执行下一帧。
如果按照每秒执行30帧的频率,交流还是过于频繁,会带来较大的网络负担。可以吧多个帧合称为一轮,每一轮向服务端同步一次指令
帧同步还可以配合投票法来防止作弊。例如,某个玩家击中另外一个玩家,由于所有客户端运行结果严格一致,它们都可以向服务端发送“谁击中了谁”的消息。服务端收集这些信息,如果半数以上的玩家都发送了击中消息,才认为有效
同步策略
预测(玩家本地自身的状态)
和解(收到服务端的权威状态后,返回服务端上一次的权威状态,在应用之后预测输入)
插值(移动旋转之类的平滑处理)
实现原理:
老状态+输入=新状态
定义状态:比如:时间,玩家位置,旋转
定义输入:有哪些输入可以改变状态,比如:移动,攻击,进入和离开
实现状态计算:计算老状态到新状态的规则
后端:
接收输入,存起来
定期同步,广播输入以及计算状态
前端:
后端输入为权威状态
前端输入为预测状态
客户端先回滚到服务器上一次的状态,在计算最新的权威状态,在应用预测输入(这是一瞬间的事情,对于玩家来说,其实不太会被影响),