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

关于猪灵在交易结束后投掷产物相关特性的代码分析

2023-08-14 02:47 作者:我才是小灰灰  | 我要投稿

作者:我才是小灰灰、VBFa、HeyBlack233

组织:BSR原版技术生存服务器

本文档遵循知识共享协议CC-BY-NC-SA。

本文档使用MCP-Reborn项目生成的代码作为分析的源代码,分析全程会使用MCP-Reborn采用的反混淆命名。

MCP-Reborn工具链接:https://github.com/Hexeption/MCP-Reborn

 本文pdf版百度云链接:https://pan.baidu.com/s/1njBnBUXa6ovDwSNuFGA81A?pwd=r78r 

提取码:r78r

如果不想感受代码的折磨可以直接跳到最后,看省流版总结。



正文

在MCP-Reborn项目中,猪灵行为相关的代码位于net.minecraft.entity.monster.piglin包中。相较于其他实体而言猪灵是特别的,Mojang为其单独构建了整个包(12个类)来处理猪灵的行为逻辑,而其他生物则大多是一个或者两个类。

net.minecraft.entity.moster包

关于猪灵的投掷行为,我们可以用简单的关键字搜索定位到net.minecraft.entity.monster.piglin.PiglinTasks这个类的第273行的throwItems方法:

throwItems方法的实现

首先第274行定义了一个optional对象,其泛型类型为玩家实体,根据后面的赋值操作我们可以推测,这里的optional对象表示这个猪灵记忆中最近的可视玩家

紧接着第275行是一个if判断,其条件是optional对象存在。可以猜测表示的是那个最近的可视玩家存在。(在程序设计中,Optional类型通常用来封装一个容纳了对象本身和None的结构,这样做可以把空指针类型化,对程序员友好)。

那么第276行的行为就很好理解了,当这个最近的可视玩家存在时,则尝试向这个玩家实体所在的位置投掷这些物品实体对象

理解了存在时的行为,那么当没有最近的可视玩家存在的时候就是278行需要处理的行为了,根据字面意思它会任意(random我倾向于理解为任意而不是随机)向一个位置投掷这些物品实体对象。这部分行为则是我们想要更深入了解的,所以继续阅读关于throwItemsTowardRandomPos这个方法的定义。根据调用的方式不难看出,这个方法和我们目前分析的throwItems处于同一个类中。实际它们的定义是紧挨着的。紧接着读下面的283行:

throwItemsTowardRandomPos及相关方法的实现

分析图中代码不难看出,无论是投掷给玩家还是投掷到一个任意位置,它最终都是封装成投掷给某个给定的坐标位置。284行是投掷到任意位置的处理,它调用了一个方法getRandomNearbyPos来获取一个坐标位置,入参是猪灵实体,说明这个坐标还是和猪灵实体相关的,这是我们稍后要特别关注的方法。而在288行处理的就是存在最近的可视玩家时的行为。不难理解就是获取这个玩家实体的坐标位置。紧接着的292到298行就是具体的投掷行为,它需要保证要投的东西不能是空的,然后遍历这个要投的东西,一个一个的投出来(每个东西都是一个ItemStack类对象,可以理解为同种物品的堆叠),这里要格外注意的是,无论投的什么东西,它都在给定这个方法的目标坐标位置上叠加了向量(0,1,0),也就是y轴加1。这对精确预测投掷位置还是很重要的(实际上由于不影响xz两个轴,不会对后续的分析产生什么实质性的影响)。

现在回到我们刚刚需要特别关注的getRandomNearbyPos方法。它的定义在第662行:

getRandomNearbyPos方法的实现

这个方法看起来短小精悍,实际上却给出了猪灵投掷行为的一个关键可预测性质。663行尝试调用一个随机位置生成器生成一个坐标,这里的入参有三个,除了猪灵实体外还有两个常数4和2,目前是不明所以的,但是没关系,我们先记得有这么个常数,之后分析getLandPos方法的时候自然可以理解。而664行表明,如果我们调用getLandPos方法获取到的向量是空的话,方法就会直接返回猪灵实体的位置作为目标位置。也就是说这里的特性是:如果我们能让getLandPos方法给出一个空对象,那么我们就可以精准的让猪灵每次的投掷物都是投在猪灵实体所在位置的上方1格处。

为了达到这个目的我们需要进一步分析getLandPos方法,它属于类net.minecraft.entity.ai.RandomPositionGenerator。我们可以在这个类的第27行找到我们调用的那个重载方法(我们的入参有三个,一个实体对象,两个整数)的定义:

getLandPos及其重载方法的实现

可以看到,这个方法重载调用了32行定义的方法,多了一个入参是一个方法指针,这种传参方式往往是要给定参考的值(也就是说其实可以直接给double类型的BlockPos的,但是并不是所有参考值都是需要立刻计算的,通过这种方式传递值可以避免直接计算消耗计算资源,而是延后到需要的时候再计算,兴许后面的条件就不需要计算了,这是一种常见的优化方式),这里的类型看起来是double。根据它的名称我们猜测它是当前寻路的目标位置

进一步我们继续向下分析,可以看到34行调用了generateRandomPos方法,入参12个,看起来都像是缺省值,没有需要格外注意的常数。12个入参的generateRandomPos方法的定义在第78行:

generateRandomPos方法的实现

这个方法看起来相当庞大,不过分析起来没有那么复杂。还记得我们的目标是什么吗,我们需要让这个方法返回一个空对象。根据137行的返回语句可知,返回空对象的方案就是保持flag1变量在最后还是false。

这个flag1对象是在89行定义的,所以我们只需要关注89-135行的代码就好了。这里是一个看起来很大的for循环,而我们的目标是让flag1保持false,而89行的定义里flag1一开始就是false,所以我们只要关注什么行为会导致flag1变成true就可以了(顺带一提,这个for循环很简单,就是尝试10次)。那么可以直观的看到唯一的一句flag1=true在129行。也就是说如果129行不会被执行,那么执行到137行跳出整个for循环后flag1一定是false,方法返回空指针。那么着眼于129行,我们可以看到它在一个5层if嵌套里。

把这5层的条件列出来,分别是95行、115行、122行、124行和126行。我们只要确保这5层的条件里有任何一个能一直不满足即可打成我们的目标:

先看第一层95行,它尝试获取到一个BlockPos对象,而赋值的右边是getRandomDelta方法。其定义在这段程序紧接着的140行:

getRandomDelta方法的实现

它的入参有6个,我们回顾下29行、34行和94行的传参,以及81行的定义,不难得出在我们要分析的问题中(猪灵投掷物品这个行为下),这里的入参是:基于猪灵实体得到的随机数生成器、常数4、常数2、缺省值0、缺省值null、当前寻路的目标位置。142行的if会进行判断,第一个条件就是第4个入参是否为null,在我们的分析中这里恒为null,所以只需要关注154行到159行的else段。这里使用了第1个入参的随机数生成器连续生成了三个随机整数i, j, k。155-157这三行的写法不难看出是修改i, j, k三个整数的取值区间。这里使用到了第2、3、4个入参也就是三个常数4, 2, 0。代入可以得到:i和k表示[-4, 4]的随机数,j表示[-2, 2]的随机数。结合这个方法的名称getRandomDelta不难理解,这个方法返回的是一个随机的方块坐标偏移量。在我们的分析下这个偏移量会在x轴和z轴的±4以及y轴±2的范围内。

回归我们一开始的目的,我们尝试寻找5重if中哪个可以稳定的不满足,但实际上我们的分析在154行就可以确定这第1重if是一定会被满足的。不过不要气馁,我们还有4重if可以分析,并且之前的分析也不是没有意义的,继续看第2重if。

第2重if虽然只有一行,但是条件实际上有好多层,先看第1层的由and连接起来的4个条件。第一个是blockPos3.getY() >= 0,这个条件用到了blockpos3,在114行定义。114行的定义出现了三个整数j、k、l,这三个整数是什么呢?往上找找看96-98行,这不就是我们刚刚分析第一重if时的i、j、k嘛。也就是说这里114行生成了一个从猪灵实体所在位置开始计算加上这个偏移量的新的BlockPos。(这里有意的忽略了99-112行的行为,这里处理的是Restriction的一些行为,它会让xz轴的偏移更小,只有原来的一半也就是±2,但是实际发生的事情是没有本质变化的。)

那么第一个条件就很好理解了,首先这个新的位置要是在y=0及以上。在我们的实际使用场景中这件事是肯定会被保证的(y=0以下的机器怎么做收集啊),那么我们分析第二个条件blockPos3.getY() <= getMaxBuildHeight(),这个条件也非常的直观。这个获取了世界的最大建造高度,超过这个高度也是不行的。这个也在实际场景中被保证了(让猪灵在最大建造高度围在一个区域里也是个超级困难的事情吧)。

这下只剩下两个条件了,这俩条件看起来很复杂,仔细剥开,第三个条件是和刚刚的Restriction有关的,根据83-87行可以得知当没有发生Restriction时,这里恒为true。(这里有意忽略发生Restriction的情况,因为要讨论的代码就太多太多了。简单来说就是在发生Restriction时,这里会判断是否是within的,由于99-112行的偏移缩减,实际上within是成立的。)

最后一个条件则是寻路相关的。它调用了80行定义的寻路器,这是从猪灵实体对象里获取到的。而根据方法名推断,这个方法用于判断blockPos3是否是一个稳定的目的点。在net.minecraft.pathfinding.PathNavigator类的第321行定义:

isStableDestination方法的实现

这里判断的依据是323行的调用,其代码在net.minecraft.block.AbstractBlock类的497行定义:


isSolidRender方法的实现

根据定义可知,它判断的是blockPos3所处位置下方的那个方块是否为可遮挡的且形状完整的。这和固体方块并非是完全相同的定义。根据实验测定,我们已知满足这些特性的方块有:灵魂沙、收回的活塞、浮冰、红石块;而不满足的方块有:玻璃、伸出的活塞和活塞头、粘液块。我们找到了一个可能可以作为衡量标准的依据:当使用透视视角观察完全处于实心墙体内的这些方块时,它是否能被渲染出,如下图所示。

具有可遮挡的且形状完整性质的方块可以在实墙的透视视图中渲染出来

这里似乎还有一个疑点是,灵魂沙是碰撞箱不完整的方块(在上表面裸露的情况下有1/16的缺失),但它是形状完整的,理由可见下图。当玩家实体踩在灵魂沙上时,灵魂沙的渲染边界依然是完整方块的,但是实际的实体碰撞箱是小了1/16的。

灵魂沙的碰撞边界少了1/16,但是渲染边界是完全的

到这里我们似乎有了一个很好的发现,简单总结一下刚刚的分析结论。我们分析了generateRandomPos方法的第93行的for循环。它表示猪灵在扔物品时候的行为逻辑。简单总结,当交易结算完毕,猪灵想要扔出物品时,会选择一个坐标计算扔出方向。假如在它的记忆之中存在最近的玩家的时候,它会朝着那个玩家的方向扔物品;当不存在最近的玩家的时候,它会在自身坐标xz±4和y±2的范围内随机选择一个位置,判断它下方的方块是否是可遮挡的且形状完整的(或者对寻路器而言是稳定的位置),这个随机选择会进行10次(根据之后的分析,10次随机里选择距离猪灵实体最近的那一次作为最终结果)。这里就得到了我们可以利用的特性:如果我们在猪灵实体所在位置周围可以搜索的范围内(注意考虑下方,这里y是要-1的)都没有满足可遮挡的且形状完整的方块,那么这10次搜索都无法使得flag1变成true。从而使得这个方法返回空指针。根据之前的分析就可以让猪灵在自身位置投掷它的交易掉落物了。

到这里似乎我们已经得到了一条完整可用的特性了,不过之前的分析还没有进行完全,也就是说仍有更多特性存在的可能性在之后的if结构里。接下来针对这部分进行深入的分析。(对之前的分析已经感到大脑膨胀的可以考虑跳过后续的部分了。对完整的特性保持好奇心的同学也可以稍微休息一下,缓口气接着读。)

generateRandomPos方法的实现(114-133行)

考虑假如猪灵周围可搜索的范围内存在满足可遮挡的且形状完整的方块,并且它上方的那个方块被10次循环选中了,那么程序就会执行到116行的if判断上。这个判断的条件是一个入参,根据34行的调用可知,这个入参采用的缺省值true,也就是恒成立。那么我们就要分析117-119的这个moveUpToAboveSolid方法了。这个方法的调用是一个稍微复杂的语法,它的第三个入参传递了一个lambda表达式的匿名方法作为入参。这个方法非常好理解,就是用来判断给定的方块是否是固体方块(此固体方块的定义可能和其它场景不同,为防止二义性,我们会在附录列出所有符合固体方块性质的Material类型,如果还需要具体的Block类型请自行根据Material列表在net.minecraft.block.Blocks类里查找)。这里另外需要注意的是第二个入参,根据34行的调用可知,第二个入参随机数生成的两个值都是缺省的0,那么这里的随机数也只能生成为0,也就是在我们的分析中,这个方法第二个入参稳定是0。这个方法的定义在同一个文件的162行:

moveUpToAboveSolid方法的实现

针对这个方法逐行分析,首先第一个条件判断,我们已知给定的第二个入参恒为0,所以不满足条件,不可能触发164行的异常。第二个条件判断,实际上是把传进来的坐标(刚刚搜索到的blockPos3)进行我们传递的那个是否为固体方块的判断,也就是说,如果刚刚检索到的那个位置的方块不是固体方块,那么就直接返回那个位置(这里的逻辑是正确的,因为上一层if的条件里囊括了这个位置的下方不是可遮挡的且形状完整的方块,因此不可能出现虚空投掷的情形)。如果这个搜索到的位置是固体方块,对应168-181行的语句,那么就会向上逐个寻找,直到找到不是固体方块的方块,或者达到世界最大建造高度。172-178行的循环是在找到那个最高方块之后会向下寻找,但由于我们第二个入参是0,所以向下寻找是直接不满足for循环条件,一次都不会执行。所以这一段都可以忽略掉。最后,返回找到的目标坐标,赋值变成了新的blockPos3。

接下来便是第三重if的判断了,在第122行,分为两部分,以或运算相连。一个是入参,结合34行的调用,传入的是缺省值false,所以不需要关注。另一边则是对刚刚向上检索完的结果——新的blockPos3进行了判断。不难理解,这里是判断选中的这个位置的流体状态是否是流动的水。如果是水也可以不用执行flag1=true。此时可以总结第二个特性:当在猪灵周围xz±4和y±2的范围内存在可遮挡的且形状完整的方块时,如果这个方块向上检索的第一个非固体方块流体状态是流动的水,那么猪灵依然不会尝试对这个位置扔物品,而是在自己所在的位置扔物品。

最后便是第4重if了,诶不是一共有5重吗?那就让我们先看下第5重,第5重是两个数值的判断,d0和d1。显然d0一开始被设置成了负无穷(第90行),而126-130行很容易看出是一个简单的状态转移,目标是在10次检索中找到最合适那个目标执行,忽略更差的目标。也就是说,这一层if不可能改变最终的目标(也就是导致无法执行flag1=true,最后方法返回空指针)。它若是会被执行到,无论如何都会被执行一次(存在比负无穷更好的解)。所以我们可以放弃对第5重if的幻想。我们更应该着眼于第4重if,发现我们最终完全版的特性。

generateRandomPos方法的实现(123-131行)

第4重if看起来和寻路息息相关。实际上第123行就尝试调用了外部的WalkNodeProcessor来对刚刚选到的那个位置进行PathNodeType的判断。getBlockPathTypeStatic方法的定义在net.minecraft.pathfinding.WalkNodeProcessor的第398行:

getBlockPathTypeStatic方法的实现

简单来说这个方法是用来对目标位置进行寻路类型的判断的,它会判断目标位置的寻路类型,满足一些特定情况还会进一步的判断它下方的位置的寻路类型作为它的寻路类型返回。具体判断类型的方法则是这个getBlockPathTypeRaw,定义在下面的464行:

getBlockPathTypeRaw方法的定义

这里的一大坨子特判看上去很多,但实际上我们只需要关心其中的部分就好,因为实际上在先前的if判断中已经过滤了大部分条件,能挺到这一步的方块必须是不是流动水非固体方块,在这个范围内我们分析这里的代码(接下来会大量的查询Material的表,见附录1,针对哪个方块是什么Material还望读者可以自己动手查阅,方法就是此前提到的在net.minecraft.block.Blocks类中查找)。

