新闻详情

新闻详情

首页 / 资讯中心 / 详情

【Unity】TankBattle联机坦克大战(一)帧同步的基石“确定性”

发布时间:2026/9/8 19:16:51来源:尧图网络
【Unity】TankBattle联机坦克大战(一)帧同步的基石“确定性”
更新日期2026年8月6日。项目源码获取源码。索引帧同步的确定性一、数学库1.FixFloat2.FixFloat基础运算3.FixFloat重载运算符二、随机数三、容器四、逻辑与渲染分离LogicFrameBehaviour五、时间管理六、物理七、乐观策略帧同步的确定性本篇我们直接切入正题率先引入需要实现帧同步技术的前置工作。已知的是帧同步最大的难点在于确定性。所有客户端的逻辑必须完全一致哪怕是一个浮点数误差最终都会导致“蝴蝶效应”让不同玩家屏幕上的战局出现差异即“不同步”。那么总结一下实现帧同步确定性的技术清单大致如下数学库实现确定性的定点数或基于整数缩放避免浮点误差。随机数必须使用确定性伪随机数生成器且所有客户端使用相同的随机种子。容器避免使用无序容器如 DictionaryHashSet改用有序容器且保证顺序的确定性。逻辑与渲染分离所有游戏逻辑移动、攻击判定等都应该在逻辑帧中执行渲染帧只负责根据逻辑计算出的结果去渲染画面实现“逻辑驱动渲染”。时间管理所有客户端使用相同的逻辑帧计数器而不是真实时间。物理要么避免物理要么使用确定性物理引擎或自研简单碰撞。乐观策略服务器不等待所有玩家的输入而是以固定频率比如每秒30帧将当前收集到的所有输入指令打包广播出去。一、数学库1.FixFloat众所周知浮点数float的计算会带来误差所以必须要实现确定性的定点数来替换float避免浮点误差且在逻辑帧中的任何计算都不能涉及到float数据。如下是我们实现的定点数FixFloat/// summary/// 确定的浮点型/// /summarypublicstructFixFloat{privateint_integer;privateint_fractional1;privateint_fractional2;privateint_fractional3;privateint_fractional4;/// summary/// 确定的浮点型/// /summary/// param nameinteger整数部分/param/// param namefractional1小数部分小数点后第1位/param/// param namefractional2小数部分小数点后第2位/param/// param namefractional3小数部分小数点后第3位/param/// param namefractional4小数部分小数点后第4位/parampublicFixFloat(intinteger,intfractional10,intfractional20,intfractional30,intfractional40){_integerinteger;_fractional1fractional1;_fractional2fractional2;_fractional3fractional3;_fractional4fractional4;}}FixFloat只精确到小数点后4位在我们的游戏中已完全足够其设计理念是将每一位小数使用整型int单独存储摒弃了基于整数缩放的策略因为其容易导致数据溢出。比如一个小数15.1675其使用FixFloat定义如下newFixFloat(15,1,6,7,5);2.FixFloat基础运算我们先实现FixFloat的两个基础运算方法加法和减法-乘法*和除法/暂不考虑我们游戏中也几乎用不到基于这两个基础运算方法我们才能进一步重载FixFloat的运算符这是自定义数据类型的必要工作。FixFloat基础加法运算从低位开始逐位相加满10进1/// summary/// FixFloat基础加法运算a0 且 b0/// /summaryprivatestaticFixFloatAddition(FixFloata,FixFloatb){boolenterUpfalse;intfractional4a._fractional4b._fractional4;enterUpfractional410;if(enterUp)fractional4-10;intfractional3a._fractional3b._fractional3(enterUp?1:0);enterUpfractional310;if(enterUp)fractional3-10;intfractional2a._fractional2b._fractional2(enterUp?1:0);enterUpfractional210;if(enterUp)fractional2-10;intfractional1a._fractional1b._fractional1(enterUp?1:0);enterUpfractional110;if(enterUp)fractional1-10;intintegera._integerb._integer(enterUp?1:0);returnnewFixFloat(integer,fractional1,fractional2,fractional3,fractional4);}FixFloat基础减法运算从低位开始逐位相减不足时向前借1/// summary/// FixFloat基础减法运算a0 且 b0 且 ab/// /summaryprivatestaticFixFloatSubtraction(FixFloata,FixFloatb){if(a._fractional4b._fractional4){a._fractional3-1;a._fractional410;}intfractional4a._fractional4-b._fractional4;if(a._fractional3b._fractional3){a._fractional2-1;a._fractional310;}intfractional3a._fractional3-b._fractional3;if(a._fractional2b._fractional2){a._fractional1-1;a._fractional210;}intfractional2a._fractional2-b._fractional2;if(a._fractional1b._fractional1){a._integer-1;a._fractional110;}intfractional1a._fractional1-b._fractional1;intintegera._integer-b._integer;returnnewFixFloat(integer,fractional1,fractional2,fractional3,fractional4);}FixFloat基础加法运算的两个参数必须0。FixFloat基础减法运算的两个参数必须0且被减数始终减数。提炼出这两个基础运算方法意欲何为我们举个例子a b情况1a 0b 0则a b直接调用基础加法运算得到最终结果。情况2a 0b 0且a abs(b)则a b a - abs(b)直接调用基础减法运算得到最终结果。情况3a 0b 0且a abs(b)则a b -(abs(b) - a)先调用基础减法运算再将结果符号逆转得到最终结果。情况4a 0b 0则a b -(abs(a) abs(b))先调用基础加法运算再将结果符号逆转得到最终结果。也即是说这两个基础运算方法可以涵盖FixFloat的所有加、减运算。3.FixFloat重载运算符基于此我们便可以重载FixFloat的运算符了。运算符FixFloat FixFloatpublicstaticFixFloatoperator(FixFloata,FixFloatb){//a、b均 0直接调用【基础加法运算】if(a0b0){returnAddition(a,b);}//a、b均 0//先对两者的绝对值调用【基础加法运算】//再将结果逆转正变负负变正//Flip用于逆转一个FixFloat的符号elseif(a0b0){FixFloatabsAAbs(a);FixFloatabsBAbs(b);returnFlip(Addition(absA,absB));}//a 0、b 0elseif(a0b0){FixFloatabsAAbs(a);//将绝对值更大的一方作为被减数调用【基础减法运算】if(absAb){returnSubtraction(b,absA);}//如果更大的一方为负数则最终结果需要逆转符号else{returnFlip(Subtraction(absA,b));}}//a 0、b 0elseif(a0b0){FixFloatabsBAbs(b);//将绝对值更大的一方作为被减数调用【基础减法运算】if(aabsB){returnSubtraction(a,absB);}//如果更大的一方为负数则最终结果需要逆转符号else{returnFlip(Subtraction(absB,a));}}else{thrownewArgumentException($FixFloat 的 运算出现了意料之外的情况参数1{a}参数2{b});}}-运算符FixFloat - FixFloatpublicstaticFixFloatoperator-(FixFloata,FixFloatb){//a、b均 0直接调用【基础减法运算】//如果被减数较小则最终结果需要逆转符号if(a0b0){if(ab){returnSubtraction(a,b);}else{returnFlip(Subtraction(b,a));}}//a、b均 0//先对两者的绝对值调用【基础减法运算】//如果被减数较小则最终结果需要逆转符号elseif(a0b0){FixFloatabsAAbs(a);FixFloatabsBAbs(b);if(absAabsB){returnFlip(Subtraction(absA,absB));}else{returnSubtraction(absB,absA);}}//a 0、b 0//负数 - 正数 两个数的绝对值相加 * -1elseif(a0b0){FixFloatabsAAbs(a);returnFlip(Addition(absA,b));}//a 0、b 0//正数 - 负数 两个数的绝对值相加elseif(a0b0){FixFloatabsBAbs(b);returnAddition(a,absB);}else{thrownewArgumentException($FixFloat 的 - 运算出现了意料之外的情况参数1{a}参数2{b});}}然后是与整型int的运算符与float的运算符不用考虑因为float不能参与逻辑运算。运算符FixFloat intpublicstaticFixFloatoperator(FixFloata,intb){//将b构造为一个FixFloat参与运算returnanewFixFloat(b);}-运算符FixFloat - intpublicstaticFixFloatoperator-(FixFloata,intb){//将b构造为一个FixFloat参与运算returna-newFixFloat(b);}运算符FixFloat FixFloatpublicstaticbooloperator(FixFloata,FixFloatb){//取两个FixFloat中的对比位GetContrastBit(a,b,outintaValue,outintbValue);//对比位的大小直接决定了整个FixFloat的大小returnaValuebValue;}/// summary/// 取两个FixFloat中的对比位从高位至低位第一个数值不相等的位/// /summaryprivatestaticvoidGetContrastBit(FixFloata,FixFloatb,outintaBit,outintbBit){intaValuea._integer;intbValueb._integer;if(aValuebValue){aValuea._fractional1;bValueb._fractional1;if(aValuebValue){aValuea._fractional2;bValueb._fractional2;if(aValuebValue){aValuea._fractional3;bValueb._fractional3;if(aValuebValue){aValuea._fractional4;bValueb._fractional4;}}}}aBitaValue;bBitbValue;}比如有如下两个数FixFloatanewFixFloat(1,2,4,5,6);//1.2456FixFloatbnewFixFloat(1,2,3,5,6);//1.2356取对比位也即是小数点后第二位从左至右第一个数值不相等的位a的对比位为4b的对比位为3所以a b。其他比较运算符同理。当然也有转换为float的情况一般是渲染需要publicfloatToFloat(){return_integer_fractional1/10f_fractional2/100f_fractional3/1000f_fractional4/10000f;}//转换int直接抛弃小数部分publicintToInt(){return_integer;}二、随机数必须使用确定性伪随机数生成器且所有客户端使用相同的随机种子。这里直接使用System.Random随机数即可每一关的所有随机事件随机生成关卡AI随机行为都应该指定一个随机种子且该种子必须被记录战斗回放时也使用该种子进行随机才能保证绝对一致的游戏内容。/// summary/// 关卡种子关卡生成时所有随机行为使用此种子/// /summarypublicintLevelSeed{get;set;}1;/// summary/// 敌人AI种子敌人AI所有随机行为使用此种子/// /summarypublicintAISeed{get;set;}1;三、容器避免使用无序容器如DictionaryHashSet改用有序容器如List且保证顺序的确定性。四、逻辑与渲染分离所有游戏逻辑移动、攻击判定等都应该在逻辑帧中执行渲染帧只负责根据逻辑计算出的结果去渲染画面实现逻辑驱动渲染。LogicFrameBehaviourLogicFrameBehaviour支持逻辑帧更新的行为组件其将逻辑与渲染分割开来游戏场景中的坦克、子弹等需要进行帧同步的对象都继承至此组件/// summary/// 支持逻辑帧更新的行为组件/// /summarypublicclassLogicFrameBehaviour:HTBehaviour{/// summary/// 逻辑帧更新/// /summary/// param nameframeIndex当前逻辑帧编号/parampublicvirtualvoidOnLogicFrame(intframeIndex){}/// summary/// 渲染帧更新/// /summary/// param namefactor逻辑帧之间的间隔系数/parampublicvirtualvoidOnRenderFrame(floatfactor){}}逻辑帧更新方法由统一的入口调用至于这个入口方法OnLogicFrame单人模式时由本地的逻辑帧时钟调用多人模式时由服务端的逻辑帧时钟调用回放模式时由回放系统的逻辑帧时钟调用/// summary/// 当前逻辑帧编号/// /summaryprivateint_logicFrameIndex0;/// summary/// 所有的支持逻辑帧更新的行为组件/// /summaryprivateListLogicFrameBehaviour_logicFrameBehavioursnewListLogicFrameBehaviour();/// summary/// 逻辑帧更新/// /summary/// param nameindex逻辑帧编号/parampublicvoidOnLogicFrame(intindex){//记录当前逻辑帧编号_logicFrameIndexindex;//调用所有组件的逻辑帧更新for(inti0;i_logicFrameBehaviours.Count;i){if(_logicFrameBehaviours[i].enabled_logicFrameBehaviours[i].gameObject.activeSelf){_logicFrameBehaviours[i].OnLogicFrame(_logicFrameIndex);}}}渲染帧更新的方法就直接由OnUpdate调用即可在Unity的每一个渲染帧调用一次我们的渲染帧/// summary/// 逻辑帧之间的时间间隔/// /summaryprivatefloat_logicInterval;/// summary/// 逻辑帧计时器/// /summaryprivatefloat_logicTimer0;publicoverridevoidOnUpdate(){base.OnUpdate();if(_logicTimer_logicInterval){_logicTimerTime.deltaTime;}floatfactor_logicTimer/_logicInterval;for(inti0;i_logicFrameBehaviours.Count;i){if(_logicFrameBehaviours[i].enabled_logicFrameBehaviours[i].gameObject.activeSelf){_logicFrameBehaviours[i].OnRenderFrame(factor);}}}渲染帧更新的factor参数为渲染画面从上一逻辑帧更新到当前逻辑帧的进度0-1可以将其理解为渲染插值因为每秒30帧逻辑画面会有些卡顿所以需要插值技术来让画面变得平滑。factor 0代表完全处于上一逻辑帧factor 0.4代表处于上一逻辑帧到当前逻辑帧之间的时间长度的0.4位置factor 1代表完全处于当前逻辑帧。五、时间管理所有客户端使用相同的逻辑帧计数器而不是真实时间。比如2秒后让坦克爆炸不能使用定时器2秒后执行而应该使用逻辑帧计数器定时60帧1秒30帧后执行/// summary/// 所有的帧计时器/// /summaryprivateListFrameTimer_frameTimersnewListFrameTimer();/// summary/// 逻辑帧更新/// /summary/// param nameindex逻辑帧编号/parampublicvoidOnLogicFrame(intindex){//逻辑帧更新......//逻辑帧计数器更新for(inti0;i_frameTimers.Count;i){//每一个逻辑帧流逝计数器减1_frameTimers[i].Frame-1;//计数完毕后执行回调if(_frameTimers[i].Frame0){_frameTimers[i].Callback?.Invoke();_frameTimers.RemoveAt(i);i-1;}}}/// summary/// 添加一个帧计时器/// /summary/// param nameframe计时的帧数/param/// param namecallback计时结束回调/parampublicvoidAddFrameTimer(intframe,HTFActioncallback){_frameTimers.Add(newFrameTimer(){Frameframe,Callbackcallback});}六、物理我们将完全摒弃Unity中的物理效果使用自研的简单碰撞用来判断子弹碰撞坦克碰撞砖块等。七、乐观策略传统的帧同步有一个致命缺陷所有人都要等网络最慢的那个玩家提交输入指令导致整体卡顿为了解决这个问题必须引入乐观策略。服务器不等待所有玩家的输入而是以固定频率比如每秒30帧将当前收集到的所有输入指令打包广播出去。如果某个玩家因网络延迟指令未及时送达服务器不会等他会直接广播他没有任何操作这避免了少数人拖垮全队网络差的玩家只会感到自己操作有延迟而不会影响到其他人的体验。虽然我们的坦克大战是双人联机游戏但映射到三人、四人联机也是同理的当然本篇只是做了一个大致的总结有些内容后续会循序展开。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

