欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-03-25 20:17:46
...

unity帧同步实现机制分享:

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

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

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

 

7.重播:检查不同步的重要手段, 这个其实就是保存的所有战斗的初始化数据跟所有的操作,然后客户端依次执行,得到同步log,比对以查问题。

8.定点数:使用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.  

  8.  

上一篇: 观察者设计模式java详解

下一篇: