Revethere's Blog

分享一些超级有用的芝士

关于博客未来近一年的文章规划及我的一点碎碎念

其它2026/11/14

新开了一个好题分享的系列,不同于“题目+答案”形式,该系列主要集中于一题多解以及一些我自己在做题时候的一些思考。也算是目前比较现实的能稳定更新的内容了。

之前提过写 FFT,莫反等些东西,但我似乎有点高估了自己的能力高三还是太牢了以至于这些东西还停留在想法,最坏的情况或许是高考后再写。纯理论的比如拉格朗日插值那一篇确实太生硬了,目前也没有找到比较好的方法去表达。数列估计还是会写的,看看年底有没有机会吧。

Minecraft 还有好多好多东西可以写,之前还想着做视频的(欸我怎么挖了这么多坑啊),但是按现在的情况来看得等到大学了,就怕别人把我题材抢了 QAQ,保守估计短的可以做十几分钟,长的一个多小时(坚持手搓动画特效谢谢,不太喜欢现在 AI 做的视频,缺点感觉)。 自从接触底层理论之后觉得研究这些是很有意思的事情,比上学好玩 XD. 理解游戏的底层逻辑何尝不是拥有一种世界观呢。

在想怎样才能让文章的语言更有意思,虽然这会不可避免的提高文章的阅读门槛,但泡沫剧式是我所摒弃的,做这些东西本就是兴趣使然,同时我也不会认为大部分的中学生能看懂我写的东西。文人和莽夫的气质是不一样的,虽然我真的很讨厌语文教育,带点幽默感总比年猪活得久,语文教育不等同于语文。目前的文章都还差点意思,作为一个出生在千禧年末期的小孩毕竟没经历过互联网爆发的那段时期,那个时候的中文互联网的质量远大于如今,至少在我看来如此。提到这,或许后面也会开几篇对如今社会议题的一些看法(选题不会太尖锐,更多的应该会是一些小细节)。

言既于此,高三加油。

💚

MORE

近期好题分享 26.11.14

学习笔记2026/11/14
Problem2026 · 浙江台州质量评估

定义在 $R$ 上的函数 $f(x)$ 的导函数为 $f’(x)$,若存在常数 $k (k > 1)$,对于任意实数 x,都有 $f(4 - x) = e^{2k(2 - x)} f(x)$,且当 $x \ne 2$ 时,$(x - 2) \left[ f’(x) - k \cdot f(x) \right] > 0$,其中 $e$ 为自然对数的底数,则不等式 $e^{3k} f(\ln x) < x^k f(3)$ 的解集为

A.$(k, 3k)$B.$(e, 3e)$C.$(e, e^3)$D.$(e^k, e^{3k})$
Solution

纸老虎来的。

看到题干第一时间想到的是类对称性、构造、同构整合到一块。如果你的注意力不算惊人的话第一个对称性的条件并不能带来比较有价值的信息,但显然可以猜测某个函数关于 $x = 2$ 对称。不妨先将其放到一边看看最后一个式子,不难发现是同构:

$$e^{3k} f(\ln x) < x^k f(3)$$

$$\frac{f(\ln x)}{x^k} < \frac{f(3)}{e^{3k}}$$

$$\frac{f(\ln x)}{e^{k \cdot \ln x}} < \frac{f(3)}{e^{k \cdot 3}}$$

其实构造完了就可以猜了

令 $F(x) = \frac{f(x)}{e^{kx}}$,这样第二个条件也能用了:

$$F’(x) = \frac{f’(x) - k \cdot f(x)}{e^{kx}}$$

$$(x - 2) \left[ f’(x) - k \cdot f(x) \right] > 0 \Leftrightarrow (x - 2)F’(x) > 0$$

即 $(x - 2)$ 与 $F’(x)$ 同号。因此函数 $F(x)$ 在 $(-\infty, 2)$ 上单调递减,在 $(2, +\infty)$ 上单调递增。

相比于从第三个条件构造函数来说,从第二个条件出发构造或许会难一点,如果题见的少的话。

同理第一个条件也能够翻译了:

$$f(4 - x) = e^{2k(2 - x)} f(x)$$

$$f(4 - x) = e^{k(4 - x) - kx} f(x)$$

$$\frac{f(4 - x)}{e^{k(4 - x)}} = \frac{f(x)}{e^{kx}}$$

即函数 $F(x)$ 关于 $x = 2$ 对称。答案也就显而易见了。

Problem2025 · 南开期中(节选)

数列 ${a_n}$ 的通项公式为 $a_n = 3n$,数列 ${b_n}$ 的通项公式为 $b_n = 3n - 1$。将数列 ${a_n}, {b_n}$ 的所有项从小到大重新排列得到新数列${d_n}$,在数列 ${d_n}$ 的前 $2n$ 项中随机取一项 $d_k (k \in \mathbb{N}^{*}, k \le 2n)$,再从不大于 $d_k$ 的项中随机取一项将其记为 $X$,若 $X$ 的期望为 $E(X)$,求证 $E(X) > \frac{3n}{4} + \frac{11}{8}$

Solution

计算量取决于你的思路。提供两种解法。

解法一:

固定 $d_k$ ,对于每一个 $d_k (k \in \mathbb{N}^{*}, k \le 2n)$ 其对 $E(X)$ 的贡献为 $d_i (i \in \mathbb{N}^{*}, i \le k)$ 的期望。令:

$$S_k = \sum_{i = 1}^{k} d_i$$

$d_i$ 等概率,则 :

$$E(X | d_k) = \frac{S_k}{k}$$

所以:

$$E(X) = \frac{1}{2n} \sum_{k = 1}^{2n} \frac{S_k}{k}$$

由于 ${d_n}$ 是由两数列合并过来,且排列为 ${ b_1, a_1, b_2, a_2 \cdots b_n, a_n}$,因此仅对 $n$ 分奇偶讨论即可

$$\begin{aligned} E(X) &= \frac{1}{2n} \sum_{k = 1}^{2n} \frac{S_k}{k} \cr &= \frac{1}{2n} \left( \sum_{k = 1}^{n} \frac{S_{2k - 1}}{2k - 1} + \sum_{k = 1}^{n} \frac{S_{2k}}{2k} \right) \cr &= \frac{1}{2n} \left( \sum_{k = 1}^{n} \frac{\sum\limits_{i = 1}^k [(3i - 1) + 3i]}{2k - 1} +\sum_{k = 1}^{n} \frac{\sum\limits_{i = 1}^k [(3i - 1) + 3i] - a_k}{2k} \right) \cr &= \frac{1}{2n} \left( \sum_{k = 1}^{n} \left(\frac{3}{2}k + \frac{1}{4} + \frac{1}{4(2k - 1)}\right) + \sum_{k = 1}^{n} \left( \frac{3}{2}k + 1 \right) \right) \cr &= \frac{1}{2n} \left( \frac{3n(n + 1)}{2} + \frac{5}{4}n + \frac{1}{4} \sum_{k = 1}^{n} \frac{1}{2k - 1}\right) \cr &= \frac{3}{4}n + \frac{11}{8} + \frac{1}{8n} \sum_{k = 1}^{n} \frac{1}{2k - 1} > \frac{3}{4}n + \frac{11}{8} \end{aligned}$$

QED.

解法二:

看起来上面这一坨式子并不太优雅虽然如果好好写的话还是能看的,解法二作为我当时做这道题的想法与解法一相比就相形见绌了,因为涉及到调和数需要交换求和导致整个式子又臭又长。有耐心的可以看看精简版 QAQ.

