unity c#帧同步网络优化方案
程序员文章站
2024-03-25 20:17:46
...
unity帧同步实现机制分享:
看了很多文章,也在自己的游戏中亲自编写,整理并实现了一套帧同步方案,废话不多,直接分享。
7.网络重播:这块是帧同步必须做的,我们需要把服务器发过来的每一帧,以及随机种子,英雄数据记录下来,用于重播,有人问为什么要重播呢?因为重播是查不同步的重要手段,你大概率都不知道你干什么导致不同步了,那么你可以利用你的保存的数据用每个玩家重播一遍,看看差异在哪里,从而定位到导致不同步的代码。
8.定点数的使用:既然要同步,那必须使用定点数了,因为浮点数的计算结果不一定一样,使用定点数,可以保证结果的一致性。我们使用的是long 保存,把浮点数左移14位的做法,然后重载所有云算符。
7.重播:检查不同步的重要手段, 这个其实就是保存的所有战斗的初始化数据跟所有的操作,然后客户端依次执行,得到同步log,比对以查问题。
8.定点数:使用long保存,把浮点数左移14位,再重载操作符,这个网上应该有很多,就不啰嗦了。
- 传输协议:战斗使用冗余UDP(我们暂时用的是3),什么是冗余呢,就是说UDP协议不保证客户端肯定能收到,那么每次下发一帧的时候,会携带前2帧的数据,这样,即便丢包,也能保证大概率客户端稳定。
- 超时TCP重传机制,网络非常不好的情况,那么,就会造成丢帧较多,即便冗余也可能缺帧
//如果客户端的帧和服务器的帧误差小于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%的方式插值。
- 帧缓存算法:我这里提供一下最简单的一种,当然也是最稳定有效的算法,网上有很多可查的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; } } }
上面代码很多参数可调,根据实际项目决定,基本可以实现客户端流畅运行,不过会适当增加自己的操作延迟,其实就是用延迟换流畅。
- 帧执行逻辑:为了应付网络抖动,可能缺帧或者多帧的情况,客户端要流畅,至少保证每逻辑帧能有一帧的数据。我的处理是,没有帧的时候就等,因为项目原因,做不到预测回滚,或者说比较麻烦,因此这里暂时就是没有帧的时候会小卡一下,多帧的时候,采用每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); } } }
哈,看到快速模式了对吧,这个是下面要讲的。
-
-
上一篇: 观察者设计模式java详解