UP | HOME

Move Sync

最近在看移动同步相关的东西,发现网上已经有人写了很好的文章。这里就只做一些简单思考和学习笔记吧。文章结尾处有原文章地址。

<!– more –>

网络同步演示

原始文章中,网络延迟模拟的方法是,发送数据时,随机一个时间间隔,在该时间间隔以后处理发送的数据,这种方式无法保证模拟的网络数据按照发送的顺序被依次处理。
我采用了拥堵概率(delayRate)来模拟网络延迟,发送数据时,将数据放入一个队列中,取数据时,随机一个 0 到 1 的数值 randValue:
如果 randValue<delayRate 则说明产生拥堵,本次循环中不处理数据;
如果 randValue>delayRate 则说明没产生拥堵,本次循环中处理数据;

状态更新及航位预测法

引入原因: 状态更新和航位预测用来降低网络同步频率,从而降低服务器响应压力。

状态更新

在同步数据之前,首先判断状态(速度,加速度,方向)是否改变,如果没有改变则不需要进行同步。
如果存在加速度,需要做下面一些判断,任何判断为是,则需要进行同步:

  • 判断加速度是否改变
  • 判断速度大小是否改变,用前一次同步的状态来计算出当前的速度,判断计算出来的当前速度和真实的当前速度是否相等
  • 判断方向是否改变

航位预测

在同步数据之前,按照前一次同步过的状态,预计算出现在的位置。如果预计算得到的位置和真实的位置的差值超过某个阙值,则进行状态同步。
在实现航位预测时,增加了 lastSyncedMoveInfo 变量,来存储前一次同步过的移动信息。不能直接使用 lastMoveInfo 的原因是,如果两帧之间的变化小于阙值时,不会改变 lastSyncedMoveInfo,但是,lastMoveInfo 是需要改变的。

航位预测是状态更新的补充,航位预测可以避免累积错误。

在包含状态更新的情况下,是否需要航位预测?
状态更新是否存在累积错误?

平滑算法

引入原因:

当当前位置和目标位置的差值大于一帧移动的距离时(航位预测法中预测位置和目标位置),为了避免闪现(跳跃)可以采用平滑算法,将角色从当前位置平滑移动到目标位置。

  • 为什么会产生当前位置和目标位置差值大于一帧移动的距离的情况?

这是因为网络延迟时间的变化造成的,例如,一般情况下网络延迟为 60ms,当网络情况很好时网络延迟为 30ms,当网络情况不好时网络延迟为 120ms 或更多。举例如下:
obj1 在 t=0ms 时,开始移动,速度为 2m/s,方向为(1,0)
sobj1 在 t0=30ms 时,开始移动,速度为 2m/s,方向为(1,0) – 网络情况良好
sobj1 在 t0=60ms 时,开始移动,速度为 2m/s,方向为(1,0) – 网络情况一般
sobj1 在 t0=120ms 时,开始移动,速度为 2m/s,方向为(1,0) – 网络情况不好

obj1 在 t=10ms 时,速度变为 4m/s,方向为(0,1)
sobj1 在 t1=t0+10ms+30ms 时,速度变为 4m/s,方向为(0,1)
– 网络情况良好 t1 可能为(30+10+30) (60+10+30) (120+10+30)
sobj1 在 t1=t0+10ms+60ms 时,速度变为 4m/s,方向为(0,1)
– 网络情况一般 t1 可能为(30+10+60) (60+10+60) (120+10+60)
sobj1 在 t1=t0+10ms+120ms 时,速度变为 4m/s,方向为(0,1)
– 网络情况不好 t1 可能为(30+10+120) (60+10+120) (120+10+120)
从上面分析可以看出,obj1 在速度为 2m/s,方向为(1,0)状态下只运动了 10ms,当网络状态从良好变为一般时,sobj1 在相同状态下运动了 10ms+(60-30)ms;当网络状态良好变为不好时,sobj1 在相同状态下运动了 10ms+(120-30)ms;

平滑处理中有以下几点需要注意:

  • 目标位置 不是当前从服务器发来的位置,而是发来的位置加上延迟时间内又移动的距离,所以目标位置稍远一些。
  • 平滑时间

平滑时间为当前时间和同步数据包被发送时的时间差,这个值和网络延迟相关。所以延迟越打平滑时间越长。
平滑时间的选取可以采用最近 N 次统计的网络延迟时间的加权平均值

  • 平滑过程中收到新的同步数据包,则停止当前平滑操作,按照新接收到的数据进行新的平滑操作。
  • 插值会出现平滑方向和物体朝向(速度方向)不一致的情况。