固定 $d_i$,对于每一个 $k$,对 $d_i$ 的贡献为 $\frac{1}{k}$。(因为推导太复杂了符号不会严格遵循题干)因此:

$$\begin{aligned} E(X) &= \sum_{i = 1}^{2n} d_i \left( \frac{1}{2n} \sum_{j = i}^{2n} \frac{1}{j} \right) \cr &= \frac{1}{2n}\left( \sum_{i = 1}^n 3i\sum_{j = 2i}^{2n}\frac{1}{j} + \sum_{i = 1}^n(3i - 1)\sum_{j = 2i - 1}^{2n}\frac{1}{j} \right) \cr &= \frac{1}{2n}\left( 3\sum_{i = 1}^n i\left( \sum_{j = 2i}^{2n}\frac{1}{j} + \sum_{j = 2i - 1}^{2n}\frac{1}{j} \right) - \sum_{i = 1}^n \sum_{j=2i - 1}^{2n}\frac{1}{j} \right) \cr &= \frac{1}{2n}\sum_{j = 1}^{2n} \frac{1}{j} \left( 3\sum_{i = 1}^{\lfloor \frac{j}{2} \rfloor} i + 3\sum_{i = 1}^{\lfloor \frac{j+1}{2} \rfloor} i - \left\lfloor \frac{j + 1}{2}\right\rfloor \right) \cr &= \frac{1}{2n}\sum_{k = 1}^n \left( \frac{3k^2 - k}{2k - 1} + \frac{3k^2 + 2k}{2k} \right) \cr &= \frac{1}{2n}\sum_{k = 1}^n \left( 3k + \frac{5}{4} + \frac{1}{4(2k - 1)} \right) \cr &= \frac{3}{4}n + \frac{11}{8} + \frac{1}{8n} \sum_{k = 1}^{n} \frac{1}{2k - 1} > \frac{3}{4}n + \frac{11}{8} \end{aligned}$$

QED.

饿啊…

