本文系统梳理了帧同步与状态同步的核心思想、执行流程、优缺点对比及典型应用场景等,以及帧同步中保证多端确定性的常见问题与解决方案;并总结了网络延迟优化手段。由于本人没有实际的商业项目经验,所以只是基于各种资料以及个人的玩具Demo所总结的。

概括总结

【什么是帧同步?什么是状态同步?】

帧同步让所有的客户端在相同的逻辑帧内执行相同的确定性操作来保证网络同步,其核心思想是相同的输入+相同的处理=相同的结果。

基本流程是,玩家一操作,客户端立即将该操作指令发送给服务端,服务端按照固定的时间频率(即逻辑帧)转发来自所有玩家的操作指令,在客户端收到操作指令后,以相同的逻辑执行并渲染,从而实现多客户端的网络同步。实际的实现细节可能有所不同,比如客户端收集的数据发给谁——存在P2P架构和CS架构;服务端是否需要收集齐所有客户端的数据才允许转发——严格锁步帧同步和乐观帧同步…..

其最核心问题是——如何保证多客户端的确定性。也就是说,怎么保证在多端接收到了相同的输入后,能够产生相同的结果

状态同步是一种由服务器作为唯一权威状态来源,客户端仅负责采集输入、渲染显示的同步方式。 最终的游戏结果是由服务端计算出来的,而不是各客户端自己,这是帧同步和状态同步最大的区别

基本流程是,客户端按照一定的频率(也有可能是立即,比如ow)将收集到的操作指令转发给服务端,服务端收到操作指令后运行完整的游戏逻辑,并计算出最新的游戏状态,并将该状态进行广播,客户端收到后直接更新本地角色的状态,这使得多客户端和服务器始终是一致的

二者比较:

  • 带宽开销:帧同步更优。因为帧同步传输的数据都是几个字节的操作指令;而状态同步下,至少服务端给客户端传回的的数据一定是完整的状态,开销更大

  • 操作手感方面:帧同步更优。一是因为帧同步下结果的计算发生在客户端本地,其可以实现逻辑层面的预测,操作手感更佳;而状态同步下结果的计算发生在服务端,客户端只能做视觉层面的预测或者一些简单的逻辑预测,比如提前播放动画、UI显示等。

  • 反作弊:状态同步更优。帧同步的计算逻辑发生在客户端,所以容易作弊,在最传统的P2P下更严重,在CS架构下可以在服务端方面进行数据检验,有所缓解;而状态同步的计算逻辑发生在服务端,作弊难度大。

  • 代码实现层面:状态同步下服务端在收到操作指令后,可以立即计算并广播结果,因为客户端并不负责结算结果,只负责表现;而帧同步下服务端在收到操作指令后,需要缓存并等待一个Tick,收集到所有人的操作指令后再统一转发,·因为帧同步下计算在客户端,所有客户端必须在第n帧执行相同的输入集,以保证多端的同步性。进一步的,对于乐观帧同步下,如果某个玩家的操作指令没到,会将其认定为空操作或是延续上一帧操作,此时的统一等待一个Tick不再只是为了公平性,更像是为了给所有玩家一个统一的输入提交窗口期,以尽可能地保证不同网速玩家间的公平性。

为什么帧同步更容易作弊?如何尽可能地防范?

因为帧同步下,客户端有完整的运行逻辑,如果服务器不对结果加以验证,很容易出现对客户端数据篡改的问题;而状态同步的完整运行逻辑在服务端,客户端主要是起到表现的作用,就算篡改数据,依然会被服务端计算所矫正

  • 实时验证。每个玩家每段时间将自己端的数据上报,对关键的状态做Hash检验,由服务器比对各个玩家数据的一致性,如果谁数据和其他人不一致了,则表示此人发生了不同步,即Hash投票。但是,此时程序是无法分辨出究竟是自己的代码出了问题,还是有人用外挂修改了数据。必须通过后台的人工追查才能定位问题。所以,王者其实是有一个允许的不同步值(大约在万分之五)的,以防误判

  • 加密数据。由于客户端有完整游戏数据位于内存中,所以需要对这部分数据进行加密、代码完整性检验、代码混淆等,防止作弊者轻而易举地获取并修改这些数据。对于透视这样的挂,应对策略是,通常不会让服务端下发所有的可见单位,或者延迟下发

  • 离线验证。帧同步下,战斗结束后服务器收集收集整场的操作序列,然后加速播放战斗(几十上百倍),最后校验结果,其成本较低

