关于实体运动的一些小总结

B站影视 欧美电影 2025-10-04 04:58 1

摘要:对于这几位,它们个体之间存在差异,但是具体框架继承自 ThrownEntity。

正文格式。

有人问了,所以我写了。

算是一次讨论的总结,很琐碎,也许没什么价值。

本文主要基于1.21.0分析,可能穿插其它版本的部分会进行说明。

需要一点英文水平,因为有些方法名是显而易见的。

希望能启发到你。

弹射物的分支 - 投掷物

对于这几位,它们个体之间存在差异,但是具体框架继承自 ThrownEntity。

它们的运动遵循 tick 方法,以下将只以末影珍珠为例。

vec3d可能会在此文中频繁出现,根据情景含义略有不同。

此处指本刻开始时的各轴速度赋值给vec3d,然后将这个速度作为位移,

得到 该刻 - 实体阶段 - 该投掷物 运算结束时的位置( d, e ,f )。

在计算最终位置后,通过 setVelocity 方法,

使得 该刻 - 实体阶段 - 该投掷物 运算结束时的速度缩放 h 倍(乘以系数 h )。

系数 h 在默认情况下为 (float) 0.99 ,在水中为 (float) 0.8

在速度更新结束后,通过 applyGravity 方法,

使得 该刻 - 实体阶段 - 该投掷物 运算结束时的y轴速度减去 getGravity 的返回值。

对于具体的投掷物,如果它没有重写(比如EnderPearlEntity),那么 getGravity 默认返回0.03。现代Java不指定数据类型默认为 (double) 0.03;对于1.20.4及以下,这个返回值是 (float) 0.03

从这里也能看出来,所谓的“Velocity”或者“Motion”,其具体含义更像是一种运动趋势。

最后,通过 setPosition 方法,

使得 该刻 - 实体阶段 - 该投掷物 运算结束时的位置被设置为( d, e ,f ),

位置更新结束。

对于1.21.2,这个流程是这样的:

首先应用重力效果 applyGravity,应用阻力效果 applyDrag

于是 上一刻 - 实体阶段 - 该投掷物 运算结束时的速度被更新了。

然后 vec3d = this.getPos.add(this.getVelocity)

getVelocity 指的是刚才被更新过的速度,以它作为位移。

接着 setPosition(vec3d),

使得 该刻 - 实体阶段 - 该投掷物 运算结束时的位置被设置为 vec3d

到此结束,如果你看过之前的内容,你大概就能知道珍珠的实体运动公式的由来了。

接下来介绍游戏中最通用的运动方法:move

这一部分比较繁杂,我只会挑一部分自己感兴趣的分析。

玩家,盔甲架,下落的方块,TNT,鱼漂,载具,以及生物(环境生物 - 蝙蝠,飞行生物 - 恶魂,幻翼,寻路生物 - PathAwareEntity,史莱姆生物,末影龙)这些生物的自主运动都使用了 move 方法。

后续介绍的活塞以及其它方式也可以让不属于上述的实体应用 move 方法。

基本上,不属于上述的实体如果进行自主运动,那么多半是在它们自身的类中通过重写的 tick 方法实现的。

完整的 move 方法,无需阅读。

如果noClip == true,那么添加位移直接返回

记录移动前的状态,如果移动类型是 PISTON,进行活塞相关调整

处理蜘蛛网,浆果丛,细雪

调整潜行位移

调整沿轴运动,沿轴碰撞,步行辅助

进行运动精度的判断。自由落体导致的摔落伤害射线检测。更新位置

处理碰撞

确定实体下方方块,并应用摔落伤害。如果实体死亡直接返回

在碰撞发生的那个轴取消速度

处理竖直碰撞。粘液块,床相关的弹跳效果

处理幽匿与声音相关的事件

根据实体下方方块应用摩擦力

C2 碰撞方法

我们来看投掷物的 tick 如何进行碰撞检测

很容易可以看出,hitResult 是一个碰撞检测,checkBlockCollision 又是一个碰撞检测。

这里传入了两个参数,投掷物实例和一个 canHit 方法。

这个 canHit 方法接受一个实体,如果以下一个或多个条件成立,则返回true