第四五步奇偶讨论省了点过程应该看得懂吧(

不知道为什么标准答案写得这么构式,最后还要用放缩(可以去搜搜长啥样),还是我的优雅(确信

Problem

如图,$O$ 是平行四边形 ABCD 所在平面内的一点,且满足 $\angle AOB = \frac{1}{2} \angle BOC = \frac{\pi}{6},\ 2\sqrt3|\overrightarrow{OA}| = 2|\overrightarrow{OB}| = 3|\overrightarrow{OC}| = 6$,则 $|\overrightarrow{OD}| = (\ )$

A.2B.$\sqrt3$C.$\sqrt2$D.1

Solution

解法一:

$$|AO|, |BO|, \angle AOB \Rightarrow |AB| = |CD| = \sqrt3, \angle AOB = \frac{2\pi}{3}$$

$$\angle AOB, \angle BOC \Rightarrow \angle AOC = \frac{\pi}{2}$$

五边形内角和为 $540^\circ$

$$\angle COD + \angle CDO = 540^\circ - \angle AOB - \angle AOC - ( \angle ABC + \angle BCD) = 150^\circ \Rightarrow \angle COD = \frac{\pi}{6}$$

$$|OC|, |CD|, \angle OCD \Rightarrow |OD| = 1$$

故选 C.

解法二:

题是归属于向量的,但是一点也没有上面的几何法优雅。所以意义素?

$$|\overrightarrow{OD}| = |\overrightarrow{OC}| + |\overrightarrow{CD}| = |\overrightarrow{OC}| + |\overrightarrow{OA}| - |\overrightarrow{OB}|$$

$$\begin{aligned} {|\overrightarrow{OD}|}^2 &= (|\overrightarrow{OC}| + |\overrightarrow{OA}| - |\overrightarrow{OB}|)^2 \cr &= {|\overrightarrow{OC}|}^2 + {|\overrightarrow{OA}|}^2 + {|\overrightarrow{OB}|}^2 + 2 |\overrightarrow{OC}| \cdot |\overrightarrow{OA}| - 2 |\overrightarrow{OC}| \cdot |\overrightarrow{OB}| - 2 |\overrightarrow{OA}| \cdot |\overrightarrow{OB}|\end{aligned}$$

所有条件已知,带入解出 $|OD|$ 即可。

题挺常规的,但是自己刚补向量注意力总是不那么惊人 ):

MORE

多项式构造:高斯消元与拉格朗日插值

竞赛2026/9/15

前言

拉格朗日插值其实在初中搞竞赛的时候就学过了,鉴于当时学识浅薄,其实并没有理解其底层的原理。在熬过两年高中、成为苦逼高三生之后,终于有能力、想法和不太充裕的时间来填以前的坑了。

除了高中数学课内的知识外(也不算很课内吧😶‍🌫️),此篇还杂糅了相当部分的信息学竞赛内容,建议选择性阅读。

高斯消元

高中阶段高斯消元最典型的使用场景大多应用于求解多元一次方程组、待定系数法求函数解析式、解析几何求交点与位置关系等。后文将用一道简单的例子来展示高斯消元的用法。

函数解析式求解

已知二次函数 $f(x)$ 过点 $(1, 2)$、$(2, 9)$、$(3, 22)$,求该函数的解析式。

一般做法是设:

$$f(x) = ax^2 + bx + c$$

将三个点带入方程:

$$\begin{cases} a + b + c = 2 \cr 4a + 2b + c= 9 \cr 9a + 3b + c = 22 \end{cases}$$

通过「加减消元法」求解该三元一次方程组即可。高斯消元则是把方程组改写为「增广矩阵」的形式:

$$\left[ \begin{array}{cccc|c} a_{11} & a_{12} & \cdots & a_{1n} & b_1 \cr a_{21} & a_{22} & \cdots & a_{2n} & b_2\cr \vdots & \vdots & \ddots & \vdots & \vdots \cr a_{m1} & a_{m2} & \cdots & a_{mn} & b_m \end{array} \right]$$

并将该矩阵变成类似如下的「行阶梯形」:

$$\left[ \begin{array}{cccc|c} c_{11} & c_{12} & \cdots & c_{1n} & d_1 \cr 0 & c_{22} & \cdots & c_{2n} & d_2\cr \vdots & \vdots & \ddots & \vdots & \vdots \cr 0 & 0 & \cdots & c_{mn} & d_m \end{array} \right]$$

即对于第 $i$ 行的主元在第 $i$ 列,且主元下面的数都变为 0. 因此上述方程就可以被改写为:

$$\left[ \begin{array}{ccc|c} 1 & 1 & 1 & 2\cr 4 & 2 & 1 & 9\cr 9 & 3 & 1 & 22 \end{array} \right]$$

为了达到目标状态进行如下操作:

$$\xrightarrow{R_2 \leftarrow R_2 - 4R_1} \left[ \begin{array}{ccc|c} 1 & 1 & 1 & 2\cr 0 & -2 & -3 & 1\cr 9 & 3 & 1 & 22 \end{array} \right]$$

$$\xrightarrow{R_3 \leftarrow R_3 - 9R_1} \left[ \begin{array}{ccc|c} 1 & 1 & 1 & 2\cr 0 & -2 & -3 & 1\cr 0 & -6 & -8 & 4 \end{array} \right]$$

$$ \xrightarrow{R_3 \leftarrow R_3 - 3R_2} \left[ \begin{array}{ccc|c} 1 & 1 & 1 & 2\cr 0 & -2 & -3 & 1\cr 0 & 0 & 1 & 1 \end{array} \right]$$

最后再回代求解得 $(a, b, c) = (3, -2, 1)$.

目前来看,高斯消元更像是把方程组写得更整齐、把加减消元的过程做的更规范的工具罢了。

拉格朗日插值

显而易见的是若用待定系数法构造多项式,需要解一个 $n$ 元线性方程组,高斯消元的时间复杂度为 $O(n^3)$,$n$ 稍微大一点开销就爆炸了。拉格朗日插值则提供了一种绕过解方程组、直接构造多项式的方法。首先记一个结论:

对于 $n$ 个点 $(x_i, y_i)$,若 $\forall i \ne j , x_i \neq x_j$,则一定存在唯一一个次数不超过 $n - 1$ 的多项式 $y = f(x)$,满足 $f(x_i) = y_i$

根据因式定理:

若多项式 $f(a) = 0$,则 $f(x)$ 必含有因式 $(x - a)$;反之若 $f(x)$ 含有因式 $(x - a)$,则 $f(a) = 0$

如果一个多项式有 $m$ 个不同的跟,则至少有 $m$ 个因式相乘,次数最小为 $m$. 即一个次数不超过 $m$ 的多项式,最多有 $m$ 个根。

后文将会对其唯一性进行简单证明。

推导

不妨从最简单的情形入手。现在有两个点:

$$(x_0, y_0), (x_1, y_1)$$

两点确定一条直线。因此该直线方程可以用两点式写成:

$$\frac{y - y_0}{y_1 - y_0} = \frac{x - x_0}{x_1 - x_0}$$

若把它写成 $y = L(x)$ 的形式:

$$L(x) = y_0 + \frac{y_1 - y_0}{x_1 - x_0}(x - x_0)$$

这就是一个过 $(x_0, y_0), (x_1, y_1)$ 两点的一次函数。但如果将其推广到更多的点,这样的写法就太不方便了:它把 $y_0$ 和 $y_1$ 混在了一起。

不如换一种整理方式:

$$ \begin{aligned} L(x) &= y_0 + \frac{y_1 - y_0}{x_1 - x_0}(x - x_0) \cr &= y_0 \left( 1 - \frac{x - x_0}{x_1 - x_0} \right) + y_1 \frac{x - x_0}{x_1 - x_0} \cr &= y_0 \frac{x - x_1}{x_0 - x_1} + y_1 \frac{x - x_0}{x_1 - x_0} \end{aligned}$$

令:

$$\ell_0(x) = \frac{x - x_1}{x_0 - x_1}, \ell_1(x) = \frac{x - x_0}{x_1 - x_0}$$

设点 $(x_0, y_0) = (1, 1), (x_1, y_1) = (2, 2)$,并将 $y = y_0\ell_0(x)$ 与 $y = y_1\ell_1(x)$ 表示在图像上:

它们显然满足:

$$\ell_0(x_0) = 1, \ell_0(x_1) = 0$$

$$\ell_1(x_0) = 0, \ell_1(x_1) = 1$$

即 $\ell_0$ 只在 $x = x_0$ 处「打开」,在 $x = x_1$ 处「关闭」。$\ell_1$ 与之相反。即:

$$L(x) = y_0 \ell_0(x) + y_1 \ell_1(x)$$

这个形式就是一次拉格朗日插值,其中 $\ell(x)$ 被称为「开关函数」非权威定义,只是便于理解。

二次函数的推导稍显复杂,其根本是先不处理第三个点,用前两个点做一次插值后构造并加上一个在 $x_0, x_1$ 处为 0 的修成项 $c(x - x_0)(x - x_1)$,再根据第三点的坐标求出 $c$,整理并化为插值的形式。

更高次项的多项式操作类似,感兴趣的读者可以自行推导。现在将其推广到一般形式。

对于已知的 $n + 1$ 个点:

$$(x_0, y_0), (x_1, y_1), \cdots , (x_n, y_n)$$

新的「开关函数」$\ell_i(x)$ 需满足:

  • 当 $x = x_i$ 时,$\ell_i(x) = 1$
  • 当 $x = x_j$ 且 $i \neq j$ 时,$\ell_i(x) = 0$

显然可以这样构造:

$$\begin{aligned} \ell_i(x) &= \frac{(x - x_0)(x - x_1) \cdots (x - x_{i - 1})(x - x_{i + 1}) \cdots (x - x_n)}{(x_i - x_0)(x_i - x_1) \cdots (x_i - x_{i - 1})(x_i - x_{i + 1}) \cdots (x_i - x_n)} \cr &= \prod_{j\neq i} \frac{x - x_j}{x_i - x_j} \end{aligned}$$

当 $x = x_i$ 时,分子与分母相同,整个式子的值为 1,即开关「开」。

再把所有的「开关」加起来:

$$\begin{aligned} L(x) &= y_0\ell_0(x) + y_1\ell_1(x) \cdots + y_n\ell_n(x) \cr &= \sum_{i = 1}^n y_i \ell_i(x) \end{aligned}$$

唯一性证明

设次数不超过 $n - 1$ 的多项式 $F(x), G(x)$,满足:

$$F(x_i) = y_i, G(x_i) = y_i$$

令:

$$H(x) = F(x) - G(x)$$

因为 $F(x)$ 与 $G(x)$ 的次数均不超过 $n - 1$,所以 $H(x)$ 的次数也不超过 $n - 1$。

对于每一个 $i$:

$$H(x_i) = F(x_i) - G(x_i) = y_i - y_i = 0$$

即 $x_1, x_2 \cdots x_n$ 这 $n$ 个数都是 $H(x)$ 的根。 $H(x)$ 至少有 $n$ 个根。

回顾前文的结论:

一个次数不超过 $m$ 的多项式,最多有 $m$ 个根

但 $H(x)$ 的次数不超过 $n - 1$ 却有 $n$ 个根,这与上述结论矛盾。除非:

$$H(x) \equiv 0$$

即:

$$F(x) - G(x) \equiv 0$$

所以:

$$F(x) = G(x)$$

证毕。

高中阶段所能用到的所有拓展知识到此结束,以下内容则与信息学竞赛强相关,学有余力的读者可以接着往下看

重心拉格朗日插值

这部分的推导相当常规,所以决定增加一点本人对于此概念的碎碎念😗

刚开始看到这个概念时以为是解决高次项插值的的,就是把下图的插值函数 $L_{10}(x)$ 重新拟合到更接近原函数 $f(x) = \frac{1}{1 + (\frac{x}{2})^2}$ 的算法。

注意到 $L_{10}(x)$ 在 $x = 9$ 与 $x = -9$ 附近上下剧烈震荡,与原函数 $f(x)$ 相差甚大,这就是典型的「龙格现象」。。噢你问这个图怎么跟上面不一样因为上面那个是拿 Desmos 画的这个是 Matplotlib,Python 画图还是太优雅了 awa

回到正题。不难看出,在上述插值的做法下对于每一次查询都是 $O(n^2)$ 的,多次查询的时间显然是爆炸。每插入一个值,整个多项式都需要重新构造,因此传统的拉格朗日插值在特定情境下并不优雅。重心拉格朗日插值则提供了一种在 $O(n)$ 内求值与更新多项式的方法。

将 $\ell_i(x)$ 拆开:

$$\ell_i(x) = \frac{\prod\limits_{j \neq i} (x - x_j)}{\prod\limits_{j \neq i} (x_i - x_j)}$$

显然分母部分只与已知点相关,不妨令:

$$w_i = \frac{1}{\prod\limits_{j \neq i} (x_i - x_j)}$$

则:

$$\ell_i(x) = w_i \prod_{j \neq i} (x - x_j)$$

其中, $w_i$ 被称之为「重心权重」。补全连乘,令:

$$\ell(x) = \prod_{j = 1}^n (x - x_j)$$

所以:

$$\begin{aligned} \ell_i(x) &= w_i \frac{\prod\limits_{j = 1}^n (x - x_j)}{x - x_i} \cr &= w_i \frac{\ell(x)}{x - x_i}\end{aligned}$$

$$\begin{aligned}L(x) &= \sum_{i = 1}^n y_i w_i \frac{\ell(x)}{x - x_i} \cr &= \ell(x) \sum_{i = 1}^n \frac{y_i w_i}{x - x_i} \end{aligned}$$

如何消掉这个突兀的 $\ell(x)$ 呢?注意到对于多项式 $1$:

$$\begin{aligned} 1 &= \sum_{i = 1}^n 1 \cdot \ell_i(x) \cr &= \sum_{i = 1}^n \ell_i(x) \cr &= \sum_{i = 1}^n w_i \frac{\ell(x)}{x - x_i} \cr &= \ell(x) \sum_{i = 1}^n \frac{w_i}{x - x_i} \end{aligned}$$

则:

$$\ell(x) = \frac{1}{\sum\limits_{i = 1}^n \frac{w_i}{x - x_i}}$$

所以:

$$L(x) = \frac{\sum\limits_{i = 1}^n \frac{y_i w_i}{x - x_i}}{\sum\limits_{i = 1}^n \frac{w_i}{x - x_i}}$$

至于它为什呢叫「重心」呢是因为最后推导出的公式形似加权平均:

$$L(x) = \frac{\sum{权重}_i \cdot y_i}{\sum{权重_i}}$$

跟「龙格现象」有冇关系。

不嘻嘻。

MORE

Minecraft 实体运动公式拓展推导

理论2025/12/14

前言

本文内容包含大量数学与物理公式,需要具备一定的数理基础阅读。

因为一些客观因素博客中的部分公式并不完全,建议下载 PDF 版本(见附件)食用更佳。

实体基本运动公式

一些必要参数及定义

  • 速度:$v_H$、$v_Y$,表示实体在水平、垂直上的速度。
  • 初始垂直速度:$v_{Y, 1}$,取决于跳跃速度或服务器垂直击退参数。
  • 方块滑度系数:$f_S$。
  • 移动乘数:$k_M$。
  • 效果乘数:$k_E$。
  • 速度阈值:$v_{th} = 0.005 \text{ m/tick}$,当实体在某条轴上的速度经衰减后小于该值时,对应轴上的速度归零,仅保留加速度(1.9 及以上版本阈值变为 $0.003 \text{ m/tick}$)。

水平运动递推公式

  • 水平地面速度公式:$$v_{H, t} = \underbrace{v_{H, t-1} \times f_{s, t-1} \times 0.91} _ \text{动量保留} + \underbrace{0.1 \times \left( \frac{0.6}{f_{s, t}}\right)^3 \times k_M \times k_E} _ \text{加速度}\tag{1.2.1}$$
  • 水平空中速度公式:$$v_{H, t} = \underbrace{v_{H, t-1} \times 0.91} _ \text{动量保留} + \underbrace{0.02 \times k_M} _\text{加速度} \tag{1.2.2}$$
  • 水平空中飞行公式:$$v_{H, t} = \underbrace{v_{H, t-1} \times 0.91} _ \text{动量保留} + \underbrace{0.05 \times k_{M-fly}}_\text{加速度} \tag{1.2.3}$$

垂直运动递推公式

  • 垂直速度公式:$$v_{Y, t} = \left( v_{Y, t-1} - \mathop{0.08} \limits_\text{重力} \right) \times \mathop{0.98}\limits_\text{阻力} \tag{1.3.1}$$

为了方便表示,后文中设公式 (1.2.1)、公式 (1.2.2)、公式 (1.2.3) 中所有动量保留项的系数为 $\alpha$,加速度项为 $\beta$。公式 (1.3.1) 展开后同理。

稳态(渐进)速度推导

经典力学下的运动

$$\frac{dv}{dt} = a - fv \tag{2.1.1}$$

$$v = v_0 e^{-ft} + \frac{a}{f} (1 - e^{-ft})\tag{2.1.2}$$

$$\frac{v}{v_0} = \lim\limits_{t_0 \to 0} (1 - f t_0)^{\frac{t}{t_0}}, a = 0\tag{2.1.3}$$

其余见 『低版本 pvp 中玩家最优 kb 的点击频率下限分析』 中 『经典力学下的运动』小结,这里不过多阐述。

水平稳态(渐进)速度

假设实体在运动过程中所有状态不变($f_S$ 为常数),原递推式为一阶线性差分方程:$$v_t = \alpha , v_{t-1} + \beta \tag{2.2.1}$$

齐次方程 $v_t^{(h)} = \alpha v_{t-1}^{(h)}$ 的解为 $v_t^{(h)} = C \alpha^t$。由于非齐次项 $\beta$ 为常数,设其特解为 $K$,代入原方程 (2.2.1) 得:$$K = \alpha K + \beta$$

即 $K = \frac{\beta}{1 - \alpha}, \ \alpha \neq 1$,特解为:$$v_t^{(p)} = \frac{\beta}{1 - \alpha}$$

方程 (2.2.1) 的通解为齐次解与特解之和:$$v_t = C \alpha^t + \frac{\beta}{1 - \alpha}$$

当 $t = 0$:$$v_0 = C + \frac{\beta}{1 - \alpha}$$

将 $C = v_0 - \frac{\beta}{1 - \alpha}$ 代入通解得:$$v_t = \left( v_0 - \frac{\beta}{1 - \alpha} \right) \alpha^t + \frac{\beta}{1 - \alpha} = v_0 \alpha^t + \beta \frac{1 - \alpha^t}{1 - \alpha} \tag{2.2.2}$$

由于 $|\alpha| < 1$ 恒成立,该方程收敛。当 $t \to \infty$,稳态速度:$$v_{\infty} = \frac{\beta}{1 - \alpha}$$

代入 $\alpha = 0.91 f_s$ 和 $\beta = 0.1 \left( \frac{0.6}{f_s} \right)^3 k_M k_E$ 得:$$\boxed{v_{\infty} = \frac{0.1 \left( \frac{0.6}{f_s} \right)^3 k_M k_E}{1 - 0.91 f_s}} \tag{2.2.3}$$

水平飞行稳态(渐进)速度

这里提供另一种的推导方法。

已知每刻(tick)的时间长度 $\Delta t = 0.05s$,对于连续的时间 $t$ 与刻数 $n$:$$t = n \Delta t, \ v(t) \approx v_n$$

将原递推方程移项得到:$$v_n - v_{n - 1} = (\alpha - 1) v_{n - 1} + \beta$$

同时除以 $\Delta t$:$$\frac{v_n - v_{n - 1}}{\Delta t} = \frac{(\alpha - 1) v_{n - 1}}{\Delta t} + \frac{\beta}{\Delta t}$$

令:$$f = \frac{1 - \alpha}{\Delta t}, \ a = \frac{\beta}{\Delta t}$$

得到微分方程 (2.1.2),其积分因子 $\mu(t) = e^{ft}$。两边同时乘以 $\mu(t)$:

$$e^{ft} \frac{dv}{dt} + f e^{ft} v = a e^{ft}$$

$$\frac{d}{dt} \left( e^{ft} v \right) = a e^{ft}$$

对 $t$ 积分:$$e^{ft} v = \int a e^{ft} , dt = \frac{a}{f} e^{ft} + C$$

两边除以 $e^{ft}$,得到:$$v = \frac{a}{f} + C e^{-ft}$$

初始时 $v(0) = v_0$,则将常数 $C = v_0 - \frac{a}{f}$ 代入通解得到公式 (2.1.2)。当 $t \to \infty$,稳态速度:$$v_{\infty} = \frac{a}{f} = \frac{\beta}{1 - \alpha}$$

代入 $\alpha = 0.91$ 和 $\beta = 0.05 k_{M-fly}$ 得:$$\boxed{v_{\infty} = \frac{0.05 k_{M-fly}}{0.09}} \tag{2.2.4}$$

部分状态下的稳态速度

状态 $k_M$ $k_{M-fly}$ $k_E$ $f_S$ $v_{\infty} \ (\text{m/s})$
地面正常移动 $1.0 \times 0.98$ / $1.0 \times 1.0$ $0.6$ $4.317$
地面正常疾跑 $1.3 \times 0.98$ / $1.0 \times 1.0$ $0.6$ $5.612$
地面斜 $45°$ 疾跑 $1.3 \times 1.0$ / $1.0 \times 1.0$ $0.6$ $5.727$
地面正常疾跑(速度 II) $1.3 \times 0.98$ / $1.4 \times 1.0$ $0.6$ $7.857$
地面斜 $45°$ 疾跑(速度 II) $1.3 \times 1.0$ / $1.4 \times 1.0$ $0.6$ $8.018$
空中正常移动飞行 / $1$ / / $10.889$
空中正常疾跑飞行 / $2$ / / $21.778$

垂直稳态(渐进)速度

自由落体终端速度

前文的推导过程已经很详细了,因此这部分只给出结论。

根据公式 (1.3.1) 得到终端速度:$$\boxed{v_{\infty} = -3.920 \text{ m/tick} = -78.400 \text{ m/s}}\tag{2.4.1}$$

位移相关公式推导

单一方向位移与时间关系推导

位移 $s(t)$ 为前 $t$ 个速度之和:$$s(t) = \sum_{i=0}^{t-1} v_i$$

根据位移与速度关系式 (2.2.2),代入 $v_i$:$$s(t) = \sum_{i = 0}^{t - 1} \left( \alpha^i v_0 + \beta \frac{1 - \alpha^i}{1 - \alpha} \right) = v_0 \sum_{i = 0}^{t - 1} \alpha^i + \frac{\beta}{1 - \alpha} \sum_{i = 0}^{t-1} (1 - \alpha^i)$$

分别计算两个求和:

$$\sum_{i = 0}^{t-1} \alpha^i = \frac{1 - \alpha^t}{1 - \alpha}$$

$$\sum_{i = 0}^{t - 1} (1 - \alpha^i) =\sum_{i = 0}^{t - 1} 1 - \sum_{i = 0}^{t - 1} \alpha^i = t - \frac{1 - \alpha^t}{1 - \alpha}$$

即:$$s(t) = v_0 \frac{1 - \alpha^t}{1 - \alpha} + \frac{\beta}{1 - \alpha} \left( t - \frac{1 - \alpha^t}{1 - \alpha} \right)$$

整理并化简:$$\boxed{s(t) = \left( \frac{v_0}{1 - \alpha} - \frac{\beta}{(1 - \alpha)^2} \right) (1 - \alpha^t) + \frac{\beta}{1 - \alpha} t}\tag{3.1.1}$$

跳跃过程中水平与垂直位移关系函数推导

疾跑跳跃时水平速度会增加 0.2,但在这里不讨论初速度的问题。

将公式 (1.3.1) 中对应的 $\alpha$ 和 $\beta$ 代入公式 (3.1.1) 中:$$y(t) = \left( \frac{v_0}{1 - 0.98} - \frac{0.0784}{(1 - 0.98) ^ 2}\right) (1 - 0.98^t) + \frac{0.0784}{1 - 0.98} t$$

化简后得到:$$y(t) = \left( \frac{v_0}{0.98} + 4 \right) 0.98^t - 3.92$$

令 $K = \frac{v_0}{0.98} + 4,\ i = 0.98^t$:

$$y = K i - 3.92$$

则:$$i = \frac{y + 3.92}{K}$$

$$t = \log_{0.98} i = \frac{\ln i}{\ln 0.98}$$

若公式 (3.1.1) 表示水平移动的速度与时间关系,令:$$\gamma =\frac{v_0}{1 - \alpha} - \frac{\beta}{(1 - \alpha)^2},\ \delta = \frac{\beta}{1 - \alpha}$$

则:$$x(t) = \gamma (1 - \alpha^t) + \delta t \tag{3.2.1}$$

将 $\alpha^t$ 用 $i$ 表示:$$\alpha^t = e^{t \ln \alpha} = e^{\frac{\ln i \ln \alpha}{\ln 0.98}} = i^{\frac{\ln \alpha}{\ln 0.98}}$$

将 $\alpha^t$ 代入公式 (3.2.1) 得:$$x = \gamma (1 - i^{\frac{\ln \alpha}{\ln 0.98}}) + \delta \frac{\ln i}{\ln 0.98}$$

再代入 $i$:$$x = \gamma\left( 1 - \left( \frac{y + 3.92}{K} \right)^{\frac{\ln \alpha}{\ln 0.98}}\right) + \delta \frac{\ln \left(\frac{y + 3.92}{K} \right)}{\ln 0.98} \tag{3.2.2}$$

将 $\gamma = \frac{v_0}{1 - \alpha} - \frac{\beta}{(1 - \alpha)^2},\ \delta = \frac{\beta}{1 - \alpha}, \ \alpha = 0.91, \ \beta = 0.02 k_M, \ K = \frac{v_0}{0.98} + 4 $ 代入公式 (3.2.2) 得:$$x = \left( \frac{v_0}{0.09} - \frac{0.02 k_M}{0.0081} \right) \left[ 1 - \left( \frac{y + 3.92}{\frac{v_0}{0.98} + 4}\right) ^ {\frac{\ln 0.91}{\ln 0.98}} \right] + \frac{0.02 k_M}{0.09 \ln 0.98} \ln \left( \frac{y + 3.92}{\frac{v_0}{0.98} + 4} \right) $$

再略微化简一下:$$\boxed{x = \frac{100}{81}(9v_0 - 2k_M) \left[ 1 - 0.91 \left( \frac{y + 3.92}{v_0 + 3.92} \right)^ {\frac{\ln 0.91}{\ln 0.98}} \right] + \frac{2k_M}{9} \left( 1 + \log_{0.98} \left( \frac{y + 3.92}{v_0 + 3.92} \right) \right) } \tag{3.2.3}$$

附件

Minecraft 实体运动公式拓展推导.pdf

更新日志

25.12.14 $\LaTeX$ 写完 PDF 附件

25.12.21 抽空复制粘贴稍微改了一下文章同步到博客

持续更新

MORE

低版本 pvp 中玩家最优 kb 的点击频率下限分析

理论2025/10/18

前言

⚠️硬核警告⚠️

英译版见『Minimum KB Click Frequency Lower Bound Analysis』.

本文包含大量数学公式和代码分析,建议具备一定理论基础阅读。

欢迎各位大佬指正🥰。

嗯……AI 帮了点小忙,还是挺好看出来的,对吧。


物理学框架下的 Minecraft 实体运动

经典力学下的运动

$$\frac{dv}{dt} = a - fv \tag{1.1.1}$$

$$v = v_0 e^{-ft} + \frac{a}{f} (1 - e^{-ft}) \tag{1.1.2}$$

$$\frac{v}{v_0} = \lim\limits_{t_0 \to 0} (1 - f t_0)^{\frac{t}{t_0}}, a = 0 \tag{1.1.3}$$

公式 (1.1.1) 描述了物体在恒定驱动力和正比于速度的阻力共同作用下,速度从变化逐渐达到稳定平衡的动态过程。这也是后续所有公式推导的基础。

公式 (1.1.2) 是通过求解公式 (1.1.1) 得到的精确解(通解)。其描述了在任何初始速度 $v_0$ 和任何驱动力 $a$ 的情况下,速度随时间变化的完整规律。

公式 (1.1.3) 则是将驱动力 $a=0$ 时的特殊情况代入公式 (1.1.2) 得到的结果。表明物体仅在阻力作用下,速度从初值 $v_0$ 开始指数衰减到零的过程。

其中,$f$ 为阻力系数。

但是,Minecraft 是以 tick(后续将称为 gt)为单位进行离散更新,即 $dt$ 和 $t_0$ 的最小值为 1gt,不能无限趋近于 0。因此这一规则在游戏中不能精确实现。

Mojang 对于实体运动的实现

以下是实体在水平和空中运动源代码简化版。已删去不必要和不相关的代码。

  • 在 net/minecraft/entity/EntityLivingBase.java 中:

    public void moveEntityWithHeading(float strafe, float forward) {
        /**
         * 环境检测与移动类型判断
         * isInWater()、isInLava()、isFlying
         * 水、岩浆、梯子等特殊状态移动处理(else if)
         */
        
        float f4 = 0.91F; // 惯性系数
        
        if (this.onGround) {
            f4 = this.worldObj.getBlockState(
                new BlockPos(
                    MathHelper.floor_double(this.posX),
                    MathHelper.floor_double(this.getEntityBoundingBox().minY) - 1,
                    MathHelper.floor_double(this.posZ)
                )
            )
            .getBlock()
            .slipperiness * 0.91F;
        } // 获取下方方块滑度(后续将 slipperiness 称为滑度系数)
        
        float f = 0.16277136F / (f4 * f4 * f4);
        /**
         * (0.6*0.91)^3 = 0.16277136
         * 移动系数
         */
        float f5;
        
        if (this.onGround) f5 = this.getAIMoveSpeed() * f;
        /**
         * f5 = this.landMovementFactor * f;
         * landMovementFactor 基础情况下为 0.1,疾跑时*1.3,受药水效果影响
         */
        else f5 = this.jumpMovementFactor;
        /**
         * jumpMovementFactor = 0.02;
         * 疾跑时*1.3,与药水效果无关
         */
        this.moveFlying(strafe, forward, f5); // 速度计算
        
        // 重新计算 f4
        // 嗯···屎山代码这一块(
        
        this.moveEntity(this.motionX, this.motionY, this.motionZ); // 实体移动
        
        // 未加载区块重力处理
        
        this.motionY -= 0.08D; // 重力
        this.motionY *= 0.9800000190734863D; // 空气阻力
        this.motionX *= (double)f4; this.motionZ *= (double)f4; // 摩擦力(水平)
        
        // 玩家肢体动画更新
    }
    
  • 方块的滑度系数在 net/minecraft/block/Block.java 等文件中定义:

    // Block.java
    public class Block {
        // 其它方块属性
        public float slipperiness;
        public Block(Material blockMaterialIn, MapColor blockMapColorIn) {
            // 其它属性初始化
            this.slipperiness = 0.6F;
        }
    }
    
    // BlockIce.java
    public class BlockIce extends BlockBreakable {
        public BlockIce() {
            this.slipperiness = 0.98F;
        }
    } // 冰
    
    //BlockPackedIce.java
    public class BlockPackedIce extends Block {
        public BlockPackedIce() {
            this.slipperiness = 0.98F;
        }
    } // 浮冰
    
    // BlockSlime.java
    public class BlockSlime extends BlockBreakable {
        public BlockSlime() {
            this.slipperiness = 0.8F;
        }
    } // 史莱姆块
    

    滑度系数将用 $f_s$ 在后续的公式推导中来表示。

    特别地,蓝冰在 1.13 被加入,滑度为 0.989。此处仅作补充。

  • 实体在地面的移速(通过 getAIMoveSpeed() 函数返回,本质上是变量 landMovementFactor 的值)在文件 net/minecraft/entity/EntityLivingBase.java 定义:

    public abstract class EntityLivingBase extends Entity {
        private float landMovementFactor;
        public float getAIMoveSpeed() {
            return this.landMovementFactor;
        }
    }
    

    其基本值在 net/minecraft/entity/player/PlayerCapabilities.java 给出:

    public class PlayerCapabilities {
        private float walkSpeed = 0.1F;
    }
    

    疾跑状态的修饰在 net/minecraft/entity/EntityLivingBase.java 中定义:

    public abstract class EntityLivingBase extends Entity {
        private static final AttributeModifier sprintingSpeedBoostModifier = 
            (new AttributeModifier(
                sprintingSpeedBoostModifierUUID,
                "Sprinting speed boost",
                0.30000001192092896D,
                2
            )).setSaved(false);
    }
    

    疾跑速度则是通过 net/minecraft/entity/ai/attributes/ModifiableAttributeInstance.java 中的方法实现:

    private double computeValue() {
        double d0 = this.getBaseValue(); // 属性基本值获取
        
        for (AttributeModifier attributemodifier : this.func_180375_b(0)) {
            d0 += attributemodifier.getAmount();
        }
        double d1 = d0;
        for (AttributeModifier attributemodifier1 : this.func_180375_b(1)) {
            d1 += d0 * attributemodifier1.getAmount();
        }
        for (AttributeModifier attributemodifier2 : this.func_180375_b(2)) {
            d1 *= 1.0D + attributemodifier2.getAmount();
        }
        // 三种类型修饰叠加
        
        return this.genericAttribute.clampValue(d1);
    }
    

    实体在空中的移速(jumpMovementFactor)在 net/minecraft/entity/EntityLivingBase.java 中定义:

    public abstract class EntityLivingBase extends Entity {
        public float jumpMovementFactor = 0.02F;
    }
    

    疾跑时在 net/minecraft/entity/player/EntityPlayer.java 中更新:

    public abstract class EntityPlayer extends EntityLivingBase {
        public void onLivingUpdate() {
            if (this.isSprinting()) {
                this.jumpMovementFactor = (float)(
                    (double)this.jumpMovementFactor +
                    (double)this.speedInAir * 0.3D
                );
            }
        }
    }
    

    屎山代码这两种运动实现还不一样 ):

  • 实体的「motion」在 net/minecraft/entity/Entity.java 中更新:

    public void moveFlying(float strafe, float forward, float friction) {
        float f = strafe * strafe + forward * forward;
        
        if (f >= 1.0E-4F) {
            f = MathHelper.sqrt_float(f); // 模长为 1 的单位向量
            if (f < 1.0F) {
                f = 1.0F; // 避免斜向速度异常
            }
            
            f = friction / f;
            strafe = strafe * f;
            forward = forward * f;
            // 摩擦系数应用
            
            float f1 = MathHelper.sin(this.rotationYaw * (float)Math.PI / 180.0F);
            float f2 = MathHelper.cos(this.rotationYaw * (float)Math.PI / 180.0F);
            
            this.motionX += (double)(strafe * f2 - forward * f1);
            this.motionZ += (double)(forward * f2 + strafe * f1);
            // 2D 旋转矩阵
        }
    }
    

    strafe 和 forward 变量在 net/minecraft/util/MovementInputFromOptions.java 中获取按键输入更新:

    public void updatePlayerMoveState() {
        this.moveStrafe = 0.0F;
        this.moveForward = 0.0F;
    
        if (this.gameSettings.keyBindForward.isKeyDown()) {
            ++this.moveForward;
        }
        if (this.gameSettings.keyBindBack.isKeyDown()) {
            --this.moveForward;
        }
        if (this.gameSettings.keyBindLeft.isKeyDown()) {
            ++this.moveStrafe;
        }
        if (this.gameSettings.keyBindRight.isKeyDown()) {
            --this.moveStrafe;
        }
        
        // 其它按键事件
        
        /**
         * 潜行事件
         * moveStrafe 和 moveForward 乘以 0.3
         */
    }
    

