跳转到内容

第 31 章 · Lag 补偿的可行边界

约 6 分钟 难度:4 动手章

这一章解决:命中、抓取、区域判定遇到延迟时,哪些可以近似补偿,哪些不能写成公平承诺。

先看一下

轻实时玩法里,玩家 A 开枪时在自己屏幕上瞄准了敌人。请求到达 GameState Owner 时,敌人已经移动了一段距离。如果 Owner 用「收到请求那一刻」的位置判定,A 会觉得明明瞄到了却没命中;如果完全信 A 上报的命中,又会给作弊和不同步留下空间。

这一章会拿到什么

  • Lag 补偿原模型需要的两个前提:权威服务器 + 历史状态缓冲
  • VRChat 里四类判定的可行边界:hitscan、投射物、抓取、区域触发
  • 一份「接受客户端命中报告前先检查什么」的片段
  • 一张玩法取舍表:合作 PvE、派对、PvP 分别怎么放宽

依赖前面

  • 第 26 章的轻实时范式和子弹本地预演
  • 第 29 章的本地预演边界
  • 第 30 章的 Owner 确认回包
  • 第 20 章的版本号与请求幂等

传统 Lag compensation(延迟补偿)的核心是服务器历史,而不是开枪客户端的口头报告。服务器保存一段历史状态;收到开火命令后,服务器估算开枪者当时看到的是哪一个历史时间点,把目标碰撞体临时回退到那个时刻做命中检测,再恢复到当前状态。

这个模型至少需要两件事。

第一,服务器是最终权威。客户端只能上报输入,不能决定自己命中了谁。第二,服务器有统一的历史状态缓冲,能回答「100 ms 前目标在哪里」。

VRChat 世界缺这两个前提。GameState Owner 是玩家客户端;每个对象的 Owner 分散在不同玩家手上;Udon 没有自动保存全局历史状态的机制。因此本章只讨论近似:在哪些玩法里可以接受客户端报告,报告前做哪些合理性检查,哪些玩法不应该承诺公平命中。


判定类型VRChat 近似风险
hitscan发起者本地射线命中,向 Owner 上报目标和命中点,Owner 做距离 / 角度 / 冷却检查客户端可伪造命中,需要限制用途
投射物本地预演飞行,Owner 只裁决伤害或结果不同客户端看到的弹道可能不同
抓取 / 拾取依赖对象 Owner 和 VRC Object Sync,必要时请求转移 Owner物理不同步时会出现抢夺错位
区域判定Owner 或本地客户端检测进入区域,再由 GameState 验证阶段和位置范围高频进入 / 离开会撞事件速率和边界抖动

这张表的用法是先定玩法承诺。合作 PvE 可以接受「客户端报告命中 + Owner 合理性检查」,因为目标是顺滑和可玩。竞技 PvP 不适合这样写成公平命中,因为被命中的一方没有可信服务器保护。


hitscan(瞬时射线命中)最像传统 Lag 补偿的场景。VRChat 近似版通常让发起者本地先射线检测,然后把命中报告发给 GameState Owner

public void FireLocalHitscan()
{
if (pendingShotRequestId >= 0) return;
int reqId = NextShotRequestId();
RaycastHit hit;
bool hitSomething = Physics.Raycast(muzzle.position, muzzle.forward, out hit, maxRange);
PlayLocalShotFx(hitSomething, hit.point);
SendShotReport(reqId, hitTargetId, hit.point, muzzle.position, muzzle.forward);
}

Owner 端不能直接接受报告。至少做四个检查:请求是否重复、冷却是否满足、距离是否离谱、角度是否大致合理。

private bool AcceptShotReport(ShotReport r, VRCPlayerApi shooter)
{
if (r.requestId <= lastProcessedShotId[shooter.playerId]) return false;
if (TooSoonToShoot(shooter.playerId)) return false;
if (Vector3.Distance(r.origin, r.hitPoint) > maxRange + rangeSlack) return false;
if (Vector3.Dot(r.direction.normalized, (r.hitPoint - r.origin).normalized) < minAimDot) return false;
return TargetStillAllowed(r.targetId);
}

它的定位是合理性检查:把明显不可能的报告拒绝掉,把正常延迟下的报告放行。反作弊能力要等第 33 章单独讨论。


投射物:同步结果,不同步每一帧

Section titled “投射物:同步结果,不同步每一帧”

投射物更适合「本地预演 + 结果确认」。所有客户端都同步一颗子弹每帧的位置,成本高且容易错位。第 26 章已经把子弹轨迹列为本地预演,不同步。

一颗火球可以这样写:发起者本地生成视觉弹道;同一时间向 Owner 发送 requestId、起点、方向、技能 ID;Owner 检查冷却和资源后接受;命中发生时发起者或目标对象报告命中;Owner 最终写 HP / 分数同步字段,并播确认事件。

这条链路不会让每个客户端看到完全相同的火球轨迹。它只保证最终共识字段一致:谁掉血、分数加多少、敌人是否死亡。

如果火球本身是玩法核心(例如需要躲弹幕),就要降低弹幕数量、降低速度、扩大碰撞体、允许「擦边不算」的宽容区间,别把它设计成高精度竞技弹道。


抓取 / 拾取首先是 Ownership 问题。对象当前 Owner 决定它的同步空间状态;抢夺时先处理 Owner 转移,再处理表现。VRC Object Sync 会同步位置 / 旋转和少量基础物理状态,但不等于同步完整自定义物理规则。第七部会单独展开对象同步和物理。

区域判定通常更稳。合作防守里的「进入治疗圈」「站在占点区域」不需要回退 100 ms。用 Owner 或本地检测进入区域,再让 GameState 检查阶段、队伍、区域 ID 和节流即可。判定形状可以做大一点,边界上加 0.2 到 0.5 秒滞后退出,减少网络和物理抖动造成的来回跳。

区域玩法比精确射击更适合 VRChat 的多人约束。这属于玩法承诺与平台能力匹配。


挑一条试。

  • 把 hitscan 的 minAimDot 调宽。命中更顺后,误判会增加到什么程度?
  • 设计一个区域技能:站在圈内 1 秒后回血。它需要 Lag 补偿吗?哪些字段要同步,哪些只做本地表现?
场景预期实际
1. 低延迟命中敌人本地先播命中特效,Owner 确认后 HP 下降
2. 超出距离上报命中Owner 拒绝,HP 不变,本地预演撤销或自然结束
3. 冷却中连续开火后续报告被拒绝,lastProcessedShotId 推进
4. 迟入玩家加入不补播历史子弹,只看到敌人当前 HP / 死亡状态
5. 两人同时命中同一目标Owner 按请求序号 / 当前 HP 裁决,不让 HP 跌破下限
  • 能说出传统 Lag 补偿需要权威服务器和历史状态缓冲
  • 能解释为什么 VRChat 里只能做近似补偿
  • 能给 hitscan 命中报告列出至少 4 个合理性检查
  • 能区分投射物的视觉轨迹和最终伤害状态
  • 能判断一个玩法是否应该避开高精度 PvP 命中承诺