看了很多文章,也在自己的游戏中亲自编写,整理并实现了一套帧同步方案,废话不多,直接分享。
网络重播:这块是帧同步必须做的,我们需要把服务器发过来的每一帧,以及随机种子,英雄数据记录下来,用于重播,有人问为什么要重播呢?因为重播是查不同步的重要手段,你大概率都不知道你干什么导致不同步了,那么你可以利用你的保存的数据用每个玩家重播一遍,看看差异在哪里,从而定位到导致不同步的代码。
定点数的使用:既然要同步,那必须使用定点数了,因为浮点数的计算结果不一定一样,使用定点数,可以保证结果的一致性。我们使用的是long 保存,把浮点数左移14位的做法,然后重载所有云算符。
//如果客户端的帧和服务器的帧误差小于Margin的帧数,则为安全边际,
//否则需要重新从服务器请求丢失的帧信息
ret = (serverFrame - clientFrame) <= nFrames;
///距离大于1.5s的位移,暂时先直接拉扯
///如果x/z小于一帧位移距离,也直接拉扯
///其余情况 匀速移动,减速,加速情况后续会加
private Vector3 LerpPos(Vector3 start, Vector3 end, float intepoation)
{
Vector3 pos = new Vector3(start.x, start.y, start.z);
//pve加速模式 或 pvp落后很多帧情况下,直接移动到对应位置
if (GameLogic.TestMode || FrameDriver.Current.GetIsRunFast())
{
pos.x = end.x;
pos.z = end.z;
}
if (Vector3.Distance(start, end) > Speed * 1.5f)
{
pos.x = end.x;
pos.z = end.z;
}
else
{
float dis = Mathf.Sqrt((end.x - start.x) * (end.x - start.x) + (end.z - start.z) * (end.z - start.z));
if (dis != 0)
{
float moveX = (end.x - start.x) / dis * Speed * intepoation;
float moveZ = (end.z - start.z) / dis * Speed * intepoation;
if (Mathf.Abs(start.x - end.x) > Mathf.Abs(moveX))
{
pos.x = start.x + moveX;
}
else
{
pos.x = end.x;
}
if (Mathf.Abs(start.z - end.z) > Mathf.Abs(moveZ))
{
pos.z = start.z + moveZ;
}
else
{
pos.z = end.z;
}
}
}
var positionY = GameScene.Current.mMapHypsogramInfo.GetHeight_View(pos.x, pos.z) + L2VRiseHigh;
pos.y = Mathf.Lerp(start.y, positionY, LERP_POS_T);
return pos;
}
private int LerpToAngle(ref float angle, int dstAngle, float intepoation)
{
var delta = Tr.DeltaAngle((int)angle, dstAngle);
if (Abs(delta) > 5F)
{
var d = Mathf.Lerp(0, delta, 0.4F * Time.deltaTime * 60);
angle += d;
}
else
{
angle = dstAngle;
}
return delta;
}
上面是所有可控制物体的位置以及角度运算,位置采用匀速,角度采用每次update40%的方式插值。
using System;
using System.Text;
namespace YHP1
{
///
/// 网络抖动值
///
public class NetJitterTime
{
private const int MAX_CALC_DELAY_LEN = 100;
private const int AVERAGE_NUM = 5;
private const int DISCARD_NUM = 2;
private float[] delayArr = new float[MAX_CALC_DELAY_LEN];
private float[] sortedDelayArr = new float[MAX_CALC_DELAY_LEN];
private bool isInitialed = false;
private int oldestIndex = 0;
public void Update(float aDelay)
{
if (!isInitialed)
{
Initial(aDelay);
return;
}
InsertOneDelay(aDelay);
}
//按照升序,初始化所有的延迟
private void Initial(float aDelay)
{
sortedDelayArr[oldestIndex] = aDelay;
delayArr[oldestIndex] = aDelay;
int currentindex = oldestIndex - 1;
while (currentindex >= 0)
{
if (sortedDelayArr[currentindex] > sortedDelayArr[currentindex + 1])
{
Swap(ref sortedDelayArr[currentindex], ref sortedDelayArr[currentindex + 1]);
currentindex--;
}
else
{
break;
}
}
oldestIndex++;
if (oldestIndex >= MAX_CALC_DELAY_LEN)
{
isInitialed = true;
oldestIndex = 0;
}
}
public void InsertOneDelay(float aDelay)
{
oldestIndex = oldestIndex % MAX_CALC_DELAY_LEN;
float deletDelayVale = delayArr[oldestIndex];
delayArr[oldestIndex] = aDelay;
oldestIndex++;
int sortIndex = BinarySearch(sortedDelayArr, 0, MAX_CALC_DELAY_LEN - 1, deletDelayVale);
if (sortIndex < 0 || sortIndex >= MAX_CALC_DELAY_LEN)
{
Logger.LogError("InsertOneDelay sortIndex is a invalid number : " + sortIndex);
return;
}
sortedDelayArr[sortIndex] = aDelay;
//替换最老数据以后使用插入排序
if (sortIndex - 1 > 0 && sortedDelayArr[sortIndex - 1] > sortedDelayArr[sortIndex])
{
sortIndex--;
while (sortIndex >= 0)
{
if (sortedDelayArr[sortIndex] > sortedDelayArr[sortIndex + 1])
{
Swap(ref sortedDelayArr[sortIndex], ref sortedDelayArr[sortIndex + 1]);
sortIndex--;
}
else
{
break;
}
}
}
else if (sortIndex + 1 < MAX_CALC_DELAY_LEN && sortedDelayArr[sortIndex + 1] < sortedDelayArr[sortIndex])
{
sortIndex++;
while (sortIndex < MAX_CALC_DELAY_LEN)
{
if (sortedDelayArr[sortIndex] < sortedDelayArr[sortIndex - 1])
{
Swap(ref sortedDelayArr[sortIndex], ref sortedDelayArr[sortIndex - 1]);
sortIndex++;
}
else
{
break;
}
}
}
}
private float GetJitterTime()
{
if (isInitialed)
{
float minSum = 0;
for (int i = DISCARD_NUM; i < AVERAGE_NUM + DISCARD_NUM; i++)
{
minSum += sortedDelayArr[i];
}
float maxSum = 0;
for (int i = MAX_CALC_DELAY_LEN - 1 - DISCARD_NUM; i >= MAX_CALC_DELAY_LEN - AVERAGE_NUM - DISCARD_NUM; i--)
{
maxSum += sortedDelayArr[i];
}
float jitter = (maxSum - minSum) / AVERAGE_NUM;
if (jitter < GameConfig.FrameIntervalL / 2)
{
return 0;
}
return jitter;
}
else
{
return 0;
}
}
public int GetJitterLength()
{
float jitterTime = GetJitterTime();
int result = UnityEngine.Mathf.CeilToInt(jitterTime / (GameConfig.FrameIntervalL * 1000));
return result;
}
public static void Swap(ref T a, ref T b)
{
T t = a;
a = b;
b = t;
}
//二分搜索找到排序中的最老的值的index
public static int BinarySearch(float[] arr, int low, int high, float key)
{
int mid = (low + high) / 2;
if (low > high)
return -1;
else
{
if (arr[mid] == key)
return mid;
else if (arr[mid] > key)
return BinarySearch(arr, low, mid - 1, key);
else
return BinarySearch(arr, mid + 1, high, key);
}
}
public void Clear()
{
isInitialed = false;
oldestIndex = 0;
}
}
}
上面代码很多参数可调,根据实际项目决定,基本可以实现客户端流畅运行,不过会适当增加自己的操作延迟,其实就是用延迟换流畅。
protected void ExecutesLogicFrames(float delta)
{
if (mFrameBuffer.LastFrame == 0)
return;
int jitterLength = NetworkSyncManager.Instance.GetJitterLength();
if (ShouldRunFast(jitterLength))
{
int K = 0;
while (true)
{
var L = mFrameBuffer.NextFrame();
if (!ExecuteEachFrame(L))
{
return;
}
++K;
if (K > 50)
{
break;//一次最多跑50帧,避免主线程卡死
}
}
}
else
{
mTickTime += delta;
if (mTickTime < GameConfig.FrameIntervalL && mFrameBuffer.IsConsumeBuffer(jitterLength))
{
return;
}
var L = mFrameBuffer.NextFrame();
if (L != null)
{
mTickTime = 0;
ExecuteEachFrame(L);
}
}
}
哈,看到快速模式了对吧,这个是下面要讲的。
10.快速模式呢,就是追帧,跟大家差的太远了,正常执行已经很难追上了:
private bool ShouldRunFast(int jitterLength)
{
if (FastMode)
return true;
if (mIsRunFast)
{
if (mFrameBuffer.CacheFrameNum() <= jitterLength)
mIsRunFast = false;
}
else
{
if (FrameInfo.ServerFrame - FrameInfo.CurFrame > 90)
mIsRunFast = true;
}
return mIsRunFast;
}
这里简单给了一下是否快速执行,