tldraw editor.overlays 实战:用 OverlayManager 读取指针悬停的 Overlay 并做点命中测试 2026/9/8 20:04:57

tldraw editor.overlays 实战:用 OverlayManager 读取指针悬停的 Overlay 并做点命中测试

tldraw editor.overlays 实战:用 OverlayManager 读取指针悬停的 Overlay 并做点命中测试 【免费下载链接】tldraw Build infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK. 项目地址: https://git…

阅读更多 →
Ultralytics YOLO 的 DeepXBackend:DEEPX NPU 推理后端源码解析与部署实战 2026/9/8 20:04:57

Ultralytics YOLO 的 DeepXBackend:DEEPX NPU 推理后端源码解析与部署实战

Ultralytics YOLO 的 DeepXBackend:DEEPX NPU 推理后端源码解析与部署实战 【免费下载链接】ultralytics Ultralytics YOLO26, YOLO11, YOLOv8 — object detection, instance segmentation, semantic segmentation, image classification, pose estimation, object…

阅读更多 →
YOLOv5动物检测实战:2000张高质量数据集工程指南 2026/9/8 20:04:57

YOLOv5动物检测实战:2000张高质量数据集工程指南

简介:本资源是一套开箱即用的YOLOv5动物目标检测实战套件,面向深度学习初学者与计算机视觉入门者,聚焦多类别动物识别任务,有效降低目标检测项目从数据准备到模型部署的学习门槛。资源包共3923个文件,含1959张标注图像…

