科学的Minecraft:速度?重力加速度?自由下落极限速度?
在现实生活中,我们把物体在某段时间内的位移与这段位移所用时间的比值叫做“速度”。那么,在一个虚拟的世界中,是否有速度呢?
警告:这篇文章会有大量的代码以及原理讲解,请慎重观看!
这篇文章涉及到的表观现象:各种有关于移动的挂,MC中速度的表示,MC中重力加速度的计算,自由下落极限速度
一.Minecraft的速度定义
首先,MC里面定义了两种测量速度的字段:deltaMovement与speed。
deltaMovement是一个矢量,它决定了在这1tick内实体运动方向和距离,用它除以1tick就是平均速度。
v平均 = deltaMovement / 1t = deltaMovement / 0.05 (TPS=20)
speed则是标量,相当于平均速率的量度,是一个计算平均速率的参数。

二.非玩家的LivingEntity移动
首先,我们要了解speed到底储存的位置——也就是Attribute里面。Attribute里面储存了一个MOVEMENT_SPEED,也就是移动速度。而我们得到的速度则是通过直接读取speed字段,而这个字段正是由Attribute得出的。
由于代码又看不到具体调用,只好又修改LivingEntity.setSpeed,看看它的堆栈。

运行/de:debuginsert net.minecraft.world.entity.LivingEntity 2036 false dump插入代码,得到了以下堆栈:


通过Server的堆栈我们可以看出生物(Mob)的运动都是由MoveControl处理的,下面先讲一下Mob和LivingEntity的移动方式。
move的计算就在它的tick方法里面。这个代码比较长,所以只会讲一部分。
//注:其实生物运动分为navigation->move(X,Z轴上的移动)->look->jump(Y轴上的移动),这里只找出了有关于speed的部分

由于代码过长,我参考了1.15.2的MDK源码作为参考,下面我只会讲这一大部分里面的STRAFE(你没听错,这个单词意思是扫射)部分,这里联系到了一会要讲的move。
首先,获取到speed(65行),同时计算得出实际的行进速度(66行)。之后获得生物根据面朝方向的向前(67行)和左右方向(68行)上的移动距离,计算出实际上的位移(69-72行),获得这1tick内能前进的距离的参数(73行,这时候f5单位是s^-1)。接下来应该是获取到将要走到的位置,检测是否能到达(74-86行)。之后,设置speed(87行),传递X,Z方向上的位移(88-89行),设置状态为WAITING(90行)。
这里我们看到了怎么设置speed,但是没有看到move,不用着急,这只是我们了解它的第一步。

紧接着,我们发现Mob这里的逻辑完成之后又调用了上一级的LivingEntity.travel,这个就是我们下一步说的移动的关键之一。
还记得MoveControl里面的“传递X,Z方向上的位移”吗,我们可以看到,这个方法的传入值就有xxa和zza,那么我们在这里找到move的可能性就很大了(ArmorStand没有继承Mob而直接继承了LivingEntity,它是一个例外)
由于这个方法有200行,所以我只能讲解一部分,发出一部分代码。

下面对这段代码进行解释:
获取脚下方块(1889行),之后得到行进阻力(1890-1891行),通过moveRelative方法进行阻力行进运算(1892行)。对移动加上攀爬时的Y轴方向速度(1893行),将这些值代入,move中进行实际的移动(1894行)。(这些为该刻处理运动的部分)
获取现在的移动速度(1895-1899行),进行Y轴速度的判断:
1.具有漂浮效果,将Y轴速度改为 漂浮等级+1-原Y轴速度
2.如果重力存在,将Y轴速度向下加0.08(没有缓降效果)/0.01(具有缓降效果)
3.在客户端运行且区块未加载,将Y轴速度改为-0.1或0
进行此判断后重新计算速度,现速度X,Z轴上速度为原先的速度乘以阻力比例,Y轴则是判定结果乘上0.98。(这里是下一次移动前的准备)

