跳转到内容

第 32 章 · 修正、插值与优雅降级

约 5 分钟 难度:4 动手章

这一章解决:本地预演和 Owner 确认不一致时,哪些状态要硬修,哪些表现可以平滑过渡,哪些特效不回滚。

先看一下

第 29 章允许本地先播反馈,第 30 章让 Owner 回执确认,第 31 章把命中判定降成近似。近似一定会猜错:本地以为技能放出去了,Owner 因冷却拒绝;本地以为打中了,Owner 判定目标已经死亡;本地看到物体在 A 点,Object Sync 下一帧把它拉回 B 点。

这一章会拿到什么

  • 四层修正策略:UI、共识状态、位置、特效
  • 一份平滑修正位置的最小片段
  • 一张「硬修 / 平滑 / 不回滚」决策表
  • 网络拥塞时的降级顺序

依赖前面

  • 第 29 章的本地预演
  • 第 30 章的确认回包
  • 第 31 章的命中边界
  • 第 6 章和第 20 章的网络预算与版本号

「预测错了就回滚」听起来简单,实际会把不同层级混在一起。VRChat 里更稳的做法是按层级修正。

层级例子修正方式
UI 状态按钮、准星、冷却条立即改回或切到确认状态
共识状态分数、HP、阶段、胜负以同步字段为准,硬修
空间位置投射物影子、被同步对象显示位置插值 / 平滑到确认位置
一次性特效音效、火花、挥刀动作通常不回滚,自然结束

这样分层后,代码不需要追求「整个世界回到过去再重放」。本卷的目标是让玩家感到连贯,同时让共识状态保持可恢复。


UI 是最适合硬修的层。Owner 拒绝请求后,按钮恢复、冷却条取消、短提示显示原因。这里不要做长插值,因为 UI 表达的是操作状态,慢慢滑回去反而像卡住。

private void CancelLocalPreview(byte reason)
{
pendingRequestId = -1;
predictedButtonPressed = false;
skillButton.interactable = true;
cooldownFill.fillAmount = 0f;
ShowRejectHint(reason);
}

如果 Owner 接受请求,UI 切到真实冷却。真实冷却的开始时间最好来自同步字段或 Owner 回执里的时间戳,而不是本地按下按钮那一刻。否则高延迟玩家的冷却会比低延迟玩家早结束。


分数、HP、阶段、胜负这类状态没有平滑空间。同步字段到了就应用,旧本地猜测立刻丢弃。

public override void OnDeserialization()
{
if (stateVersion == lastAppliedStateVersion) return;
lastAppliedStateVersion = stateVersion;
hpView.SetValue(currentHP);
scoreView.SetValue(totalScore);
phaseView.Apply(phase);
}

第 20 章的 stateVersion gating 在这里继续生效。修正发生在版本边界上,而不是每个字段各自跳。


位置修正:短距离平滑,长距离瞬移

Section titled “位置修正:短距离平滑,长距离瞬移”

位置类状态适合平滑,但要设阈值。误差小,插值能消掉抖动;误差大,慢慢滑过去会显示穿墙、倒飞、被橡皮筋拉走。

private void UpdateVisualPosition()
{
float d = Vector3.Distance(visualPosition, targetPosition);
visualPosition = d > snapDistance
? targetPosition
: Vector3.Lerp(visualPosition, targetPosition, Time.deltaTime * correctionSpeed);
visualRoot.position = visualPosition;
}

这段用于视觉影子,不直接抢 VRC Object Sync 管的物理对象。官方 VRC Object Sync 自己有插值和瞬移相关 API,例如瞬移时需要标记 discontinuity,避免插值拖尾。第七部讲物理对象时会回到这一点。


音效、火花、挥刀动作大多数时候不回滚。原因很简单:它们已经被玩家感知了。强行停止或倒放,反而更明显。

技能被拒绝时,可以让本地抬手动画自然结束,再用 UI 提示说明失败。命中火花如果已经播出,但 Owner 拒绝伤害,可以不回收火花,只是不播伤害数字、不改 HP。这样玩家看到的是「打出去了但没造成伤害」,比火花突然消失更稳定。

例外是长时长特效,例如持续 5 秒的范围圈、引导光束、长音效。这类特效应有一个可取消入口,收到拒绝或修正时淡出,而不是继续完整播完。


网络拥塞时,不是所有反馈都同等重要。先保共识,再保操作反馈,最后才保装饰。

优先级保留降级
1GameState / PlayerObject 共识字段不降级,只节流发送频率
2发起者本地 UI 反馈保留,但减少远端事件
3关键确认回执保留字段兜底,事件回执可丢
4远端装饰特效降低频率或只本地播放
5复盘 / 战报细节延后写入或合并同步

Networking.IsClogged 可以作为拥塞信号之一。出现拥塞时,停止发送高频装饰事件,不要停止共识字段更新。共识字段如果也过载,说明同步模型本身需要拆字段、降频或重设计。


挑一条试。

  • snapDistance 从 3 改成 0.3。小误差会更快硬修,但视觉抖动会不会变多?
  • 设计一个可取消的长时长特效:Owner 拒绝后 0.2 秒淡出。哪些变量是本地的,哪些要同步?
场景预期实际
1. Owner 拒绝技能UI 立即恢复,音效自然结束,分数 / HP 不变
2. HP 同步值与本地预估不同HP 以同步字段硬修,不做慢速滑动
3. 小位置误差视觉影子平滑靠近目标位置
4. 大位置误差直接 snap,避免长距离穿墙滑动
5. 网络拥塞停止远端装饰事件,保留共识字段和本地 UI
  • 能按 UI、共识状态、空间位置、特效四层选择修正方式
  • 能解释为什么分数 / HP 不做语义平滑
  • 能写出位置误差小插值、大误差瞬移的阈值逻辑
  • 能说明为什么一次性音效通常不回滚
  • 能按优先级做网络拥塞降级