欢迎光临散文网 会员登陆 & 注册

谈谈MC战斗(四):移速

2022-08-26 13:34 作者:道家深湖  | 我要投稿

关键结论:

  1. 设某常规生物,在常规地面上的移动速度属性(movement_speed)为SPD,单位格每刻,则有:

    1. SPD值小于1时,则生物的实际速度为v%3D%5Cfrac%7B490%7D%7B227%7D%20%5Ccdot%20SPD%5E2%20%5Capprox%202.16%20%5Ccdot%20%20SPD%5E2%20

    2. SPD值大于等于1时,则没有平方,改为v%5Capprox%202.16%5Ccdot%20SPD

  2. 代码里的motionX、motionY、motionZ等变量并不是实际速度在对应方向的分量,而是实际速度乘了磨损系数后的一个没什么意义的值。

  3. 常规地面是指“slippery”值为0.6的方块,该值以后简称光滑系数。不在此列的方块包括冰与浮冰(0.98)、粘液块(0.8)。考虑方块摩擦系数的公式较为复杂,将在过程中给出。

  4. 常规生物是指使用了默认的MoveHelper的步行生物,史莱姆、恼鬼、鱿鱼等移动方式特殊的生物除外。

  5. 以上速度指的是加速完毕、到达稳态的情况,生物需要一个起步和加速的过程,详细公式后面给出。在特殊的光滑系数下,生物可以无限加速,没有速度上限;或者根本跑不起来。

  6. 以上速度公式不含生物在液体中、浮空、陷入蜘蛛网等特殊效应,但任何直接影响速度属性的效果(如药水、小僵尸移速加成)都适用。

wiki当下的解释,可以说错的离谱

好,接下来给出详细的分析。需要注意的是,我们需要优先探寻最普遍、最基本的规律,因此要搁置很多特例,如史莱姆等不走寻常路的,就留待以后分析,不然根本看不完了。

如果你看wiki的话,不仅公式是错的,格/秒的数据是错的(wiki上僵尸是9.9m/s,想想上学时自己跑50m、100m的速度就知道有多离谱),而且也没有把特殊的生物分开看待。要知道,生物进水之后还会受到SWIM_SPEED影响呢,和陆地生物混为一谈也太过分了。等我全弄完了就去修改。

多数的AI生物运动是由多个模块综合驱动的,大体上可以分为四个:

  1. 负责选择攻击目标的targetTask;

  2. 负责执行攻击、前往目标等行为的task。比如鱿鱼作为水生动物,就有一个很特别的行动AI,EntitySquid.AIMoveRandom;

  3. 落实移动或者花式移动的EntityMoveHelper,以及PathNavigate。恼鬼、恶魂、守卫者、兔子、史莱姆有自己的特殊Helper;

  4. 实体自己update里的逻辑,最后以Entity::move中调用Entity::resetPositionToBB()收尾。对,MC是先改包围盒,然后根据包围盒反推位置来移动的,想不到吧。

targetTask只负责选定攻击目标,不执行具体操作。比如僵尸看到玩家,前来追杀;其中僵尸心里选择目标就是靠targetTask里的EntityAINearestAttackableTarget做到的,实际跑起来追杀则是后面的事。

僵尸能实际跑起来追杀它的攻击目标,靠的是EntityAIZombieAttack,这个ZombieAttack其实就是继承自EntityAIAttackMelee,并且加了个抬手动画而已。这个Melee在爬行者、末影人、末影螨、铁傀儡、蠹虫、卫道士、狼里都用到了,是最常见的一种怪物攻击AI:接近目标,并近战攻击。蜘蛛和北极熊有这个AI的其他变种,先搁置。

如果你去观察EntityAIAttackMelee的构造函数调用,你会发现这里面可以传一个速度参数,它的作用是与移速属性值相乘。比如你填0.5,就等于怪物出击时移速属性折半。实际上,一般这里填的都是1,毕竟,一般你想让一个怪跑的快点或者慢点的时候,你都是直接去调它的属性值,没必要分两个地方各填写一个因子。僵尸这里的系数是1,所以也很适合用来分析。

那么这个近战攻击,手有多长?

哈哈,不是一个常数,与自己和对方的包围盒尺寸都有关,这个计算公式连量纲都很混乱,总之是略长于自己包围盒边长的两倍。对于僵尸这个width为0.6的生物来说,近战范围就是1.2格多一点。

包围盒是方形且轴对齐的,但是算距离的时候却是两点直接拉一条线过去看距离的,忽略了包围盒的形状和角度,所以僵尸实际上可触及的范围,只分析XZ平面的话,那就是个圆形。

我们看Melee的shouldExecute部分,第61和70行,都有一个关键的接口调用:

也就是说,AI对象本身并不是每一个Tick,都在算到底这一步子该迈到哪的,它只会通知寻路类去处理这事。他不精确到步,只精确到宏观目标。

我们的下一个阶段就登场啦,实体隐藏的小宠物,PathNavigate和EntityMoveHelper。

看看EntityLiving的构造函数吧。多数生物都会自己构建一个PathNavigateGround对象留着用,准备在地面上走来走去,守卫者、蜘蛛和鹦鹉有他们特殊的寻路对象构造,就不管了。还有一些弃疗的生物连这个navigator都不用,就直接在AI和update里强行移动,比如鱿鱼,我们也先不管他。

LivingBase的onLivingUpdate里会调用updateEntityActionState ,进一步在EntityLiving里调用helper的onUpdateMoveHelper。总之这个东西也是在tick里实时更新的主。这个tick只维护基本的状态,实际的移动执行在另一个地方。


我们看EntityLivingBase::onLivingUpdate:2616,这里直接调用了一个实体的travel,这里也就是最复杂的加减速对决发生之处了。


那个f6,我们可以很明显地看出,他是0.91倍的方块光滑系数。物品实体等不是0.91,但我们先不管。至于为什么是0.91不是0.92,我推测只是mojang当时随便填了一个觉得还行的数值。

这getSlipperiness获得到的是多少呢?0.6。绝大多数方块都是这数值。冰块0.98,粘液块0.8,也就是这俩方块更滑。更滑的方块移动速度更快还是慢呢?我们后面算完就知道了。

f7很诡异,他给出了0.16277136这么一个莫名其妙的数字,但你实际推演过就会发现,当方块光滑系数为0.6时,f7的计算值约为1,这个数值实际上是(0.91x0.6)的立方。也就是说,对于多数方块而言,f7这个乘数的影响可以直接忽略。当方块光滑系数变为0.6μ的时候,把f7当做μ的负三次方即可。

getAIMoveSpeed返回什么呢?多数情况,它就直接等于移动速度的属性值。

好了,归了包堆算这么一大圈,f8在多数情况下就等于移速本身。

我们接着看。接着调用了一个moveRelative,这也是最热闹的地方了。我们考虑最简单的情况,实体朝正前方以冲锋,速度系数1,那么此时strafe、vertical都为0,forward等于移速属性SPD*0.98,f8等于SPDμ^-3,在μ为1的时候就传了俩SPD进去。

你说这0.98哪来的?

好,解释了哪来的0.98,再继续看流程。

你看到这可能要说了,这motionXYZ不就是速度分量吗?是,这时候确实是,但后面就不是了。先别急,看看这moveRelative的实现吧。

我给他简化一下。

前面我们说了,foward是SPD*0.98,strafe和up是0,那么f一开始就等于SPD*0.98。

step2里,如果SPD*0.98小于1,那么step1就会使得除以f 变成除以1,即忽略这一步骤,f直接变成friction;motion增加forward * friction = SPD * SPD * μ^-3*0.98。多数情况下μ=1,那么这里就是直接增加了SPD * SPD*0.98。

如果大于1的话,就会变成friction / SPD,直接等于μ^-3,motion增加的就是forward,也就是SPD  * μ^-3*0.98。 多数情况下μ=1,那么就是增加了SPD*0.98。一般来说,我们不会去太卡这个临界区,可以简单地把0.98SPD>1的判据改为SPD>1,误差不大。


this.move和这个moveRelative之间还算了个f6=0.6μ,这f6干嘛的呢?得在执行完了move之后你才看到。

好么,这都move移动完了,您还跟这改motion执行衰减呢,有意思么?您就不能把这个放到下一帧的开头么,非得把motion放在一个不当不正的位置,与实际速度脱节?改完了还在下一帧里影响moveRelative,作为下一次运动速度的初始值。

哦对,还有这:

这是咱们一开始看到的0.98的来源。您在travel之前先给moveForward做了个98%衰减,以至于前面我们分析时认为moveForward=SPD的假设也不精确了。我真是……

被Mojang气晕

我该不会是地球上第一个理清楚这玩意的人吧?

谈谈MC战斗(四):移速的评论 (共 条)

分享到微博请遵守国家法律