我最早接触帧同步是很多年前做一个小型格斗类Demo。当时项目选型的时候主程说了一句我们要不然别同步状态了直接同步指令。当时我一脸懵后来被现实狠狠教育了一轮才明白这句话的分量。做了几年实时对战类游戏之后回头再看帧同步反而觉得它是网络同步方案里最优雅、也最考验工程纪律的一种思路。这篇文章想把帧同步技术从原理到踩坑完整拆开讲一遍尽量用我实际项目里验证过的方式来讲不写教科书只写能用的东西。帧同步简单说就是所有客户端在同样的帧节奏里、接收同样的输入序列、执行完全相同的逻辑计算从而让所有端的游戏世界保持一致。它不像状态同步那样到处广播我这边血量剩多少、位置在哪而是广播这一帧玩家按了什么键。因为同步的是指令而非结果网络传输数据量小得多这在早期移动端网络环境下是一个巨大优势。但代价也很明显整个逻辑必须严格确定性任何一点平台差异、浮点误差、遍历顺序变化都可能让两端在世界观里逐步分道扬镳。这篇文章适合正在做实时对战类游戏格斗、MOBA、RTS、体育竞技的开发者也适合想了解网络同步方案选型的客户端或服务器工程师。无论你是第一次听说帧同步还是已经在项目里踩过不同步的坑下面这些内容应该都能帮到你。1. 帧同步和状态同步到底在同步什么很多新手容易把网络同步想成把一个人的游戏状态复制给另一个人。这其实是状态同步的思路服务器或者主机端算出所有单位的位置、血量、动画状态然后周期性广播给其他客户端其他客户端负责渲染显示。状态同步之所以让很多人觉得自然是因为它符合直觉——把结果告诉别人就行。但问题也很明显结果数据的量很大。一个几十人对战的场景每个对象的位置、速度、朝向、血量、Buff列表每秒钟要同步多次带宽占用直线上升。而且状态同步天然让两端对我看到的和我操作的之间存在感知延迟操作手感容易发闷。帧同步的思路完全不同。它不关心结果是什么只关心每个玩家在这段时间内做了什么操作。可以这样类比状态同步就像你开车的时候副驾驶一个人不断用对讲机告诉你方向盘角度多少、踩了多深的油门你照着做帧同步则是大家手里拿着同一份乐谱、听着同一个节拍器只要每个乐手按谱子演奏乐团听起来就是整齐的。1.1 为什么同步输入能省这么多带宽一个玩家在一帧内的操作浓缩成数据其实非常少。格斗游戏里无非就是方向键状态加几个按键位可能是二十几个比特MOBA游戏里一帧内的有效操作也就是移动指令和技能指令就算把鼠标坐标压缩传输一个输入包也通常不超过几十字节。对比一下一个状态包可能要把几十个单位的状态全部打包光坐标和朝向就是好几百字节加上Buff、血量、技能状态动辄上千字节。帧同步把每帧要同步所有状态变成每帧只同步所有玩家的输入传输量直接下降一个数量级。早期我们做帧同步核心的时候单帧全量广播数据在数十人场景下不超过1KB这在移动网络下是状态同步很难做到的。当然省带宽只是表面优势。帧同步最核心的价值是可复现性因为所有端执行的是同一套逻辑、同一份输入序列整个对局可以被当作一条输入流完整记录下来。这意味着回放、观战、断线重连的实现会变得非常轻量——你要存的只是输入日志而不是海量状态快照。1.2 帧同步的适用边界别什么项目都往上套帧同步看着美好但它不是万能的。RTS、格斗、动作类游戏是帧同步的天然主场因为这些游戏逻辑复杂、单位多但玩家输入频率相对可控。而MMO、吃鸡类大地图射击游戏情况就复杂得多玩家数量多、地图超大、交互长期且开放帧同步要求所有人跑同一份逻辑这在超大场景下代价过高也不利于服务器承载可变数量的玩家。这类项目往往还是用状态同步甚至更复杂的AOI同步方案。另外帧同步对游戏逻辑的纯度要求极高。你的逻辑层里但凡混进去一点点渲染相关的分支、用了系统当前时间的函数、依赖了物理引擎里非确定性的部分都可能在不同设备上出现不同的表现。我曾经在一个项目里遇到两端在半小时后角色位置差了半个身位排查了很久最后发现是因为逻辑里有一个对随机数种子的初始化依赖了系统时钟。这类问题在状态同步下几乎不可见但在帧同步下就是致命的。所以在项目选型阶段请先问自己几个问题这个游戏的对局是不是封闭且短时的玩家的输入能不能被明确抽象出来逻辑层是否可以被严格隔离成确定性模块如果三个答案都是肯定的帧同步值得认真考虑。2. 确定性帧同步这条船的龙骨如果说帧同步是一艘船那确定性就是龙骨。没有确定性一切都白搭——因为帧同步的整个系统建立在相同输入、相同代码、相同顺序必然产生相同结果这个前提上。但现实世界不是数学课本不同设备、不同编译器、不同CPU架构处理同一段代码可能会得出不同的结果。这部分我想把最容易破坏确定性的几个因素完整梳理一遍。2.1 逻辑帧先把时间切成均匀的片帧同步的第一个核心概念是逻辑帧Lockstep Frame也叫Tick。游戏渲染的帧率是不稳定的高刷屏可能120FPS低端机可能只有30画面复杂时还会掉帧。如果逻辑跟着渲染帧走端与端之间执行的节奏就会彻底乱套。所以帧同步把游戏时间切成固定长度的逻辑帧比如每帧33毫秒约30FPS所有逻辑计算都按这个节拍推进。渲染层可以100FPS甚至更高但每渲染两三帧采一次逻辑世界的最新状态就行。逻辑层绝对不能依赖渲染时间也不能用真实时间只能用逻辑帧序号 帧步长来推演。这样只要每个端从第0帧开始执行相同的输入序列它们就会在同一步调上走到同一个未来。实际操作上逻辑帧步长的选择要考虑游戏类型。格斗游戏追求操作精度通常会选择16ms约60FPS甚至更低RTS游戏单位多、逻辑重33ms甚至40ms更常见。我们的项目最开始选了16ms后来发现中等设备上逻辑负载撑不住降成33ms后手感还在可接受范围内。这个参数最好做成配置方便后期调优。2.2 浮点数的坑不同设备上的0.000001差异确定性的头号杀手几乎毫无悬念是浮点数。IEEE 754标准虽然规定了浮点数的存储格式但不同语言、不同编译器、不同CPU在运算时会产生微小的舍入差异。比如同样的a / b在某些平台上可能会使用更高精度的中间值比如x87指令集的80位扩展精度结果和普通64位浮点的舍入结果不同。这种差异在单次运算里几乎看不出来但一帧帧累积下来单位位置就会慢慢偏移最终肉眼可见。规避手段有多层。第一层是尽量用定点数替代浮点数也就是把所有坐标、速度、加速度都换算成整数来表示。比如把位置用厘米表示速度用每帧多少厘米表示加速度用每帧每帧多少厘米表示。虽然计算时要自己处理溢出和除法的舍入但整数运算在所有平台上基本是确定性的注意除法在不同语言的取整规则要自己约束好。第二层是如果必须用浮点就要避免未定义行为并且只使用标准库中严格保证确定性的数学函数。sin、cos、sqrt、pow这些函数在不同平台的实现可能是不同的尤其是sin/cos不同库的查表算法和精度策略完全不同。我见过一个项目用了自研引擎的Mathf.Sin结果在iOS和Android上表现不一致后来只能自己用多项式逼近重写了一套数学库。第三层是尽量减小数值的绝对大小。很多确定性Bug不是算法错了而是中间结果太大导致最后精度不足。比如一个地图坐标达到百万级别再用32位浮点存步长稍微小一点就根本推不动。设计逻辑时就把世界尺度收敛到合理范围比事后修浮点要省心得多。2.3 随机数同一个随机序列两端必须一致游戏逻辑离不开随机数暴击是否触发、掉落哪件装备、AI是不是恰好做出某个决策。在帧同步里随机数必须是伪随机且可复现的——即给定同样的种子和同样的调用顺序必须产生完全相同的随机序列。这里有一个经典陷阱如果代码里用了某个语言内置的随机函数但A端的调用次数和B端不一致比如某个随机数在A端因为分支条件不同没有调用在B端却调用了整个序列就从那个点开始错位后续所有随机结果全部对不上。所以帧同步项目里随机数必须由一个统一的、可注入的随机数服务来管理而且这个服务要在逻辑层内自己实现不依赖语言内置库。最好还要把随机数调用序号打印出来一旦两端序列错位日志里一眼就能看到是从第几个随机数开始分的岔。我们项目里用的随机数实现是一个标准的线性同余发生器LCG每个逻辑帧开始前把种子存一份快照。遇到对局不一致时对比两端的种子快照能非常快速定位到是哪一帧、哪个系统多调用了一次随机数。2.4 遍历顺序你以为的稳定其实换个设备就乱了逻辑层里经常需要对对象列表做遍历先算所有人的移动再结算所有攻击。在大多数情况下遍历顺序对结果没有影响。但如果遍历中涉及修改集合比如A单位移动时触发了对B单位的伤害而B在后续遍历里又要被处理遍历顺序就可能影响最终的结算结果。比如一个List里有A、B、C三个单位A先动A的攻击把C打死了C在列表里被移除但另一台设备上由于某种原因B先被处理B的攻击把哪个单位打死了后续A的处理就完全改变了。这种顺序相关的问题在状态同步里可以被服务器弱化但在帧同步里必须严格处理。解决办法是尽量将逻辑改造成读取快照-统一计算-提交结果的模式。每个逻辑帧先对当前世界状态拍一个只读快照所有系统的计算都基于快照不会因为遍历过程中修改了对象而影响后续读取。另外所有对象的遍历顺序要固定比如按对象ID排序后遍历不能依赖HashSet的迭代顺序那在不同运行时下可能是随机或插入序相关的。2.5 物理引擎帧同步里用物理引擎真的要慎重很多游戏用了现成的物理引擎做碰撞和运动模拟。物理引擎本身为了性能大量使用了近似算法、多线程并行和不确定性优化这让它在帧同步里几乎成了定时炸弹。同一段物理模拟在AMD和Intel CPU上跑出的结果都可能是不同的更不用说不同移动设备的GPU调度差异。如果你非要在帧同步里带物理引擎至少要保证以下几点物理引擎要能锁定固定步长所有物理查询和修改都要在逻辑线程内完成关闭多线程物理并且对物理结果做额外的确定性封装。这在实践中很难完全做到所以很多帧同步项目最终选择自研轻量级碰撞逻辑比如圆碰撞、AABB把复杂物理外包给渲染层去做表现逻辑层只保留确定性计算部分。我们自己的经验是物理表现可以花哨逻辑判定必须极简。格斗游戏里判定框可以用简单的矩形、圆形还更容易调平衡真正需要布娃娃、碎裂效果的部分全放到渲染层根据逻辑事件播放就行那一层不参与确定性约束。3. 实操搭建一个帧同步核心讲完了原理我们说说怎么从零搭一个能跑的帧同步核心。这一部分我会按真实项目的骨架来梳理去掉业务细节只保留纯同步框架。看清这个骨架后面往里面填自己的玩法逻辑就不会跑偏。3.1 整体架构输入上行、广播下行、本地直连帧同步的网络拓扑通常有两种主流形态一种是有服务器中转的权威模式所有客户端的输入先发给服务器服务器按帧号排列后广播给所有客户端另一种是主机模式主机收集所有玩家输入再统一广播。前者更适合作弊防护和正式运营后者适合房间类小规模对战比如2到8人的联机游戏。核心逻辑对这两种形态是通用的区别只在于谁负责裁定这一帧的输入集合是什么。服务器模式的好处是权威性所有客户端上报输入服务器按帧号整理后广播客户端不用信任彼此。主机模式的好处是低延迟适合局域网或者小规模联机但主机天然有作弊优势。我做过的项目大多采用服务器中转模式哪怕逻辑帧只有30服务器也只做转发不跑逻辑这样后续做帧校验、断线重连、观战都方便。设计时要注意一个关键点帧同步中真正想竞争的是操作延迟所以从客户端采集输入到服务器广播到达这个链路的延迟应该尽可能低。一般做法是客户端不等待服务器确认本地输入立即进入本地缓冲并先执行同时上报服务器。服务器广播的只是该帧最终认可的输入集合客户端收到后再做修正。这种机制叫本地预表现Prediction能够极大提升操作手感。3.2 核心循环采集、锁帧、推进、校验一个帧同步循环的伪代码大概是这样的// 游戏主循环LockstepLoop uint32_t frameIndex 0; while (running) { // 1. 等待当前帧所有玩家的输入就绪超时策略另说 InputBag bag waitForAllInputs(frameIndex); // 2. 按固定顺序应用输入到逻辑世界 for (auto playerInput : bag.orderInputsByPlayerId()) { world.applyInput(playerInput.id, playerInput.actions); } // 3. 推进逻辑世界一帧 world.tick(LOGIC_FRAME_DELTA); // 4. 提交本帧结果给渲染层 renderer.publishSnapshot(world.getStateSnapshot()); // 5. 可选生成确定性校验哈希 uint32_t hash world.computeDeterministicHash(); if (hash ! expectedHash) { syncErrorHandler.report(frameIndex, hash); } frameIndex; }这里面第一步最关键所有端必须拿到相同顺序的输入集合之后才能推进到下一帧。如果某个玩家的输入因为网络抖动迟迟没到整个对局就得等。这就是帧同步在弱网环境下所有人一起卡的原因。为了解决这个问题帧同步设计里会引入帧缓冲机制每个客户端维护一个输入缓冲区提前缓存之后几帧的输入本地执行时滞后于实时若干帧这样网络晚到几个帧的包不会马上引发卡顿。帧缓冲的深度通常在一个固定范围比如2到4帧。如果缓冲被消耗到0说明网络在这个瞬间已经跟不上所有端必须等待。关于动态帧缓冲的调节后面常见问题部分会再展开讲。3.3 输入定义把玩家的操作压成稳定的结构帧同步输入的定义要讲究紧凑且稳定。紧凑是为了省带宽稳定是为了解析时不产生歧义。一个常用的输入结构如下struct PlayerInput { uint16_t frameId; // 这一帧的编号 uint8_t playerId; // 哪个玩家的输入 uint16_t moveX; // 方向量可以是摇杆向量编码 uint16_t moveY; uint8_t buttons; // 按键位掩码1攻击、2跳跃、4技能1…… uint8_t extra; // 扩展字节比如鼠标/x/y增量编码 };方向量我建议先用整数编码比如把摇杆角度和力度量化成16个方向等级。直接传输浮点坐标不是不行但要在确定性框架里保持数值稳定就得多做一层规范化所以不如从源头就量化成整数。按键用位掩码表达最方便一个字节最多可以承载8个按键状态不够再加字节。对大多数游戏来说一个玩家的单帧输入封装成定长结构后不会超过8字节考虑包头和批处理一帧全员输入也就几十字节。输入结构尽量定长不要用可变长度数组也不要塞字符串。定长结构的好处是解析简单、对齐方便、网络封包处理效率高。如果某个玩法需要上传额外数据比如MOBA里的鼠标点击目标可以用固定字节的坐标编码或目标ID替代。3.4 逻辑与渲染的隔离那条不能跨越的线帧同步项目最大的工程纪律是逻辑层和渲染层的严格隔离。逻辑层只允许调用确定性模块数值计算、输入处理、战斗规则、AI决策全部在纯逻辑空间内执行。渲染层只负责拿逻辑层的快照去做表现播放动画、更新模型位置、生成特效。怎么保证隔离最实用的办法是编译期隔离逻辑层做成独立模块禁止引用渲染引擎的任何头文件。如果开发用的自研引擎做不到这么彻底的隔离至少要在代码规范上明确逻辑类只能包含纯数据类型不允许出现渲染相关的成员。代码评审里一旦有人把Transform或者GameObject放进逻辑类直接打回。逻辑和渲染之间的数据交换统一通过状态快照来完成。每一帧逻辑推进结束后把本帧需要用到的状态复制到一个纯数据快照结构里渲染层拿这个快照去驱动表现。快照结构本身可以很精简不必把全世界的状态都扔过去只暴露渲染层需要看的部分——比如单位ID、位置、朝向、状态机编号、播放中的动画名。3.5 服务器广播按帧分批带帧号加校验服务器中转模式里服务器要维护每位玩家的最新输入并且以固定节拍广播。广播包的结构大致如下帧号、玩家数、每个玩家的输入数据以及可选的帧哈希校验值。帧哈希由服务器算还是客户端算取决于项目的防作弊策略但至少两个客户端之间要具备对比哈希的能力。广播频率理论上应该等于逻辑帧率但网络层并不总能精确按33ms发一个包。实践中服务器一般把多个逻辑帧的输入聚合在一个网络包里发送比如每3个逻辑帧发一个包。这样既降低了网络包数量又给客户端留出了缓冲空间。需要注意的是网络包里的帧间隔要固定客户端解析时不能以网络包到达时刻为准只能以帧号为准。做好这个设计后帧同步核心就基本成型了。下一步是开发工具链确定性回放工具、两端状态对比工具、帧号追踪日志。这些工具在调试确定性Bug时能为你节省大量时间而且越早建立越好。我们项目有句话叫没有日志比没有同步更可怕因为在帧同步里日志就是你的显微镜。4. 常见问题与排查技巧实录这部分我想把实际项目里踩过的一些坑和解决思路完整记录下来。帧同步的问题不像普通网络同步那样画面表现错一点点而是直接表现为两端整个世界分道扬镳。问题一旦发生排查手段与普通Bug完全不同。4.1 不同步了怎么快速定位分岔点帧同步项目最恐怖的现象就是一开始看起来完全正常打了十几分钟后两端的某个单位位置开始出现几个像素的偏差然后这个偏差像滚雪球一样越来越大。定位这种问题的标准做法是日志 校验。在逻辑层里每隔固定帧数比如30帧计算一次全世界的确定性哈希。哈希算法可以用简单的累计MD5或者更轻量的CRC64把关键世界的状态所有单位的位置、血量、Buff状态、随机数种子等串起来计算。把哈希值打进日志同时上报其他端。一旦某一帧的哈希值对不上就立刻暴露出分岔点精确到了帧级。然后就可以用帧回放来逐帧对比两端在这帧前后的数据。具体做法是逻辑层把每帧的输入、随机数调用序列、关键状态变更记录下来。对比发现问题帧后把两端在问题帧前的输入序列拉出来做逐帧比较找一个输入不一致、随机数序列不一致或者某个函数被调用了不同次数的位置。我们遇到的大部分确定性Bug最终都指向以下三种情况之一随机数调用序不同、浮点运算差异、集合遍历顺序不同。4.2 弱网卡顿帧缓冲怎么动态调帧同步在弱网环境下很难避免打起来一顿一顿的体验因为逻辑进度要等最慢的玩家。一个缓解手段是动态帧缓冲。具体做法是客户端先设定一个目标缓冲区深度比如缓存4帧输入才开始执行网络状况好的时候缓冲会逐渐累积超过4帧这时可以适当调低深度减小操作延迟网络出现抖动、缓存被消耗到低于安全值时则重新调高深度等待缓冲恢复。注意动态调度的频率不能太高否则体验会忽快忽慢。一般以几百帧为单位做一次平滑调整。另外缓冲调整本身也是需要同步的你不能A端在帧10执行、B端在帧12执行那逻辑就彻底错乱了。所以动态缓冲的调整要么通过服务器统一广播所有端同时改变帧缓冲深度要么干脆只在开局时固定一个缓冲深度中途不调整。项目早期建议先固定缓冲深度用稳定换简单。还有一个比较实际的技巧把逻辑帧率做成可配置项。帧率越高单位时间内的逻辑计算次数越多对CPU的压力也越大弱网下更容易因为计算卡顿导致输入延迟。某些对精确性要求不太高的玩法可以把逻辑帧率降到20甚至15配合插值渲染手感反而更稳。4.3 断线重连为什么帧同步反而更简单状态同步的断线重连是一个非常复杂的话题因为要为新加入的玩家生成一份完整的世界快照还要确保快照之后的增量更新对得上。但帧同步的断线重连就简单得多新玩家只需要从0帧开始回放整条输入流到当前帧为止就能重建整个世界状态。当然回放整局可能很慢所以实践中会做优化服务器保存关键帧的世界快照比如每300帧存一份新玩家先加载最近的快照再回放从快照到当前帧的输入日志最多300帧。回放计算量一般可以接受因为逻辑计算本身是确定性的而且往往比实时速度快得多。我们之前在性能一般的手机上重放300帧逻辑约10秒对局只需要不到2秒。观战模式也是同样的原理观战者只需要接收服务器转发的输入流在本地跑一份逻辑实例就能渲染出与所有选手完全同步的比赛画面。观战者不需要向服务器上报输入也不会影响对局。这套机制天然适合做录像回放、高亮集锦、甚至AI托管。4.4 作弊与安全性帧同步的软肋要心里有数帧同步一个绕不开的问题是作弊检测困难因为所有客户端都在本地跑完整逻辑输入数据就是一切的源头。理论上一个作弊客户端可以直接修改本地内存、让自己获得无限金钱、或者向服务器上报正常输入却在本地执行修改后的逻辑。防作弊的方式主要靠服务器端影子仿真服务器也维护一份确定性逻辑收到所有人输入后按同样的逻辑快速推演如果客户端上报的某些状态与服务器推算结果不符就判定异常。但这会很耗费服务器CPU所以很多项目只在关键对局或排位赛中开启。另一个常见手段是延迟校验客户端除了上报输入还定期上报自己世界状态哈希值服务器在不同时间点对比自己的仿真结果。即使发现了作弊也只能事后封号无法做到实时阻止破坏游戏体验。所以如果项目对反作弊要求很高纯帧同步方案可能不是最优选需要结合服务器状态验证混用。4.5 逻辑黑洞千万别把真实时间引入逻辑层前面提到过随机数依赖系统时钟的问题类似的逻辑黑洞还有更多比如AI决策里用了DateTime.Now判断白天还是黑夜、逻辑层里调用Thread.Sleep来模拟动画时长、或者在逻辑计算里读取了设备的时区。任何依赖真实时间、真实设备环境、外部输入的逻辑调用都会把确定性撕开口子。我的建议是给逻辑层建立一份禁用清单写进团队开发规范里明文禁止。清单大致包括禁止读取系统时钟、禁止调用非确定性随机方法、禁止使用浮点环境相关的特性、禁止直接使用语言的默认哈希表迭代、禁止直接依赖物理引擎结果、禁止在逻辑层使用任何I/O操作文件、网络。把这份清单贴到每个开发的工位或者写进代码评审的Checklist比事后排查要省事得多。4.6 帧同步的测试策略自动化就是保命符帧同步项目测试的最大特点是可以完全自动化。因为逻辑是确定性的你可以在纯逻辑环境中跑一局完整对局然后对比输出结果。我们团队有一套做法在CI流水线里每天跑几百局双人机对局让两个AI玩家用完全相同的行为进行对局最终对比两端的状态哈希序列。只要任何一个不同CI自动报错并定位到分岔帧。这种做法在普通网络同步项目里几乎不可能因为网络同步有随机性每次对局结果可能不同。但在帧同步里确定性就是测试的基石。你可以写出确定性的自动化测试用例固定随机种子、固定输入序列、固定初始状态然后断言最终世界状态。这样的测试速度快、稳定、不会出现本地能过服务器挂的玄学问题。给新接触帧同步的团队一个建议在上线排期前至少留出一到两周的调试工具建设时间把确定性日志、帧回放、状态哈希对比、CI自动化测试这四样东西搭起来。没有它们帧同步项目的后期维护一定会在某一个版本里失控。5. 从帧同步出发还能往哪扩展帧同步的思想其实可以延伸到很多地方。比如回合制游戏、同步演算类的模拟系统、甚至有团队把帧同步的思路用在了前端状态管理上——只要保证同样的事件序列、同样的纯函数reducer状态必然一致那整个应用就是可回放、可预测的。这和帧同步的确定性哲学完全同源。在实际游戏项目里如果资源和技术积累到位还可以在帧同步基础上叠加状态同步的局部优化。比如全局用帧同步但对一些超大范围的AOE表现或者粒子特效在线程安全的前提下用状态同步做补充。不过要提醒一句这种混合方案一定要把两份逻辑的边界划得干干净净别在一个系统里同时依赖两套同步逻辑否则排查问题时会非常痛苦。另外现在很多开发者开始把帧同步的确定性回放用在AI训练和数据分析上。因为对局可以被完整复现你可以把顶级玩家的输入流喂给AI模型去学习也可以在一场对局结束后精确分析某个决策点前后的状态变化。这些能力是状态同步项目很难提供的。写到这里我对帧同步的整体理解基本都表达完了。回头看帧同步并不是什么高深莫测的技术它的核心其实特别朴素让每个端都成为同一只乐队的乐手共享同一份乐谱和节拍器。麻烦的地方在于现实世界里每一台设备都有自己的小脾气你要保证所有乐手严格照着乐谱演奏还不能混进去任何即兴发挥。如果你正在项目里为网络同步方案头疼我真心建议从帧同步开始搭建一个小型Demo两个客户端、一组固定输入、一个简单战斗逻辑用不了几天你就能直观体会到同步指令而非状态带来的巨大快感也会对网络同步的本质有更深的理解。之后无论是继续做帧同步还是转去做其他同步方案这段经验都会成为你技术底子里很扎实的一部分。