通过分析上述代码实现可以得知:

实体在水平方向上的「motion 属性」,在数值上等于当前速度向量在各坐标轴的分量乘以对应的阻力系数(垂直方向上的「motion」计算略有不同)。

若将 $t_0$ 时间内的「motion」视为平均速度 $M$,那么「motion」与阻力系数的乘积在物理意义上表示在该时间段内阻力对实体产生的冲量。

实体的「motion 属性」本质上是实体运动计算过程中的中间量,仅在游戏执行 moveFlying() 函数计算后,「motion 属性」可被视为实体的速度(或该时间段内的平均速度近似值)。

线性运动公式推导

为了方便理解公式,在这里定义一些必要参数:

  • 速度:$v_H$、$v_Y$,实体在水平与垂直上的速度。
  • 初始垂直速度:$v_{Y,1}$,取决于跳跃速度或服务器垂直击退参数。
  • 滑度系数:$f_s = 0.6$(空气中不受该阻力影响)。
  • 移动乘数:$k_M = 1.3 \times 0.98 = 1.274$(停止状态下为 0)。
  • 效果乘数:$k_E = 1 \times 1 = 1$.
  • 动量阈值:$v_{th} = 0.005$,当实体在某条轴上的速度经衰减后小于该值时,对应轴上的速度归零。