阅读更多 →
yaak桌面API客户端终极指南:REST到gRPC的接口测试一站搞定 2026/9/8 20:04:57

yaak桌面API客户端终极指南:REST到gRPC的接口测试一站搞定

yaak桌面API客户端终极指南:REST到gRPC的接口测试一站搞定 【免费下载链接】yaak The most intuitive desktop API client. Organize and execute REST, GraphQL, WebSockets, Server Sent Events, and gRPC 🦬 项目地址: https://gitcode.com/GitHub_…

阅读更多 →
Clawdbot桌面夹爪机器人:大模型驱动的具身智能入门实战解析 2026/9/8 20:04:57

Clawdbot桌面夹爪机器人:大模型驱动的具身智能入门实战解析

手头正好在梳理一条叫 Clawdbot 的桌面级机器人项目——一只装在机械臂末端、会“听指令抓东西”的夹爪,配合摄像头和大语言模型,能从语音或文本指令里理解“把那瓶水拿过来”这种自然语言,然后定位、抓取、递给你。去年开始这类“夹爪大模型…

阅读更多 →
开源数字员工平台UniEmployee:从架构到落地,理解AI自动化审批与可追溯设计 2026/9/8 20:01:56

开源数字员工平台UniEmployee:从架构到落地,理解AI自动化审批与可追溯设计

最近我在琢磨怎么把团队里零散的 AI 自动化脚本整合成一个真正能用的“数字员工”体系,翻了大量开源项目后发现一个很对胃口的方向:一个叫 UniEmployee 的开源 AI 数字员工平台。它的定位很直白——不是那种你问一句它答一句的聊天机器人,而是…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