关于阻力比例,公式是这样的:
当实体站于地面,v = speed * (0.21600002 / (blockFriction ^ 3)
当实体在天空中,v = flyingSpeed (0.2)
这个方块阻力和动摩擦因数μ差不多,算得的速度应该是speed*(0.6/friction)^3,因为浮点数误差,所以原先的0.216变成了0.21600002,不过这对我们得出结论没有影响。
下面来看它调用的最终级方法:move
这个方法定义在Entity里面,也就是所有实体公用的,不像LivingEntity的travel一样只适用于LivingEntity和它的子类Mob。
由于这里又是150行代码,我们还是讲精华部分。

下面解读一下代码,注意,这里所有的XXX > 1.0E-7D都是在判断移动发生,由于浮点数误差,计算后的数据即使你看的是0,其实还是有很微小的值,用XXX == 0是不能起效果的:
(491和505行:报告状态,对于移动判断没有用处,就是调试用的)
492-496行:如果阻碍运动参数大于0,则应用阻碍,并将下一刻的参数和位移置为0
498行:应用潜行(对于玩家的)
499行:计算碰撞,返回实际的位移
500-503行:如果位移不为0,启用真正的移动
真正的移动分为了两部分:第一步,setBoundingBox,设置包围盒。此时,该实体的包围盒已经变化了,但是渲染位置不变;第二步,setLocationFromBoundingbox,从现在的包围盒位置获取到现在的实体位置,这时候,渲染位置才改变到正确位置。
好了,我们看到了LivingEntity移动的全过程,那么如果不是LivingEntity呢?
三.非LivingEntity的移动
这里我将讲这两个非LivingEntity实体:下落的方块,点燃的TNT。
1.下落的方块(FallingBlockEntity)
下落的方块的移动是写死到tick里面的,由于又是一大串代码,还是取其精华。

这几段代码里面清楚地写到了move和deltaMovement,可以判断出:这就是它运动的方式。可是你可能会问:活塞推动下落方块也会造成位移,这里没有体现啊?其实,move的第一个参数,MoverType里面定义了不同的移动方式,这种运动方式不需要实体自己计算,活塞推动是MoverType.PISTON,而实体自己移动是SELF。
2.点燃的TNT(PrimedTNT)
点燃TNT的位移是在两个地方计算:tick和构造函数。

这个代码有两个部分,一部分是构造函数的,另一个在tick里面。tick里面的代码和FallingBlockEntity的代码大致相同,不再解释。构造函数中的代码是点燃TNT的时候,TNT会“蹦一下”,这个的计算就是这里体现的。具体来说,这一刻跳起的高度应该是0.2(浮点数误差x3),而X、Z轴上则是通过随机数产生一个方向位移0.02。
四.玩家的移动
还记得最上面的那张Client堆栈吗,我们在那里发现了Player的踪迹。我们通过这条线索向下查:

这里能够看出,speed是由服务器端计算的,并将这个值存到Abilities里面发送回来。
但是,deltaMovement去哪了?

由于Player是LivingEntity的子类,翻找过后,发现在服务器端的ServerPlayer覆盖了tick并且没有进行super调用,而我们普通LivingEntity进行移动正是通过tick调用aiStep进而调用travel实现移动的!并且在这个新的tick里面,并没有aiStep和travel的踪迹......
那么ServerPlayer是怎么实现移动的??
那么在这个问题解答之前,大家可以想一想飞行挂、快速移动和载具加速这种东西是怎么产生的。并且,如果你用过tweakeroo的灵魂出窍(FreeCamera),在灵魂出窍的时候被推动,你有可能就悬空了......在这些现象的背后,是什么原理?
对,没错。因为服务器端只负责了“移动你”,但是不负责“计算使你运动”,而客户机端才真正负责了玩家的自身(PLAYER)实体移动。关于这一点为何这么实现,其实也很简单的道理——因为你可以操控玩家,而其他的实体都靠着物理引擎和AI直接运算。
//注:服务器端不是什么玩家移动都靠客户机端,因为活塞移动等计算是在服务器进行的,因为活塞移动等是服务器操作,活塞移动同时就进行了move方法调用。而客户机端掌握的移动只有PLAYER的自身移动。
为了证明这一点,我们可以去翻找LocalPlayer的源码。

在LocalPlayer里面,我们发现了sendPostion发送位置,这代表了客户机计算位置的正确性。为了继续证实,我们翻找一下ServerboundMovePlayerPacket的执行者,也就是ServerGamePacketListenerImpl。

由于代码长度太大,120行左右,就简单说一下它干了什么:
检查数据的正确性
如果玩家还在等待区块加载传输,则“固定”玩家位置,防止因为客户端区块未加载导致的掉入虚空
如果玩家是乘客,那么进行加载区块缓存
如果玩家睡眠时的位移超过1,将被强制传送回床上
如果在1tick内玩家发送了超过5个移动请求,那么警告“<player> is sending move packets too frequently (<packets> packets since last tick)”,并且强制合并为一个包(也就是为什么移动会弹回的原因之一)
如果玩家不是在进行维度变更,或者鞘翅飞行速度检查启用时进行鞘翅飞行,如果速度过快(公式就不给了),将警告“<player> moved too quickly! <x>,<y>,<z>”,并强行传送回之前的位置(弹回原因二)
在这之后,进行实际移动,MoverType为PLAYER
判断玩家移动的正确性,不正确警告“<player> moved wrongly!”
处理摔落伤害等数据更新
通过这些,我们就能看出Player到底是怎么移动的了。
五.MC的重力加速度到底是多少
通过前文,我们基本上了解了移动是怎么发生的了,下面就来看看重力加速度到底是多少吧。
首先是LivingEntity里面的分析,抄一下之前的话:
获取现在的移动速度(1895-1899行),进行Y轴速度的判断,
如果重力存在,将Y轴速度向下加0.08(没有缓降效果),
判定结果乘上0.98。
这样,我们得出了一个有关于速度的以tick为自变量的函数,记为F(x)

由于浮点数的误差,这个值很快就会固定到3.92m/tick(实测是3.9200038147008747,因为有浮点数误差),也就是78.4m/s,这也就是生物自由下落极限速度。

这时候,如果我们认为下落开始一瞬间不计空气阻力,那么重力加速度就是x=0时的导数值
F'(0)=0.07919
也就是约为31.7m/s^2
下面是客户端的验证,这里用到的是fabric端,用了miniHUD的功能。

我们看到,随着速度越来越接近78.4m/s,速度增长越慢,在78.4m/s彻底停止,也就认证了我们的理论。
然而这就结束了吗?并没有。非生物实体也有类似的公式,像下面这样:
其中friction代表阻力,accel代表“每刻的加速度”。
由上面的公式,可以得到速度公式:
终端速度
具体的阻力和“加速度”依照下表,来自Minecraft Wiki“实体”页面:

六.有关于粘液块的事情
粘液块是一个能将detlaMovement进行修改的方块,在你站立于上方时,你本身会有一个向下的“趋势位移”(在上一篇专栏有讲),而粘液块能修改它,导致你站在粘液块上时deltaMovement会很欢快地跳动。具体可以看看验证视频。

专栏中使用的Minecraft反混淆+动态修改程序:MCDynamicExchanger,地址https://github.com/Nickid2018/MCDynamicExchanger
数学公式绘制:MathType 函数绘制:几何画板
Minecraft:反混淆1.14.4端+动态修改程序
fabric端:模组是maLiLib,miniHUD,(tweakeroo,litematica,itemscroller,worldedit)
启动器:HMCL 3.3.172
分配内存:512MB
系统:Windows 7 32位 运行内存2GB
验证视频:

如果有任何问题或者文章中有错误,可以在评论区告诉我,我会修改掉错误的。