第 32 章 · 修正、插值与优雅降级
这一章解决:本地预演和 Owner 确认不一致时,哪些状态要硬修,哪些表现可以平滑过渡,哪些特效不回滚。
先看一下
第 29 章允许本地先播反馈,第 30 章让 Owner 回执确认,第 31 章把命中判定降成近似。近似一定会猜错:本地以为技能放出去了,Owner 因冷却拒绝;本地以为打中了,Owner 判定目标已经死亡;本地看到物体在 A 点,Object Sync 下一帧把它拉回 B 点。
这一章会拿到什么
- 四层修正策略:UI、共识状态、位置、特效
- 一份平滑修正位置的最小片段
- 一张「硬修 / 平滑 / 不回滚」决策表
- 网络拥塞时的降级顺序
依赖前面
- 第 29 章的本地预演
- 第 30 章的确认回包
- 第 31 章的命中边界
- 第 6 章和第 20 章的网络预算与版本号
修正不是一刀切
Section titled “修正不是一刀切”「预测错了就回滚」听起来简单,实际会把不同层级混在一起。VRChat 里更稳的做法是按层级修正。
| 层级 | 例子 | 修正方式 |
|---|---|---|
| UI 状态 | 按钮、准星、冷却条 | 立即改回或切到确认状态 |
| 共识状态 | 分数、HP、阶段、胜负 | 以同步字段为准,硬修 |
| 空间位置 | 投射物影子、被同步对象显示位置 | 插值 / 平滑到确认位置 |
| 一次性特效 | 音效、火花、挥刀动作 | 通常不回滚,自然结束 |
这样分层后,代码不需要追求「整个世界回到过去再重放」。本卷的目标是让玩家感到连贯,同时让共识状态保持可恢复。
UI 修正:快而明确
Section titled “UI 修正:快而明确”UI 是最适合硬修的层。Owner 拒绝请求后,按钮恢复、冷却条取消、短提示显示原因。这里不要做长插值,因为 UI 表达的是操作状态,慢慢滑回去反而像卡住。
private void CancelLocalPreview(byte reason){ pendingRequestId = -1; predictedButtonPressed = false; skillButton.interactable = true; cooldownFill.fillAmount = 0f; ShowRejectHint(reason);}如果 Owner 接受请求,UI 切到真实冷却。真实冷却的开始时间最好来自同步字段或 Owner 回执里的时间戳,而不是本地按下按钮那一刻。否则高延迟玩家的冷却会比低延迟玩家早结束。
共识状态:以字段为准
Section titled “共识状态:以字段为准”分数、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 秒的范围圈、引导光束、长音效。这类特效应有一个可取消入口,收到拒绝或修正时淡出,而不是继续完整播完。
网络拥塞时的降级顺序
Section titled “网络拥塞时的降级顺序”网络拥塞时,不是所有反馈都同等重要。先保共识,再保操作反馈,最后才保装饰。
| 优先级 | 保留 | 降级 |
|---|---|---|
| 1 | GameState / 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 不做语义平滑
- 能写出位置误差小插值、大误差瞬移的阈值逻辑
- 能说明为什么一次性音效通常不回滚
- 能按优先级做网络拥塞降级
- VRChat Creator Docs · Network Specs and Tips — Manual / Continuous 同步、带宽限制、
OnPostSerialization的官方说明。 - VRChat Creator Docs · VRC Object Sync — 位置同步和瞬移相关 API。
- 第 20 章 · 版本号、序号与幂等 —
stateVersiongating。 - 第 29 章 · 即时反馈与本地预演 — 本地预演分层。
- 第 31 章 · Lag 补偿的可行边界 — 命中近似的边界。