首先468行的isAir是判断Material是否为空气的,这个是符合条件的,它会把PathNodeType设置为OPEN,这里可以画个重点留意一下。接着往下看,470行判断了活板门和荷叶,它使用了两个取反的条件做与判断,如果我们反着看,他就变成了两个不取反的条件的或判断,也就对应之后503行的else域是活板门或者是荷叶的情况。其中活板门固体方块,可以直接排除;荷叶的Material是PLANT,所以不是固体方块,此时的PathNodeType为TRAPDOOR。接着往下看,实际上连着的if就是471行开头的这一系列if-else-if结构了。首先第一个是仙人掌,仙人掌的Material不是PLANT而是单独的CACTUS,CACTUS固体方块,所以不会被选中。紧接着是473行的浆果丛,浆果丛的Material是PLANT所以不是固体方块,所以浆果丛是可以被选中的,它会把PathNodeType设置为DAMAGE_OTHER。再往下是475行的蜂蜜块,蜂蜜块的Material是CLAY所以固体方块。477行是可可豆,可可豆的Material是PLANT,不是固体方块,所以能够选中,此时的PathNodeType为COCOA。继续往下开始一些其他条件的判断了,481行判断是否是水,实际上如果是水已经在上一层if被否决掉了,所以这里可以跳过。483行是流动岩浆的判断,流动岩浆其实没有被过滤,而且流动岩浆不是固体方块,所以流动岩浆会被选中,PathNodeType为LAVA。485行判断方块是否是燃烧着的方块,这个方法定义就在紧接着的第508行:

isBurningBlock方法的定义

根据定义,火、岩浆、岩浆块和燃烧中的营火都会被判断为燃烧着的方块,其中只有岩浆是可以被选中的,它们的Material是FIRE,不是固体方块。487行、489行和491行是对门的判断,但是所有的门都固体方块。493行是对铁轨的判断,铁轨的Material是DECORATION,不是固体方块,所以铁轨是可以被选中的,它的PathNodeType会被设置为RAIL。495行是对树叶的判断,但是树叶固体方块。最后497行也是我们熟悉了多个条件取反的与运算,可以反着看作是多个条件的或运算,其中的条件就是这个方块是栅栏、墙、栅栏门,但是这三者都是固体方块,所以都不会被选中,不需要关心500行的else块。而剩下的就是不满足之前这些特判的最后条件了,第498行,如果不是前述的所有类型,那么就会调用blockstate的isPathfindable方法来获得是否是可循路的,以此来给定PathNodeType是BLOCKED还是OPEN。至此,这个getBlockPathTypeRaw方法分析完毕,我们知道了在满足前述条件——向上搜索到的第一个不是固体的方块作为新的位置,不同的方块对应不同的Material会使得获取到的PathNodeType有什么样的不同。这时候我们再次回归主线,重新看那个generateRamdomPos方法的逻辑,也就是我们第4层if里的内容:

generateRandomPos方法的实现(123-131行)

在123行获取到PathNodeType后,紧接着124行的判断会把这个type扔给猪灵实体对象的getPathfindingMalus方法,得到一个浮点数之后和0进行比较。关于getPathfindingMalus的定义在net.minecraft.entity.MobEntity类的140行:

getPathfindingMalus方法的实现

这里可以看到它是有个稍微复杂的机制的,所以我们精简来看,长话短说,直接看返回值的可能性。返回值是一个三元表达式,根据条件不同可以是那个变量f,也可以是调用这个type的getMalus方法获得一个值出来。关于那个f的部分比较复杂,这里不过多深入,我们不妨假设f是我们不好控制的部分,那么那个getMalus方法就是这里的关键了。另外要注意的一点是还记得上面的目标吗?我们希望让这个malus不是0,这样就不会进入126行开始的第5重if,我们的flag1就不会变成true,进而方法的返回值变成空指针,猪灵就会朝着自己所在的位置扔东西了。这个getMalus方法在net.minecraft.pathfinding.PathNodeType这个枚举类的第35行:

PathNodeType枚举类的实现