二者的应用场景?

帧同步更适用于场景人数少、对实时性追求较高的游戏,比如MOBA、RTS、格斗游戏;而状态同步更适用于场景人数多、复杂物理、对反作弊要求高的场景,比如MMO、FPS、开放世界等。

之所以帧同步没法适用于人数较多的游戏,一是因为帧同步的逻辑计算在客户端本地,计算耗时随着同步玩家数量的增多而增多;二是因为帧同步没法像状态同步一样做类似于AOI的优化,其需要自始至终地知道场上所有玩家的信息,计算所有玩家的结果,否则后续无法实时地保证同步

帧同步作为网络同步方案,首先要针对网络延迟做优化;其次,由于其自身在客户端处理逻辑、计算结果的特点,所以需要保证确定性


帧同步确定性同步

为什么会出现不同步的问题?

核心在于,帧同步的计算是客户端完成的,服务器不是状态的权威来源,只负责操作执行的转发,如果不同的客户端下计算出了不同的结果,就会出现不同步的问题。 比如对于同一条移动执行,可能A客户端计算出移动1m,而B客户端下计算出移动1.1m,久而久之就会出现明显的不同步

都有哪些因素容易引起不同步的问题?

  • 随机数。 帧同步下多客户端需要使用完全相同的随机数种子,保证多端的随机结果是相同的

  • 浮点数导致的精度丢失。 在不同的硬件环境下,相同的浮点数可能会造成不同的运算结果,虽然这个结果在这一帧内影响有限,但是由于滚雪球效应,最终的战斗结果可能就会出现非常明显的不同步问题了。对于这个问题,采用定点数去代替浮点数做计算。而浮点数精度问题就会间接导致Unity自带的物理系统、动画系统等不能直接用于帧同步,因为其都包含了浮点数的计算

  • 逻辑执行顺序。 由于使用到了一些插件,而这些插件的Update并不能由帧同步去控制,导致两边客户端可能执行某个计算片段的时间并不是在同一逻辑帧。解决这个问题的方案是,不要使用这些插件,尽量去使用开源插件或者自己写,保证Update函数掌控在自己的逻辑中,去避免出现不同步的问题。

  • 容器与算法。 Dictionary遍历时是无序的,就算不同客户端下有完全相同的操作序列,也可能因为哈希种子、扩容时机等不同而导致遍历顺序的不同。如果需要遍历,可以使用SortedDictionary,其类似于C++基于红黑树的map;或者可以额外在List中转存一份遍历。不过List.Sort是基于快排实现的,而快排是不稳定的,所以同样需要自行实现排序算法,否则,容易出现这样的情况:两个客户端排序后,比如说血量最高的东西,或者距离最近的对象,由于存在两个距离和血量一模一样的对象,所以排序后返回的结果不一样

  • 本我思想引入的主观逻辑。 首先需要肯定的是,本我的区分是需要的,但是不能因为区分本我而在逻辑层引入主观逻辑。本我区分的作用在于:对于摇杆输入,技能释放等操作输入指令,只有在本地角色创建时才进行监听,其余玩家创建时不需要;对于预测,可以只针对本地玩家进行,对于其他玩家不需要预测。那什么叫做引入主观逻辑呢?比如说有一些逻辑是先执行我然后再执行别人,这样执行顺序就会根据“我”的变化而发生变化,从而导致不同步。

  • 逻辑层和渲染层混杂在一起。

逻辑层和渲染层分离:

一是逻辑层的更新需要采用固定的Tick。这个Tick不是由Update驱动的,也不是通过类似于FixedUpdate的通过累计Time.deltaTime驱动的,而是由服务端的帧号驱动的,其完全脱离了Unity的渲染更新

二是逻辑层不能依赖于渲染层更新逻辑状态。渲染层只能从逻辑层中读取状态并渲染表现,而不能修改逻辑状态。这是因为渲染的帧率在不同的客户端下是不固定的,不能让确定性的逻辑状态受到渲染帧率的影响。


网络延迟优化

