05 LivingEntity 运动机制
LivingEntity 是实体中的一大类,包括了玩家、生物和盔甲架。虽说这里搞理论计算没有弹射物和 TNT 实用,但见外国跑酷理论已经有所发展了 **[10]**,我们也不能落后对吧。本节内实体若无特殊说明均指 LivingEntity。视线水平投影方向如无特殊说明均指保持偏航角不变平视时的视线方向。
给出一些基础定义:
流体深度阀值:一个长度,在实体眼部高度不小于 0.4m 时为 0.4m,否则为 0。
AI 加速度:在 AI 加速过程中实体受到的加速度的一个决定量,与实体前 / 右向加速度系数决定实体的真正水平移动加速度,但通常不等于真正的加速度。
地面移动加速度:实体在地面运动时或有满级深海探索者且在水底地面运动时的 AI 加速度。
飞行移动加速度:实体在空中运动(不算鞘翅飞行)时的 AI 加速度。值一般固定为,疾跑的玩家为。
可攀登方块:minecraft:climbable标签内的方块和下方有同侧梯子的活板门。
1 LivingEntity 的自由运动
与其它大部分实体不同,LivingEntity 的运动没有被直接放到tick()方法中,其运动大多是在travel()方法中进行的。
默认的 travel () 方法可以分成四大部分:实体与水接触时,赤足兽以外的实体与熔岩接触但并不与水接触时,鞘翅飞行时和其它情况。如果实体有缓降状态,所有重力加速度变为给出数值的 1 / 4。为防止 Y 轴 Motion 落入之间而被归零,流体中的部分重力作用(包括第一种情况的第 9 步、第二种情况的第 3.2 步)中会有特殊处理,将受到重力后 Y 轴 Motion 在之间的实体的 Y 轴 Motion 直接设为 - 0.03m / gt。
在第一种情况中运算过程如下(LivingEntity.travel(),1936-1970):
- 取 0.8(未疾跑时)或 0.9(疾跑时)作为
- 获取实体拥有的深海探索者附魔的有效等级 h,最大为 3,没有这一附魔时为 0,实体若未着地则减半。
- 如果没有海豚的恩惠效果,取,否则直接 j = 0.96
- 取,其中 Ms 为实体的地面移动加速度以为单位的数值(见 6.2)
- 以 g 为 AI 加速度数值进行 AI 加速
- 以当前 Motion 作为位移趋势进行基于 Entity.move() 的移动
- 如果移动过程中发生了水平碰撞且坐标所在方块网格处为可攀登方块,将 y 轴 Motion 设为 0.2m / gt
- 将 X、Z 轴 Motion 乘以 j,将 y 轴 Motion 乘以 0.8
- 如果实体没有 noGravity 标签,受到 0.005的重力加速度
- 如果水平方向发生了碰撞则取一个底面中心与现在碰撞箱底面中心在同一竖直线上,底面中心的 Y 坐标为移动前的坐标上方 0.6m 的与该实体碰撞箱大小相等的长方体区域,将该区域沿现在 Motion 指定的偏移量和方向移动,若区域内没有任何固体方块和流体,则将 Y 轴 Motion 设为 0.3m / gt
此时实体的运动类型是 FCMDG(即流体加速 ->AI 加速 -> 移动 -> 阻力 -> 重力,各字母具体含义见 3.1)在实体没有深海探索者附魔是时实体的流体阻力为 0.2或 0.1,取决于实体是否在疾跑。在实体拥有深海探索者时,实体的 AI 加速度会随等级递增,最高与该实体的地面行走速度相同;阻力也随等级递增,最高为 0.464,与在普通方块地面上的阻力相同,加上运算顺序可以证明实体在拥有最高等级的深海探索者附魔时在水下的地面上行走的速度和在地面行走的速度相同。有海豚的恩惠效果时阻力最低,达到 0.04。最后一步我怀疑是与实体上岸有关,因为如果去掉这一步实体就无法上去在水面上方 0.5m 以上(台阶、附魔台或是灵魂沙)的岸边,而正常情况下则完全可以。
在第二种情况中运算过程如下(LivingEntity.travel(),1971-1991):
- 以 0.02作为 AI 加速度进行 AI 加速
- 以当前 Motion 作为位移趋势进行基于 Entity.move() 的移动
- 如果实体浸入熔岩深度不大于流体深度阀值:
- 水平轴上受到 0.5的阻力,Y 轴上受到 0.2的阻力
- 如果实体没有 noGravity 标签,受到 0.005的重力加速度
- 如果实体如果实体浸入熔岩深度大于流体深度阀值,各轴上受到 0.5的阻力
- 如果实体没有 noGravity 标签,继续受到 0.02的重力加速度
- 同第一种情况的第 10 步
你没想错,足够深熔岩和水中相比熔岩中实体的下落速度确实比水中快。
好了,说正事,这里实体的运算流程是(实体浸入熔岩深度不大于流体深度阀值)或(实体如果实体浸入熔岩深度大于流体深度阀值),别的倒没什么好说的。
第三种情况在 6.3 节还要详细说明,在此从略。
第四种情况可以说是最常见,也是最常用的情况了,在实体在上面三种情况之外时被用到,流程如下(LivingEntity.travel(),2036-2055):
- 取实体下方(坐标下方略多于 0.5m 处)方块的滑度 s
- 在实体着地时,取 j = 0.91s,否则 j = 0.91
- 在实体着地时,取作为 AI 加速度进行 AI 加速,其中 Ms 为地面移动加速度,否则取实体的飞行加速度作为 AI 加速度进行 AI 加速
- 如果坐标所在方块网格处为可攀登方块,实体的 X、Z 轴中单轴 Motion 会被限制在之间,Y 轴 Motion 小于 - 0.15m/gt 时会被设为 0.15m/gt。如果实体是潜行中的玩家,且可攀登方块不是脚手架,实体的 Y 轴 Motion 会被归零。
- 以当前 Motion 作为位移趋势进行基于 Entity.move() 的移动
- 如果移动过程中发生了水平碰撞或在尝试跳跃且坐标所在方块网格处为可攀登方块,将 Y 轴 Motion 设为 0.2m / gt。
- Y 轴 Motion 的一些运算
- 如果拥有漂浮效果将 Y 轴 Motion 改为
- 否则如果这是在客户端上的运算,而且实体所在区块未加载(连边界加载都不是),实体的 Motion 会被设为 - 0.1m / gt(Y 轴坐标大于 0 时)或直接归零(Y 轴坐标不大于 0 时)
- 如果实体上述两步中条件均未被满足且实体的 NoGravity 标签为 false,受到 0.08的重力加速度。
- 将水平轴上 Motion 改为原来的 j 倍,Y 轴 Motion 改为原来的 0.98 倍。
这时实体的运算流程为 CMGD。可以发现,空中运动时 Y 轴的阻力为 0.02,与 Wiki 相符;而水平轴上为 0.09,明显与 Wiki 不符。另外,滑度还可以决定实体的 AI 加速度和阻力系数,其中 AI 加速度与滑度的立方成反比,阻力系数相对于滑度线性递减。
恶魂和幻翼有一套不同的方案:可以说是默认的删减版:
在实体接触水时,每 gt 以 0.02 的 AI 加速度进行 AI 加速,然后以当前 Motion 作为位移趋势进行基于 Entity.move() 的移动,最后各轴的 Motion 乘以 0.08,完成。
在实体接触熔岩但不接触水时,每 gt 以 0.02 的 AI 加速度进行 AI 加速,然后以当前 Motion 作为位移趋势进行基于 Entity.move() 的移动,最后各轴的 Motion 减半,完成。
在其它情况,实体的 AI 加速和阻力系数的决定与默认类似,顺序相同,但没有漂浮效果和可攀登方块相关的运算。
除此之外,有些运动运算被安排在了travel()方法外部,具体的流程如下:
- 实体基础运算
- 跳跃冷却更新
- 实体绝对值小于 0.003m/gt 的 Motion 分量被归零(在 JE1.8.9 及之前,此处的阈值为 0.005m/gt)
- 若已经死亡,将前向和侧向加速度系数都改为 0
- AI 或输入更新
- 跳跃相关判断及运算
- 前向和侧向加速度系数乘以 0.98
- 鞘翅耐久更新
- 执行 travel () 方法
- 激流相关更新
- 实体挤压
其中第 3 步应该是要使实体运动的停止更加干脆利索。
2 AI 及属性对运动的影响
Minecraft 中几乎所有生物都有 AI,玩家的控制在某种意义上也可被称为 AI。盔甲架没有 AI,也不会自己控制自己移动,在此不再考虑。最近红石科技搬运组在搬运一个讲解实体 AI 的视频系列(目前更新中),如有需要推荐去看一下。
Minecraft 中,生物 AI 主要由Goal、Task、GoalSelector和EntityNavgation四个类和BodyControl、JumpControl、LookControl和MoveControl四个直接控制生物的类组成。
Goal 和 Task(可以翻译成目标和任务)表示的是生物的一个整体的行为,或者说就是某种非条件或条件反射,再或者说就是实体将长期拥有的习惯,大部分是从生物创建以来一直就有的,但有时侯也会在生物创建后发生改变,如骷髅的武器被替换,猫被驯服时。
GoalSelector(可以翻译成目标选择器,但请勿与命令中的目标选择器混淆)会根据各 Goal 的优先级等信息安排 Goal 对生物各个方面的控制。
EntityNavgation(可以翻译成实体的导航,好吧,很像机翻)负责为生物进行寻路,是比 MoveControl 和 JumpControl 高级的运动控制。
Move/Jump/LookControl 通过修改生物的 AI 加速度和加速度系数等属性较直接地控制着生物的运动。
B 站上红石科技搬运组搬运了一系列(疑似停更,只有一期但还是很有价值)关于生物 AI 的讲解视频,讲得很通俗,如果需要了解生物 AI 建议关注一下。
LivingEntity 类定义了几个关于运动的字段:
(1) movementSpeed:决定实体的地面移动加速度的一个实数,等于地面移动加速度以为单位的数值。对于玩家总为generic_movement_speed属性值,对于生物由种类、AI 状态和generic_movement_speed属性共同决定。
(2) flyingSpeed:决定实体的飞行移动加速度的一个实数,等于飞行移动加速度以为单位的数值。默认为 0.02,疾跑的玩家为 0.026,被骑乘且被控制的猪、马和赤足兽由 AI 状态和generic_movement_speed属性共同决定,具体来说是 movementSpeed 的 0.1 倍。
(3) forwardSpeed:表示实体前向加速度系数的一个实数,可以为负数,在每 gt 中travel()方法执行前会被乘以 0.98。
(4) sidewaysSpeed:表示实体侧向加速度系数(准确点说是右向的)的一个实数,可以为负数,在每 gt 中 travel () 方法执行前会被乘以 0.98。
(5) upwardSpeed:实体向上的加速度系数,也是实体向上的 AI 加速度以为单位的数值,可以为负数,在每 gt 中travel()方法执行前会被乘以 0.98。
在 forwardSpeed、sidewaysSpeed 和 upwardSpeed 的平方和不大于 1 时,实体进行 AI 加速时会给实体加上一个朝向实体视线的水平投影方向,大小等于 AI 加速度与 forwardSpeed 的乘积的加速度、一个方向为实体视线的水平投影方向的正右方,大小等于 AI 加速度与 sidewaysSpeed 的乘积的加速度以及一个竖直向上的,大小数值上等于 upwardSpeed 的加速度。如果 AI 加速将要进行时 forwardSpeed、sidewaysSpeed 和 upwardSpeed 的平方和大于 1,实体会保持比例不变缩放这三个 speed(得到三个对应的新值,对应字段的内容不变)使其平方和等于 1,再使用缩放后的值进行上述加速。大部分实体通常会选择向前直走来到达目的地,所以侧向及向上的速度系数一般为 0。在运动过程中,由于加速度与阻力的作用,实体的水平方向 Motion 会很快地趋近于一个最大值。
在上一节中已经说明,实体脚下的方块滑度越大,加速度就越大,阻力也就越小,那么,就可能存在一个平衡点,使得实体的最终速度达到一个极值。
在无流体的地面上时,陆生生物实体和玩家的水平方向运算流程可表示为 CMD(AI 加速 -> 移动 -> 阻力),也就是 AMD,假设 sidewaysSpeed 和 upwardSpeed 都为 0,将上一节的结论代入对应公式,得出最终速度(由每 gt 的位移表示)的表达式及 Ms・forwardSpeed = 1m / gt 时的图像,其中 Ms 为地面移动加速度:
图 6.2 与 s 之间关系曲线
对其关于 s 求导,得
求得其 [0,1] 间的零点只有一个,约在 s=(约为 0.82418)处,此时最终速度大小最小,约为 1.54331Ms・forwardSpeed。另外还可求得 s 等于各常见方块滑度时的情形:
(1) s = 0.6 时,最终速度大小约为 2.20264Ms・forwardSpeed;
(2) s = 0.8 时,最终速度大小约为 1.55101Ms・forwardSpeed;
(3) s = 0.98 时,最终速度大小约为 2.2103Ms・forwardSpeed;
(4) s = 0.989 时,最终速度大小约为 2.23265Ms・forwardSpeed。
但是粘液块上的实际行走速度要远小于 1.55101Ms・forwardSpeed(约 0.58807Ms・forwardSpeed),因为粘液块本身就会给实体减速,具体情况可参考 5.6 节。
MoveControl类中为生物提供的最主要的用于进行 AI 加速的方法是 moveTo(double x, double y, double z, double speed),这个方法需要指定一个目的地 (x, y, z) 和一个决定速度的实数speed。执行后实体的地面移动加速度以为单位的数值和 forwardSpeed 会被设为generic_movement_speed属性值与 speed 的乘积,并在简单判定后决定是否需要跳跃。最后,如果到达了目的地就将 forwardSpeed 设为 0。
研究所有生物的 AI 加速情况似乎不大现实,这里暂时不再说明。
除了 AI 加速之外,玩家和生物的跳跃也可以归入 AI 控制的运动的范围内,不过这里只研究比较一般的情况。在实体尝试跳跃时,游戏会检查下列条件是否成立:
A. 实体在水中
B. 实体在熔岩中
C. 实体着地
D. 实体浸入对应流体(实体接触的流体,如果同时接触水和熔岩则选定熔岩)深度不大于流体深度阀值
E. 实体跳跃冷却时间已经结束
如果满足下列全部条件,则可以执行从地面上跳起的行为(即将 Y 轴 Motion 以 m/gt 为单位的数值改为 0.42 + 0.1 跳跃提升等级,并将跳跃冷却时间重设为 10gt,此时如果实体正在疾跑,将实体的 Motion 沿视线的水平投影方向加上 0.2m/gt):
(1) E 成立
(2) C 成立
(3) D 成立或 A、B 均不成立
写成布尔代数式就是
如果下列条件成立,则尝试执行从水中向上游的行为(即将 Y 轴 Motion 加上 0.04m/gt,蜜蜂为将 Y 轴 Motion 加上 0.01m/gt)
(1) A 成立
(2) C、D 中至少有一个不成立
写成布尔表达式就是
如果满足下列条件,则尝试执行从熔岩向上游的行为(即将 Y 轴 Motion 加上 0.04m/gt,蜜蜂为将 Y 轴 Motion 加上 0.01m/gt,岩浆怪较为复杂)
(1) B 成立
(2) C、D 中至少有一个不成立
(3) 写成布尔表达式就是
如果跳跃运算进行时发现实体中断了对跳跃的尝试,跳跃冷却会立即结束。
实际上目前不是很确定这些条件是正确的,因为找不到合适的实验方案来验证。故此处给出相关代码实现如下以供参考(LivingEntity.tickMovement():2169-2183):
上面说到,generic_movement_speed属性与实体的 AI 加速度的确定有关,进而影响着实体的行走速度。(玩家速度与它成正比,生物速度与它的平方成正比,不知道这一差异是否为有意为之的。)余下的部分主要将说明一下这一属性的确定方式。
实体属性值的确定一般有两个元素参与:基础值(base value)与修饰符(attribute modifier)。其中,基础值是一个实数,在 Wiki 上已经比较详细地给出了 **[11]**(不过不要过于相信下一列的移动速度)。修饰符是一个获取属性值时对基础值进行的一个运算步骤(不改变基础值本身),Wiki 上在那一页同样也说明了,较为常见的有:
(1) 实体在疾跑时 generic_movement_speed 属性值被增加为原来的 1.3 倍。
(2) 实体每级速度效果能使其 generic_movement_speed 属性值增加 20%。
(3) 实体每级迟缓效果能使其 generic_movement_speed 属性值减少 15%。
(4) 一些幼年生物的 generic_movement_speed 属性值较大,如幼年僵尸是成年僵尸的 1.5 倍,幼年猪灵是成年猪灵的 1.2 倍等。
(5) 在实体在灵魂沙上面且有灵魂疾行附魔时,generic_movement_speed 属性值加上。
易知 generic_movement_speed 属性值的最终值与前三种修饰符的作用顺序无关。第四种修饰符总是最先被应用。
另外 generic_movement_speed 属性值存在上下限,分别为 1024 和 0
3 鞘翅飞行
我不是很确定研究鞘翅飞行是否很有实用价值,但见它那么常用也就提一下吧。
在实体使用鞘翅飞行时,其运动完全由其视线方向决定,水平速度最终会与视线方向的水平投影同向,不再受 AI 加速,其它加速也因位置原因一般很少受到。期间如果实体发生水平方向的碰撞,实体会受到一个由速度和碰撞位置决定的碰撞伤害;如果实体着地,鞘翅飞行会终止。
鞘翅飞行时,实体的运动运算流程如下(LivingEntity.travel(), 1992-2035,为方便理解做了一定改动):
图 6.3.1 鞘翅飞行相关代码
这里我们主要研究视线平视或斜向下的情形, 不考虑其它加速,此时:
我们也可以看出,在 Y 轴上,实体的运动明显是 ADM 型,带入对应公式,有
由源码也可以得出:
其中为实体在水平方向上的最大合加速度,
我们发现,鞘翅飞行的实体在 X、Z 轴上的运动也是 ADM 型,代入对应公式,又有:
这时,我们得到了在视线水平或斜向下时由视线俯仰角得到最终水平合速度以及最终竖直速度的方法。至于斜向上的情况个人能力不足,只能写段小程序做出图像,没有得出表达式。
下面是实体在由静止飞行 72000gt 后水平速度与俯仰角的关系图:
图 6.3.2 水平速度与俯仰角的关系图
在表达式 (6.3.6) 中求出关于的导函数,求得其在定义域上仅有一个零点,也就是说图中那个最高点对应俯仰角正切的平方就是这个值,可以求得此时俯仰角约为 53.366 度。将其带入式 (6.3.6),求得此时实体飞行速度约为 3.3888m/gt, 即 67.776m/s,与实验值 3.38879125m/gt 高度接近,也明显高于 52 度俯冲的速度 3.38383913m/gt,所以,Wiki 上那个说 52 度俯冲最快的那些个地方又得改了。
如果希望更加详细地了解一些其它方面的实验数据、仰视时的情况和一些实际问题,可以查阅这两个专栏 **[12]**。
4 源码走读
4.1 LivingEntity.travel():统一的环境运动
travel() 是 LivingEntity 的核心运动方法,根据实体所处的环境(空气、水、岩浆、飞行)走不同的分支:
updateVelocity(g, movementInput) 是关键的输入映射 —— 它将 WASD 操纵杆的输入值(movementInput,三维向量,形如 (左右, 上下, 前后))结合 movementSpeed 属性转化为 Motion 的实际变化量。g = 0.02F 是空气中行走的默认加速度(水中会因 Depth Strider 而提高)。
4.2 tickMovement ():每 tick 的运动预处理
canMoveVoluntarily() 返回 false 时(如被栓绳拉住、被眩晕),实体仅受空气阻力 —— 不能主动移动,但已有惯性会逐渐衰减。
4.3 跳跃的 "设值" 语义
LivingEntity jump() 方法在 1.20.1 中:
setVelocity(x, d, z) 直接将 Y 分量设为 d,而非在原值上加 d。这验证了原文关于 "跳跃是设值而非加速" 的论述 —— 下落的实体跳跃时,之前的负 Y Motion 被丢弃,从 0.42 重新开始。