速度方向和变化的位移方向是一致的,只需要在平滑的过程中依据变化的位移方向修改物体朝向即可。

  • 插值中遇到障碍点的处理。

插值之前对 当前位置 到 目标位置进行寻路,产生多个路径点,再在这些路径点直接进行插值。

  • 停止不自然问题

因为网络延迟不稳定的缘故,所以,物体会或多或少行走 deltaDelayTime*velocity 距离。平滑这部分距离的时候,应该让平滑时间短一点,避免慢悠悠的停下来。

线性插值

线性插值代码:

  var smoothTime = smoothTimer = curTime - svrData.sendTime;
  var targetPos = svrData.position + svrData.velocity * smoothTime;
  var smoothLength = targetPos - startPos;

  // 在每一帧
  if (smoothTimer>0)
   {
       smoothTimer -= deltaTime;
       curPos = curPos + deltaTime / smoothTime * smoothLength;
   }

立方样条插值

实现立方样条插值需要 4 个坐标点:
坐标点 0:开始位置(即本地当前位置)
坐标点 1:坐标 0 经过一定时间后的位置(速度为当前速度)
坐标点 3:最终位置(即网络协议发送的最新位置加上一定的延迟时间后的位置)
坐标点 2:坐标 4 反方向移动一定时间后的位置(速度为网络最新速度)
插值坐标公式为:
x = At3 + Bt2 + Ct + D
y = Et3 + Ft2 + Gt + H
其中
A = x3 - 3x2 + 3x1 - x0
B = 3x2 - 6x1 + 3x0
C = 3x1 - 3x0
D = x0
E = y3 - 3y2 + 3y1 - x0
F = 3y2 - 6y1 + 3y0
G = 3y1 - 3y0
H = y0

加权平均插值

curPos = curPos + (targetPos - curPos)/slowdownFactor
slowdownFactor 为缓动因子,slowdownFactor 越大,平滑速度越慢。

加权平均插值的好处是不需要记录 dt。

帧锁定算法

帧锁定同步算法描述:
帧锁定同步分为两个部分,第一部分是发送客户端的移动信息,第二部分是接收服务器返回的移动信息,并利用该信息执行移动逻辑。具体实现参考下面代码即可。

帧锁定同步算法代码:

  int svrKeyframe = 0;
  int syncInterval = 5;
  int sendCurFrame = 0;
  List<MoveInfo> infoList = new List<MoveInfo>();
  void ProcessClientMoveControl()
  {
      if (sendCurFrame <= svrKeyframe + syncInterval)
      {
          // curMoveInfo 为当前帧 收集到的移动信息
          infoList.Add(curMoveInfo);

          if (sendCurFrame == svrKeyframe + syncInterval)
          {
              SyncData syncData = new SyncData();
              syncData.infoList = new List<MoveInfo>(infoList.ToArray());
              syncData.keyframe = sendCurFrame;
              Send(syncData);

              infoList.Clear();
          }
          sendCurFrame++;
      }
      else
      {
          // block game. wait for other player
      }
  }

  List<SyncData> syncDataList = new List<SyncData>();
  int receiveCurFrame = 0;
  void UpdateClientMove()
  {
      SyncData syncData = Receive();
      if (syncData!=null)
      {
          // 更新 关键帧编号
          svrKeyframe = syncData.keyframe;
          syncDataList.Add(syncData);
      }

      if (syncDataList.Count>0)
      {
          SyncData tmp = syncDataList[0];
          int idx = receiveCurFrame - (tmp.keyframe - syncInterval);
          if (idx<0)
          {
              Debug.Log("Error : client frame smaller than keyframe begin!");
              quitGame();
          }
          else if (idx<tmp.infoList.Count)
          {
              MoveInfo info = tmp.infoList[deltaFrame];
              if (info!=null)
              {
                  preMoveInfo = info;
              }
              else
              {
                  info = preMoveInfo;
              }

              if(info!=null)
              {
                  UpdateGameObjectMove(info);
              }
              // 处理完一个关键帧内的数据后,将这个关键帧数据删除
              if (idx == tmp.infoList.Count-1)
              {
                  syncDataList.Remove(tmp);
              }

              receiveCurFrame++;
          }
          else
          {
              Debug.Log("Error : client frame bigger than keyframe end!");
              quitGame();
          }
      }
      else
      {
          // block game. wait for other player.
      }
  }

参考链接