由此推导得到实体运动的递推关系(主要参考 Minecraft Parkour Wiki):

  • 地面水平速度公式:$$v_{H,t} = \underbrace{v_{H,t-1} \times f_{s, t-1} \times 0.91} _ \text{动量保留} + \underbrace{0.1 \times \left( \frac{0.6}{f_{s,t}}\right)^3 \times k_M \times k_E} _ \text{加速度} \tag{1.3.1}$$

  • 空中水平速度公式:$$v_{H,t} = \underbrace{v_{H,t-1} \times 0.91} _ \text{动量保留} + \underbrace{0.02 \times k_M} _ \text{加速度} \tag{1.3.2}$$

  • 垂直速度公式:$$v_{Y,t} = ( v_{Y,t-1} - \mathop{0.08}\limits_\text{重力} ) \times \mathop{0.98}\limits_\text{阻力} \tag{1.3.3}$$

考虑到实际应用场景,上述推导中大部分系数或乘数均使用特殊值。

在后续『最优 kb 的点击频率下限分析』章节中,将加入点击行为对速度的影响机制。


鼠标点击分析

双击延迟

经测试,鼠标双击的触发延迟主要分布在 $[16, 27]\ \mathrm{ms}$ 区间范围内,受硬件性能、驱动程序、系统环境等影响,在不同的测试环境下略有差异。

