移动预测与回滚(CMC)
「网络有延迟,玩家按下 W 却要等服务器确认才动,操作感就毁了。」UE 的
CharacterMovementComponent(CMC)用一套本地预测 + 服务器复算 + 回滚重放的闭环解决这个矛盾:本地立刻动、服务器权威纠错、错了平滑拉回。这套是所有联机动作游戏手感的地基,也是面试最爱挖的深水区。
一句话结论
本地立即执行并缓存 move,上报服务器复算,服务器只在偏差超阈值时纠正,客户端回滚到纠正点重放未确认的 move。
场景问题
打个比方:你在餐厅点菜,可以选「点一道等厨房确认一道」(等服务器→卡顿),也可以「按记忆中的菜单先自己吃着,厨房账单来了再对账」(本地预测)。CMC 选后者:你按 W 角色立刻走(本地预测),同时把「我从第 100 帧开始按 W 走了 3 帧」记进小本本(SavedMove 队列)发给服务器;服务器按同样规则复算,如果结果和你自己走的差不多就点头,差太多就发回「你其实该在这个位置」(纠正),你回到那个位置,把小本本里还没对账的操作重新走一遍(回滚重放)——因为在等对账期间你可能又按了 A、按了空格。
实现方案
1. 完整闭环
- 本地立即执行:
AutonomousProxy上输入直接驱动移动,0 延迟手感。 FSavedMove_Character:每步移动存一份(输入、Δ时间、结果位置、时间戳),进队列等服务器确认。ServerMove(Server RPC):把 move 上报。服务器是权威,用相同的移动代码复算。- 确认或纠正:服务器复算结果与客户端上报比较,偏差在
MaxPositionError内就确认(客户端丢弃已确认 move);超出就ClientAdjustPosition下发权威位置。 - 回滚重放:客户端收到纠正,把位置设回权威值,然后重新执行队列里所有尚未确认的 move(因为这些操作服务器还没见过,得在纠正后的基础上重新算),得到新的当前位置。
2. 关键函数与参数
ReplicateMoveToServer:客户端打包 move 发给服务器。ServerMoveHandleClientError:服务器端比较、判断是否要纠正的核心。ClientUpdatePositionAfterServerUpdate:客户端收到纠正后触发回滚重放。MaxPositionError(如NetworkMaxSmoothUpdateDistance等相关阈值):偏差容忍度,太小会频繁纠正(抖动),太大会给作弊留空间。NetworkSmoothingMode:SimulatedProxy 表现平滑模式——Disabled(直接跳)/Linear(线性插值)/Exponential(指数逼近,默认,最平滑)。
3. 三种角色的表现差异
| 角色 | 是谁 | 怎么动 |
|---|---|---|
| Authority(服务器) | 权威 | 复算 move,结果是真相 |
| AutonomousProxy(本人客户端) | 我操控的角色 | 本地预测 + 回滚重放,0 延迟手感 |
| SimulatedProxy(别人客户端) | 别人的角色 | 收复制的位置/速度,做插值/外推平滑(SmoothClientPosition),不预测 |
关键区别:只有 AutonomousProxy 做预测回滚,SimulatedProxy 只做插值。你看别人是「过去的平滑回放」,看自己是「即时预测」。这也是 COND_AutonomousOnly/COND_SimulatedOnly 存在的原因,见 属性复制。
4. 移动包带宽优化
移动上报是最高频的网络流量(每帧或近每帧),必须省:
CombineMove:把连续的、方向一致的小 move 合并成一个上报,减少包数。ServerMovePacked(UE5):位打包压缩 move 数据(输入、时间戳、位置),比逐字段发省很多。UE4.26+ 起逐步引入 packed movement RPC。- 上报频率与时间戳告警:客户端上报带时间戳,服务器检测时间戳异常(跳变、过快过慢)——既是防作弊(加速外挂),也是异常连接告警。
5. 症状—根因对照表
| 症状 | 根因 | 排查方向 |
|---|---|---|
| 橡皮筋(走回去) | 服务器频繁纠正,客户端预测和服务器复算长期不一致 | 移动逻辑客户端/服务器路径不一致;MaxPositionError 太小;网络丢包高 |
| 抖动/顿挫 | SimulatedProxy 插值不平滑,或纠正太频繁 | 检查 NetworkSmoothingMode;NetUpdateFrequency 太低;纠正阈值太敏感 |
| 穿墙/瞬移 | 预测与权威差异被强行拉回,或碰撞在两端不一致 | 客户端/服务器碰撞体不一致;重放逻辑有副作用 |
自定义移动的铁律
任何自定义移动(钩爪、冲刺、飞行)都必须保证客户端预测路径和服务器复算路径完全一致——同样的输入、同样的代码、同样的结果。路径不一致 = 服务器每帧都纠正 = 橡皮筋。这是自定义 CMC 最常见的翻车点:只写了客户端表现,没走 SavedMove/复算那套,或两端逻辑有分支差异。
6. 一句话记住这几组容易混的概念
| 一组 | 差别一句话 | 类比 |
|---|---|---|
| 预测 vs 插值 | 预测是「我自己的角色,先动再对账」;插值是「别人的角色,把收到的几个点连成平滑轨迹」 | 自己先吃着 vs 把邮寄来的几张快照连成动画 |
| 纠正 vs 回滚重放 | 纠正是服务器说「你该在这」;回滚重放是回到那个位置后把还没对账的操作重走一遍 | 改掉账面数字 vs 从那笔账起把后面几笔重记 |
| 橡皮筋 vs 抖动 | 橡皮筋是位置被反复拉回(两端逻辑不一致);抖动是画面不平滑(插值/更新频率问题) | 走两步被拽回去 vs 走得一格一格跳 |
| 预测 vs 延迟补偿 | 预测解决「我的动作要立刻响应」;补偿解决「我打别人打不打得中」 | 先自己走 vs 回看录像判越位(见 延迟补偿) |
排障口径也就清楚了:位置被拉回去 → 查两端逻辑是否一致(橡皮筋);位置对但看着卡 → 查插值与 NetUpdateFrequency(抖动)。这两个症状根因完全不同,方向搞反了会白查很久。
为什么这么做
本地预测:延迟是物理现实(光速 + 路由),等服务器确认再动必然有 RTT 的手感延迟。本地立即执行把延迟从操作感里消除——你按键角色马上动。
服务器权威复算:客户端不可信(作弊、丢包、时钟漂移)。服务器用相同逻辑复算并持有真相,才能既给手感又防作弊。
只在超阈值时纠正 + 回滚重放:大多数时候客户端预测和服务器复算一致(同样的代码同样的输入),没必要纠正。只在偏差超阈值才拉回,且回滚后重放未确认 move,保证纠正不丢失玩家在延迟窗口内的后续操作——否则你按的 A、跳跃会凭空消失。
为什么别的选择不行
- 不预测,等服务器确认再动:每次操作都有 RTT 延迟,动作游戏手感直接死。
- 纯客户端权威(信客户端位置):瞬移、加速、穿墙外挂全放行,见 反作弊。
- 每次都纠正(哪怕偏差极小):频繁拉回 = 橡皮筋和抖动,且浪费带宽。
- 纠正后不重放未确认 move:延迟窗口内玩家的后续操作全丢,表现为「回退 + 卡顿」。
- 自定义移动两端逻辑不一致:服务器复算结果永远和客户端不同,持续纠正=永久橡皮筋。
沉淀结论
CMC 闭环:本地立即执行 → SavedMove 入队 → ServerMove 上报 → 服务器相同逻辑复算 → 偏差超 MaxPositionError 才 ClientAdjustPosition → 客户端回滚到权威位置并重放未确认 move。三角色:Authority 复算权威、AutonomousProxy 预测回滚、SimulatedProxy 插值。移动包靠 CombineMove/packed 省带宽。铁律:自定义移动两端逻辑必须一致,否则橡皮筋。
记忆口诀
闭环:本地立即动 / SavedMove 入队 / ServerMove 上报 / 服务器复算 / 超阈值纠正 / 回滚重放未确认
函数:ReplicateMoveToServer / ServerMoveHandleClientError / ClientUpdatePositionAfterServerUpdate / MaxPositionError
三角色:Authority 权威复算 / AutonomousProxy 预测回滚 / SimulatedProxy 插值(SmoothClientPosition)
省带宽:CombineMove 合并 / ServerMovePacked 位打包 / 时间戳防加速外挂
症状:橡皮筋=两端不一致 / 抖动=插值/纠正问题 / 穿墙=碰撞不一致
铁律:自定义移动客户端服务器路径必须一致
内容来源
综合整理。主要参考:Epic 官方文档 Character Movement Component、Networked Movement、UE 源码 CharacterMovementComponent.cpp(FSavedMove_Character/ServerMove/ClientAdjustPosition)、社区经典 GDC 分享与 Cedric Neukirchen《Multiplayer Compendium》。
相关专题:AutonomousProxy/SimulatedProxy 角色定义见 Gameplay Framework;ServerMove 作为 Server RPC 的调用前提见 RPC;
COND_AutonomousOnly/COND_SimulatedOnly见 属性复制;命中判定的延迟补偿见 延迟补偿;作弊防护见 服务器权威与反作弊。
自测:合上资料能说清楚吗?
CMC 的预测—纠正—回滚闭环完整流程是什么?
参考答案
① 客户端输入本地立即移动;② 每步存入 FSavedMove_Character 队列(未确认);③ 通过 ServerMove RPC 上报(时间戳+输入+结果位置);④ 服务器用相同移动代码复算;⑤ 比较客户端结果与复算结果,偏差在 MaxPositionError 内就确认(客户端丢弃已确认 move),超出则 ClientAdjustPosition 下发权威位置;⑥ 客户端回滚到权威位置,重放队列里所有未确认 move 得到新位置。
为什么纠正后要「重放未确认 move」?
参考答案
从上报到收到纠正有一个 RTT 的延迟窗口,这期间客户端又执行了新的 move(继续走、跳、拐弯)。如果只是把位置设回服务器权威值就停,这些后续操作会凭空消失,表现为回退+卡顿。所以要在权威位置基础上把队列里所有服务器还没确认的 move 重新执行一遍,保留玩家意图。
AutonomousProxy 和 SimulatedProxy 的移动表现有什么本质区别?
参考答案
AutonomousProxy(你自己的角色)做本地预测 + 回滚重放,0 延迟手感;SimulatedProxy(别人的角色)不预测,只收复制的位置/速度做插值/外推平滑(SmoothClientPosition,NetworkSmoothingMode)。所以你看自己是即时预测,看别人是「过去的平滑回放」。
玩家反馈「橡皮筋」,怎么排查?
参考答案
橡皮筋 = 服务器频繁纠正,即客户端预测和服务器复算长期不一致。查:① 移动逻辑客户端/服务器路径是否一致(自定义移动最常见,只写了客户端表现没走复算);② 碰撞体两端是否一致;③ MaxPositionError 是否太小导致过度敏感;④ 网络丢包是否过高。铁律是自定义移动两端逻辑必须完全一致。