09-客户端预测与服务器校正

3781 字
19 分钟
09-客户端预测与服务器校正

第 9 章:客户端预测与服务器校正#

导入#

上一章把信任边界放在服务器:客户端提交 Request,服务器验证并构造 Command,最终由服务器提交公共战斗事实。但如果客户端在收到 Result 之前完全不动,玩家会明显感到网络往返延迟。

客户端预测解决的是“等待期间本地看起来发生了什么”,不是“客户端先决定了游戏事实”。客户端可以先进入 Casting、播放起手动画、更新本地资源显示、启动冷却倒计时,甚至生成一个待确认的表现投射物;这些变化都必须能够在服务器拒绝或修正时撤销,或者被权威状态覆盖。

因此,预测的核心定义是:

在已知权威基线之上,客户端暂时应用一组可逆的本地变化,并为它们保存足够的证据,以便服务器结果到达后确认、撤销或重算。

这一定义包含两个约束:

  • 预测变化必须有边界。
  • 预测变化必须有记录。

9.1 什么值得预测#

不是所有变化都应该预测。

越可逆、越本地可见、越不影响公共规则,就越适合预测。越不可逆、越影响其他 Actor、越依赖服务器碰撞和规则,就越应该等待权威结果。

9.1.1 火球术适合预测什么#

对本地施法者,推荐预测以下内容:

  • 本地角色进入 Casting;
  • 本地输入锁或施法动作句柄;
  • HUD 中 Mana 的临时显示减少 30;
  • 2 秒冷却 UI 的临时启动;
  • 起手动画、音效和本地相机反馈;
  • 一个带有待确认标记的纯表现投射物。

这些变化服务于手感,且可以在拒绝时通过日志撤销,或在确认时与服务器对象映射。

9.1.2 火球术不应预测为权威事实#

以下内容不能因为本地按键就写成最终事实:

  • 目标的最终 Health;
  • 命中后直接伤害 120 已经发生;
  • 目标已经拥有权威 Burning;
  • 目标已经死亡;
  • 服务器一定生成了某个投射物实例;
  • 其他客户端已经看到命中和伤害。

客户端可以预测自己的瞄准轨迹和表现投射物,但目标的伤害、死亡、效果应用和公共对象生命周期必须等待服务器。

9.2 Prediction Journal:把预测变成可管理的数据#

预测失败通常不是因为没有播放动画,而是因为没有保存“我改过什么”。如果客户端只做:

Mana -= 30
Cooldown.Start(2)
PlayCastAnimation()

服务器拒绝时,代码就必须在别处猜测当前 Mana 是否还被其他预测改变过、冷却是否已经被下一次输入覆盖、动画是否已经进入不可逆阶段。多个技能连续输入后,这种猜测很快会失控。

PredictionJournal 是按预测范围保存的记录集合。它记录本地变化的原因、基线和收束方式,而不是保存一份可以取代服务器状态的“客户端真相”。

9.2.1 一条记录应该包含什么#

建议每条 PredictionEntry 至少包含:

PredictionKey
RequestId
Scope Activation / Impact
BaseAuthorityRevision 创建时所依据的权威基线
LocalOrder 本地预测提交顺序
Status PendingUnknown / Confirmed / Rejected / Corrected
ReversibleChanges 可逆本地变化及其逆操作
ReplayableIntent 仍可重放的输入意图或命令参数
PresentationHandles 动画、音效、表现投射物等句柄

其中 ReversibleChanges 不一定要实现成一堆闭包。闭包容易捕获过期对象和隐含状态。更稳妥的做法是保存结构化操作,例如:

Change:
Resource = Mana
PredictedDelta = -30
Inverse = +30
BaseValue = 100
OverlayValue = 70

这样确认时可以删除 overlay,拒绝时可以根据当前权威基线重建。

9.2.2 预测层和权威层分开#

一个简单的客户端状态模型可以写成:

AuthoritativeState(revision = 77):
Mana = 100
CooldownEnd = 0
PendingPrediction:
Mana overlay = -30
Cooldown overlay = 2 seconds
RenderedState:
Mana = 70
Cooldown = 2 seconds

当另一个权威 Snapshot 把 Mana 更新成 65 时,客户端把 65 作为新基线,根据仍然有效的 pending 预测决定最终显示。

这可以用两种方式实现:

  1. Overlay 模型:权威状态单独保存,待确认变化作为临时层叠加。
  2. 重建模型:每次基线变化时,先清空渲染状态,再应用权威基线和剩余预测。

对于资源、冷却和标签,重建模型通常更容易验证;对于粒子、动画和对象句柄,则需要额外的表现收束逻辑。

9.3 Confirm、Reject、Correct 的语义#

9.3.1 Confirm:预测已经被权威事实覆盖#

Confirm 表示服务器提交的阶段与客户端预测在该协议定义的可预测范围内一致。客户端要做的是把预测记录收束到权威基线:

  1. 找到对应的 PredictionKey 和 RequestId。
  2. 接受 Result 中的 AuthorityRevision 和 AuthoritativeState。
  3. 删除或标记已确认的逆操作。
  4. 把表现投射物映射到服务器对象,或者平滑切换到权威对象。
  5. 不要再次应用一次“Mana -30”或重新创建一套不可逆效果。

9.3.2 Reject:请求没有成为权威事实#

Reject 表示该阶段没有提交。客户端需要逆序回滚对应预测记录,然后应用 Result 随附的权威状态:

  1. 将目标预测记录标记为 Rejected。
  2. 按依赖关系撤销本条记录创建的本地变化。
  3. 以服务器返回的 Mana、冷却和标签覆盖本地基线。
  4. 清理待确认的动画、表现投射物和输入锁。
  5. 重新计算仍处于 pending 的后续预测。

“逆序”很关键。若玩家在火球术之后又预测了一个冲刺,拒绝火球时不能把整个角色状态恢复成最初状态,从而抹掉冲刺。应该先撤销火球自己的变化,再在当前权威基线上重放仍有效的冲刺预测。

9.3.3 Correct:服务器事实存在,但本地不同#

Correct 表示服务器已经提交了事实,但本地预测与之不一致。典型情况包括:

  • 服务器实际资源基线与客户端显示不同;
  • 服务器确定的发射起点或时间与客户端不同;
  • 服务器命中的目标和本地表现目标不同;
  • 权威对象已经存在,但其位置、生命周期或状态与预测不同。

Correct 的处理通常是:

  1. 找到受影响的预测记录和依赖它的较新记录。
  2. 暂时撤销这些预测对渲染状态的影响。
  3. 应用服务器提供的权威 Snapshot 或 Delta。
  4. 保留仍然有效、尚未终结的本地意图。
  5. 在新权威基线上重放这些 pending 预测。
  6. 对动画、音效和 VFX 做表现收束,避免重复播放。

如果本地还有更晚的预测,直接覆盖会让它们消失;如果直接保留旧预测,服务器纠正又会被再次抵消。

9.4 回滚、校正、重放#

这三个词在讨论中经常混用,但实现责任不同。

回滚(Rollback)#

回滚是撤销本地预测。它回答的是:“这个本地临时变化不再有效,怎么把它移除?”回滚不能替代权威同步,也不能假定资源只被这一条预测改变过。

校正(Correction)#

校正是把服务器的事实写入新的本地权威基线。它回答的是:“服务器说现在真实状态是什么?”校正可能是某个属性、冷却、标签、对象位置或阶段状态的完整 Snapshot,也可能是带 revision 的 Delta。

重放(Replay)#

重放是在新权威基线之上重新应用尚未终结且仍然合法的本地意图。它回答的是:“在服务器的新事实之上,较晚的本地输入还应该显示什么?”

一个安全的顺序通常是:

冻结表现写入
│
├─ 收集受影响的 pending entries
├─ 移除它们的临时 overlay / 预测对象
├─ 应用服务器权威基线
├─ 丢弃已拒绝或已过期的意图
├─ 重放仍有效的规则层预测
└─ 收束表现层,不重复触发不可逆演出

规则重放和表现重放要分开。重新计算一次本地冷却显示是规则层重放;重新播放完整起手动画、音效和镜头震动则是表现层行为。后者如果无脑执行,校正一次就会让玩家听到重复声音、看到重复投射物。

9.5 客户端处理 Result 的顺序#

客户端收到 Result 后,不要立即调用“成功回调”。先做协议层过滤:

Receive Result
│
├─ 校验消息版本和 RequestId
├─ 丢弃已经处理的 ResultVersion
├─ 丢弃早于当前 AuthorityRevision 的状态
├─ 暂存依赖未满足的阶段结果
├─ 找到 PredictionKey 对应的 Journal
└─ 根据 Kind 执行 Confirm / Reject / Correct

9.5.1 乱序结果#

假设客户端先收到 Impact,后收到 Activation。如果 Impact 依赖激活创建的服务器能力实例,客户端可以把 Impact 暂存到有限窗口,等激活 Result 或 Snapshot 到达后再处理。如果当前权威 revision 已经超过该 Impact,旧结果就应该丢弃或转为重新同步请求。

旧消息不能因为“它是可靠 RPC”就覆盖新基线。可靠通常只代表传输层尽力交付,不代表你的业务状态机自动完成版本仲裁。

9.5.2 重复结果#

PredictionKey 对应的记录已经是 Confirmed、Rejected 或 Corrected 后,再收到同一 ResultVersion,客户端应当只更新必要的确认游标。

9.5.3 多个 pending 预测#

客户端可能连续预测多个技能:

P1 火球 Activation:Mana -30
P2 冲刺 Activation:Stamina -20
P3 火球 Impact:待确认命中表现

服务器拒绝 P1 时,客户端应该按 BaseAuthorityRevision、LocalOrder 和依赖关系找到受影响记录:撤销 P1,保留合法的 P2,重新评估依赖 P1 的 P3。

9.6 延迟期间的 PendingUnknown#

预测并不消除网络延迟,只是把等待期间的本地体验变得可用。对每个请求,客户端都应保留 PendingUnknown,直到收到明确的 Result、Snapshot 或连接状态终止。

延迟期间可以做有限的表现降级:

  • 显示待确认的资源数字;
  • 让预测投射物使用较弱的材质;
  • 暂不触发依赖命中的公共 UI;
  • 在超过合理窗口后提示连接状态。

但不要自动走 Reject 分支。客户端超时不是服务器拒绝,尤其不能在超时后先退款、再用新 RequestId 重试。

如果服务器返回 Processing,它可以不是最终的 AbilityResultKind,而是一个阶段状态。客户端继续保留 Journal,直到对应的 Confirm、Reject 或 Correct 到达。

9.7 重连时如何处理预测日志#

预测日志依赖某个权威基线和连接期间的对象上下文。连接断开后,这些前提通常都不再可靠。

重连时推荐:

  1. 停止继续向旧连接写入预测记录。
  2. 接收新的 Snapshot 和 AuthorityRevision。
  3. 清除无法由服务器证明仍有效的旧 Journal。
  4. 让 Snapshot 胜出,重建资源、冷却、标签和可见对象。
  5. 仅对协议明确允许恢复的 RequestId 做查询或幂等补发。

不默认重放断线前的本地输入,原因是服务器可能已经处理过,也可能已经错过施法窗口。对于一个已经提交但客户端没收到 Result 的火球请求,正确手段是以原 RequestId 查询或补发,由服务器返回历史结果。

9.8 火球术的三条收束路径#

9.8.1 Confirm:预测和权威一致#

客户端权威基线:

AuthorityRevision = 77
Mana = 100
Cooldown = Ready

按键后,客户端预测:

PredictionKey = 901
RequestId = session-a:000184
Mana overlay = -30
Cooldown overlay = 2 seconds
Casting = true
表现投射物 = pending

服务器验证通过,提交 Mana 从 100 到 70,冷却 2 秒,并返回 Activation + Confirm。客户端接受新的权威 revision,确认本地 Mana 显示为 70,把 pending 投射物映射到服务器投射物,移除本地逆操作。它不会再次执行 Mana -30。

命中时,服务器应用 120 点直接伤害和 Burning 的 5 秒持续时间;客户端根据 Impact Result 更新公共表现。即使本地预测投射物已经看起来命中,也不能提前把目标的 Health 写成最终值。

9.8.2 Reject:服务器没有提交#

客户端在本地显示了 Mana 70 和 2 秒冷却,但服务器发现角色实际上仍处于冷却,返回:

Kind = Reject
Stage = Activation
Code = OnCooldown

客户端对 PredictionKey = 901 执行回滚:

移除 Mana -30 的 overlay
恢复服务器返回的当前 Mana
移除 2 秒冷却的预测显示
取消 Casting 输入锁
销毁或淡出待确认表现投射物

如果同一时间还有较新的冲刺预测,先收束火球,再在服务器当前基线上重新应用仍然有效的冲刺显示。

9.8.3 Correct:服务器提交了不同事实#

假设客户端以旧的权威基线预测火球,但服务器在这期间已经确认了另一个资源消耗,新的权威基线显示 Mana 为 65。服务器仍然确认火球激活,但返回带有 AuthoritativeState 的 Correct:

Kind = Correct
Stage = Activation
AuthoritativeState:
Mana = 65
Cooldown = 2 seconds

如果校正发生在 Impact,服务器的命中对象、发射起点或命中时刻可能与本地不同。客户端应以服务器的 120 点直接伤害和 Burning 5 秒、每 1 秒结算一次的权威事件为准,而不是用本地碰撞结果覆盖目标状态。

9.9 不同变化的预测策略#

变化客户端可否预测服务器结果处理建议
本地Casting 状态可以Activation Confirm/Reject记录句柄,拒绝时移除
Mana 显示减少 30可以,作为 overlay权威 ManaConfirm 删除逆操作,Reject/Correct 重建
2 秒冷却 UI可以权威冷却结束时间不直接相信本地计时,按服务器时间校正
起手动画、音效可以阶段结果允许表现收束,避免重放不可逆演出
本地表现投射物可以服务器投射物Confirm 时映射,Correct 时平滑对齐
目标直接伤害 120不应作为最终事实预测Impact Result只展示待确认表现,等待权威 Health
Burning 5 秒效果不应写入目标权威状态Effect Result/Snapshot服务器应用,客户端同步持续状态
目标死亡不应预测为最终事实权威死亡事件等待服务器,防止错误触发后续奖励
09-客户端预测与服务器校正
https://blog.rouming-fei.top/posts/09-client-prediction-and-server-correction/
作者
废江流
发布于
2026-10-02
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
废江流
提灯寻影,灯到影灭。
分类
标签
站点统计
文章
19
分类
3
标签
16
总字数
62,531
运行时长
0 天
最后活动
0 天前

文章目录