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。

为了让符号清楚,记:

DA=distance⁡(Alocal,Bdisplay on A) D_A=\operatorname{distance}(A_{\mathrm{local}},B_{\mathrm{display\ on\ A}})

这是 A 出手时看到的距离。记 B 的本地攻击范围为 RBR_B,A 的攻击范围为 RAR_A。A 出手的基本条件是:

DA≤RA D_A\le R_A

后文单独写 DAD_A 时,指的就是出手时刻 tAt_A 的这个距离;带有 (t)(t) 的写法表示把两端放在同一个窗口 tick 比较。

如果只用水平一维近似,就是 A 与自己画面中的 B 的水平距离不能超过攻击范围。实际游戏仍然要使用准星射线、包围盒和视线方向。

在原版 1.8.9 的普通近战场景里,可以把这个水平攻击距离近似看成 3 格;这里的“3 格”是理解公式的基准,不替代实际客户端根据准星和实体包围盒做出的检测。

先约定后文的时间记号:tAt_A 是 A 客户端发出攻击包的 tick;tPt_P 是 A 出手时看到的 B 的过去位置所对应的 tick;tVt_V 是 B 客户端收到这次攻击造成的击退速度包的 tick。危险窗口从 tPt_P 延伸到 tVt_V,两端之间的完整 tick 数记为 N=tV−tPN=t_V-t_P。

二、无延迟时,A 看到的三格也不等于 B 看到的三格

这一节先讨论一个理想基线:位置包没有额外的网络传输时间,量化误差也暂时忽略。也就是说,双方都能立即得到服务器的最新位置;但远端玩家仍然要经过 EntityOtherPlayerMP 的三 tick 插值,所以两边的显示位置仍可能不同。网络位置年龄会在下一节作为单独的参数加入。

常见的直觉是:A 出手时距离 B 不超过攻击范围,双方距离大约三格,所以 B 也应该处于同样的距离。但 A 和 B 使用的不是同一组坐标。

  • A 使用自己的本地位置,以及 A 客户端里经过插值的 B 的显示位置;
  • B 使用自己的本地位置,以及 B 客户端里经过插值的 A 的显示位置。

两边的远端实体都不是服务器的实时位置。服务器位置先量化为 1/321/32 格,再通过位置包发送;EntityOtherPlayerMP 收到目标后,用三 tick 插值逐步更新显示位置。由于玩家每 tick 的位移可能变化,显示位置留下的滞后也会随最近几 tick 的运动变化。网络延迟会让位置包携带更早的服务器位置,从而进一步增加这段位置年龄。

记 A 在自己的客户端看到的 B 的滞后量为 LBL_B,记 B 在自己的客户端看到的 A 的滞后量为 LAL_A。沿着 A 到 B 的水平正方向,在双方仍未交错时,有:

DA(t)≈Dreal(t)−LB(t) D_A(t)\approx D_{\mathrm{real}}(t)-L_B(t) DB(t)≈Dreal(t)+LA(t) D_B(t)\approx D_{\mathrm{real}}(t)+L_A(t)

其中 Dreal(t)D_{\mathrm{real}}(t) 是时刻 tt 两名玩家真实位置的距离,DA(t)D_A(t)、DB(t)D_B(t) 分别是两端在同一时刻看到的距离。两者相减得到:

DB(t)−DA(t)≈LA(t)+LB(t) D_B(t)-D_A(t)\approx L_A(t)+L_B(t)

这就是第一个容错来源:双方看到的远端玩家都带有各自的插值滞后。A 出手时看到的距离,可以比 B 之后看到的距离更近;差值大约等于两边远端位置滞后的总和。

三、速度决定插值滞后有多大

EntityOtherPlayerMP 的三 tick 插值不是固定向量,而是在当前显示位置和目标位置之间逐步靠近。运动越快、加速越明显,显示位置追赶目标所留下的滞后就越大。

速度不是恒定的。按照 EntityOtherPlayerMP 的三 tick 插值,可以用最近几 tick 的历史位移加权近似滞后。令 uX(t)u_X(t) 表示玩家 XX 在 tick tt 的实际位移,按距观察时刻从近到远排列;令 aXa_X 表示位置包到达观察端前经历的网络位置年龄。无额外网络年龄时取 aX=0a_X=0;存在网络延迟时,把它作为历史索引的偏移量。忽略量化误差时,五项近似为:

LX(t)≈∑j=15wj uX(t−aX−j) L_X(t)\approx\sum_{j=1}^{5}w_j\,u_X(t-a_X-j)

收包相位的权重为:

(w1,w2,w3,w4,w5)=(1,23,23,29,29) (w_1,w_2,w_3,w_4,w_5)=\left(1,\frac23,\frac23,\frac29,\frac29\right)

无包相位的权重为:

(w1,w2,w3,w4,w5)=(1,1,13,13,19) (w_1,w_2,w_3,w_4,w_5)=\left(1,1,\frac13,\frac13,\frac19\right)

实际使用哪一组,取决于该 tick 是否刚好收到位置更新。速度突然改变时,最近一项的权重最大,所以 A 攻击时附近的历史位移和 B 危险窗口开始时附近的历史位移会直接影响两边的插值滞后。A 越向前加速,B 端看到的 A 越容易落后;B 越向后加速,A 端看到的 B 越容易落后。

例如,某一端最近五个 tick 的同向位移依次为 0.10,0.08,0.05,0.03,0.020.10,0.08,0.05,0.03,0.02 格。如果这一刻恰好收到位置包,三 tick 插值给出的滞后近似为

L≈1⋅0.10+23⋅0.08+23⋅0.05+29⋅0.03+29⋅0.02≈0.20 格 L\approx 1\cdot0.10+\frac23\cdot0.08+\frac23\cdot0.05 +\frac29\cdot0.03+\frac29\cdot0.02 \approx0.20\text{ 格}

如果没有收到位置包,则使用另一组权重,结果约为 0.210.21 格。两次计算使用的是同一组历史位移,但收包相位不同;速度正在变化时,不能用“当前速度乘一个固定 tick 数”替代它。

这里的两个滞后对应的不是同一个窗口端点。A 判定能否攻击时,看到的是窗口开始处、也就是 tPt_P 附近的 B;因此使用 LB(tP)L_B(t_P)。B 判定 A 的攻击距离时,对应的是 A 发出攻击的那一刻;A 的攻击包和该 tick 的位置同步属于同一客户端状态,所以用上帝视角记为 tAt_A,对应的滞后是 LA(tA)L_A(t_A)。

DBcheck−DA≈LA(tA)+LB(tP) D_B^{\mathrm{check}}-D_A \approx L_A(t_A)+L_B(t_P)

正式计算时,LA(tA)L_A(t_A) 和 LB(tP)L_B(t_P) 都使用上面的历史位移加权;不能把速度替换成一个恒定值。

这个加权式是三 tick 插值的近似,不是原版的恒等式。速度刚发生突变、位置包发送不规律、玩家落地或转向时,应使用每个客户端真实的 display、target 和剩余插值步数;位置量化还会带来最多约 1/321/32 格的单包误差。

四、网络延迟怎样把“手”变长

A 出手时,A 使用的是窗口开始时已经同步到自己客户端的 B 显示位置。A 的攻击包还要经过 A 到服务器的链路,服务器处理后再经过服务器到 B 的链路,才会让 B 收到击退速度。

危险窗口由两个连续区段组成:A 看到 B 的过去位置到 A 出手的前置区段,以及攻击包从 A 发出到 B 收到击退速度的在途区段。两段连接起来,统一从 tPt_P 计算到 tVt_V。

设 tSt_S 为服务器处理这次攻击的 tick。前文已经定义 tAt_A、tPt_P、tVt_V 和 NN。

因此窗口长度可以写成:

N=(tA−tP)+(tV−tA)=tV−tP N=(t_A-t_P)+(t_V-t_A)=t_V-t_P

在这 NN 个 tick 中,B 还会继续向后移动。A 的攻击包和位置同步都在同一个客户端 tick 中产生:攻击包在处理左键时发出,位置同步在 tick 末尾发出;前者经服务器触发击退,后者再由服务器转发给 B。对 B 端来说,A 的真实前进已经包含在这条位置同步的历史中,不应再作为一个独立的窗口位移项重复加入;A 的运动只通过 B 端远端 A 的插值滞后体现。