这个实体可以被弹射物击中,

没有发射者(投掷器或发射器),

离开了发射者,

不是发射者正在骑乘的载具。

我们分析 ProjectileUtil.getCollision

首先进行方块碰撞检测,world.raycast流程如下:

world.raycast 接收 RaycastContext 对象,此处传入起点,终点,检测箱类型 (collision),规则 (忽略流体),投掷物实例。

创建起点与终点的连线。沿着从起点到终点的直线,依次遍历路径上的每一个方块网格。每处于一个新的方块网格中时,检查碰撞。如果碰撞,返回 BlockHitResult 对象。

BlockHitResult 可以被看做一个容器,它包含以下内容:

精确的碰撞点 (Vec3d);方块的网格坐标 (BlockPos)

命中的朝向 (Direction);是否射入方块内部 (boolean)

所以ProjectileUtil.getCollision的第一个判断语句的含义是:

定义一个方块碰撞点 (Vec3d),它是由 world.raycast 得到的精确的碰撞点。把它作为终点传递给下一个判断语句。

分析调用的 getEntityCollision 方法

首先 getEntityCollision 方法接收维度信息,投掷物实例,起点,终点,碰撞检测空间,canHit,和一个参数 (float) margin

entity.getBoundingBox.stretch(velocity).expand(1.0)

此处传入的 碰撞检测空间 是投掷物原有碰撞箱向外扩大 velocity 得到的碰撞箱。

这里的含义是:投掷物起点和终点连接,在空间中形成的矩形,再继续往外延伸一格(避免浮点误差)

margin 传入的值为 (float) 0.3

然后是一个 for-each 循环,对于 world.getOtherEntities 返回的对象列表,依次将其中的每一项赋值给 entity3

world.getOtherEntities 比较复杂,可以理解为碰撞检测空间内的每一个实体(除了自身),经过 canHit 方法的判断后都会被返回。

可以理解为一次粗略筛选,在这个碰撞检测空间中的所有可能到达终点路径都被考虑了。

而实际在这一刻中发生的一定是连接起点和终点的最短路径,于是在循环内部:

对于每一个 entity3,让它的碰撞箱向外延伸 margin,即 (float) 0.3

然后进行一次 ProjectileUtil.raycast 方法,判断投掷物是否会撞到碰撞箱扩大后的实体。

每次只记录距离最近的那次命中,最终会得到在投掷物路径上最近的那个碰撞实体。

所以ProjectileUtil.getCollision的第二个判断语句的含义是:

对于起点和方块碰撞点的连线,通过 getEntityCollision 方法,找到符合要求的最近实体进行碰撞。

那么 hitResult 碰撞检测后,进入 checkBlockCollision 的碰撞检测。

接下来介绍基于 move 的方块碰撞,在 move 方法中:

adjustMovementForCollisions 方法:

介绍了沿轴运动和步行辅助,其中沿轴运动的运算顺序如下:

关于沿轴运动,相信各位也能体会到了。不同于真实世界中的运动,沿轴运动首先将速度沿三个轴分解,然后以特定顺序计算各轴的碰撞和位移。

可能各位也注意到了这整个调用过程中没有处理关于实体的碰撞,实体碰撞不在 move 方法中。对于LivingEntity,它是 pushAwayFrom 方法:

对于船(唯一有实体碰撞的载具),它也是 pushAwayFrom 方法,不过略有不同:

这样就可以在竖直方向上堆叠船了,而且水平方向上其它实体也能推动船。

我知道你在想什么,在船的 tick 中,先执行上船,再执行推动。

这么看来,实体挤压是没有y轴增量的。这就需要其它方式对实体进行三维合成。

对于一般的LivingEntity,TNT或者粘液块弹射都是不错的选择。如果是船的话,那就只能自求多福了(气泡柱,弹射)。

可能有人会说,根据实体血量TNT会有最大向上速度值。但是原版是有伤害冷却的,理论上一瞬间可以使实体受到任意程度的TNT向上速度。

C3 活塞

一切从活塞的方块实体开始

此处为 PistonBlockEntity.pushEntities 方法

活塞实体对物体的推动效果在它的 tick 中应用,这就包括pushEntities

可能你对 i 所代表的含义有些困惑,这是正常的。

在 getIntersectionSize 方法中:

所以 i 是使实体在推动方向上离开活塞扫过空间的最小距离。

这也从代码层面解释了所谓 “粘性推动” 和 “嵌入推动” 究竟是什么。

使用 i 的具体效果就是使实体看起来紧贴活塞表面,且当 i > 0.5 时则放弃应用这种效果。因为0.51是活塞推动实体的限制(在第四节有详细解释),所以当 i > 0.5 时,位移总是0.51,这也就是所谓的 “嵌入推动”;当 i

对于活塞头拉回,应用 push:

对于活塞头收回事件,总是会在最后保留地加上一个 j 的偏移值。

由于 j 是 (d , amount) 取最小值加上0.01,amount是0.5,而 d 在 getIntersectionSize 返回零时为0.01,所以这种情况下 j = 0.02

这就是为什么使用活塞头拉回矫正实体时,总会是0.02的偏移量而不是通常的0.01

需要注意的是 push 方法仅在方块为活塞头且收回时调用,除此以外并没有任何地方调用这个方法。

活塞执行移动的具体逻辑是 moveEntity方法:

所以我们知道了,活塞的推动调用了 entity.move 方法。

这样一来,很多东西就能被揭示了。

push 的主要目的是在活塞头收回时使实体不会卡在活塞方块中。

它只对活塞头的收回生效。比如当活塞头和底座即将把实体夹住,这个方法便会被触发,使得实体移动到活塞前方。具体效果就是实体看起来紧贴活塞表面。

活塞头拉回的具体分析 - 图例

尝试分析该例。

盔甲架碰撞箱底面为 0.5 * 0.5,高度为无关量。

箱子的侧面为一个像素,即0.0625宽。

现在盔甲架紧贴箱子,gt0 - EU,关闭拉杆。

gt1 - TE

此刻 i = 0.8125,于是取最小值即位移0.51,盔甲架向活塞收回方向运动0.51格。

因为盔甲架有0.0625的部分在活塞头方块外,所以 d - e = 0.1875 > 0.01,push 不执行。

gt2 - TE

此刻 i = 0.8025,于是取最小值即位移0.51,盔甲架尝试向活塞收回方向运动0.51格。

活塞头在收回时触发 move 方法,实际位移0.3025格后与活塞底座发生沿轴碰撞,额外的位移被阻止。

进入 push 方法。当活塞头完全收回时,(d - e) = 0.0

d = 0.25 + 0.01,j = d + 0.01 = 0.27,因此实体移动到活塞头前方0.02处。

活塞收回完毕,实体到位。

C4 瞬时实体传送带

来看看 move 中对于活塞推动效果的限制

adjustMovementForPiston 方法

calculatePistonMovementFactor 方法

于是真相大白,同一刻内活塞推动造成的单轴位移最大为0.51

因为每次活塞推动都会写入该轴的 pistonMovementDelta,而它在达到0.51后因为d的赋值运算导致推动造成的效果为零,也就是不再移动。

这个数组担任了计数器的作用。如果实体在一刻内位移了0.51,接着向相反的方向位移0.51。那么它的计数器显示为零,即没有移动,实际上也的确如此。

计数器所记录的,是根据传参决定的 “逻辑位移” 。

但是如果实体向相反的方向移动的时候,这种移动被阻挡,那么有趣的事情发生了:

实体位移了0.51,但是它的计数器却显示为零,即它没有位移!

那么在这一刻之中,重复这个操作,便能到达世界尽头。

这是实体的真实位移,可以被叫做 “实际位移” 。

这种阻挡运动的方法便是碰撞,上文中的沿轴运动便是此列。

由于使用活塞时会调用 move,因此就算是弹射物也可以应用沿轴运动方法。最常见的应用就是水平矫正时,通过活塞将投掷物推向碰撞箱特殊的方块,以此矫正它的位置。

C5 细雪及其它截停

在move方法中:

此处我说的减速系数专属于细雪,蜘蛛网,浆果丛。如果你在寻找灵魂沙,粘液块等减速效果,请移步Wiki搜索 “滑度”。

这些减速效果的调用全部都在 onEntityCollision 中,这导致了一道奇观:

投掷物经过 checkBlockCollision 可以获得减速系数,但是因为投掷物自身没办法调用 move 方法,导致减速效果既无法应用,也无法清零。

这时,如果发生了活塞推动,那么投掷物在应用完活塞的推动效果后得到的movement,会乘以减速系数,然后传入 adjustMovementForCollisions 开始沿轴运动。

同时三轴的速度也因为 move 方法的成功调用而清零了。由于三轴速度全消,下一刻的位置就不会改变,这就是有时候放一桶细雪会显著增高矫正成功率的原因。

我们都知道,方块的 onEntityCollision 方法,投掷物会根据 checkBlockCollision 调用。

在 move 中,有这样一句:

this.tryCheckBlockCollision;

所以基于 move 方法的实体也会应用方块的各种效果,比如蜂蜜块下滑。

其中 isSliding :

其中 updateSlidingVelocity :

所以只看y轴的话,蜂蜜块的下滑效果是这样的:

...小于-0.13 -> 设置为-0.05 -> 自由落体一段时间 ->小于-0.13...

以1.21.0为例,它的变化应该是这样的:

在三个固定的速度间循环

如果在某一时刻实体从蜂蜜块上滑落,并进行自由落体,

那么它所有后续的速度都是可知的,因为蜂蜜块下滑总是从-0.05开始。

而且最关键的一点是,蜂蜜块下滑并不会响应速度大于-0.08的情况。

这意味着想让向上运动的投掷物应用蜂蜜块下滑是不可能的,也可以通过突然使速度大于-0.08终止蜂蜜块下滑。

C7 弹跳效果

在move方法中:

其中 onEntityLand 是被床和粘液块重写过的,为了赋予掉落在上面的实体弹跳效果。

所以继续沿着调用的路线,可以知道最初的调用来自 blockPos 的 getLandingPos

这是一个很不好的信号:@Deprecated(已过时)

继续看实际的调用 - getPosWithYOffset:

作为对比,这里是1.20.1之前的 getLandingPos:

可以看出,在1.20.1之前,支撑点方块由实体的位置决定。

实体的位置默认为其碰撞箱底面的几何中心,而 floor 算法造成了一道奇观:

只要实体位置不在粘液块或者床的方块范围内,那么一定不会应用弹跳。

紧贴头颅自由落体 - 不发生弹跳

以玩家为例,在1.20.1之前紧贴头颅落到粘液块上不会弹跳。

因为头颅为八个像素,相当于外圈为四个像素宽。

而玩家的碰撞箱底面为0.6 * 0.6,以实体位置在底面中心来算,至少需要侵入粘液块0.3格,而四个像素是0.25格,所以不能弹跳。

紧贴花盆自由落体 - 发生弹跳

而花盆为六个像素,相当于外圈五个像素宽。

五个像素是0.3125格,可以判断粘液块为支撑点,因此可以弹跳。

在1.20.1之后,支撑点根据 supportingBlockPos 判定,它是在 move 中 setOnGround 的调用时发生的:

由于过于复杂,在此简述:

supportingBlockPos 判定是由碰撞箱在下一刻接触的方块决定的,如果有多个方块,则按区域文件中方块的写入顺序决定。

由于不是栅栏,墙,栅栏门时,获取支撑点是pos.y - (double)offset

即pos.y - 0.2f,这一点无论是1.20.1之前还是之后都一样。

那么在粘液块上放置高度不超过 0.2f 的方块,仍能享受到弹射效果。

因为此时的支撑点仍然是下方的粘液块。

这个方块可以是地毯,活板门,小型水晶簇等。

由于弹跳这部分并不会改变此刻的位置,因此弹跳开始的位置是沿轴运动的碰撞位置。

这也就是说:如果实体落在了铺着地毯的粘液块上,那么它会从地毯的高度开始弹跳。

这就是我目前关于弹跳的全部解释了。

C8 结语

只是在解释一些被讲过千百遍的东西罢了。

Author: nyxon

来源:May时尚一点号

相关推荐