若两次点击分别落在相邻的 tick 中,则称为「有效双击」。

有效双击的概率计算

设第一次点击的时间为 $T$,服从区间 $[0,50]$ 上的均匀分布:$$T \sim \mathcal{U}(0,50)$$

双击延迟为 $D$,服从区间 $[16,27]$ 上的均匀分布:$$D \sim \mathcal{U}(16,27)$$

则「有效双击」的概率:$$P(T + D \geq 50)$$

$T$ 和 $D$ 相互独立,其联合概率密度函数为:

$$f_{T,D}(t,d) = \frac{1}{50} \times \frac{1}{11} = \frac{1}{550},\ t \in [0,50],\ d \in [16,27]$$

展开积分并计算所求概率:

$$P(T + D \geq 50) = \iint\limits_{t + d \geq 50} f_{T,D}(t, d)\ dt\ dd = \int_{16}^{27} \int_{50 - d}^{50} \frac{1}{550}\ dt\ dd = \frac{473}{1100} = \frac{43}{100}$$

即「有效双击」的触发概率为 43%.

如果你不想阅读这部分略微令人头皮发麻的积分还可以通过条件期望来推导该概率:

$$P(T + D \geq 50) = P(T \geq 50 - D) = E[P(T \geq 50 - D \mid D)]$$