网络延迟优化可以从两方面来理解,一方面是实实在在地优化网络,比如从减少数据传输、减少丢包、降低RTT等,可以理解为从根本上解决问题;另一方面是承认网络延迟是不可避免的,直接从玩家体验感入手,弱化玩家对于网络延迟的感受

弱化玩家延迟感受

乐观帧同步+输入缓冲buffer(帧同步专属)

方案一的致命缺点: 对网络延迟和丢包极其敏感。 只要有一个玩家的指令因为网络延迟没有及时到达,其他所有玩家的游戏画面都会卡住等待,体验极差。

优化方式:

乐观帧同步:客户端只要是有按键的交互,就将操作指令发送给服务端,服务端按照固定的时间频率(即逻辑帧)转发来自所有玩家的操作指令,不管有无玩家迟到,客户端只负责执行这些操作指令

输入缓冲:客户端收到服务端的指令后将其缓存在buffer中,大约往后推迟一个时延后再执行,而不是立即执行。 如果其他玩家的操作指令没有在采集时到达,而是由于网络延迟随后才到,那么客户端也不需要完全锁步等待,也就不会导致一个玩家的卡顿导致其他玩家买单。其会继续向后执行,因为这些被执行的buffer中的同步数据是齐全的,已经提前预留出了一定的缓冲时间。而对于晚到的操作指令,其会被放在对应的buffer中,(也有可能直接丢弃,取决于究竟延迟了多少才到达)并在后续执行

优点在于,游戏运行较为稳定,网络波动的影响没那么大;缺点在于,玩家的输入永远是被延迟执行的,手感没那么好

进一步地,考虑将逻辑和渲染完全分离,并根据网络延迟情况以及机器性能来调整推迟的时延时间。比如,对于延迟较高的场景下,就拉长时延时间,以尽可能地保证游戏运行的稳定,不至于因为其他玩家的数据未到达而等待或是丢失其他玩家指令;对于机器性能较差的场景,同样可以拉长时延的时间,以让更多的时间用于渲染。注意,这并不会导致不同步的问题,因为逻辑帧的执行是完全一致的

项目中,如果发生了丢包怎么处理?——KCP解决可靠性,输入缓冲解决实时性

底层传输使用KCP,利用ACK、序列号和快速重传保证输入包最终送达。游戏层采用输入缓冲机制,客户端实际执行帧会落后网络帧若干帧,为重传预留时间。如果发现某逻辑帧缺失,会暂存后续帧并等待补帧,保证执行顺序一致。对于长时间无法追上的客户端,则通过超时检测触发掉线、AI托管或断线重连机制,避免影响整个对局。


逻辑与渲染分离(帧同步专属)

逻辑与渲染分离的好处之一是可以让游戏更为流畅。对于逻辑更新,追求的是稳定,如果逻辑更新频率过高会导致网络和计算压力较大,过低会导致卡顿;而渲染更新则追求高帧率,即高刷新次数,二者的处理策略不同。

比如项目中的逻辑更新为15帧,即66ms进行一次逻辑帧更新,而渲染帧则锁定在60帧。如果让逻辑帧也为60帧,则压力过大,如果让渲染帧也为15帧,会导致游戏角色在表现上不够丝滑,影响流畅程度,所以将二者分离处理

延迟补偿(状态同步)

主要解决的问题:抵消客户端和服务端延迟带来体验感差的问题。比如”明明看起来打中了,但对面没掉血”。具体来说,玩家A在 T0 时刻于位置 P_old 开枪。这个开枪指令需要经过 Ping (n毫秒) 才能到达服务器。在 T0 到 T0+n 这段时间里,玩家A(以及其他所有玩家)可能还在继续移动。当服务器在 T0+n 时刻收到开枪指令时,玩家A的当前位置可能已经更新到了P_new。如果服务器直接使用当前时刻 T0+n 的世界状态(即玩家A在P_new,其他玩家也在他们当前的位置)来进行射线检测,那么弹道就是从P_new发射的。这完全不符合玩家A在T0时刻开枪时的真实游戏画面,导致判定不准确。

解决方案:延迟补偿。服务器知道这个开枪指令是玩家在n毫秒前 (T0时刻) 发出的。于是,服务器立即将整个游戏世界的状态回滚到 T0 时刻。具体操作就是从历史缓冲区里,把所有玩家的位置、朝向等信息恢复到n毫秒前的样子。在这个“过去的时空”里,服务器使用完全相同的命中判定逻辑,检查从玩家A在 P_old 位置发出的射线,是否能命中T0时刻的其他玩家。完成计算后,服务器瞬间将世界状态恢复到现在 (T0+n),并广播开枪结果(如命中谁、造成多少伤害)。