这个枚举类理解起来非常简单,可以看到,PathNode被规定了好几种可能性,每个都有一个默认的malus。我们的需求是让malus不为0,那么可以剔除掉OPEN、WALKABLE、WALKABLE_DOOR、TRAPDOOR、RAIL、DOOR_OPEN以及COCOA。结合我们刚刚探索过的那一堆特判的方块,只剩下了浆果丛、岩浆(流动和非流动的都可以)和火是可以的。值得注意的是刚刚分析到的最后逻辑,也就是满足不是固体方块的同时又不是我们特判的那些方块的,这个时候会调用那个isPathfindable方法来判断是否可循路。这个方法的定义在net.minecraft.block.AbstractBlock类的96行:

isPathfindable方法的实现

简单来说就是判断被选中的那个方块是不是碰撞边缘完整的,如果是碰撞边缘完整的,它的PathNodeType就会是BLOCKED,不是则为OPEN。我们能想到的满足之前的条件并且还有碰撞边缘的方块就是雪片了,但是我们实测下来只有5、6、7层的雪片会让猪灵不朝它扔东西,8层会让猪灵向它上方的空气扔东西(表现的和固体方块一致),小于5层则会朝着它扔东西。这部分逻辑我们翻看了雪片的代码实现发现,它覆写了这个isPathfidable方法,在net.minecraft.block.SnowBlock类的30行:

SnowBlock关于isPathfindable方法的覆写实现

小于5层的雪片是被特判了的,所以行为不一致。如果有同学能够想到其他的满足非固体方块但又不在之前的特判里的方块类型,还不是雪片的,欢迎自行实验验证这里的特性,即被选中的那个方块是不是碰撞边缘完整的,如果是碰撞边缘完整的,它的PathNodeType就会是BLOCKED,不是则为OPEN。表现为BLOCK是-1不是0,所以不会触发第5重if,不会更新flag1为true,也就不会让猪灵朝着它扔东西。

心细一点的同学可能会还记得,我们似乎遗忘了一个方法。关注刚刚第4重if那里123行的调用。似乎调用的是getBlockPathTypeStatic方法,而我们只分析了getBlockPathTypeRaw方法。这俩似乎是有区别的。没错!我刚刚也提到了之后会分析这俩的差异,现在搞清楚getBlockPathTypeRaw了我们就可以专心的搞清楚getBlockPathTypeStatic和getBlockPathTypeRaw的差异了。

getBlockPathTypeStatic方法的实现

可以看到,getBlockPathTypeStatic方法里在402行和404行分别调用了两次getBlockPathTypeRaw方法,402行的调用是获取选择到的这个方块本身对应的PathNodeType,而404行的则是它下方一格的那个方块对应的PathNodeType。而这里403行if的条件就是敲门砖。如果我们所选中的那个方块的PathNodeType是OPEN,那么就尝试获取它下方一格的方块,当满足406-418行的某个特殊判断时,用它下方方块的PathNodeType替换它本身的PathNodeType返回。回顾一下刚刚的分析,只有一种情况会给出OPEN,那就是被选中的方块是AIR(其实还有另一种情况,就是不满足之前的特判,同时isPathfidable是true的,目前可能的是5层以下的雪片)。这个时候会判断这个方块下方的方块的PathNodeType。406行是多个特判的堆叠,如果下方方块是WALKABLE、OPEN、WATER、LAVA中的任何一个,那么这个pathnodetype就变成OPEN;反之变成WALKABLE。此后406行判断下方方块的PathNodeType是否是DAMAGE_FIRE的,在之前的分析对应的是燃烧着的方块,对应着火、岩浆、岩浆块和燃烧中的营火,此时这四种都是满足条件的。也就是说在选中方块的PathNodeType是OPEN的情况下,其下方的方块是火、岩浆、岩浆块和燃烧中的营火的任何一个都会让它的PathNodeType变成DAMAGE_FIRE,使得第4层if比较的结果不为0,跳过flag1=true的变化,猪灵会朝所在位置扔物品。410行的判断是仙人掌,它和燃烧着的方块这里的逻辑是一样的,同时DAMAGE_CACTUS也不为0,所以仙人掌也会满足特性。414行的是浆果丛的特判,但是浆果丛本来就会被选中,所以不会走到这里的逻辑。最后的418行判断的是蜂蜜块,蜂蜜块是固体方块,所以它本身不会被选中,但是它上方若是空气,则会满足这里的逻辑,所以蜂蜜块是可以的。最后是423行的判断,如果这时候还没有被任何一个特判选中,并且没有满足405行的条件,那么就会进入424行的逻辑。这个checkNeighbourBlocks方法的定义在紧接着的430行:

checkNeighbourBlocks方法的实现

相信大家对刚刚的代码已经很熟悉了,这里就加快点节奏,这个方法直接遍历以刚刚获取到的非固体方块为中心的3x3x3的全部方块,挨个获取他们的PathNodeType。如果这些方块是仙人掌、浆果丛和燃烧着的方块(火、岩浆、岩浆块和燃烧中的营火),就会返回一个DANGER_CACTUS、DANGER_OTHER和DANGER_FIRE的PathNodeType。在之前的枚举列表里这三个的值都不为0,也就是满足特性。最后453行的判断是周围这3x3x3方块有没有流动水,会触发一个WATER_BORDED的TYPE,也不是0(但是说很多猪灵交易所在地狱,所以大概率很难搞到水),因此也满足。

至此,我们完全了解了getBlockPathTypeStatic方法生成PathNodeType的逻辑。简单总结一下,在默认情况下,经历第2重if之后选择了一个blockPos3,从这个位置开始向上进行方块选择,选择第一个不是固体方块的方块。第3重if检验这个方块不是水,然后尝试获取这个方块的BlockPathType,并且当这个BlockPathType的权重不是0的时候跳过第5层if的执行,也就不会有flag1=true,最后表现为猪灵直接把物品扔在自己所在的位置。满足这个权重不是0的条件可以归纳成:向上选取到的不是固体方块的方块是浆果丛、岩浆(流动和非流动)、火和5-7层的雪片;如果选取到的是空气或者小于5层的雪片,则其下方的方块需要是火、岩浆(非流动)、岩浆块、燃烧中的营火、仙人掌、蜂蜜块;如果下方方块不是刚刚说的这些,同时也不是流动水和流动岩浆,那就会进行最后的判断,以选中方块为中心的3x3x3的方块中至少一个方块是仙人掌、浆果丛、火、岩浆(非流动)、岩浆块、燃烧中的营火或者流动水。这三个判断依次执行。

到这里我们的分析基本上完成了,关于generateRandomPos方法我们已经完整的分析完毕。这个方法就是处理猪灵投掷产物的关键代码。以下是对于特性的总结和结语。


总结(极度省流版)

针对猪灵在交易结束后的投掷产物相关的特性,我们自上而下的对源代码进行了分析。通过源代码我们可以对整个特性的流程做出以下总结:

1.   猪灵在接收到金锭,计算完交换的产物后,会进行投掷方向的计算;

2.   投掷方向的计算会优先考虑获取它AI记忆中最近的玩家,如果这个玩家存在,则朝着玩家投掷物品;

3.   如果不存在记忆中最近的玩家(实际实验可以用遮挡视线完成),会任意选取一个位置投掷;任意选取位置的过程总结如下:

a)   首先划定猪灵实体所在位置xz±4y±2的范围,在其中会进行10次随机选取位置;

b)   如果选取到的位置是可遮挡的且形状完整的方块(另称:可固体渲染方块或者寻路稳定方块,不同于固体方块或者完整方块的定义),则继续执行本次选取。(目前测试已知满足这个性质的方块有:灵魂沙、收回的活塞、浮冰、红石块;而不满足的方块有:玻璃、伸出的活塞和活塞头、粘液块。针对这个性质的判别我们提出了一个衡量标准,但不能完全证明,在文章中部位置有提到。)

c)   从这个选取到的可遮挡的且形状完整的方块向上寻找第一个非固体方块,如果不存在非固体方块则搜索到y=世界最大建造高度的那个方块;

d)   判断那个方块不是流体水,之后计算那个方块的PathNodeType进行进一步判断,实际上是对那个方块和周围方块类型的判断;

