Combo 容错的计算
上一篇文章把问题定义成了一个窗口判断:从需要回看的 B 过去位置开始,一直到 B 收到 A 造成的击退速度包。在这篇文章里,我们进一步问:根据这个窗口,A 应该怎样移动和出手,才能稳定开启或维持 Combo?
这里讨论的是操作条件和几何关系,不介绍自动化策略,也不使用模拟器中的具体配置。
文中的源码对应关系如下:PlayerControllerMP.attackEntity 创建并发送攻击包;EntityOtherPlayerMP.onLivingUpdate 让远端实体从当前位置向同步目标插值;NetHandlerPlayClient.handleEntityVelocity 收到速度包后,把击退速度写入客户端实体的 motion(实体内部保存的速度分量)。它们共同决定了“出手时看到什么”和“对手收到击退前看到什么”。
一、真正需要控制的是哪一拍
在整个窗口里,A 不需要每一拍都处于攻击距离。A 只需要在自己发送攻击包的那一拍进入攻击范围,攻击包就会沿网络发送出去。
因此,实战中最关键的是 A 出手的这一 tick。通常 A 会在这一拍最靠近 B:A 正在向前移动,B 也可能正在向后移动,双方的相对距离达到一个局部最小值。只要这一拍 A 能命中 B,后面的窗口检查就转化为另一个问题:B 在收到击退速度之前,是否仍有机会命中 A。
为了让符号清楚,记:
这是 A 出手时看到的距离。记 B 的本地攻击范围为 ,A 的攻击范围为 。A 出手的基本条件是:
后文单独写 时,指的就是出手时刻 的这个距离;带有 的写法表示把两端放在同一个窗口 tick 比较。
如果只用水平一维近似,就是 A 与自己画面中的 B 的水平距离不能超过攻击范围。实际游戏仍然要使用准星射线、包围盒和视线方向。
在原版 1.8.9 的普通近战场景里,可以把这个水平攻击距离近似看成 3 格;这里的“3 格”是理解公式的基准,不替代实际客户端根据准星和实体包围盒做出的检测。
先约定后文的时间记号: 是 A 客户端发出攻击包的 tick; 是 A 出手时看到的 B 的过去位置所对应的 tick; 是 B 客户端收到这次攻击造成的击退速度包的 tick。危险窗口从 延伸到 ,两端之间的完整 tick 数记为 。
二、无延迟时,A 看到的三格也不等于 B 看到的三格
这一节先讨论一个理想基线:位置包没有额外的网络传输时间,量化误差也暂时忽略。也就是说,双方都能立即得到服务器的最新位置;但远端玩家仍然要经过 EntityOtherPlayerMP 的三 tick 插值,所以两边的显示位置仍可能不同。网络位置年龄会在下一节作为单独的参数加入。
常见的直觉是:A 出手时距离 B 不超过攻击范围,双方距离大约三格,所以 B 也应该处于同样的距离。但 A 和 B 使用的不是同一组坐标。
- A 使用自己的本地位置,以及 A 客户端里经过插值的 B 的显示位置;
- B 使用自己的本地位置,以及 B 客户端里经过插值的 A 的显示位置。
两边的远端实体都不是服务器的实时位置。服务器位置先量化为 格,再通过位置包发送;EntityOtherPlayerMP 收到目标后,用三 tick 插值逐步更新显示位置。由于玩家每 tick 的位移可能变化,显示位置留下的滞后也会随最近几 tick 的运动变化。网络延迟会让位置包携带更早的服务器位置,从而进一步增加这段位置年龄。
记 A 在自己的客户端看到的 B 的滞后量为 ,记 B 在自己的客户端看到的 A 的滞后量为 。沿着 A 到 B 的水平正方向,在双方仍未交错时,有:
其中 是时刻 两名玩家真实位置的距离,、 分别是两端在同一时刻看到的距离。两者相减得到:
这就是第一个容错来源:双方看到的远端玩家都带有各自的插值滞后。A 出手时看到的距离,可以比 B 之后看到的距离更近;差值大约等于两边远端位置滞后的总和。
三、速度决定插值滞后有多大
EntityOtherPlayerMP 的三 tick 插值不是固定向量,而是在当前显示位置和目标位置之间逐步靠近。运动越快、加速越明显,显示位置追赶目标所留下的滞后就越大。
速度不是恒定的。按照 EntityOtherPlayerMP 的三 tick 插值,可以用最近几 tick 的历史位移加权近似滞后。令 表示玩家 在 tick 的实际位移,按距观察时刻从近到远排列;令 表示位置包到达观察端前经历的网络位置年龄。无额外网络年龄时取 ;存在网络延迟时,把它作为历史索引的偏移量。忽略量化误差时,五项近似为:
收包相位的权重为:
无包相位的权重为:
实际使用哪一组,取决于该 tick 是否刚好收到位置更新。速度突然改变时,最近一项的权重最大,所以 A 攻击时附近的历史位移和 B 危险窗口开始时附近的历史位移会直接影响两边的插值滞后。A 越向前加速,B 端看到的 A 越容易落后;B 越向后加速,A 端看到的 B 越容易落后。
例如,某一端最近五个 tick 的同向位移依次为 格。如果这一刻恰好收到位置包,三 tick 插值给出的滞后近似为
如果没有收到位置包,则使用另一组权重,结果约为 格。两次计算使用的是同一组历史位移,但收包相位不同;速度正在变化时,不能用“当前速度乘一个固定 tick 数”替代它。
这里的两个滞后对应的不是同一个窗口端点。A 判定能否攻击时,看到的是窗口开始处、也就是 附近的 B;因此使用 。B 判定 A 的攻击距离时,对应的是 A 发出攻击的那一刻;A 的攻击包和该 tick 的位置同步属于同一客户端状态,所以用上帝视角记为 ,对应的滞后是 。
正式计算时, 和 都使用上面的历史位移加权;不能把速度替换成一个恒定值。
这个加权式是三 tick 插值的近似,不是原版的恒等式。速度刚发生突变、位置包发送不规律、玩家落地或转向时,应使用每个客户端真实的 display、target 和剩余插值步数;位置量化还会带来最多约 格的单包误差。
四、网络延迟怎样把“手”变长
A 出手时,A 使用的是窗口开始时已经同步到自己客户端的 B 显示位置。A 的攻击包还要经过 A 到服务器的链路,服务器处理后再经过服务器到 B 的链路,才会让 B 收到击退速度。
危险窗口由两个连续区段组成:A 看到 B 的过去位置到 A 出手的前置区段,以及攻击包从 A 发出到 B 收到击退速度的在途区段。两段连接起来,统一从 计算到 。
设 为服务器处理这次攻击的 tick。前文已经定义 、、 和 。
因此窗口长度可以写成:
在这 个 tick 中,B 还会继续向后移动。A 的攻击包和位置同步都在同一个客户端 tick 中产生:攻击包在处理左键时发出,位置同步在 tick 末尾发出;前者经服务器触发击退,后者再由服务器转发给 B。对 B 端来说,A 的真实前进已经包含在这条位置同步的历史中,不应再作为一个独立的窗口位移项重复加入;A 的运动只通过 B 端远端 A 的插值滞后体现。
记这段完整危险窗口内 B 的总后退位移为 。它指的是从 到 之间 B 实际移动的总距离,而不是网络包中的速度字段。
当 B 收到 A 的攻击包对应的速度时,B 在这一 tick 的准星检测仍使用击退生效前的本地位置;击退速度要到随后移动阶段才第一次改变 B 的位置。A 当时在 B 客户端中仍然是经过插值的远端实体。相对于 A 出手那一刻,B 在窗口中继续向后移动会直接改变 B 的视角距离。若只看这部分,距离增加量近似为:
这部分是窗口移动带来的直接容错。A 的前进不会再单独列成 ;它只会改变 B 端远端 A 的插值滞后量。B 看到的是较早的 A 位置,而不是 A 在收到击退前的实时位置。
五、合并得到 Combo 容错公式
A 出手时看到的距离必须不超过 A 的攻击范围:
B 是否能在窗口中反击,取决于 B 在自己的客户端看到的距离是否曾经降到 B 的攻击范围以内。为了让一次攻击具备稳定的 Combo 条件,需要让窗口内最危险的 B 距离仍大于 。
把两部分差异合在一起,可以得到一个实用的近似关系。窗口开始时 A 端看到的 B 的滞后记为 ,A 出手时 B 端对应的 A 滞后记为 :
其中:
- 是 A 出手 tick 看到的 B 的显示距离;
- 是从窗口开始 到 B 收到击退速度 期间,B 的总后退位移;A 的真实前进不作为独立项加入;
- 是 A 出手时和窗口开始时两个对应状态的远端插值、网络位置年龄造成的视角距离差。
在 B 持续向后、窗口内距离总体增大的情况下,可以把 B 的攻击范围写成一个端点容错条件:
这条不等式表示:即使 A 出手时已经贴近到自己的攻击范围边缘,B 的后退位移和双方的远端显示滞后,合起来仍要把 B 看到的距离推到 B 的攻击范围之外。
把三 tick 历史加权代入后,最终的 Combo 容错条件可以写成:
六、逐项分析容错来源
这个式子中的每一项代表的不是同一种东西。
A 出手时看到的距离
是 A 在攻击那一 tick,用自己的本地位置和画面中 B 的插值位置得到的距离。它必须满足 ,所以在通常 的情况下, 最多只能把左边推到攻击范围边界,不能单独制造超出 的容错。B 在危险窗口中的位移
表示 B 从窗口开始到收到击退速度包之间的总后退位移。这一项通常占容错的最大部分:危险窗口越长,B 后退得越远,B 收到击退前与 A 的距离就越大。两个速度加权项:插值造成的额外距离
来自 A 的插值滞后:
来自 B 的插值滞后:
这两项不是 A、B 在窗口里额外走过的距离,而是三 tick 插值根据历史位移留下的显示滞后。A 的向前运动会让 B 端看到的 A 落后,B 的向后运动会让 A 端看到的 B 落后。它们确实增加容错,但通常没有 那么大。
因此,B 向后移动时会同时带来两份收益:一份是窗口期间实际后退的 ,另一份是 B 的历史后退位移增大后带来的插值滞后项。如果 B 不向后移动,甚至向 A 前进, 会变小甚至抵消;此时主要只剩 A 的本地运动通过插值给出的那一部分容错。在双方攻击范围相同、没有额外几何优势的情况下,这通常不足以稳定满足上面的不等式。
七、一个数值例子
下面用一组具体数据代入公式。假设 A 在 出手,A 看到的 B 的位置属于 ,B 在 收到击退速度包。危险窗口内有 个 tick 的移动区间,并假设 B 在这段时间内始终向后移动,总后退位移为:
双方的攻击范围取 格。A 出手时看到的距离取 格,因此 A 的攻击条件已经满足。
1. 计算 A 的插值滞后
假设 B 端这一刻刚好收到位置包,A 最近五个相关 tick 的位移为 格,使用收包相位权重:
2. 计算 B 的插值滞后
假设 A 端这一刻没有收到位置包,B 最近五个相关 tick 的向后位移为 格,使用无包相位权重:
3. 合并并验证
将四项放回 Combo 容错公式:
因为
所以这个例子满足端点容错条件,余量约为 格。这里的数值也能直接看出各项的大小: 只能达到攻击范围边界, 提供了 格的主要窗口位移,两个插值项合计约 格。若 B 不后退,第一项位移贡献消失,最终结果就会减少 格;若 B 还向 A 移动, 还会进一步变小。
这个例子验证的是端点公式的代入和算术结果。实际判定仍要在窗口内逐 tick 检查 B 的本地位置与 B 眼中的 A 显示位置;只要其中一拍距离不超过 ,就不能把这次攻击视为稳定 Combo。