对于固定的 $D,\ T \sim \mathcal{U}(0, 50)$,且 $50 - D \in [23, 34] \subseteq [0, 50]$,有:

$$P(T \geq 50 - D \mid D) = \frac{50 - (50 - D)}{50} = \frac{D}{50}$$

得到:

$$P(T + D \geq 50) = E\left[\frac{D}{50}\right] = \frac{1}{50} E[D] = \frac{1}{50} \times \frac{16 + 27}{2} = \frac{43}{100}$$

与积分计算的结果一致。

🧠☠️


击退算法杂谈

原版击退算法

当玩家攻击时调用 net/minecraft/entity/player/EntityPlayer.java 文件中的 attackTargetEntityWithCurrentItem() 函数处理该过程:

public abstract class EntityPlayer extends EntityLivingBase {
    public void attackTargetEntityWithCurrentItem(Entity targetEntity) { 
        /**
         * 可攻击检查
         * 基础伤害计算
         * 附魔伤害计算
         */
        
        int i = 0;
        i = i + EnchantmentHelper.getKnockbackModifier(this);
        if (this.isSprinting()) {
            ++i;
        }
        // 击退效果计算
             
        /** 
         * 伤害有效性检查
         * 暴击判断
         * 火焰附加
         */
        
        double d0 = targetEntity.motionX;
        double d1 = targetEntity.motionY;
        double d2 = targetEntity.motionZ;
        boolean flag2 = targetEntity.attackEntityFrom(DamageSource.causePlayerDamage(this), f);
        // 伤害应用
        
        if (flag2) {
            if (i > 0) {
                targetEntity.addVelocity(
                    (double)(-MathHelper.sin(
                            this.rotationYaw * (float)Math.PI / 180.0F) * (float)i * 0.5F),
                            0.1D,
                    (double)(MathHelper.cos(
                            this.rotationYaw * (float)Math.PI / 180.0F) * (float)i * 0.5F)
                    ); // 第二阶段击退计算
                
                this.motionX *= 0.6D;
                this.motionZ *= 0.6D;
                this.setSprinting(false);
                // 攻击者减速并取消疾跑状态
            }
            
            if (targetEntity instanceof EntityPlayerMP && targetEntity.velocityChanged) {
                ((EntityPlayerMP)targetEntity).playerNetServerHandler.sendPacket(
                    new S12PacketEntityVelocity(targetEntity)
                );
                targetEntity.velocityChanged = false;
                targetEntity.motionX = d0;
                targetEntity.motionY = d1;
                targetEntity.motionZ = d2;
            } // 避免重复叠加击退
        
        /**
         * 伤害成功后续处理
         * 攻击失败处理
         */
    }
}
// net/minecraft/entity/EntityLivingBase.java
public abstract class EntityLivingBase extends Entity {
    public boolean attackEntityFrom(DamageSource source, float amount) {
        /**
         * 检查实体是否对特定伤害源免疫
         * 客户端检查
         * 死亡检查
         * 特殊伤害免疫(抗火)
         * 装备减伤
         * 伤害刻(特别地,该时间段内更高的伤害会覆盖原有伤害)
         * 攻击者判断
         * 视觉更新(伤害动画)
         */
    
        double d1 = entity.posX - this.posX;
        double d0;
        for(d0 = entity.posZ - this.posZ;
            d1 * d1 + d0 * d0 < 1.0E-4D;
            d0 = (Math.random() - Math.random()) * 0.01D) {
            d1 = (Math.random() - Math.random()) * 0.01D;
        } // 距离过近的随机击退

        this.attackedAtYaw = (float)(
            MathHelper.atan2(d0, d1) * 180.0D / Math.PI 
            - (double)this.rotationYaw
        );
        
        this.knockBack(entity, amount, d1, d0);  // 执行击退
        
        // 其它操作
    }
    
