关于网路游戏地图中人物的移动的同步一直以来的都是mmo,moba,fps等游戏的核心逻辑,也是开发的重点和难点,其实现的质量直接影响的玩家的体验。一个高效,实时,平滑的同步往往需要后端和前端相互配合才能达到最佳效果。下面将结合前端代码 uinty 代码及后端 c 代码详细讲解同步算法的实现。
题外话
关于同步显示的需求最早始于网游诞生之前,美军战斗机上的雷达屏幕需要显示多机编队的位置,是实上在网游上有大量的这样minimap,在几十年前这绝对属于高精尖。
同步包括下面几个部分:
1.服务器与客户端时间同步
- 步进一致性
服务器与客户端的当前时间是不一直的,即使双方都使用远程原子钟校准,也会不可抗拒的受本地系统负载,原子钟服务器负载,网络延时等因素的影响。但是时间的步进可以趋近于一致的,当前系统可以拿到步进(tick)可以达到 100 ns(纳秒)级 (10 ticks = microseconds 微秒 ,s(秒)、ms(毫秒)、μs(微秒)、ns(纳秒),其中:1s=1000ms,1 ms=1000μs,1μs=1000ns),而我们做同步用到的 ms(毫秒)就足够了,所以我们有理由把服务器与客户端的时间步进当做是同步的来处理,也就是不管服务器与客户端的时间点是否同步,但随着时间的推移,服务器时间每增加一毫秒的单位,客户端的时间也增加一毫秒的单位。有了“步进一致”的基础后我们就可以完成下面的工作。
- 基准网络延时的估算(请求返回时间延时量)
服务器与客户端相互通信,网络延时是不可避免的,虽然在物理层,电子流动速度的逼近光速,但信号是需要电平高低的变化才能完成传递,加上链路层复杂的硬件连接,物理地址寻址、数据的成帧、流量控制、数据的检错、重发,各个模块的缓冲,控制总线的逻辑消耗,tcp协议栈为保证数据可靠性传输对数据的层层包裹与寻址操作,以及上下行带宽,物理服的节点连同距离,各个节点负载,数据的在网络传输的延时就不可避免的产生了。其次网络传输速率受双方系统负载以及拓普网络负载变化的影响存在不可避免的波动性,所以延时量本身也在不断变化中。而我们需要做的第一件事情就是估算一个客户端到服务器加上服务器到客户端的延时,也就是客户端一个请求到服务器返回的时间,这个时间可以由客户端多少向服务端请求并返回的值进行求平均而来,我们这里实现的12次连续请求返回,每次保存请求返回的时间间隔,在完成12次后进行排序,去掉头尾,对剩下的10个求平均,这样在一定程度上排除网络波动的影响。
网络延时(请求+返回)获取的具体细节如下
1.客户端记录当前时间戳,作为发送时间戳
2.客户端向服务器发送测试消息,这个消息的数量很小能够标识类型就足够了,我们的实现4字节长度头加两字节标识。
3.服务器向客户端回射测试消息(消息内容同客户端请求)
4.客户端收到返回后,再当前时间戳-发送时间戳得到网络延时(请求+返回)
5.连续执行以上步骤12次,对网络延时数据集合进行排序,去掉头尾,对剩下的10个求平均
- 时间差计算
通过上个步骤我们拿到了网络延时量(请求+返回),这个值拿到后我们就可以推算出客户端与服务端的时间差,有了时间差由于“步进一致”性,只要消息中带上各自的时间戳,我们就可以推算出当前消息的延时量了,计算方式为 时间差 = 服务器时间 + 延时量/2 - 客户端时间
1.客户端向服务器请求服务器当前系统时间utc
2.服务器向客户端返回服务器当前系统时间utc
3.客户端拿到服务器当前系统时间utc后 + 延时量/2 - 客户端当前系统时间utc ,得到一个精确到毫秒的时间差,客户端保存该值
4.客户端向服务器发送 时间差
5.服务器保存 时间差 到相应客户端数据中
有了时间差,客户端可以做一个封装,GetServerTime() 获取当前服务器时间,在以后服务器向服务端发消息时带上自己当前时间戳,到达后我们根据 GetServerTime() 的值就可以判断出本次消息延时量从而对同步对象的速度有有依据的增加,同样服务端也可以封装 GetClientTime()
客户端
long GetServerTime(){
//客户端当前系统时间utc + 时间差
}
服务端
long GetClientTime() {
//服务端当前系统时间utc - 时间差
}
2.同步协议处理
一个同步协议在非p2p架构下需要以下步骤
客户端 A ——> 服务器——> 客户端 (N) N等于不包含A的其他客户端
着其中包含了 客户端 A的上行延时 + 客户端 (N) 下行延时,而之前我们保存的时间差可以用来判断当前具体消息的延时量,有了延时量我们就可以对坐标进行修正。
发送时间(客户端),方向,速度,发送时坐标
1.客户端 A ———————————————————————> 服务器
2.服务器根据 发送时间(客户端),时间差 ,方向和速度将发送时坐标修正即时坐标
发送时间(服务器),方向,速度,服务器即时坐标
3.服务器 ———————————————————————> 客户端 (N)
3.客户端 (N)根据发送时间(服务器),方向,速度,服务器即时坐标再次修正即时坐标
3.客户端逻辑
- 移动发起端 (player)
1.用户在操作方向遥感时(方向遥感发生位移),把当前方向,速度,坐标发给服务器
2.方向遥感的角度变化时再次把当前方向,速度,坐标发给服务器
3.用户离开方向遥感后把当前方向,速度,坐标发给服务器,
这三个操作都放在 update()中执行
- 移动同步端 (OtherPlayer)
客户端收到同步协议并完成 服务器即时坐标到即时坐标的修正后,我们需要操作显示对象移动到指定位置,此时当前位置到目标位置必然存在一个距离差,我们需要将显示对象平滑的移动到目标位置,如果速度不为0,我们还要让显示对象继续沿当前方向移动移动 ,所以这里需要同步状态机做切换来控制显示对象移动规则,同步消息可以在任何时候打断当前状态机,重新执行状态机流转过程
enum SceneObjectOtherMoveableRunState{
idle_stop,//待机状态
to_target_position_before_run,//当前对象在有速度,移动到同步位置过程
to_target_position_stop,//当前对象停止,移动到同步位置过程
by_direction_run//根据速度和方向进行移动
}
计算网络延时的核心代码:
byte [] checkTimeDelayMsgBuff = new byte[6]; int checkTimeDelayMsgBuffIndex = 4; int checkTimeDelayMsgLen = 2; byte [] checkTimeDelayMsgLenBytes = new byte[4]; long checkTimeDelaySend; /* 样本数组 */ long []checkTimeDelaySample = new long[12]; /* 样本数组下标 */ int checkTimeDelaySampleIndex = 0; bool checkTimeDelayLoop = false; public delegate void OnCheckTimeDelayCompleteMethod(); public OnCheckTimeDelayCompleteMethod onCheckTimeDelayComplete; public void CheckTimeDelayRunOnce(OnCheckTimeDelayCompleteMethod onCheckTimeDelayCompletCallBack = null){ onCheckTimeDelayComplete += onCheckTimeDelayCompletCallBack; checkTimeDelayLoop = false; CheckTimeDelay (); } public void CheckTimeDelayRun(OnCheckTimeDelayCompleteMethod onCheckTimeDelayCompletCallBack = null){ onCheckTimeDelayComplete += onCheckTimeDelayCompletCallBack; checkTimeDelaySampleIndex = 0; checkTimeDelayLoop = true; CheckTimeDelay (); } public bool IsCheckTimeDelayComplete(){ return checkTimeDelaySampleIndex == checkTimeDelaySample.Length; } void CheckTimeDelay(){ if (m_ClientSocket != null){ /* 数据包长度 **/ checkTimeDelayMsgLenBytes = BitConverter.GetBytes (checkTimeDelayMsgLen); checkTimeDelayMsgBuff [0] = checkTimeDelayMsgLenBytes [0]; checkTimeDelayMsgBuff [1] = checkTimeDelayMsgLenBytes [1]; checkTimeDelayMsgBuff [2] = checkTimeDelayMsgLenBytes [2]; checkTimeDelayMsgBuff [3] = checkTimeDelayMsgLenBytes [3]; /* 数据类型表示,这个可以自定 **/ checkTimeDelayMsgBuff [4] = 0; checkTimeDelayMsgBuff [5] = 1; m_ClientSocket.Client.Send (checkTimeDelayMsgBuff, 0, 6, SocketFlags.None); /* 保存发送时间戳(毫秒 **/ checkTimeDelaySend = (DateTime.UtcNow.ToUniversalTime ().Ticks - 621355968000000000) / 10000; } } void CheckTimeDelayReturn(){ /* 延时时间戳样本 当前时间戳(毫秒) - 发送时间戳(毫秒) **/ checkTimeDelaySample [checkTimeDelaySampleIndex++] = (DateTime.UtcNow.ToUniversalTime ().Ticks - 621355968000000000) / 10000 - checkTimeDelaySend; if (checkTimeDelaySampleIndex < checkTimeDelaySample.Length) { if(checkTimeDelayLoop) CheckTimeDelay (); }else if(onCheckTimeDelayComplete!=null){ // CheckTimeDelay Complete checkTimeDelayLoop = false; onCheckTimeDelayComplete(); } } public long GetCurTimeDelay(){ return checkTimeDelaySample[checkTimeDelaySampleIndex]; } public long GetMeanTimeDelay(){ long temp = 0; int size = checkTimeDelaySample.Length; /* 排序 */ for(int i = 0 ; i < size-1; i ++) { for(int j = 0 ;j < size-1-i ; j++) { if(checkTimeDelaySample[j] > checkTimeDelaySample[j+1]) //交换两数位置 { temp = checkTimeDelaySample[j]; checkTimeDelaySample[j] = checkTimeDelaySample[j+1]; checkTimeDelaySample[j+1] = temp; } } } long sum = 0; /* 去掉头尾求均值 */ for (int i = 1; i < size - 1; i++) { sum += checkTimeDelaySample [i]; } return(sum / (checkTimeDelaySample.Length - 2)); } public void CheckTimeDelayPrint(){ string str = ""; for (int i = 0; i < checkTimeDelaySample.Length; i++) { str += checkTimeDelaySample [i] + ","; } Debug.LogFormat ("{0},GetMeanTimeDelay {1}",str,GetMeanTimeDelay()); }
服务端 c 获取时间戳
#include <sys/time.h> struct timeval tv; gettimeofday(&tv,NULL); printf("time seconds:%ld microsecond:%d\n",tv.tv_sec,tv.tv_usec); //线程休眠,测试延时 usleep(16666);//16.666 ms = 16666 microsecond
希望这篇文章能对您的工作有所启发!510982172@qq.com thc 2017.10.7