4.5 预测模式推广:开火、交互与自定义预测系统
CMC 解决了「移动」这一块的预测。但游戏里还有大量「需要即时反馈、服务器又要权威」的操作:开火、开宝箱、放技能、上车。这些场景的预测思路是一模一样的四步。
预测的四步模板
① 本地执行:客户端立即执行操作(播动画/产生本地表现)
② 记录证据:保存「预测时的输入、时间戳、序列号」
③ 服务器裁决:服务器按权威逻辑计算结果
④ 对账校正:客户端比较预测结果与服务器结果,不一致则回滚重放
移动里的 ①②③④ 是 CMC 自动完成的。其他场景需要自己实现,但骨架相同。
例子:预测式开火
// 客户端:按下开火键
void UWeaponComponent::StartFire()
{
if (GetOwnerRole() == ROLE_Authority) { Fire(); return; }
// 客户端预测:
PlayFireAnimation(); // ① 立即播(手感)
PredictedFireSeq++; // ② 记录序列号
ServerFire(PredictedFireSeq, AimDir); // ③ 发给服务器裁决
SpawnLocalTracer(AimDir); // 弹道特效立即播
}
服务器:
void UWeaponComponent::ServerFire_Implementation(uint32 Seq, FVector Dir)
{
if (!ConsumeAmmo()) { ClientFireResult(Seq, false); return; }
const bool bHit = DoHitScan(Dir); // 服务器权威判定
ClientFireResult(Seq, true); // ④ 结果回客户端
}
客户端收到结果:
void UWeaponComponent::ClientFireResult_Implementation(uint32 Seq, bool bSuccess)
{
if (Seq <= LastConfirmedSeq) return; // 过时结果,忽略
LastConfirmedSeq = Seq;
if (!bSuccess) PlayDryFireSound(); // 预测错误:没子弹
}
注意:伤害数字不在预测范围内(必须服务器算),只有「开火这一下的动画和音效」做预测。
什么时候不该预测
预测有适用边界:
- 结果有随机性 / 依赖服务器持久状态:开宝箱出什么、抽卡结果——必须等服务器。预测错了再改,玩家暴怒。
- 操作会被别的玩家/世界状态否决:开门(门正在被别人开)、抢装备。预测成功但服务器拒绝 → 玩家看到「开了又关上」的抽搐。此时适合「短预测」——只播 0.1-0.2 秒的启动动画,结果仍由服务器驱动。
- 纯表现高频(如持续激光渲染):预测即可,反正下一帧就校正。
对账频率与成本
每次预测都带一个 Seq → 服务器每次回结果 → 客户端对账。高频操作不要每帧对账:可以攒 N 帧一次,或只在「结果与预测不符」时才回。CMC 的做法可以参考:它不是每次都广播,而是有误差才校正。
失败检测与回滚的体感设计
预测失败时玩家会看到「我明显操作了但没生效」。缓解手段:
- 校正动画:别直接瞬移/掐断动画,放一个小过渡(0.05s 的 blend)
- 音效兜底:预测失败时播「被拒」音效(上膛空响),玩家心智上接受「是弹夹空了不是我卡了」
- 结果延迟显示:伤害飘字永远以服务器值为准,不预测
一个反面案例
某项目的开门系统:客户端预测播放了开门动画 1.2 秒,结果 200ms 后服务器判定「你距离不够」(网络波动导致位置偏差)→ 门播了一半被服务器拉回。玩家看到门「抽搐」。修复:预测范围缩短到「手放上把手」的前 0.15 秒,门的开启动画完全由服务器的复制状态驱动。预测深度与「服务器能否可靠确认」成正比——越容易被世界状态否决的操作,预测得越浅。
心得:预测是个「度」的艺术。零预测=糊,全预测=抽搐。方法是在「操作 → 服务器确认」的平均往返时间窗口内做文章:只有「大概率会被确认」的部分才预测,不确定的部分留给服务器。CMC 能做到完美预测是因为移动的逻辑几乎只依赖玩家自己的输入——迁移到你的系统时,先问自己「这个操作的结果是不是几乎只由我自己的输入决定」。
