ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

乐观帧(Optimistic Lockstep):服务器按节拍走,到底好在哪、坏在哪

乐观帧(Optimistic Lockstep):服务器按节拍走,到底好在哪、坏在哪 一句话定位严格锁步服务器等所有人的输入到齐 → 才推进一帧 乐观帧 ★ 服务器按固定节拍推进 → 谁没到用预测填乐观二字的含义是我乐观地假设你的输入马上就到没到我也不等先按我猜的走。核心代码区别就在十几行publicclassOptimisticServer{constintLOGIC_DT_MS66;// 15HzpublicvoidRun(){longnextTickNowMs();while(_running){nextTickLOGIC_DT_MS;SleepUntil(nextTick);// ★ 节拍器到点就走不看人脸色varinputsnewPlayerInput[_playerCount];for(inti0;i_playerCount;i){if(_pending[i].TryTake(_frame,outvarinp)){inputs[i]inp;_lastInput[i]inp;_stats.HitCount[i];}else{inputs[i]Predict(_lastInput[i]);// ★ 没到就猜_stats.PredictCount[i];// ★ 必须埋点}}BroadcastWithRedundancy(_frame,inputs);}}staticPlayerInputPredict(PlayerInputlast)newPlayerInput{MoveDirlast.MoveDir,// ✅ 连续量沿用SkillFlags0,// ★ 离散事件清零TargetSlot0,};}一旦广播出去这一帧就成了历史——迟到的输入直接作废不补、不回滚。优点优点一延迟不再传染最大价值严格锁步对局延迟 max(所有人的延迟) 乐观帧 ★ 对局延迟 服务器节拍恒定场景1 人 460ms 9 人 30ms严格锁步乐观帧好网玩家的逻辑帧率2.3 fps15 fps✅差网玩家2.3 fps15 fps部分输入丢失谁承担代价全部 10 人★ 只有他自己这才是公平的正确定义不是大家一起卡而是谁的网络差谁承担后果。优点二帧率可预测一切都好设计服务器的节拍是恒定的 → 整条链路的时序都变得确定。✅ 表现层插值有了稳定的时间基准不用猜下一帧什么时候来 ✅ Jitter Buffer 能算出精确的目标水位 ✅ 技能 CD、Buff 时长、小兵刷新全部按帧数算处处一致 ✅ 一局 15 分钟 恒定 13500 帧可精确预估回放大小、存储成本严格锁步下这些全是未知数——你不知道这一帧会耗 66ms 还是 431ms。优点三掉线不再是全场事故// 严格锁步必须处理这个人还会不会回来// → 超时设多少设短了误杀设长了全场冻结// ✅ 乐观帧断线的人天然被预测覆盖// 对服务器来说掉线和包慢了没有区别走同一条路径掉线玩家的处理从特殊分支降级成了普通情况——代码复杂度大幅下降。优点四性能开销几乎为零乐观帧 vs 严格锁步的额外成本 ✅ 不需要回滚Rollback ✅ 不需要保存历史状态 ✅ 不需要重演Re-simulation ✅ 服务器依然不跑游戏逻辑只做转发对比一下 GGPO / 回滚网络码那套方案也是先猜后修但猜错了要回滚重算——需要保存 N 帧世界快照CPU 和内存开销都不小。乐观帧是猜错了就算了——0 成本但也 0 修正。优点五服务器实现极简// 整个服务器的核心就是一个定时器 一个 Dictionary// ★ 一台机器能跑几千个房间// ★ 服务器不需要理解游戏规则换个游戏服务器代码不用改缺点缺点一输入会丢而且丢得无声无息这是最直接的代价。玩家按下大招 ↓ 包在路上耽搁了 80ms错过了这一帧的窗口 ↓ ★ 服务器已经广播了他没放技能 ↓ 包到了 → 直接丢弃 ↓ ★ 玩家感受我明明按了技能没出丢失率估算节拍 66ms网络抖动 σ 40ms → 约 2~4% 的输入落在窗口外 网络抖动 σ 100ms → ★ 约 15~20% 的输入丢失15% 的技能按不出来——这对玩家是灾难性的体验。缓解手段必须做// ① 客户端提前量本地帧号跑在服务器前面_localFrame_serverFrame_leadFrames;// leadFrames 2~4// ② 冗余输入一个包带 4 帧丢包率从 5% → 0.0006%// ③ 自适应 lead网差的人自动增大提前量if(missRate0.05f)_leadFramesMath.Min(_leadFrames1,6);⚠️注意 ③ 的代价提前量越大本地操作延迟越高。网差的玩家会同时承受丢输入和高延迟——这是没法两全的。缺点二预测必然出错而且没有修正机制// 预测规则移动沿用上一帧inputs[i].MoveDirlast.MoveDir;出错场景玩家在 t0 往右跑 t66ms 他松开摇杆停下 → ★ 包丢了 t66ms 服务器预测继续往右 → 广播 t132ms 包到了但已经作废 ↓ ★ 玩家看到自己的英雄多往右走了一段然后滑回来 ★ 更糟那一段可能正好撞进敌方塔的范围和状态同步对比差距在这里状态同步乐观帧客户端预测错了✅服务器纠正客户端回滚重演❌没有纠正错就错了代价需要保存历史状态、回滚0 开销乐观帧的哲学是“预测错误也是一种合法的游戏结果。”它不追求还原玩家真实意图只追求所有人看到同一个结果。缺点三节拍频率是一个两难的取舍节拍快30Hz33ms/帧 ✅ 操作响应快 ❌ ★ 输入窗口窄 → 丢输入更频繁 ❌ 包量翻倍弱网更难 节拍慢10Hz100ms/帧 ✅ 输入窗口宽容错高 ✅ 流量低 ❌ ★ 操作响应迟钝最坏 100ms 才被采纳 ❌ 表现层要插值更多容易糊MOBA 通常选 15Hz66ms——因为技能前摇能盖住这个延迟。射击游戏不行60Hz 才够而 60Hz 下乐观帧的输入丢失率会高到不可接受。缺点四表现层必须做大量补救工作乐观帧把复杂度从服务器转移到了客户端表现层。// 必须做的一堆事// ① 本地操作立即响应否则手感极差Presentation.PlayCastAnim(skillId);// 逻辑上还没生效动画先播// ② 逻辑层结果和表现层不一致时要平滑过渡renderPosVector3.Lerp(renderPos,logicPos,0.2f);// ★ 不能瞬移// ③ 预测失败后的回弹要做得不刺眼if((logicPos-renderPos).sqrMagnitudeBIG_GAP)StartSmoothCorrection(0.3f);// 0.3 秒滑过去而不是瞬间拉回这是一个隐藏成本服务器省下来的复杂度最终还是要有人付——付账的是客户端表现层。缺点五不同步的检测变难了严格锁步所有端的输入序列 100% 一致 → 不同步 纯代码 bug 乐观帧 ★ 输入序列本身就含预测值 → 如果预测规则在某个端实现不一致也会不同步所以预测函数必须严格确定性// ❌ 危险预测规则里用了 float 或随机数MoveDir(ushort)(last.MoveDir*0.95f);// ★ 浮点各端可能不同// ✅ 预测必须是纯整数、无状态、完全可复现的MoveDirlast.MoveDir;// 最简单最安全一张表什么时候用场景推荐原因手游 MOBA⭐⭐⭐ 乐观帧技能前摇掩盖延迟弱网用户多RTS星际类⭐⭐ 严格锁步指令延迟是玩法一部分PC 网络稳定格斗游戏⭐⭐⭐回滚GGPO需要帧级精度且必须纠正预测FPS❌ 别用帧同步需要 60Hz 服务器权威防外挂回合制/卡牌⭐⭐ 严格锁步天然可以等正确性优先优缺点总表优点缺点延迟⭐ 不传染好网玩家不受牵连差网玩家同时承受丢输入 高提前量性能⭐ 无回滚0 额外开销—正确性所有端结果一致⭐预测错误无法修正输入完整性—⭐迟到输入直接丢弃服务器⭐ 极简不跑逻辑可跑几千房间—客户端—⭐表现层复杂度大幅上升调试—预测规则也必须确定性最后一句乐观帧的本质是一次明确的价值排序它认为九个人流畅 “一个人的输入不丢”。它不试图还原每个玩家的真实意图它只保证——十台手机看到的永远是同一个世界。哪怕那个世界里你的大招确实没放出去。而工程上真正的难点从来不是实现乐观帧几十行代码而已是把那些被丢掉的输入、被猜错的走位用前摇动画、插值和音效藏到玩家看不见的地方。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进