01 实体运动基础
1 实体运动基本属性及设定
在 Minecraft 中,所有可运动实体都有以下运动相关属性:
(1) 碰撞箱(碰撞体积,轴对齐边界箱,AABB):一个长方体区域,标记实体占用的空间。碰撞箱由两个点指定,其中一个点的任意轴上坐标都不小于第二个点的对应坐标,所指定碰撞箱是那两点之间的一个各面都垂直某一坐标轴的长方体。与实体渲染出来的样子不同,原版 Minecraft 中所有碰撞箱各沿轴视图都是矩形,俯视图都是正方形,而且不会随实体的朝向改变而旋转。
(2) 坐标:表示实体的位置的一个点,通常位于实体碰撞箱底面的中心(例外:拴绳结)。Minecraft 中的坐标系是一个典型的三维右手直角坐标系,规定东西方向分别为 X 轴的正负方向,规定上下方向分别为 Y 轴的正负方向,规定南北方向分别为 Z 轴的正负方向。与现实不同,实体的坐标变化不一定完全由速度决定,因为实体可以被以某种方式传送。在 Minecraft 中,还存在一个用于指定方块所在单元的方块坐标,由精确坐标到方块坐标的转换可以通过向下取整其分量进行(BlockPos.java, <init>)。
图 2.1 坐标系及偏航角示意图
(3) Motion(又称动量,Momentum):可认为是一个存储速度及其运算中间量的容纳一个三维向量的容器,在每次加速和阻力作用时其中存储的 "准速度" 会被改变,在移动时其中的值会被作为位移(就是那一游戏刻的平均速度)使用。可以这样说,Motion 决定速度,但是 Motion 的值并不总是严格等于速度,有时甚至会有很明显的差距,如阻力系数较大和被活塞推动时。说一个比较离谱的,堆叠的矿车被加速时其 Motion 会指数爆炸式增长,在经过一段距离(与堆叠数量有关,堆叠越多越短,堆叠 100 个时不到 20m)后可以达到无穷大,但是其单轴速度永远不会大于 0.4m / gt。不过,如果真的不喜欢这样,把后文中的 Motion 中大部分换成 "速度" 问题也不大,能理解就好。
(4) 速度:表示实体在一定时间内不计传送的坐标值的变化量的一个三维向量。因为 Motion 除速度外还存储速度的运算中间量,所以实体的速度并不等价于 Motion,尽管一般都认为它们是一回事。可以这样认为,Motion 是实体运动的决定,而速度是实体运动的结果。
(5) 眼部坐标:一个用于某些计算的点,x、z 轴上坐标与实体坐标相同,但 y 轴上会比实体坐标高出来一点,默认为实体高度的 0.85 倍,但也有例外,如箭是 0.13 格,详见附录 A。
(6) 俯仰角(pitch):用于描述竖直方向的朝向的一个角度,范围在 - 90 度~90 度,水平方向为 0 度,向上为负,向下为正。后文中用到的俯仰角皆以此为准,无论它是实体的一个属性还是一个纯粹的方向,下同。
(7) 偏航角(yaw):描述了实体的水平方向,也就是东西南北或是南偏东 20 度之类的。其取值范围在 - 180 度(不含)-180 度 (含) 之间,正南方对应 0 度,此处起顺时针为正,逆时针为负。
(8) 空气阻力系数(wiki 上称 "阻力",对应一些公式中的 "f"):实体在自由运动时会在各轴上受到一个大小等于对应轴上速度大小与对应轴上空气阻力系数的乘积,方向与对应轴上速度方向相反的阻力加速度,空气阻力系数表明了实体在自由运动时减速的快慢,其值越大,速度衰减越快。
Minecraft 中与运动相关的实体运算主要有以下六种:
(1) 重力作用(Gravity,简称G)
(2) 阻力作用(Drag,简称D)
(3) 自身运动控制决定的加速(Controlled Acceleration,简称C)
(4) 流体加速(Fluid Acceleration,简称F)
(5) 位移确定(Displacement Calculation,简称Dc)
(6) 坐标及碰撞箱更新(Position & AABB Update,简称Pu)
重力作用的含义显而易见,而流体加速在 5.7 节还要详细说明,这里不再赘述。
此处阻力的概念较为广泛,包括了空气阻力、流体阻力和地面阻力这三类在 Mojang 的设定中在某坐标轴上与该实体在该轴上速度大小成正比且反向的作用力。然而,如果试着将这一设定在牛顿力学中的表达式:
其中是阻力系数。
类推到 Minecraft 中你就会发现这一设定无法准确地被实现,因为和不能小于 1gt,更不能无限趋近于 0。实际上,Mojang 对这一运算的实现是将每个阻力作用时受阻力作用的各轴(一般含 X、Y、Z 轴,地面阻力一般只有 X、Z 轴)上的 Motion 减去对应轴上的阻力系数以为单位的数值与对应轴上 Motion 的乘积,即
其中是某一受到阻力作用的轴上的 Motion,为对应的阻力系数,。
在第 3 节公式推导中通常是一个有特别意义的值,说明实体受到阻力的本质就是 Motion 被乘以了这个数,所以在那里我会将这个数字记作 k,称为速度乘数,无单位。现有实体中 X、Z 轴上的阻力系数一般相等,这时可以认为在水平方向上的阻力系数就是 X 或 Z 轴上的阻力系数,或者说水平方向上的阻力加速度大小只与阻力系数和水平方向上的合速度大小相关,方向总是与水平方向上的合速度相反。很多实体(附录 A给出了部分例子)各轴上空气阻力系数大小都是相同的,那可以类推,这时可以认为所有方向的阻力系数就是 X、Y 或 Z 轴上的阻力系数,或者说阻力加速度大小只与阻力系数和合速度大小相关,方向总是与合速度相反。
分开来说,空气阻力是指实体在空中运动时受到的阻力,包括火球,几乎所有可通过指定 Motion 标签自主移动的实体都有这种阻力。流体阻力是指实体在与流体接触时受到的阻力,经常与流体种类有关,有时还与浸入深度有关。有些实体没有流体阻力,在流体中受到的也是空气阻力,如 TNT。空气阻力与流体阻力经常不共存,但也有例外,其中一个是物品实体(见 7.3)。地面阻力较为复杂,包括滑度作用、灵魂沙、粘液块和蜂蜜块对上方实体的减速以及 TNT 等实体在着地时受到的较大阻力,这些将在第 5 节的前几小节和第 7 节对应小节中详细说明。最后补充一下,虽然这几类阻力在这里被归为了一大类,它们在实际运算中的位置也不一定相邻,这在一些计算中可能会造成难以确定实体类型和 k 值的问题,解决方案可以参考 3.2 节。
阻力系数的单位是负一次方刻,即,这个单位有两种理解。第一个是把它写成,也就是把它理解成阻力加速度和速度的比值。第二种是把它直接就看成,即一定时间内速度减少量在原速度中的占比。
自身运动控制造成的加速主要包括生物的 AI 控制的加速、玩家控制的加速、火球类实体(见 2.3 节)的 Power 标签指定的加速以及矿车在充能铁轨上的加速等可以认为是实体在自身运算过程中 "自己要" 产生的加速。这将在 6.2 节和第 7 节对应小节中详细说明。
可能有些出乎意料,部分弹射物的坐标和碰撞箱移动是在位移确定并进行了一些其它运算后才会进行的。乍一看中间那些运算很可能会导致一些不希望出现的问题,但实际上并非如此。一方面,大部分实体(可能包含使用 move 方法(见 4.1 节)移动的所有实体,也就是烟花火箭、浮漂和其它所有非弹射物的可移动实体)的坐标和碰撞箱移动阶段在位移确定且进行完应有的预处理(如碰撞判定)后就被立即执行了,可以认为这两个阶段同属一个阶段,也就是移动(Movement,简称M);另一方面,很多时候(如实体在均匀介质中运动时,不考虑与外界方块和实体的某些交互时等)在位移确定且进行完应有的预处理后就立即执行坐标和碰撞箱移动与原顺序是等价的,也就是说此时仍然可以认为坐标和碰撞箱移动与位移确定同属一个阶段,即移动。
根据移动、阻力和重力作用的顺序,可以分六类,即 MDG(移动 -> 阻力 -> 重力)、DMG、DGM、MGD、GMD 和 GDM。部分实体的重力加速度或阻力系数为零,可认为对应阶段不存在,于是又派生出六类实体:DM、MD、GM、MG 和 M。同一非实体同一状态(如是否在水中)下的运算顺序一般不变,具体可以参考附录 A。其中部分类型没有对应实体。
重力加速度等只是加速度的一些特例,实际上为了在一些时候方便研究,可以将所有加速度(Acceleration,简称A)统一。上面涉及到的几类实体分别可以改称为 MDA(移动 -> 阻力 -> 加速)、DMA、DAM、MAD、AMD、ADM、DM、MD、AM、MA 和 M。另外还需注意,因为 Minecraft 中一个给定加速度的作用时间一般为 1gt(大概就是这个意思),即实体受到的加速度的作用时间是一样的,这里的加速度含义更接近 Motion 改变量,也类似于物理学中的冲量(只是不涉及实体的质量),所以后文中也会将加速度称作 “冲量”。
总结:加速度的本质是加法,阻力的本质是乘法。
2 实体运算位置及实体间运算顺序
众所周知,原版 Minecraft 服务端(含客户端内置的服务端)的主线运算是单线程的,也就是说 Minecraft 服务端不会在同一时间运算多个维度、进行多个运算阶段或者运算多个实体。所以,可以给出 Minecraft 服务端每 tick 的运算流程大概如下:
这张图是高度简化的,而且不同版本间图中步骤的顺序可能会有细微的的变更,比如在 1.17.1 中,计划刻步骤被向前移动到了相应维度的刷怪、随机刻与卸载这些步骤的整体之前。尽管近几年的改动很少会对玩家以外的实体运动行为造成显著的影响,但对其有一个清晰的把握在更广泛的领域中的重要性也不言而喻,如果有兴趣的话,读者可以翻一下 Fallen_Breath 的 B 站主页看一下相对完整的版本 **[2]**(需注意适用版本)或直接翻阅源代码。
图 2.2.11.16.x 服务端每 tick 运算流程图
定义:
根实体:没有骑乘任何实体的实体。
同位实体:直接骑乘同一实体的实体。规定所有根实体互为同位实体。
实体的加载包括创建新实体、重新加载已卸载区块中的既有实体和从某一维度传送到另一维度。
研究ServerWorld.tick():355-418、ServerWorld.tickEntity()和ServerWorld. tickPassenger()方法,可以得到以下几点规律:
(1) 第一个被运算的实体永远为根实体,因为骑乘者的运算依赖于根实体的运算。
(2) 所有根实体中,最近一次被加载到所在维度较早的实体先运算。
(3) 任意实体若有骑乘者,则骑乘者会紧随该实体之后运算。
(4) 一个实体本身及其所有骑乘者完成运算后下一个应运算的同位实体才(就)会开始运算,如果没有还未运算的同位实体,该实体骑乘的实体的下一个同位实体才(就)会开始运算,依次循环,直到所有实体运算完毕。
(5) 一组互为同位实体的实体中最近一次开始骑乘该实体较早的实体先运算。
(6) 所有实体运算完成前不能向世界实体运算列表加入新实体,期间创建的所有实体会在该维度整个实体运算阶段完成后按计划加入的顺序加入。但是,向区块的实体列表的加载可以正常进行。也就是说,在这段时间内中,被加入的实体在该游戏刻不会被运算,但是通常可以在其它运算中被取到(如可以被炸毁,可以被推动等)。
(7) 被标记 removed 的实体和末影龙的组成部分(EnderDragonPart)不会被运算。
看起来比较难懂,那可以举一个例子,假设某维度有按最近一次加载时间编号 1~20 的 20 个实体,编号越小的最近一次加载越早,它们的骑乘关系及运算过程如图 2-2-2 所示,其中最上方都是根实体,骑乘者有一个额外编号标明其最近一次开始骑乘所骑乘的实体的时间顺序,红色数字标明运算顺序:
图 2.2.2 实体运算顺序示意图
可以看出,实体由骑乘关系组成了一个个树形结构,每个树形结构中一个分支运算完成后下一个同级分支就会开始运算,且分支内的运算顺序是由 "根" 到 "叶" 的。若有相关知识基础,我们也可以用森林的先序遍历或 DFS 遍历算法来类比这一过程。
掌握实体运算顺序在很多情况下都是有用的,如果没有这样的实体运算顺序,现在的很多 TNT 炮就无法稳定工作,原因可以发挥一下想象力(推进 TNT 没炸完炮弹就运算飞走了)。如果还是看不懂,那记住下面这一条就可以满足绝大部分需要了:
在不涉及实体间骑乘、区块重载和跨维度时,一个维度中较早创建的实体先运算。
服务端玩家实体是一个特例,它在实体运算阶段只进行一些触发进度一类的运算,服务端运算中运动部分则是发生在整个游戏刻的最后。
3 Minecraft 实体分类
为了更好地指代多种实体,这里将基于源码中的继承关系对 Minecraft 中实体进行简单分类。
先供上 wiki 上对实体的定义:
实体(Entity)包括在 Minecraft 中所有动态的、移动中的对象。
Entities encompass all dynamic, moving objects throughout the Minecraft world.
不过个人认为还可以这样定义实体:
实体是 Minecraft 中一类独立存在的、实时更新且通常具有实质性行为的对象。
其中独立性标明了实体不是像方块实体那样依赖于其它对象而存在,运算的实时性又排除了方块一类没事不运算的对象,应该还行吧。wiki 上那一条定义个人认为并不准确,因为并不是所有实体都是可移动的。
大部分实体都可以归到四大类中:弹射物、LivingEntity、装饰物和矿车。
图 2.3 实体分类示意图
(1) 弹射物:是一类正常情况下能以某种方式被发射出去的实体,该类别中所有实体都可以通过指定 Motion 标签自主移动。其中可以由单次右键使用物品投掷的归为投掷物;没有重力且可以通过指定 Power 标签给予加速度的实体统称火球类;在落到地面后有插入地面的行为且不会立即消失的统称PersistentProjectileEntity(直译为 "持久性弹射物实体");另外还有四种实体难以归入任何一类,在此不再细说。
(2) **LivingEntity:** 是一类有生命值、属性值及使用状态效果的能力的实体,非得要翻译就是 "活的实体",除潜影贝、NoAI 标签为 true 的生物和 NoGravity 标签为 true 的盔甲架外都可以通过指定 Motion 标签自主移动。其中,玩家在类层次中还可分为客户端主玩家(ClientPlayerEntity)、客户端上存在的其它玩家(OtherClientPlayerEntity)和服务端玩家(ServerPlayerEntity),这些细节将在 7.10 节中再详细说明。生物是一类具有 AI 的 LivingEntity,更深层次的分类较接近大家的一般认知,图中完全可以说明。我并不确定可不可以认为 PathAwareEntity 就是可寻路的生物,因为其它生物也有一定的寻路机制。至于盔甲架...... 你没看错,它的很多行为很接近玩家和生物,如存在生命值,可以装备盔甲,甚至有一定的附魔和状态效果使用能力,所以 Mojang 顺便就把它归到 LivingEntity 来了。
(3) 装饰物:是一类与方块网格对齐,在移动或有非零的 Motion 时被破坏的实体。
(4) 矿车:更不必多说。
(5) 末影龙的组成部分:尽管它们仍是 Entity 类的子类,它们严格意义上来讲可能并不是一类实体,因为它是末影龙的一部分(不独立存在)且不会被运算,应该只是一个用于标示末影龙各部分范围并可以与外界进行一些交互的工具。说句题外话,我猜测之后它可能会被用于分割一些较复杂的实体的碰撞箱用于更精确的碰撞判定
(6) 余下的实体:可以通过指定 Motion 标签自主移动的有船、经验球、末影之眼、下落的方块、物品和 TNT,此处仍未列出的实体一般不具有自主运动的能力。
4 Entity 类定义的实体模型
该段内容会涉及一些源码,但非常重要,最好能看完。
就是不看代码估计大家也有不少人猜到所有实体的基类是 Entity。刨去一些 getter、setter 和形式上的代码,该类主要包含所有实体共有的一些属性(坐标、Motion 和朝向等)、大部分实体都有的行为(被下界门传送、被保存和检查方块碰撞等)以及一些不便放到其它任何类的内容。
下面给出 Entity 类中与运动相关的主要的字段:
entityId:实体的一个整数 ID,即实体 ID。每次游戏启动(服务端重启或客户端重启)后游戏会将一个计数 MAX_ENTITY_ID 置为 0,后来每次 Entity 对象被创建时该值会被加一并取加一之后的计数作为所创建 Entity 对象的 entityId。在客户端实体的 entityId 会被二次修改以与服务端保持一致,服务端玩家重生时也会将新玩家对象实例的 entityId 与设为与重生前一致,但是计数永远不会进行除上述 "加一" 操作之外的变更。1.17 中该字段被更名为 networkId,说明了它最大的用途还是两端同步,但是在一些比较偏的地方它确实也被某些运算用到了,甚至有些还被某些装置利用了 **[3]**。
pos:坐标
velocity:用于存储可通过指定 Motion 标签自主移动的实体的坐标变化趋势及其运算中间量,也就是 Motion
yaw:偏航角
pitch:俯仰角
entityBounds:碰撞箱
onGround:表示该实体着地,在使用 Entity.move()(详见 4.1)移动实体时由以下代码确定(Entity.java:546-547)
即实体在位移趋势向下时发生了竖直方向的碰撞。其中,movement 为该次移动的实际位移趋势(经活塞推动、蛛网浆果从减速和潜行相关调整后的结果),vec3d 为 movement 在经过碰撞调整后的结果。实体在某次移动中撞到地面又凭借水平位移趋势移下了平台或移到了一个坑上面后就会被错误地认为着地,被活塞向上推动的实体也不会被认为着地,个人认为这应是一个 Bug,但修复它可能要修改沿轴运动顺序并造成更严重的问题。
horizontalCollision:表示实体最近一次移动中在水平方向上发生了碰撞,即实体在 x,z 轴上实际位移趋势和实际坐标变化量在有不可忽略(差值绝对值大于)的差别。在基于 Entity.move() 方法的移动过程中确定。
verticalCollision:表示实体最近一次移动中在垂直方向上发生了碰撞,确定方式见 onGround。
stepHeight:存储实体能直接上去(如玩家和大部分生物直接走上去台阶或玩家被活塞从侧面推上完整方块)的高度。
noClip:表示实体不会与其它实体和方块发生碰撞,有时会无效。该字段为 true 的实体有末影龙的整体轮廓、运算中的恼鬼、客户端上存在的其他玩家、NoGravity 或 Marker 标签为 true 的盔甲架、旁观者玩家、潜影贝导弹、因忠诚附魔返回中的三叉戟、区域效果云和卡在方块中的物品。
movementMultiplier:位移乘数,一个三维向量,详见 5.1。
NO_GRAVITY:只是一个键,本身不存储值,但可以用于从 dataTracker 取来自 NoGravity 标签的表示实体不受重力影响的一个布尔值。
fallDistance:存储实体下落的高度用于摔落伤害等计算,可能会因为一些原因被改动而不准确。
实体基础运算在 Entity.tick() 和 Entity.baseTick() 方法中定义,大部分实体(例外:TNT、下落的方块、画、物品展示框、末影水晶、拴绳结、幻魔者尖牙和矿车)都会通过 super.tick() 来一层一层地指向 tick () 方法在 Entity 类中最基础的版本,其流程如下:
- 向客户端更新发光状态(385-387)
- baseTick (),默认如下:
- 若正在骑乘的实体被移除,则停止骑乘(394-400)
- 骑乘冷却更新
- 记录当前的水平速度和朝向备用 (402-404)
- 下界传送门相关更新(405)(tickNetherPortal ())
- 疾跑粒子(406-408)
- 流体相关更新(410-412)(updateWaterState ())
- 清空上一次更新流体时的液面高度记录(980)
- 更新水流变速(981)
- 更新熔岩变速(982-983)
- 火焰相关更新(413-428)
- 熔岩相关更新(点燃和 FallDistance 减半)(430-433)
- 虚空(-64 以下)相关更新(直接移除或受虚空伤害)(435-437)
- 向客户端更新着火状态(439-441)
- 标记已经过第一次更新(443)
我们主要关心 2.4 和 2.6,这分别将在 5.18 和 5.7 节处讲解。实际上,很多时候如果把整个实体基础运算压缩得只剩这两环节问题也不是很大。
5 误差和一个重要 Bug
因为在计算机上所谓实数只是一个有限位数(坐标,Motion 使用的双精度约有 15.65 位精度)的浮点数。所以当出现无法用浮点数精确表示的数时,就会产生浮点误差。浮点误差在大部分情况下基本可以忽略,毕竟在值不是很大,运动时间较短时,误差只在不到数十万分之几的数量级,而且 Minecraft 设计和源代码中很多地方也对浮点误差进行了专门处理,如很多方法中存在的是否小于或而非是否等于 0 的判定、有些地方将绝对值过小的 Motion 或 Motion 改变量替换为 0 以及 1.13 及以后的版本中基于 Entity.move() 的碰撞判定也会留出m 的容错空间等。
在精度要求很高时,例如涉及到取整和比较操作或数据本身就很小时,浮点数误差会导致一些诡异的问题,需要注意。下面是一些例子:
(1) 珍珠炮中珍珠高度计算时有m 左右的偏差
(2) TNT 归中时坐标即使在理论上完全重合,坐标也可能有所差异
(3) 1.13 及之前坐标值范围跨越 2 的整数次幂的位置一些装置因碰撞检查出现方向性
(4) 爆炸接触率计算中取样点的异常偏移(只是预言)
根据产生时机,可以将浮点数误差分为编译时产生的浮点数误差和运行时(运算中)产生的浮点数误差。
前者误差由存储常数所用的浮点数种类决定,在常数为整数和 0.5、0.25、1.25 和 0.0625 一类可以用 52 位有效数字以内的二进制小数表示的分数时不会差生误差,产生时大小大概在至的数量级,但有时会因误差积累达到较大值,尤其是阻力系数和速度乘数被存储为单精度浮点数时。这一误差是可以在某种程度上消除的,即在高精度计算中使用真实值而非近似值,这一真实值可以在一些浮点数转换工具中转换得到或从原始的 class 文件中找到。表 2.5 中给出了几个例子,而且附录 A 中也进行了相关说明。
表 2.5 几个数值对应的真实浮点值
后者误差大小因为坐标和 Motion 使用了精度较高的双精度浮点数而在坐标绝对值不是很大的位置显得很小,实际上世界边境处无非也就是的数量级,不经长时间累积很难被发觉,但它是无法消除的。
浮点数误差不是完全随机地出现的,有一定的规律:
(1) 坐标绝对值越大的地方由运算造成的浮点数误差越明显
(2) 坐标绝对值接近 2 的整数次幂的位置浮点数误差更可能发生(有人称之 power line bug)
(3) 速度为 0 的移动和加速度为 0 的加速不会产生浮点数误差
(4) 把属性设定为一个定值的过程中不会发生浮点数误差,除非那个定值本身就有误差
(5) 在相同位置,相同的影响造成的结果是相同的,除非存在人为的随机性,即参与运算的各元素及运算过程确定时运算结果也确定
(6) 浮点数误差的发生情况通常关于原点呈中心对称
这些只是一些关于浮点数误差的非常基础的结论,如需要深入研究,可以查阅相关资料或国际标准。
出于效率原因,Minecraft 中大部分三角函数不会被准确地求出,而是在 MathHelper 类中通过查表(间隔为 的正弦函数表)得到的,求弦函数的平均误差约在 ** 的数量级,而且取值相对离散,需要留意。
上面也提到过,在很多时候,绝对值小于或的数值会被替换为 0,这本是 Mojang 应对浮点误差的一种策略,但有些时候确实可能造成一些奇怪的问题,这里算是给出一个考虑方向。
除去各种误差,另一件必须指明的事是 Minecraft 中查询与给定范围相交的实体算法中存在一个严重 Bug。
给定一个 AABB(一个各面都垂直某一坐标轴的长方体区域),查询与该 AABB 相交的实体,则只有与该 AABB 的切比雪夫距离小于 2m 的区段(Section,又称子区块,Subchunk)中,或者说与该 AABB 向各方向扩大 2m 的范围不计边界地相交的区段中的实体会被 "看到",其它区段中的的实体在此时是 "看不到" 的。对于较小的实体,这一般不会造成问题,但如果实体的高度超过 2m(不含,下同)或宽度超过 4m,问题就会开始显现,它们超出所在区段超过 2m 的部分在某种意义上是不可见的。如图 2.5 所示,绿色方框标示的实体坐标点位于最中央的区块(俯视图只能画出来区块),则只有与给定的 AABB 与红色区域相交时该实体才会被看到,或者说该实体黄色部分与灰色部分别是可见与不可见的。
在实际操作中,如果处理大型实体时出现了活塞无法推动它们等异常现象,这将是一个主要的考虑方向。Fallen_Breath 曾发了一个视频来演示这一 Bug**[3]**,在这一特性较为明显的地方文中也进行了专门讲解。
图 2.5 getEntities () 方法的 Bug 示意图(俯视)
6 源码走读
6.1 Entity 的运动相关字段
1.20.1 Yarn 中 Entity 类的核心运动字段:
原文中提到的 pistonMovementDelta 和 pistonMovementTick 在 1.20.1 中已从 Entity 移走 —— 它们现在是 Entity 类中 move() 方法处理 MovementType.PISTON 时使用的局部变量,而非持久字段。
6.2 baseTick ():所有实体的公共基础运算
原文中提到的 "原文 2.4(下界传送门)和 2.6(流体加速)" 正对应 baseTick() 的步骤 4 和步骤 6。
6.3 实体运算顺序的源码依据
tickPassenger 的递归结构解释了原文中的 "森林的先序遍历"—— 根实体先 tick,然后递归地 tick 每一个骑乘分支上的所有骑乘者。entityList(1.20.1 中为 EntityList)的迭代顺序决定了根实体间的 tick 顺序 —— 加载早的实体在列表中靠前,先被 tick() 调用。
6.4 Entity.move() 中 onGround 的判定
这里揭示了原文中提到的 onGround 判定 Bug 的本质:groundCollision 只检查 Y 轴位移趋势是否向下 且 被阻挡。如果一个实体先是向下移动被地面阻挡,然后因为水平位移(如被活塞横向推动)使碰撞箱移出了平台的边缘,它仍然会被错误地判定为 "onGround"—— 因为 Y 轴阻挡发生在水平移动之前。这正是原文说的 "撞到地面又移下了平台" 的场景。