进一步优化,比如守望先锋中,服务端不会回滚所有玩家的位置,而是检测当前玩家的准星与附近敌人的逻辑边界(bounding volumes)是否有交集,没有的话不需要回滚,只回滚可能发生命中的玩家的位置。这样,可以让服务器减少不必要的计算。

为什么只适用于FPS?

主要原因在于游戏类型的区别,FPS游戏的信息是不对称的,而对于打击方来说,绝对的公平就是”我准星瞄准到你并开火了,那么你就应该掉血”,延迟补偿更多的是对攻击方的公平补偿。被打击方是不知道打击方的信息的,比如何时开火、何时瞄准,其掉血只能说对面枪法好。但是攻击方和被攻击方都是相对的,没有绝对的谁更有优势。

对于ACT类型的游戏,信息是对称的,攻守双方可以得知彼此的信息,比如攻击前是有前摇动画的,被攻击方可以在看到动画的时做出反应进行防御。公平性在于,我看到了你的攻击,我进行撤回防御,那么就应该是有效的防御。如果此时采用延迟补偿导致的后果:玩家A延迟比较低、玩家B延迟比较高。在A的客户端上,玩家A在T1 时间靠近B,而后立刻执行了一个后滚操作,发送到服务器。在B的客户端上,同样在T1时 间发起进攻,然后发送命令到服务器。由于A的延迟低,服务器先收到了A的指令,A开始 后滚操作,这时候A已经脱离了B的攻击范围。然后当B的指令到达服务器的时候,如果采用 延迟补偿,就需要把A回滚到之前的位置结果就是A收到了B的攻击,这对A来说显然是不公平的。

那是不是延迟越高越有优势?

并不是,延迟补偿的核心是保证指哪打哪。如果只从开火这一件事来说其确实更有优势,但是玩家开火的前提是玩家要先能看到敌人。如果延迟过高,那么始终看到的都是n秒前的敌人,瞄准的也永远都是n秒前敌人的位置,大概率是打不中的


插值

插值分为内插值和外插值(外推),内插值是一种通过已知的、离散的数据点,在范围内推求新数据点的方法(重建连续的数据信息);外插值指从已知数据的离散集合中构建超出原始范围的新数据的方法,其利用现在物体位置方向及速度推定物体未来的位置和方向。对于二者,一般采用线性插值或者多项式插值(比如贝塞尔曲线)足够满足游戏要求

主要应用:逻辑和渲染分离下,渲染层的平滑移动、回滚时的轻微调整时用到内插值;本地进行逻辑预测时用到外推

预测与回滚

大致原理:客户端不需要服务器确认并广播操作指令后才执行指令,而是在玩家输入时,就立刻执行该指令,然后将指令发送给服务器,并保存当前逻辑帧的状态快照以及各自的操作快照。当服务器的逻辑帧帧数追上了这个预测的逻辑帧,并广播给客户端后,如果服务确认的操作,和之前执行的一样(自己和其他玩家预测的操作),将不做任何改变,如果不一样(预测错误),就会将游戏整体逻辑回滚到最后一次服务器确认的正确帧,然后再追上当前客户端的帧

实际上,还需要考虑很多实现上的细节

预测哪些内容?

首先需要明确的是,预测主要是用于弱化玩家对网络延迟的感受,所以其应该围绕着“如何让玩家立即获取反馈展开”。这就告诉我们,不是预测地越多越好,反而随着预测的内容越多,维护也就越难、回滚也就越复杂,回滚不当时带来的体验也就越差。

首先,可以安全预测的内容是UI层的立即反馈,比如UI交互的高亮、引导型技能的范围显示等,其没有任何逻辑结果上的影响,只是游戏告诉玩家:我收到了你的输入,给用户最及时的反馈

其次是移动方面的预测,其虽然是在逻辑层面预测,意味着在预测失败时一定需要进行逻辑上的回滚,但是回滚修正起来较为容易,不容易穿帮。比如,通过外插值立刻朝着输入的方向移动、转向立即发生、摄像机跟随等,后续在回滚时可以通过平滑插值来微调

