网络预测与回滚
自动关联目录:网络预测与回滚
没有预测,玩家的每个操作都要等一个 RTT 才有反馈(100ms 的移动延迟在手感上是灾难)。预测的做法是:本地先演算,服务端权威,不一致就纠正。
一句话定位:预测是"先用假的结果让它跟手",纠正是"真结果来了把它拉回去"。全部难点都在"怎么拉回去才不明显"。
三种一致性模型
| 模型 | 做法 | 手感 | 适用 |
|---|---|---|---|
| 纯权威 | 所有输入发服务端,等回包 | 差(有延迟) | 回合制、慢节奏 |
| 本地预测 + 服务端纠正 | 本地先跑,不一致就修正 | 好 | 射击/动作游戏默认 |
| 完全信任客户端 | 客户端说了算 | 最好 | 无反作弊要求的合作向(有安全风险,见 Security) |
角色移动:UE 已经做好的一套
ACharacter 的 CharacterMovementComponent 内置了完整的预测:
客户端 服务端
├─ 输入 → 立刻本地移动
├─ 把这次移动存进 FSavedMove
├─ 发 ServerMove(时间戳, 输入, 结果位置)
│ ├─ 用同样输入重演一遍
│ ├─ 与客户端结果比对
│ ◄──── ClientAdjustPosition ─────┘ 不一致就下发纠正
├─ 收到纠正 → 把"纠正时刻之后"的 SavedMove 全部重演
└─ 平滑过渡到纠正后的位置| 配置项 | 作用 |
|---|---|
NetworkSmoothingMode | Linear / Exponential / Replay |
NetUpdateFrequency | 服务端下发纠正的频率 |
MaxClientSmoothingDelta | 平滑过渡的位置差上限 |
ClientPositionErrorTolerance | 误差小于此值就不纠正 |
理解了这套流程,就能解释两个经典现象:
- "我在本地撞墙了但被拉回来" → 本地预测认为能走,服务端判定撞墙
- "网络差的时候角色会瞬移" → 误差超过
MaxClientSmoothingDelta,直接硬拉
自己写预测:Prediction Key
在 GAS 里,技能预测靠的是 FPredictionKey:
FPredictionKey Key = FPredictionKey::CreateNewPredictionKey(GetAbilitySystemComponent());
// 客户端本地激活技能 → 带上 Key
// 服务端收到 → 用同一个 Key 执行,回包带上 Key 结果
// 客户端收到同 Key 的确认 → 预测成功;收到 reject → 回滚| 结果 | 处理 |
|---|---|
| 预测成功 | 什么都不用做 |
| 预测被拒 | 回滚本地已播放的表现(移除 cue、回退属性) |
| 服务端额外触发 | 客户端补上之前没预测的部分 |
预测失败必须能回滚,这是设计前提:任何"播放了就收不回"的效果(粒子、音效、UI 弹窗)都不该放进预测路径,否则会看到"闪一下又没了"。
回滚(Rewind / Lag Compensation)
服务端判定时把世界"回退"到客户端当时的视角,用于命中判定。
客户端开枪(本地时间 T)→ 发 ServerFire(T, 瞄准方向)
服务端收到时已是 T+80ms
→ 服务端把所有角色回退到 T-80ms 的位置(需要保存位置历史)
→ 在回退后的世界里做射线检测
→ 判定命中| 组件 | 说明 |
|---|---|
| 位置历史缓冲 | 每个角色保存最近 N 帧的位置与碰撞盒 |
| 时间戳同步 | 客户端与服务端时钟要对齐(UE 有 ServerTime/WorldTime 同步) |
| 校验 | 回退时间不能超出缓冲范围,也不能让客户端任意指定(否则可作弊) |
回滚是反作弊与手感之间的取舍:完全信任客户端时间等于允许"我看到哪打哪",但过度校验会让高延迟玩家打不中。
平滑:让纠正看不见
| 手段 | 说明 |
|---|---|
| 插值 | 对远程角色用 RepNotify + 平滑插值到目标位置 |
| 外推 | 预测下一帧位置,减少停顿感 |
| 缓冲 | 延迟一小段时间再播放,用一小段缓冲吸收抖动 |
| 误差阈值 | 小误差不纠正(避免抖动) |
| 速度平滑 | 位置突变时先改速度而非直接改位置 |
// 远程角色的平滑:在 OnRep 或 Tick 里插值
void AMyCharacter::Tick(float DT)
{
Super::Tick(DT);
if (!IsLocallyControlled())
{
// 目标位置来自属性复制
SetActorLocation(FMath::VInterpTo(GetActorLocation(), ReplicatedTargetLocation, DT, 15.f));
}
}不要对自主端(本地控制的)角色做插值——那会让自己的操作变得粘滞。用 IsLocallyControlled() 区分。
延迟的量化目标
| 延迟 | 手感 | 需要什么 |
|---|---|---|
| < 50ms | 几乎无感 | 无 |
| 50–100ms | 可接受 | 本地预测 |
| 100–200ms | 明显 | 预测 + 平滑 + 可能的回滚 |
| > 200ms | 很难做好 | 应限制匹配区域 |
超过 200ms 就不要指望手感了,工程上应做的是限制跨区匹配,而不是继续优化预测。
排查
| 现象 | 原因 |
|---|---|
| 自己走路有延迟感 | 没有本地预测 / 开了 bDisablePrediction |
| 角色被拉回 | 服务端纠正,本地预测与服务端结果不一致 |
| 网络差时瞬移 | 误差超阈值触发硬拉 |
| 远程角色一顿一顿 | 复制频率低且没有插值 |
| 技能"闪一下又没" | 预测被拒且没有正确回滚 |
| 打不中移动目标 | 没有做回滚,服务端用的是"现在"的位置 |
调试手段:Net PktLoss=1、Net PktLag=100(控制台模拟丢包与延迟),在开发环境里常年开着模拟弱网,否则问题只会在线上暴露。
常见坑
| 坑 | 说明 |
|---|---|
| 对本地角色做插值 | 手感变粘 |
| 预测路径里放不可逆表现 | 回滚时穿帮 |
| 服务端不做权威判定 | 作弊 |
| 依赖客户端时间戳不校验 | 可篡改时间做 lag switch |
| 不在弱网下测试 | 线上才暴露 |
| 复制频率过低 | 远程角色卡顿 |
| 用 RPC 同步位置 | 应该用移动组件或属性复制 |
| 预测失败不回滚属性 | 血量显示与实际不符 |
许可协议:CC BY
作者:Davids
本文链接:https://hustjjd.github.io/430c6993.html
更新于:2026年10月10日