关于MMO中的位移同步方案(二)
上一篇说到根据收到的最新同步坐标点,按照延迟时间和同步频率做平滑插值的方法,但是存在最新的同步点可能会覆盖掉之前还未来得及同步的关键点,导致实际位移轨迹不对的问题。这篇介绍一下在此基础上的一个改进方法,使得位移轨迹可以同步。既然导致问题的原因是总是以最新的同步点作为插值目标,导致之前还未同步的点被丢弃,那么就开一个数组,将未同步完成的关键点都缓存起来,同步完一个移除一个,后续收到的点都添加到尾部。
维护一个剩余同步路程和位移速度
插值时间仍然选取当前延迟时间加一个同步频率。
插值方式之前是用的终点坐标直接插值的做法,但是现在维护的是一个关键点队列,没法按照坐标直接插值了,改为按照位移速度同步的方式。
根据同步队列中剩余的点,可以计算出当前一共还需同步的路程,加上是否需要做预测距离,再除以选取的插值时间,可以计算出一个Speed,角色只需按照这个速度步进,就能够在一个插值时间内走完当前剩余的所有关键点。然后每次收到新的同步点的时候需要按照当前剩余的所有路程和最新的延迟时间来调整这个Speed。
为了不至于每次收到一个同步点就去遍历一遍关键点,可以维护一个剩余同步路程SmoothLeftDis,在收到新同步点的时候根据当前是否处于预测阶段,计算最新的同步路程。
若当前已在预测阶段,则表示关键点已全部同步完,直接以角色当前位置计算同步路程(此处为了不发生拉扯,角色直接从当前位置想最新点位移),否则累加最新点与队尾点的距离,更新同步路程。在根据最新坐标是否为终点,判断是否需要再加一个预测距离。同时再维护一个预测路程DrDelta。
DRing = false;
recvTime = Time.realtimeSinceStartup * 1000;
int count = positionDatas.Count;
if (!positionDatas[count - 1].isFinal)
{
if(Mathf.Approximately(SmoothLeftDis, DrDelta.magnitude) || SmoothLeftDis < DrDelta.magnitude)
{
SmoothLeftDis = 0;
}
else
{
SmoothLeftDis -= DrDelta.magnitude;
}
}
if (!Mathf.Approximately(SmoothLeftDis, 0) && SmoothLeftDis > 0)
{
SmoothLeftDis += Vector3.Distance(positionData.position, positionDatas[count - 1].position);
}
else
{
//TODO 此处可以加一个方向判断 丢弃方向相同 位移相反 后退的点 并且不是最后的终点
SmoothLeftDis = Vector3.Distance(positionData.position, m_SyncRole.localPosition);
}
smoothTime = recvTime - positionData.time + m_PosSyncInterval;
if (!positionData.isFinal)
{
DrDelta = (positionData.position - positionDatas[count - 1].position);
float deltaTime = m_PosSyncInterval;
if (!positionDatas[count - 1].isFinal)
{
deltaTime = positionData.time - positionDatas[count - 1].time;
}
float drTime = Mathf.Min(smoothTime, m_MaxReckoningTime);
float magnitude = (DrDelta.magnitude * (drTime / deltaTime));
DrDelta = DrDelta.normalized * magnitude;
SmoothLeftDis += magnitude;
smoothTime += drTime;
}
else
{
DrDelta = Vector3.zero;
}
SmoothSpeed = SmoothLeftDis / (smoothTime / 1000);
positionDatas.Add(positionData);
同步流程
同步流程比较简单,认为同步队列中的点都是还未到达的点,每次Update 按照速度步进的时候移除掉队列头部已经到达的点即可,注意队列需要保留最后一个点,作为后续收到最新点时 判断上次状态的依据。当移除到最后一个点的时候进入预测阶段。注意这里由于计算用的都是浮点数,关于每次步进的量和剩余位移路程的计算存在误差,随着误差的累计,最终有可能出现剩余路程已经为0,但是队列中的点还没有同步完的情况,所以每次发现SmoothLeftDis <= 0的时候做一次检查,加回剩余队列中的路程。
void SyncPositionSmoothDR2()
{
if (SmoothLeftDis <= 0) return;
int count = positionDatas.Count;
float delta = Time.deltaTime * SmoothSpeed;
SmoothLeftDis -= delta;
if(SmoothLeftDis < 0 || Mathf.Approximately(SmoothLeftDis, 0))
{
if(!Mathf.Approximately(SmoothLeftDis, 0))
{
delta += SmoothLeftDis;
}
SmoothLeftDis = 0;
}
Vector3 curPos = m_SyncRole.localPosition;
if(DRing)
{
m_SyncRole.localPosition = curPos + DrDelta.normalized * delta;
}
else
{
int index = -1;
for(int i = 0; i < count; i++)
{
float dis = Vector3.Distance(positionDatas[i].position, curPos);
if(delta >= dis || Mathf.Approximately(delta, dis))
{
if(Mathf.Approximately(delta, dis))
{
delta = 0;
}
else
{
delta -= dis;
}
index = i;
curPos = positionDatas[i].position;
}
else
{
break;
}
}
if(index < count - 1)
{
Vector3 dir = positionDatas[index + 1].position - curPos;
if(index >= 0)
{
float angle = LerpAngle(positionDatas[index].angle, positionDatas[index + 1].angle, delta / dir.magnitude);
SetAngle(angle);
positionDatas.RemoveRange(0, index + 1);
}
else
{
//一个都没超 角度以自己为准
}
m_SyncRole.localPosition = curPos + dir.normalized * delta;
if(SmoothLeftDis <= 0)
{
SmoothLeftDis += Vector3.Distance(m_SyncRole.localPosition, positionDatas[0].position);
for(int i=0;i < positionDatas.Count-1; i ++)
{
SmoothLeftDis += Vector3.Distance(positionDatas[i].position, positionDatas[i + 1].position);
}
if(!positionDatas[positionDatas.Count-1].isFinal)
{
SmoothLeftDis += DrDelta.magnitude;
}
}
}
else
{
if (!positionDatas[count - 1].isFinal)
{
DRing = true;
m_SyncRole.localPosition = curPos + DrDelta.normalized * delta;
}
else
{
m_SyncRole.localPosition = positionDatas[count - 1].position;
}
SetAngle(positionDatas[count - 1].angle);
if(count > 1)
{
positionDatas.RemoveRange(0, count-1);
}
}
}
}
这样不丢弃中间的路径点,相当于是以一个插值时间追赶当前剩余的所有未同步点,可以保证第三方客户端的同步轨迹基本与主客户端一致。贴个录屏吧~
存在的问题
虽然轨迹是一致了,但是第三方客户端的同步时间总是比主客户端慢。按照当前的插值时间来计算的话,第三方客户端总是会慢一个延迟时间 + 一个插值时间,即 Latency + Latency + SyncInterval,差不多是慢了2个延迟时间,对于这个问题目前没有什么好办法,因为要保证平滑的话,插值时间貌似只能选取一个跟延迟差不多的时间,否则就可能出现插值先结束而后续点没有收到,出现等待的情况。
后续
目前讲的实际上都是关于客户端的一个平滑表现,与同步这个事反而关联不是很大。 这几天在思考关于网络上的服务器做预测,并广播服务器预测之后的坐标的做法,发现这里可能是个关于同步的点。后续可能会再开一篇记录一下关于这方面的思考和实践。
不过好在MMO对于同步这方面的要求其实并不高,关于服务器与第三方收到的点都是过去时间点的问题,我们以前的做法一般都是提高同步频率,然后还是以服务器为准,只要不是PVP,倒也问题不大,因为怪物的坐标都是由服务器控制的,玩家针对怪物的一些操作以服务器判定为准基本没什么问题。
- 上一篇: 关于MMO中的位移同步方案(一)
- 下一篇: 关于UGUI绑定节点对象到toLua