e)   如果那个方块是浆果丛岩浆(流动和非流动的都可以)、5-7层的雪片(或者碰撞完整的非固体方块,这里不同的方块可能有不同的判断方式,具体可以看文中的定义),则这次选取不会产生结果;

f)   如果那个方块是空气1-5层的雪片(或者碰撞不完整的非固体方块,这里同样的不同方块有不同的判断方式,文中有详细的定义分析,但仍缺乏实验验证),并且选中的方块y>1(实际上机器不会建到那么低),则判断其下方方块,其下方方块若是岩浆(静态)、岩浆块燃烧中的营火仙人掌或者蜂蜜块,则这次选取不会产生结果;

g)   不满足上面条件的同时,若其下方方块若不是流动水或者流动岩浆,则以选中的那个方块为中心,xyz轴±1的范围内进行新的搜索,若范围内存在仙人掌浆果丛岩浆岩浆块或者燃烧中的营火,则这次选取不会产生结果;

h)   efg都没有成立,则这次选取会进行更新,具体来说会计算猪灵投掷到这个位置的优先度,若当前的优先度比记录中更高,则更新投掷位置,初始的优先度是负无穷,也就是说这个行为表现为执行了10次随机选取中找到优先度最高的那个位置进行投掷。

4.   10次选取都没有找到任何的目标位置,则猪灵会在把物品投掷在自身实体所在的位置。

至此,整个投掷产物的计算流程完毕。考虑我们能够精确控制的情况,也就是我们希望猪灵把物品投掷在自身实体所在的位置。为了做到这一点,最简单的方案是确保猪灵周围xz±4y±2的范围内没有任何的可遮挡的且形状完整的方块。稍微复杂点(但是实际生产环境中不一定可行)是要求每一个在这个区域内的可遮挡的且形状完整的方块上面必须连续保证有固体方块,直到有一个位置没有固体方块,这个没有固体方块位置必须满足上面cg所要求的条件。

到这里,文章的主体部分结束了。这次的性质探究是在服务器设计猪灵交易所的时候开始的,始于一些实验提出的问题。当时为了给出一个完备的解决这些提出问题的答案,我们选择了分析代码,但是当时对代码只做了一个宏观的分析。最后是结合了实验找出了我们实际应用的那个特性,也就是我这里总结的:确保猪灵周围xz±4y±2的范围内没有任何的可遮挡的且形状完整的方块。这也是BV1dh4y1F7Ui这个视频讲的十分笼统的一个原因。后续我想要写完这篇文章,其实是单纯的想要给这个问题一个完备的解这个事情的执念。在写这篇文章的过程中,我逐步深入的挖掘了这些特性,遇到不理解的部分还是进游戏内去做了很多的对照实验。在实验和理论分析的交错纵横下,我最后给出了目前这样的一个似乎是完备的解。但这个解并不是完全的,这里还有两朵乌云没有完全解释:第一部分是Restriction相关的,我没有解释的理由是这里要分析的代码会膨胀很多,我个人是对Restriction涉及到的特性一无所知的,希望以后有大佬可以对这些部分做出完备的解释;另一部分是getPathfindingMalus方法,我只讨论了默认值的情况,它是可能被其他的情况改变的,这部分我没有分析,也是碍于篇幅和分析的代码量。严格来说,我做的这些所有的分析其实都不是那么完备,因为我只考虑了最基层类的实现,如果子类覆写了我分析的方法,那么特性可能完全不同于我讲述的部分,这是有可能发生的。另外一点就是我们的实验并没有彻底完全的覆盖我所讲述的特性,一来是我们想不到更合理的测试样例了,比如说碰撞完全的非固体方块,我们能想到的可能只有雪片了。二来是实际上这里的分支是非常多的,测试这些东西的过程是枯燥的。所以这篇文章提到的特性是理论分析的结果,不保证完全的可用性。如果有遗漏或者分析错误的地方,还望各位理性讨论,不吝赐教。最后我想要说的,对于特性的研究应该是严谨且科学的,无论是实验还是理论分析,二者同等重要。需要交替进行,互相补充,方可找到最优的路径,以更好的效率理解特性的本质,从而运用特性。


关于猪灵在交易结束后投掷产物相关特性的代码分析的评论 (共 条)

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