ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

帧同步游戏的固定输入延迟:原理、选型与实现

帧同步游戏的固定输入延迟:原理、选型与实现 帧同步游戏设置一个“固定输入延迟”它背后的逻辑说到帧同步游戏很多刚入行的同学问的第一个问题往往是我本地按了一下攻击为什么角色要等那么一会儿才出招这个“等一会儿”是不是网络卡了其实大部分情况下不是网络出问题而是你遇到了一个在帧同步体系里几乎绕不开的设计——固定输入延迟。它不是拿来坑玩家的恰恰相反它是用来保证所有玩家的“世界”不分裂的。这篇文章想聊透一件事帧同步游戏里的固定输入延迟到底在解决什么延迟帧数怎么定实现时有哪些坑。适合正在做实时对战、格斗、RTS、或者任何需要保证“所有客户端状态一致”的同步方案的开发者参考也适合对帧同步机制有好奇心的玩家了解背后的原理。1. 帧同步的本质为什么“同步”这么难很多项目刚立项时策划拍板说“我们要做个帧同步的实时对战”程序点头说好。等到真开始动手就会发现“帧同步”这三个字里藏着的复杂度远比想象中高。首先要明确一点帧同步的核心不是“网络同步”而是“确定性模拟”。1.1 逻辑帧与渲染帧两个完全不同的概念先区分两个“帧”。渲染帧是显卡吐出来的画面帧通常跟着显示器刷新率走60Hz就是16.67毫秒一帧120Hz就是8.33毫秒一帧。逻辑帧是游戏模拟世界所用的时间步长帧同步游戏里这个值是固定的比如33.3毫秒一帧、50毫秒一帧甚至更粗。帧同步游戏里的所有状态变化——角色移动、技能判定、AI决策、伤害计算——都必须在逻辑帧的边界上发生。也就是说大家共享同一个“逻辑时钟”每推进一个逻辑帧全世界都往前走一步。这个“逻辑时钟”不会因为渲染卡顿而改变也不会因为某台设备性能差就跑得慢。它是最权威的时间基准。问题来了既然逻辑帧是固定的那网络传输造成的“时间差”怎么填补玩家在A地按了一下按键这个输入要经过网络送到其他客户端在逻辑帧的哪个时刻插入才合理如果每个人的网络延迟不同、甚至同一局内不同时刻的延迟都在波动那么同一个按键操作到达各个客户端的时间点就会错开。一旦错开确定性就崩了。1.2 网络延迟会让同一个世界分裂成两边举个最典型的例子A玩家控制角色甲B玩家控制角色乙。甲在第12逻辑帧按下了“出拳”键。A客户端本地立即处理了这个输入在第12帧就让角色出了拳。但B客户端呢如果输入要花30毫秒才能到达而一个逻辑帧是33.3毫秒那么这个输入也许能赶上第12帧也许只能赶上第13帧。如果A客户端认为甲在第12帧出拳B客户端认为甲在第13帧才出拳那么后续所有状态就全对不上了。比如第12帧原本甲应该击中乙但在B的视角里第12帧甲还没出拳乙没有受击判定而A的本地世界里乙已经被打中了。两个世界从这一刻起彻底分叉。这就是帧同步最怕的“时间线分裂”。帧同步的逻辑就像所有客户端合奏一首曲子每个客户端手里都拿着同一份乐谱必须保证每一个音符都在同一拍上响起。如果某个乐器慢了半拍整个曲子就乱了。那为什么不能用“对时”来修正比如收到一个输入后发现晚了就向前追溯补插到第12帧理论上有一些回滚方案可以尝试但帧同步体系通常走的是“一旦确定了输入顺序就绝不回头改历史”的路线因为一旦允许回溯修改历史状态就要引入回滚、重算、状态快照等一堆东西复杂度会急剧上升。2. 固定输入延迟用“等一拍”换“绝对一致”既然网络延迟必然存在输入到达各客户端的时间必然不一样那怎么办最直接的办法就是不追求“所有人都立刻收到”而是追求“所有人都在同一个指定时刻提交输入”。这个指定的“提交时刻”不是收到输入的那一刻而是收到输入之后再过一段固定时间。这段固定时间就是固定输入延迟。2.1 核心机制让所有玩家站在同一条时间线上固定输入延迟的思路可以这样理解每个客户端产生输入时不立刻把它丢进当前的逻辑帧处理而是先把输入放进一个“缓冲区”等满了N个逻辑帧之后再把这个输入提交给模拟逻辑。这个N就是固定输入延迟的帧数。打个比方大家约好“你按下按键之后不管网络快慢咱们都把这个动作算在3拍之后”。这样一来只要对应玩家的输入能在3拍之内送到其他客户端那么所有人都会在同一拍上看到“这个人出拳了”。这3拍的存在就是给网络传输留出的“宽限期”。在实际产品里固定输入延迟往往和帧同步的“等帧”机制配合。帧同步要求每一帧开始时每个客户端都必须拿到该帧所有玩家或者所有角色的输入才能推进模拟。如果缺了某个输入就得等。但如果加了固定输入延迟相当于提前“预支”了未来的输入给了网络一个缓冲时间绝大多数情况下不会走到“干等”这一步。2.2 为什么必须是固定延迟而不是动态延迟有人可能会想既然要留缓冲那干脆做成动态的呗网络快的时候延迟小一点网络慢的时候延迟大一点体验不是更好吗这个想法听起来合理但在帧同步体系里会带来一个致命问题如果每个客户端根据自己当前的网络状况动态调整“输入生效的等待时间”那不同客户端对于“同一个输入应该在哪一帧生效”的判定标准就不同了。A网络好可能等2帧就生效了B网络差等了5帧才生效。结果就是同一个输入在不同客户端被安排到了不同的逻辑帧上确定性再次被破坏。固定延迟的“固定”二字本质上是保证“全局时间线的统一”。所有客户端都按同一个N帧标准来大家就在同一时刻对同一个输入做同样的处理。这是帧同步能成立的基石之一。那么动态延迟就彻底不可用吗也不是。如果你做的就是类似动作游戏那种“单机为主、联机异步”的玩法不要求严格确定性动态延迟当然可以。但如果你明确要做“多人在线实时对战”要求所有客户端状态一致那固定延迟基本是绕不开的。总而言之固定延迟换来的不是“最优体验”而是“最稳定的确定性”。2.3 固定输入延迟与“输入手感”的关系说到这里“角色为什么等一会儿才出招”的答案就清楚了这个“等”就是固定输入延迟在起作用。延迟帧数越多玩家按下一键到角色真正行动之间的逻辑间隔就越长体感上就越“肉”。但这里有一个容易被忽略的点逻辑层面的延迟不一定要直接呈现在画面上。渲染层可以做到“本地按了就立刻播动作”只是逻辑判定要等到指定帧才真正生效。比如你按下攻击键角色立刻播放挥拳动画看起来非常跟手但这一拳真正产生伤害判定是在几个逻辑帧之后。画面可以先演逻辑得按规矩来。这种“渲染表现与逻辑判定分离”的做法是帧同步游戏里非常常见的优化手段。所以固定输入延迟到底让玩家“体感延迟”增加了多少取决于你怎么做渲染表现。如果直接把逻辑层的延迟暴露给画面那手感一定不好如果做渲染插值和本地表现预测手感可以得到很大改善。3. “延迟几帧”究竟怎么定我的参数选型方法固定输入延迟不是一个拍脑袋定的数它需要根据逻辑帧率、网络延迟分布、产品类型、甚至匹配策略来综合决定。很多人刚开始做帧同步时最常问的就是咱们这游戏设几帧延迟合适这个问题没有标准答案但有标准的求解路径。3.1 先看逻辑帧率帧时长才是计算基准一切延迟帧数的计算都要先落到“一个逻辑帧是多少毫秒”这个基准上。常见帧同步游戏的逻辑帧率有三档15Hz逻辑帧时长66.7ms适合慢节奏RTS或战棋类20Hz逻辑帧时长50ms适合多数MOBA、卡牌对战、部分格斗游戏30Hz逻辑帧时长33.3ms适合追求细腻判定的格斗和动作游戏。知道了帧时长才能把“网络延迟”换算成“延迟帧数”。比如一个逻辑帧是50ms你的网络延迟是40ms那么理论上1帧的缓冲就能覆盖如果网络延迟是80ms那1帧就不够了至少需要2帧。这里要注意所谓“网络延迟”在帧同步语境下通常指的是“单向延迟”也就是一个输入从A客户端发出到B客户端收到所花的时间不是Ping值。Ping值是往返时间RTT大致等于两倍的单向延迟但还要加上服务器处理和丢包重传的时间。我建议做参数设计时统一以“单向传输时间”为计算基准。3.2 用“延迟区间”而不是“平均RTT”来定延迟帧数很多团队喜欢统计线上玩家的平均网络延迟然后根据这个平均值去定固定延迟帧数。这是我在多个项目里看到的最容易踩的坑之一。平均延迟只能代表“大多数情况还行”但帧同步的稳定性靠的不是大多数情况而是极端情况。更合理的做法是看延迟分布尤其是P95甚至P99延迟。什么意思简单说把一局游戏里所有玩家的网络延迟从低到高排成一列95%的延迟都小于某个值这个值就是P95延迟。如果你用平均延迟去做固定延迟帧数可能表面上覆盖了70%的玩家但剩下30%的玩家会因为延迟超过缓冲时间而频繁触发等待机制游戏体验会非常糟糕。举个例子假设20Hz逻辑帧即每帧50ms。统计线上延迟分布平均RTT是60ms单向约30ms但P95的单向延迟是95ms。如果你只按平均延迟来设2帧100ms的固定延迟看起来够用但P95那部分玩家的单向延迟就已经接近95ms加上抖动和丢包很容易超过100ms于是每一段时间都要等。如果按P95来设计延迟就需要设到3帧150ms才能保证95%的情况下不触发等待机制。我个人在做参数选型时会先定两个阈值“期望覆盖的延迟上限”和“可容忍的超时比例”。比如覆盖90%或者95%的延迟情况然后换算成帧数再额外加一个安全余量帧。3.3 分档配置不同网络质量区间的推荐值线上玩家的网络差异很大一档固定延迟很难同时满足低延迟玩家和高延迟玩家。在实践中我倾向于按延迟分档在匹配时把网络情况接近的玩家放到一起每档用不同的固定输入延迟帧数。这里分享一份我常用的分档参考表以20Hz逻辑帧、帧时长50ms为例档位覆盖的单向延迟范围固定输入延迟帧数体感说明低延迟档0~40ms2帧100ms手感最为跟手适合同城或电信联通互连较好的对局中延迟档40~80ms3帧150ms大部分跨网、跨地区对局适用手感略有延迟感高延迟档80~120ms4帧200ms适用于网络较差或国际链路场景操作响应变“肉”极端档120ms以上5帧250ms只建议作为兜底匹配时应尽量避免形成这种对局为什么低延迟档也要2帧而不是1帧因为1帧50ms的缓冲只覆盖了极短的单向延迟但实际的网络抖动、突发拥塞、设备性能波动都会额外消耗时间。1帧几乎没有容错空间只要有一次超过50ms的卡顿整局就开始等待。2帧其实是一个“保底手感”和“可接受稳定性”之间比较公认的平衡点。3.4 延迟帧数与匹配系统的联动固定输入延迟不是只存在于某局游戏内的参数它还应该反推给匹配系统。在我做过的项目中匹配逻辑会预估每个玩家的网络延迟档位然后尽量把同一档位或相邻档位的玩家匹配到一起。这样所有玩家都用同一个固定输入延迟帧数体验一致不会出现一个2帧延迟的玩家和一个4帧延迟的玩家互相对战。如果匹配系统完全不管延迟那就会出现一个很尴尬的场景A玩家2帧延迟B玩家4帧延迟。同一个输入A这边生效得快B那边生效得慢两边的操作节奏完全不一样对局体验差到离谱。所以固定输入延迟从来不是一个独立参数它和匹配、延迟显示、网络质量评估都是联动的一个整体。4. 落地实现输入缓冲与逻辑推进的代码级设计理论聊了不少该到落地环节了。这一部分我会从数据结构、推进逻辑、异常处理几个方面拆开讲给出一个比较通用的实现框架。4.1 输入缓冲区把输入“延后”放进去固定输入延迟的底层是一个输入缓冲区通常按“逻辑帧索引”来组织。每个客户端本地维护一个当前逻辑帧序号 currentFrame同时维护一个环形缓冲区缓冲区里保存“未来若干个逻辑帧”的输入集合。伪代码大致如下// 输入缓冲按帧序号存储 std::mapint, PlayerInput inputBuffer; // key: 目标逻辑帧序号, value: 输入 // 固定延迟帧数 const int kFixedInputDelayFrames 3; // 每帧采集本地输入时 void OnLocalInputCaptured(PlayerInput input) { int targetFrame currentFrame kFixedInputDelayFrames; inputBuffer[targetFrame] input; // 存入目标帧的槽位 } // 从网络收到其他客户端输入时带帧号 void OnRemoteInputReceived(int remoteFrame, PlayerInput input) { inputBuffer[remoteFrame] input; }这里的核心逻辑是本地玩家按下按键的瞬间这个输入并不进入当前帧而是被登记到“当前帧N帧”的位置。远程玩家那边输入数据包里会带上它属于哪个逻辑帧收到后直接放入对应帧的槽位。4.2 模拟推进到点才消费缓冲里的输入每一帧模拟开始前游戏需要检查当前帧的所有输入是否齐全。对于本地玩家因为我们已经提前N帧把输入存好了所以到当前帧时一定拿得到。对于远程玩家就要看对方的输入包是否已经到达。如果都齐全就正常推进一个逻辑帧如果缺失就要走等待或兜底逻辑。推进逻辑的伪代码bool CanAdvanceFrame(int frame) { for (auto player : players) { if (!inputBuffer.count(frame)) { return false; // 某个玩家的输入还没到 } } return true; } void SimulationTick() { if (CanAdvanceFrame(currentFrame)) { ApplyInputs(inputBuffer[currentFrame]); SimulateWorld(); currentFrame; } else { // 输入不齐不推进逻辑帧 WaitOrHandleMissingInputs(); } }这里有一个容易忽略的细节模拟推进必须在固定时间步长下进行最好放在独立的SimulationTick里而不是直接在渲染循环里推进。否则渲染帧率波动会污染逻辑帧率。常见做法是在Unity或Unreal里用FixedUpdate固定时间步回调或者在自研引擎里用累加器来确保逻辑帧的触发间隔稳定。4.3 缺失输入与等待超时必须有一套完整的兜底策略固定输入延迟的存在不是为了保证“永远不会缺输入”而是为了“绝大多数情况下不缺”。一旦出现缺输入比如某个玩家延迟突刺、或者掉线重连如果没有兜底策略游戏就会永远卡在当前帧所有人干瞪眼。大致有三种兜底策略实际项目里经常组合使用第一种是“重复上一次输入”。也就是如果第F帧A玩家的输入缺失就默认使用A玩家在F-1帧的输入。对移动类操作用这个策略效果还行但如果在格斗游戏里玩家正在蓄力、正在防御重复上一帧输入可能导致错误的动作体验会比较怪。第二种是“暂停逻辑模拟”。所有客户端都停下来等到缺失的输入到达再继续。这种办法保证了严谨的确定性但一局游戏里如果频繁暂停玩家会感觉“世界卡住了”。通常只在超时时间较短比如几百毫秒以内时用。第三种是“对缺少的输入做标记并跳过该帧的某些依赖”。这种做法适合非关键交互比如只有移动和朝向没有精确判定的游戏可以容忍某帧输入缺失带来的微小状态不一致。但在严格帧同步的格斗或MOBA里我不太推荐因为一旦“跳过某帧”边界条件就对不上了。实际项目中我一般这样分工正常情况下固定输入延迟兜底万一真的超时先用“重复上次输入”撑一小段时间同时向缺失方发送重传请求如果超过设定的超时阈值比如200~300ms则进入短暂的逻辑暂停等待重传数据到达。这里有个原则宁可让所有人等也不要让某几个客户端单独往下跑一旦分叉整局就废了。4.4 渲染表现与逻辑判定的拆分逻辑层的固定输入延迟比较死板但渲染层可以做得灵活。为了让玩家的操作手感尽量跟手渲染表现上通常会做两件事。第一是“本地预表现”。本地玩家按下攻击键时角色立即播放攻击前摇动画画面完全不等待。逻辑层的伤害判定和状态切换则按固定延迟去走。因为前摇动画本身就有一定时长所以本地预表现并不会让玩家觉得“动画和实际不同步”反而会觉得响应很快。这能有效缓解固定延迟带来的“手跟不上脑”的感觉。第二是“渲染插值”。对于远程玩家他们的位置、状态是基于逻辑帧更新出来的但逻辑帧更新频率低比如20Hz直接拿逻辑状态渲染画面会显得一顿一顿的。所以渲染层会在两个逻辑状态之间做插值让画面平滑。这里要注意插值只能做视觉层面的平滑不能反向去影响逻辑状态。渲染层看到的是“中间值”逻辑层看到的永远只有“逻辑帧快照”。5. 上线前必须做的网络压测与常见坑固定输入延迟说得再好听也要经过真实网络的检验。我见过不少团队参数是按公式算好了代码也写完了结果一上线上测试就卡顿不断。原因往往是测试时没有做足够的网络模拟或者监控指标不完善。5.1 用网络模拟器做可控实验做帧同步开发最好在开发早期就准备一个网络模拟层。它能人为设置全局或针对某个客户端的网络延迟、抖动、丢包率、带宽上限。这样你可以在本地环境里模拟出“东南亚跨网对局”“弱网Wi-Fi环境”等场景提前把坑踩掉。具体做法是在网络层包一层虚拟网络设备拦截所有P2P或客户端到服务器的UDP/TCP包在转发时延迟若干毫秒、随机丢包若干百分比。然后你在本地开两个甚至三个客户端以不同延迟配置跑同一局游戏观察固定输入延迟是否覆盖得住。我习惯用一组标准测试用例前30秒设50ms延迟、0%丢包中间30秒设100ms延迟、5%丢包最后30秒设150ms延迟、10%丢包。每个阶段都记录全局模拟推进是否卡顿、本地输入延迟是否超出预期。这套流程能快速暴露出“延迟帧数设太低”或“重传策略不够快”的问题。5.2 常见问题与排查方法速查现象可能原因排查思路对局中所有客户端同时卡顿固定输入延迟帧数不足以覆盖网络延迟峰值查看日志中“等待输入”次数提高延迟帧数或优化匹配只有部分客户端卡顿弱网玩家的输入频繁丢包或迟到检查该玩家的报文到达间隔优化重传策略本地操作响应很肉但网络并不差延迟帧数设置过高或逻辑帧率太低观察体感延迟适当降低固定延迟帧数或提升逻辑帧率角色形象渲染一顿一顿逻辑帧率低但缺少渲染插值检查渲染层是否做了位置/状态插值掉线玩家重连后状态对不上重连恢复逻辑缺失或快照不及时补充定期状态快照重连后从快照恢复再增量重放如果发现所有客户端一起卡顿先不要怀疑网络设备优先检查“等待输入”的计数器。把这个计数打点到日志或dashboard里一局打完看总数。如果总数不多说明延迟覆盖得还不错如果总数很高那固定输入延迟参数肯定有问题。5.3 监控指标判断延迟帧数合不合理要真正判断参数选得对不对不能只靠“感觉流畅”。我会关注三个关键指标第一个是“等待帧有无率”。每1000个逻辑帧里有多少次因为输入不齐而没有推进。这个指标最好低于0.5%一旦高于1%玩家就会明显感觉到卡顿。第二个是“输入提交延迟贡献占比”。固定输入延迟帧数在总延迟里占了多少毫秒这个数字要和玩家的体感预期对齐。比如逻辑帧率20Hz下设置3帧等于150ms加上网络等其它延迟整体体感可能到200ms。如果玩家习惯60ms的本地游戏突然变成200ms差异感是很强烈的。第三个是“丢包重传次数”。如果丢包重传频繁发生说明路由质量差靠加大固定输入延迟也救不了。这种情况下需要改用更短的数据包、或者走带冗余的可靠传输协议如KCP FEC从传输层去降低重传延迟。这些监控指标最好都按“延迟档位”维度分开统计。这样一个参数是好是坏就有数据支撑不用靠猜。6. 固定输入延迟之外还能往前再走半步最后聊点我对这套机制的个人看法。固定输入延迟本质上是“用确定性的等待换取确定性的同步”它的代价是操作手感上的延迟。我见过有的项目因为过度重视“确定性”把固定输入延迟设得很大结果手感变得非常迟钝玩家流失严重。也见过有的项目为了让手感爽把延迟帧数压得太低结果线上频繁卡顿口碑崩盘。真正考验开发者的是在“确定性”和“爽快感”之间找到那个让绝大多数玩家都能接受的平衡点。我个人的一条经验是如果条件允许可以试着做“延迟档位匹配 本地预测显示 高清网络监控”这三件事的组合往往比咬着牙去调一个“完美延迟帧数”更实际。固定输入延迟不是万能药它只是帧同步网络模型里一个必备的基础设施。真正让玩家觉得好玩的永远是你围绕着它构建的整套体验设计。如果你正在调这个参数反复试了几组都觉得不对我建议你先别急着继续叠帧数回到逻辑帧率和渲染表现层看一圈。很多时候“手感肉”的问题不只在延迟帧数上也在画面反馈、动画节奏和音效触发的配合上。把渲染表现做顺了同样的固定输入延迟玩家感知到的延迟会低一大截。
返回列表