    public void knockBack(Entity entityIn, float amount, double d0, double d1) {
        if (this.rand.nextDouble() >= this.getEntityAttribute(
            SharedMonsterAttributes.knockbackResistance).getAttributeValue()) {
            this.isAirBorne = true;
            float f = MathHelper.sqrt_double(d0 * d0 + d1 * d1);
            float f1 = 0.4F;

            this.motionX /= 2.0D;
            this.motionY /= 2.0D;
            this.motionZ /= 2.0D;
            // 受击者速度衰减

            this.motionX -= d0 / (double)f * (double)f1;
            this.motionY += (double)f1;
            this.motionZ -= d1 / (double)f * (double)f1;
            // 第一阶段击退计算

            if (this.motionY > 0.4000000059604645D) {
                this.motionY = 0.4000000059604645D;
            } // 垂直击退上限
        }
    }
}
// net/minecraft/entity/Entity.java
public abstract class Entity implements ICommandSender {
    protected void setBeenAttacked() {
        this.velocityChanged = true;
    }
    public boolean attackEntityFrom(DamageSource source, float amount) {
        if (this.isEntityInvulnerable(source)) {
            return false;
        } else {
            this.setBeenAttacked();
            return false;
        }
    }
}

为了更清晰地理解击退机制,在这里定义以下参数:

  • horizontal:基础水平击退。对应 knockBack() 函数中的 f1 变量,默认值为 0.4
  • vertical:基础垂直击退。其数值与基础水平击退相同,为 0.4
  • horizontalExtra:额外水平击退。仅考虑疾跑的因素,即 i = 1,值为 0.5
  • verticalExtra:额外垂直击退。addVelocity() 函数中的叠加固定额外垂直击退,默认值为 0.1
  • verticalLimit:垂直击退上限。motionY 的最大值 0.4
  • friction:暂无合适的译法。表示玩家受击时各轴的速度衰减(区别于方块的 friction),默认值为 2.0

基于上述代码分析可知玩家受到的击退效果主要分为两个阶段:

  • 击退的第一阶段:仅与双方在 XZ 轴上的相对位置相关。
  • 击退的第二阶段:仅与攻击者的偏航角(yaw)和攻击者的状态(本文只考虑疾跑)相关。

MMC 击退算法

本小节重点讨论与原版击退算法不同之处。

详细实现见 How MMC Knockback Actually Works.

MMC 引入了以下新的或修改的参数:

totalHorizontal = 0.8835d    // 总水平击退
totalVertical = 0.9055d      // 总垂直击退
rangeFactor = 0.035d         // 距离影响系数
maxReduction = 0.4d          // 最大距离减免
startRange = 3.0d            // 距离减免起始计算距离
idleReduction = 0.6d         // 基础击退倍率
attackBuffer = 1             // 攻击缓存

// 计算后的实际值
horizontal = totalHorizontal * idleReduction             // 基础水平击退
horizontalExtra = totalHorizontal * (1 - idleReduction)  // 额外水平击退
vertical = totalVertical * 0.4d                          // 基础垂直击退
verticalExtra = 0.0d                                     // 额外垂直击退
verticalLimit = 0.4d                                     // 垂直击退上限
attackerSlowdown = 0.6d
friction = 0.0d

不难看出:

  • idleReduction 决定了基础水平击退和额外水平击退的大小,并与 totalHorizontal 的值紧密相关。
  • verticalExtra 的值归零,即玩家的垂直击退固定。
  • attackBuffer 提供伤害刻结束前的攻击窗口。
  • 增加了 rangeFactor、maxReduction、startRange 参数用于减免由于延迟造成的远距离攻击注册的击退。

特别地,当 friction = 0.0 时,受击者的动量归零,即击退完全覆盖原速度。

第一阶段击退算法:

void firstStage(EntityLiving attacker, EntityLiving victim) {
    // 省略部分伪代码
    
    double distance = Math.sqrt(distanceX * distanceX + distanceZ * distanceZ);
    double rangeReduction = calculateRangeReduction(distance)
    double modifiedHorizontal = horizontal - rangeReduction // 远距离击退衰减
    
    double magnitude = Math.sqrt(distanceX * distanceX + distanceZ * distanceZ)
    
    victim.motX -= (distanceX / magnitude) * (modifiedHorizontal * 0.5d)
    victim.motZ -= (distanceZ / magnitude) * (modifiedHorizontal * 0.5d)
    
    double yaw = Math.toRadians(attacker.yaw)
    victim.motX += -Math.sin(yaw) * (modifiedHorizontal * 0.5d)
    victim.motZ += Math.cos(yaw) * (modifiedHorizontal * 0.5d)
}

与原版不同的是,MMC 算法中第一阶段的击退同时与玩家的相对位置和攻击者的偏航角相关(占比各 50%)。并增加了 calculateRangeReduction() 函数用于减免远距离击退。

第二阶段击退算法与第一阶段相似:

void secondStage(EntityLiving attacker, EntityLiving victim, int knockbackEnchantLevel) {
    // 省略部分伪代码
    
    if (extraKBMult > 0) {
        double distanceX = attacker.locX - victim.locX
        double distanceZ = attacker.locZ - victim.locZ
        double distance = Math.sqrt(distanceX * distanceX + distanceZ * distanceZ)
        double modifiedExtraHorizontal = horizontalExtra * extraKBMult // 额外击退
        
        double magnitude = Math.sqrt(distanceX * distanceX + distanceZ * distanceZ)
        
        victim.motX -= (distanceX / magnitude) * (modifiedExtraHorizontal * 0.5d)
        victim.motZ -= (distanceZ / magnitude) * (modifiedExtraHorizontal * 0.5d)
        
        double yaw = Math.toRadians(attacker.yaw)  
        victim.motX += -Math.sin(yaw) * (modifiedExtraHorizontal * 0.5d)
        victim.motZ += Math.cos(yaw) * (modifiedExtraHorizontal * 0.5d)
    }
}

额外水平击退由 horizontalExtra 参数和 extraKBMult 共同决定。并保持与第一阶段相同的 50/50 分配。


最优 kb 的点击频率下限分析


一起来品鉴赤石 Mojang 的代码


参考资料

运动公式 - Minecraft Parkour Wiki

How MMC Knockback Actually Works

对我的世界中PVP击退的研究报告 - LSeng, CarmJos

关于修正并改进 xwj 的 MC 实体运动公式 —— 适用于MC中所有实体 - Bio-Hazard

Minecraft 实体运动研究与应用 - lovexyn0827

The ULTIMATE Guide to PING - sceyna

MCP-919 ( Fully decompiled )

NachoSpigot

致谢

蔚蓝天空Azure_Sky

时代地产

附件

更新日志

25.11.30 - 发布已基本写完的三章

MORE
123...6
avatar
Revethere

挖坑是日常
填坑是缘分

28Posts
55.2kWords
531Days