记这段完整危险窗口内 B 的总后退位移为 ΔxB\Delta x_B。它指的是从 tPt_P 到 tVt_V 之间 B 实际移动的总距离,而不是网络包中的速度字段。

当 B 收到 A 的攻击包对应的速度时,B 在这一 tick 的准星检测仍使用击退生效前的本地位置;击退速度要到随后移动阶段才第一次改变 B 的位置。A 当时在 B 客户端中仍然是经过插值的远端实体。相对于 A 出手那一刻,B 在窗口中继续向后移动会直接改变 B 的视角距离。若只看这部分,距离增加量近似为:

ΔDwindow≈ΔxB \Delta D_{\mathrm{window}}\approx\Delta x_B

这部分是窗口移动带来的直接容错。A 的前进不会再单独列成 −ΔxA-\Delta x_A;它只会改变 B 端远端 A 的插值滞后量。B 看到的是较早的 A 位置,而不是 A 在收到击退前的实时位置。

五、合并得到 Combo 容错公式

A 出手时看到的距离必须不超过 A 的攻击范围:

DA≤RA D_A\le R_A

B 是否能在窗口中反击,取决于 B 在自己的客户端看到的距离是否曾经降到 B 的攻击范围以内。为了让一次攻击具备稳定的 Combo 条件,需要让窗口内最危险的 B 距离仍大于 RBR_B。

把两部分差异合在一起,可以得到一个实用的近似关系。窗口开始时 A 端看到的 B 的滞后记为 LB(tP)L_B(t_P),A 出手时 B 端对应的 A 滞后记为 LA(tA)L_A(t_A):

DBcheck≈DA+ΔxB+LA(tA)+LB(tP) D_B^{\mathrm{check}} \approx D_A +\Delta x_B +L_A(t_A)+L_B(t_P)

其中:

  • DAD_A 是 A 出手 tick 看到的 B 的显示距离;
  • ΔxB\Delta x_B 是从窗口开始 tPt_P 到 B 收到击退速度 tVt_V 期间,B 的总后退位移;A 的真实前进不作为独立项加入;
  • LA(tA)+LB(tP)L_A(t_A)+L_B(t_P) 是 A 出手时和窗口开始时两个对应状态的远端插值、网络位置年龄造成的视角距离差。

在 B 持续向后、窗口内距离总体增大的情况下,可以把 B 的攻击范围写成一个端点容错条件:

DA+ΔxB+LA(tA)+LB(tP)>RB D_A+\Delta x_B+L_A(t_A)+L_B(t_P)>R_B

这条不等式表示:即使 A 出手时已经贴近到自己的攻击范围边缘,B 的后退位移和双方的远端显示滞后,合起来仍要把 B 看到的距离推到 B 的攻击范围之外。

把三 tick 历史加权代入后,最终的 Combo 容错条件可以写成:

DA+ΔxB+∑j=15wj(A)uA(tA−aA−j)+∑j=15wj(B)uB(tP−aB−j)>RB \boxed{\displaystyle D_A+\Delta x_B+\sum_{j=1}^{5}w^{(A)}_j u_A(t_A-a_A-j)+\sum_{j=1}^{5}w^{(B)}_j u_B(t_P-a_B-j)>R_B}

六、逐项分析容错来源

这个式子中的每一项代表的不是同一种东西。

A 出手时看到的距离

DAD_A 是 A 在攻击那一 tick,用自己的本地位置和画面中 B 的插值位置得到的距离。它必须满足 DA≤RAD_A\le R_A,所以在通常 RA=RBR_A=R_B 的情况下,DAD_A 最多只能把左边推到攻击范围边界,不能单独制造超出 RBR_B 的容错。

B 在危险窗口中的位移

ΔxB\Delta x_B 表示 B 从窗口开始到收到击退速度包之间的总后退位移。这一项通常占容错的最大部分:危险窗口越长,B 后退得越远,B 收到击退前与 A 的距离就越大。

两个速度加权项:插值造成的额外距离

来自 A 的插值滞后:

∑j=15wj(A)uA(tA−aA−j) \sum_{j=1}^{5}w^{(A)}_j u_A(t_A-a_A-j)

来自 B 的插值滞后:

∑j=15wj(B)uB(tP−aB−j) \sum_{j=1}^{5}w^{(B)}_j u_B(t_P-a_B-j)

这两项不是 A、B 在窗口里额外走过的距离,而是三 tick 插值根据历史位移留下的显示滞后。A 的向前运动会让 B 端看到的 A 落后,B 的向后运动会让 A 端看到的 B 落后。它们确实增加容错,但通常没有 ΔxB\Delta x_B 那么大。

因此,B 向后移动时会同时带来两份收益:一份是窗口期间实际后退的 ΔxB\Delta x_B,另一份是 B 的历史后退位移增大后带来的插值滞后项。如果 B 不向后移动,甚至向 A 前进,ΔxB\Delta x_B 会变小甚至抵消;此时主要只剩 A 的本地运动通过插值给出的那一部分容错。在双方攻击范围相同、没有额外几何优势的情况下,这通常不足以稳定满足上面的不等式。

七、一个数值例子

下面用一组具体数据代入公式。假设 A 在 tA=100t_A=100 出手,A 看到的 B 的位置属于 tP=97t_P=97,B 在 tV=105t_V=105 收到击退速度包。危险窗口内有 N=tV−tP=8N=t_V-t_P=8 个 tick 的移动区间,并假设 B 在这段时间内始终向后移动,总后退位移为:

ΔxB=0.40 格 \Delta x_B=0.40\text{ 格}

双方的攻击范围取 RA=RB=3.00R_A=R_B=3.00 格。A 出手时看到的距离取 DA=2.90D_A=2.90 格,因此 A 的攻击条件已经满足。

1. 计算 A 的插值滞后

假设 B 端这一刻刚好收到位置包,A 最近五个相关 tick 的位移为 0.10,0.08,0.05,0.03,0.020.10,0.08,0.05,0.03,0.02 格,使用收包相位权重:

LA(tA)=0.10+23⋅0.08+23⋅0.05+29⋅0.03+29⋅0.02≈0.1978 格 L_A(t_A)=0.10+\frac23\cdot0.08+\frac23\cdot0.05+\frac29\cdot0.03+\frac29\cdot0.02\approx0.1978\text{ 格}

2. 计算 B 的插值滞后

假设 A 端这一刻没有收到位置包,B 最近五个相关 tick 的向后位移为 0.12,0.10,0.07,0.04,0.020.12,0.10,0.07,0.04,0.02 格,使用无包相位权重:

LB(tP)=0.12+0.10+13⋅0.07+13⋅0.04+19⋅0.02≈0.2589 格 L_B(t_P)=0.12+0.10+\frac13\cdot0.07+\frac13\cdot0.04+\frac19\cdot0.02\approx0.2589\text{ 格}

3. 合并并验证

将四项放回 Combo 容错公式:

M=DA+ΔxB+LA(tA)+LB(tP)=2.90+0.40+0.1978+0.2589≈3.7567 格 M=D_A+\Delta x_B+L_A(t_A)+L_B(t_P)=2.90+0.40+0.1978+0.2589\approx3.7567\text{ 格}

因为

M=3.7567>RB=3.00 M=3.7567>R_B=3.00

所以这个例子满足端点容错条件,余量约为 0.75670.7567 格。这里的数值也能直接看出各项的大小:DAD_A 只能达到攻击范围边界,ΔxB\Delta x_B 提供了 0.400.40 格的主要窗口位移,两个插值项合计约 0.45670.4567 格。若 B 不后退,第一项位移贡献消失,最终结果就会减少 0.400.40 格;若 B 还向 A 移动,ΔxB\Delta x_B 还会进一步变小。

这个例子验证的是端点公式的代入和算术结果。实际判定仍要在窗口内逐 tick 检查 B 的本地位置与 B 眼中的 A 显示位置;只要其中一拍距离不超过 RBR_B,就不能把这次攻击视为稳定 Combo。


Combo 容错的计算
https://wtazuresky.github.io/2026/08/27/combo-operation-principle/
作者
WTAzureSky
发布于
2026年8月27日
许可协议