最需要谨慎考虑的是技能方面的预测,因为技能方面的有些预测不仅会影响实际的执行逻辑,而且后续回滚较难,容易发生穿帮,影响玩家游戏体验。

技能实际上是由多个阶段构成的,比如前摇、释放、后摇、结束。对于释放技能初始时的动画和轻微特效表现是可以预测的,比如技能的前摇动画、射击时镜头的抖动效果、蓄力特效、弹道效果等,这些内容不会实际影响逻辑,属于渲染层面的预测,较为安全,并且就算预测错误,也不会对游戏体验有较大影响,可以平滑地过渡回去,同时收益较大,可直接给玩家带来视觉和听觉上的反馈

而对于技能释放结束时的反馈效果则需要谨慎,比如飘伤、Buff效果、扣血等。一是因为有的预测是真正在逻辑层上预测,比如碰撞检测的扣血、附加Buff等,一旦预测错则必须进行逻辑上的回滚,有一定的复杂复杂度;二是因为有的预测在失败后,会给玩家带来不好的体验,比如血明明变少了却又涨回去了,明明飘伤了却又没掉血等,这部分内容最好等到服务端真正反馈时再真正执行。

不过,不能说逻辑层预测风险高、需要进行回滚就不进行预测,依然需要具体问题具体分析。 对于帧同步来说,其手感好的原因之一就是由于逻辑完全在客户端本地跑,所以本地完全可以提前预测部分逻辑层的内容,后续预测失败再进行逻辑回滚。比如早期1v1格斗游戏中用的的GGPO(Good Game Peace Out),其在逻辑层上激进预测,包括命中、扣血、击飞等,如果预测错误,比如对手实际后撤了,则再进行逻辑层的回滚修正,极大地提升了游戏时的手感。但是,并不是所有游戏都能这样做,其之所以可以这么做,是因为其数据量小,状态少。

再举一个守望先锋的例子,其击中敌方的判定、命中敌人的击中特效就是在客户端本地提前预测的,而真正的扣血、击杀则需要等待服务器验证,同时如何预测失败了,则特效会被回滚。实际上,这一点在和平精英这样的手游中也完全可以感受到——在网卡时,明明对方被打的冒血,可是结果自己却倒地了,对方没有扣除任何生命值。二者对于逻辑层的预测就更为保守,对于回滚效果差、影响游戏实际胜负的内容,则不会进行预测


什么情况下预测?

在预测成功率大的情况下预测,因为频繁的预测失败意味着频繁的回滚,反而让玩家游戏体验不好。

预测成功率直接和延迟高低有直接的因果关系:ping值越大,预测错误的概率就越大,因为无论是玩家的移动,还是技能的释放,其预测都是客户端本地基于上一逻辑帧中确认的玩家权威逻辑位置进行预测判断的,延迟越高,意味着预测时玩家的偏差越大,那么预测失败的概率就越大,回滚时给玩家带来的体验也就越差。所以,我们可以根据ping值的大小动态地调整预测策略。

这里以守望先锋的预测策略为例:

策略一:Ping = 0 (理想局域网环境)

预测:对弹道碰撞(如子弹击中墙壁的火花、弹孔)等非权威的、纯视觉增强的效果进行预测。

不预测:血条/伤害数字/击杀等权威的游戏状态,严格等待服务器回包确认。

设计逻辑:在零延迟的完美环境下,预测出错概率极低。此时预测视觉特效可以带来极致的画面流畅感,而等待权威状态确认的耗时极短(一两帧),玩家完全感知不到,却能保证100%的准确性。这是在绝对准确的前提下追求极致流畅。

策略二:Ping 正常 (例如 1ms - 219ms) 标准预测与回滚

预测:同时预测弹道和命中效果(如击中敌人时的溅血花特效)。这是完整的客户端预测。

仲裁(命中判定方法):服务器使用高效的延迟补偿技术(但不一定会回滚,而是在一定时间内为每个敌人目标设置时间的的Box,并且检查准星与该目标边界框是否相交,如果相交再进行进一步更精确的回滚判定)进行仲裁。

回滚:如果预测错误(如客户端显示命中,服务器判定未命中),则客户端需要回滚(撤销)命中效果。

