网络预测与回滚

自动关联目录:网络预测与回滚

没有预测,玩家的每个操作都要等一个 RTT 才有反馈(100ms 的移动延迟在手感上是灾难)。预测的做法是:本地先演算,服务端权威,不一致就纠正。

一句话定位:预测是"先用假的结果让它跟手",纠正是"真结果来了把它拉回去"。全部难点都在"怎么拉回去才不明显"。

三种一致性模型

模型做法手感适用
纯权威所有输入发服务端,等回包差(有延迟)回合制、慢节奏
本地预测 + 服务端纠正本地先跑,不一致就修正好射击/动作游戏默认
完全信任客户端客户端说了算最好无反作弊要求的合作向(有安全风险,见 Security)

角色移动:UE 已经做好的一套

ACharacter 的 CharacterMovementComponent 内置了完整的预测:

客户端                              服务端
 ├─ 输入 → 立刻本地移动
 ├─ 把这次移动存进 FSavedMove
 ├─ 发 ServerMove(时间戳, 输入, 结果位置)
 │                                  ├─ 用同样输入重演一遍
 │                                  ├─ 与客户端结果比对
 │  ◄──── ClientAdjustPosition ─────┘ 不一致就下发纠正
 ├─ 收到纠正 → 把"纠正时刻之后"的 SavedMove 全部重演
 └─ 平滑过渡到纠正后的位置
配置项作用
NetworkSmoothingModeLinear / 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 同步位置应该用移动组件或属性复制
预测失败不回滚属性血量显示与实际不符