unity c#帧同步网络优化方案

unity帧同步实现机制分享:

看了很多文章,也在自己的游戏中亲自编写,整理并实现了一套帧同步方案,废话不多,直接分享。

网络重播:这块是帧同步必须做的,我们需要把服务器发过来的每一帧,以及随机种子,英雄数据记录下来,用于重播,有人问为什么要重播呢?因为重播是查不同步的重要手段,你大概率都不知道你干什么导致不同步了,那么你可以利用你的保存的数据用每个玩家重播一遍,看看差异在哪里,从而定位到导致不同步的代码。

定点数的使用:既然要同步,那必须使用定点数了,因为浮点数的计算结果不一定一样,使用定点数,可以保证结果的一致性。我们使用的是long 保存,把浮点数左移14位的做法,然后重载所有云算符。

 

  1. 传输协议:战斗使用冗余UDP(我们暂时用的是3),什么是冗余呢,就是说UDP协议不保证客户端肯定能收到,那么每次下发一帧的时候,会携带前2帧的数据,这样,即便丢包,也能保证大概率客户端稳定。
  2. 超时TCP重传机制,网络非常不好的情况,那么,就会造成丢帧较多,即便冗余也可能缺帧
     //如果客户端的帧和服务器的帧误差小于Margin的帧数,则为安全边际,
     //否则需要重新从服务器请求丢失的帧信息
     ret = (serverFrame - clientFrame) <= nFrames; 

     

  3. 位置计算:所有的逻辑帧,只计算起始位置,朝向,速度,表现帧计算具体:
  4.  ///距离大于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%的方式插值。

  5. 帧缓存算法:我这里提供一下最简单的一种,当然也是最稳定有效的算法,网上有很多可查的jitterbuffer算法,实现起来,理解起来都需要费电李琦,不如来个简单的,- -,好用就行。方法很简单,我缓存最近100帧的延迟(到客户端解析的时候算时间,这个才是真正能驱动游戏的时间),然后排序,去掉几个最大最小值,然后去5个最大最小值的差平均为jitter值,直接上代码:
     using System;
    using System.Text;
    
    namespace YHP1
    {
        /// <summary>
        /// 网络抖动值
        /// </summary>
        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<T>(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;
            }
    
        }
    }
    

    上面代码很多参数可调,根据实际项目决定,基本可以实现客户端流畅运行,不过会适当增加自己的操作延迟,其实就是用延迟换流畅。

  6.  帧执行逻辑:为了应付网络抖动,可能缺帧或者多帧的情况,客户端要流畅,至少保证每逻辑帧能有一帧的数据。我的处理是,没有帧的时候就等,因为项目原因,做不到预测回滚,或者说比较麻烦,因此这里暂时就是没有帧的时候会小卡一下,多帧的时候,采用每update一次执行一帧,当执行到jitterbuffer保存的值的时候,那么就每逻辑帧时间执行一帧(1/16s)。
            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);
                    }
                }
            }

    哈,看到快速模式了对吧,这个是下面要讲的。

  7. 10.快速模式呢,就是追帧,跟大家差的太远了,正常执行已经很难追上了:

  8.         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;
            }

    这里简单给了一下是否快速执行,

  9.  

  • 1
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值