设计逻辑:这是体验和公平性的最佳平衡点。在此延迟范围内,预测出错率可控,用“偶尔需要回滚一个视觉特效”的小代价,换来了“操作立即响应”的巨大体验提升。

策略三:Ping 过高 (≥ 220ms) 禁用预测,追求确定性

完全延后:延后所有命中效果,包括弹道和击中反馈,只做最保守的特效、动画预测。

不预测:不再进行任何形式的预测。

等待确认:直接等待服务器回包后,才统一渲染所有效果。

设计逻辑:当Ping≥220ms时,网络状态已经很差,基于严重过时信息的预测,其出错率会非常高。如果继续预测,玩家会频繁看到“打中了人又无效”的现象,体验极其糟糕。因此,策略发生根本转变:宁可让操作感到明显的延迟,也要保证“所见即所得”的确定性,避免更破坏体验的预测错误

预测失败后如何回滚?

【快照是什么?怎么实现?】

回滚预测通常需要同时保存两类数据:

一是输入的历史,其作用在于——检测客户端的预测是否发生错误,因为服务端广播的都是帧操作数据,而不是计算好的状态;回滚后的快速重演,比如回滚到100帧并重新执行后,需要重新执行之前预测的101 102帧的帧操作

二是预测前缓存的状态快照,快照缓存中需要完整保存当前世界下的状态,包括所有角色的位置、血量、速度、动画状态等所有会影响后续逻辑的数据。比如,客户端维护一个缓冲区,在预测前对游戏状态对象进行深拷贝或序列化/反序列化,这实际上是一个对性能要求很高的操作。

如何优化快照,以尽可能避免快照过多导致的性能和内存上的消耗过大?快照的存储帧数优化+增量快照

【存储帧数】不要无限保留快照,只需要保证能够覆盖最大的回滚帧数即可,比如最近的8-12帧(GGPO的推荐值)

【增量快照】做周期性的全量快照,中间帧只记录被修改字段的增量值

[Full Snap @ Frame 0]
[Delta 1]
[Delta 2]
[Delta 3]
...
[Full Snap @ Frame N]   ← 定期刷新,防止 Delta 链过长

一个比较好的方式是使用ECS架构来进行增量存储。ECS将数据和行为彻底地分离,相比于OOP下对象的状态散落在各个文件中导致的快照捕获不准确,其将数据状态集中存储,此时保存快照状态非常方便,做增量存储也比较方便。

ECS在断线重连方面同样具有优势,如果断线时间过长,而服务端没有保存完整的游戏快照,客户端没有快速应用快照的方式,那么就需要将所有的操作指令从头到尾全部重新计算一遍,时间较长,而在ECS下,客户端可以非常容易地接收服务端维护的快照数据并应用,让客户端直接通过快照来同步,提升了重连的速度

【什么时候回滚?】

当客户端收到服务器确认的权威输入数据,并且发现与本地预测的输入不一致时。

​【如何执行?​​】

​a.定位分歧点​:找到预测开始出错的第一个帧(比如第100帧)。

b.​读取快照​:加载第100帧之前(如第99帧)的正确游戏状态快照。

c.​重新模拟​:从第100帧开始,使用服务器发来的正确输入,重新快速执行(重放)所有逻辑帧,直到追上当前的客户端帧。这个过程不能简单地把当前画面瞬间跳到正确状态(那样会瞬移),而是需要在一次物理时间(比如16ms的渲染帧)内,连续模拟多帧逻辑。例如,在16ms内模拟了3帧逻辑(每帧逻辑计算约5.3ms)。玩家感受的感受是,在回滚修正后的短暂瞬间,游戏世界里的动作(如攻击动画、角色移动)会以“快进”的方式播放,直到追上当前时间,然后恢复正常速度。

d.​这个过程可能发生在单次渲染帧内,需要快速完成。


其他细节问题:

回滚时的频繁创建与销毁:

对于一些特效或者飞弹(飞弹只是表现层面,真实的物理碰撞检测不一定依赖于飞弹),如果在100帧时预测时创建,等到服务端权威数据到来时,发现100帧时并没有创建。此时如果先回滚到99帧将该特效或飞弹销毁,随后又按照真实逻辑重新模拟计算整个过程,并在101帧创建,那么就可能导致两方面问题。

一是表现层上,因为一个物体经过了创建->销毁->创建的过程,可能出现闪烁、动画重播的等问题;二是性能上,一个物体重复创建销毁是由开销的,尽管一般有对象池管理

解决方法是,逻辑层和表现层分离,在回滚时如果发现某个物体需要销毁,先不在表现层销毁,也就是不Destroy(GameObject)。后续在创建对象时,判断某一个对象是否之前已经预测创建过了(比如通过hash的方法代表每个实体的唯一标识符),如果已经创建过了,则复用并作轻微修正。

回滚时对象的创建时机:

问题:假设一帧中有多个系统需要和创建出的对象交互,而回滚数据中无法得知对象是在哪个系统中创建的,就有可能因为回滚时创建对象时机和真正将其创建出来的时机的不一致,导致不同步问题。

有两种解决方法:一种是统一时刻创建销毁对象,即所有对象都在帧末尾同意加入到真正的世界中。但是该方法会导致对象的创建至少需要延迟一帧。

另一种是将在保存回滚数据时,记录下是在哪个系统中创建的,从而精准地在指定的时机创建对象


优化网络

带宽与流量优化

  • 减少需要同步的对象(状态同步专属):其核心是剔除不需要同步的对象,比如AOI管理,在视野外的对象会被完全忽略,以减少每次发送同步消息时带宽的消耗以及同步的消息量

  • 数据压缩和裁剪:其核心是尽可能地利用好消息中的每一个字节,比如如果游戏中的旋转不涉及到XZ轴的旋转,则只要传输Y轴的旋转角度即可

  • 增量同步(状态同步专属):其核心是通过增量来减少每条同步数据的数据量。以守望先锋的状态同步为例,服务端会维护两个集合——一是每一帧的脏数据集合,二是针对每一个玩家的脏数据集合,后者中会存放服务端还没给客户端同步的数据。在服务端收到客户端操作指令后,会立即模拟世界进行计算,计算出对象状态变化,写入到本帧的脏数据集合中。在当前Tick结束时,会将每一个玩家的脏数据集合和当前帧的整体脏数据集合求交集,即进行合并,这部分数据集合表示当前还没给客户端同步的数据以及当前帧的新数据。再根据客户端已经确认的状态,比如客户端已经确认收到了Tick100的数据,让新形成的集合-该玩家已经收到的状态,形成一个增量集合,将该集合打包发送。

对抗丢包

丢包过后如何补发?如果说这个包丢了,一定要等到确认丢包之后重传吗?可以参考KCP的策略

假设已经丢包,那如何尽可能地避免丢包带来的影响?

以守望先锋的做法为例:

  • 采用的是定制过的可靠UDP。在客户端,为了避免丢包带来的影响,每一帧的数据包都会包含最近几帧的数据,即便一个数据包丢了,也可以通过附近几帧的数据包将丢的数据补上,相当于是用带宽换取稳定。此外,客户端在丢包严重时,还会通过频繁发包的方式尽可能地减少丢包。

  • 服务端会为每个玩家维护一个操作指令缓冲区,服务端每次都不会立刻消费客户端最新的输入,而是延后几帧再真正将其用于模拟,具体延后几帧却决于缓冲区的大小。缓冲区增大也就意味着能“吃掉”越多的网络波动和丢包,即使连续丢几个包,缓冲区里还有之前的输入可以继续模拟,游戏世界不会卡住;但缓冲区越大也就意味着延迟越大,因为服务器总是在用“过去”的输入计算“现在”的状态,表现的效果就是虽然有延迟,但至少玩家的画面是流畅的。所以,该缓冲区的大小会随着玩家的网络状况而动态调整,在丢包严重时会尽可能增大缓冲区大小,反之减小。


总结

帧同步以「相同输入 + 相同逻辑 = 相同结果」为核心,适合少人、强实时、对操作手感要求高的竞技游戏(MOBA / RTS / 格斗),但需重点解决确定性、反作弊与断线重连问题;状态同步以服务器为权威状态源,适合人多、复杂交互、反作弊要求高的场景(MMO / FPS / 开放世界),但带宽与延迟感受是主要挑战。
在实际项目中,往往不是二选一,而是结合二者思想——通过逻辑/渲染分离、输入缓冲、预测与回滚、延迟补偿、增量同步与可靠 UDP(KCP)等手段,在一致性、流畅度与网络鲁棒性之间取得符合游戏类型的最佳平衡。