<mc源码分析>20w46a潜影贝生成
这个是我新开的一个系列,说是分析,其实就是简单的看下源码,比较仔细的了解一下游戏机制。之所以开这个坑,一是因为最近玩MC比较多,被MC独特的游戏方式所吸引,碰到很多问题比如刷铁机、刷怪塔、物品分类的时候,会很想深入研究里面的原理。而MC虽然并不是开源软件,但是多年来玩家对MC的解包反编译工作已经非常成熟,甚至官方还自己提供了用于反编译的mapping,因此只需要懂一点java,就可以深入了解游戏内的各种机制。二是因为最近刚刚宣布1.17版本,前几天出了第一个snapshot,其中有一个特性让我觉得有点怪,就是潜影贝在被其他潜影贝的子弹攻击后,有几率生成一只新的潜影贝。虽然我并不是要在MC里讲科学,但这个改动本身确实有点“不太合常理”,而且这一改动意味着将来玩家会有稳定的潜影盒获取途径,从而很大程度上影响玩家之后的游戏体验,可能会导致潜影盒“掉价”。当然我不觉得这是一件坏事,毕竟在末地到处找潜影贝的痛苦我还是感同身受的。从这一点上来说,潜影贝农场以后会是玩家必做农场,没有之一,什么女巫塔、凋零骷髅塔、刷铁机全都靠边站。当然我不是专业的java程序员,对java的了解仅限于能大概看懂代码而已,所以可能文章会有错漏,如果你发现了任何错漏,或者单纯的想和我在这方面交流,都欢迎与我联系。
多说几句,MC彻底的高自由度决定了,任何游戏机制或是特性、BUG,都会被玩家利用到极致,以刷铁机为例,本身是一种村民的保护机制,在检测到危险的时候主动召唤铁傀儡来保护村庄,但是因为铁傀儡死掉会掉落铁锭,从而导致这一机制几乎只是为了玩家刷铁锭而存在的。我对这一现象其实颇为不满,我认为要么官方不要提供这样会被玩家滥用的机制,要么官方能提供更加优雅的机制。当然这不是今天的重点,因此就不展开谈了,我之后还会针对刷铁机进行分析,到时候再讨论这个问题吧。
言归正传,我对比了20w46a和1.16.3的服务端代码中有关潜影贝的部分,这是他们的diff结果
diff 1.16.3/server/net/minecraft/world/entity/monster/Shulker.java 20w46a/server/net/minecraft/world/entity/monster/Shulker.java
41a42
> import net.minecraft.world.item.DyeColor;
46a48
> import net.minecraft.world.level.entity.EntityTypeTest;
72c74
< this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[0])).setAlertOthers());
---
> this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{this.getClass()})).setAlertOthers());
290c292
< if (var3.getY() > 0 && this.level.isEmptyBlock(var3) && this.level.getWorldBorder().isWithinBounds(var3) && this.level.noCollision(this, new AABB(var3))) {
---
> if (var3.getY() > this.level.getMinBuildHeight() && this.level.isEmptyBlock(var3) && this.level.getWorldBorder().isWithinBounds(var3) && this.level.noCollision(this, new AABB(var3))) {
292a295
> this.unRide();
305c308
< return true;
---
> return false;
336a340
> Entity var3;
338c342
< Entity var3 = var1.getDirectEntity();
---
> var3 = var1.getDirectEntity();
344c348,350
< if (super.hurt(var1, var2)) {
---
> if (!super.hurt(var1, var2)) {
> return false;
> } else {
346a353,357
> } else if (var1.isProjectile()) {
> var3 = var1.getDirectEntity();
> if (var3 != null && var3.getType() == EntityType.SHULKER_BULLET) {
> this.hitByShulkerBullet();
> }
350,351d360
< } else {
< return false;
358a368,386
> private void hitByShulkerBullet() {
> Vec3 var1 = this.position();
> AABB var2 = this.getBoundingBox();
> if (!this.isClosed() && this.teleportSomewhere()) {
> int var3 = this.level.getEntities((EntityTypeTest)EntityType.SHULKER, var2.inflate(8.0D), Entity::isAlive).size();
> float var4 = (float)(var3 - 1) / 5.0F;
> if (this.level.random.nextFloat() >= var4) {
> Shulker var5 = (Shulker)EntityType.SHULKER.create(this.level);
> DyeColor var6 = this.getColor();
> if (var6 != null) {
> var5.setColor(var6);
> }
>
> var5.moveTo(var1);
> this.level.addFreshEntity(var5);
> }
> }
> }
>
410a439,448
> }
>
> public void setColor(DyeColor var1) {
> this.entityData.set(DATA_COLOR_ID, (byte)var1.getId());
> }
>
> @Nullable
> public DyeColor getColor() {
> byte var1 = (Byte)this.entityData.get(DATA_COLOR_ID);
> return var1 != 16 && var1 <= 15 ? DyeColor.byId(var1) : null;我们主要关注的其实是其中的hitByShulkerBullet这个函数,那么先单独摘出来看看:
private void hitByShulkerBullet() {
// 被击中的潜影贝所在位置
Vec3 var1 = this.position();
// 被击中的潜影贝所处的BoundingBox(边界框)
AABB var2 = this.getBoundingBox();
// 如果 潜影贝不处于关闭状态 并且 成功的传送走了
if (!this.isClosed() && this.teleportSomewhere()) {
// var3 代表当前潜影贝17x17x17范围内的潜影贝总数
int var3 = this.level.getEntities((EntityTypeTest)EntityType.SHULKER, var2.inflate(8.0D), Entity::isAlive).size();
// var4 是一个小数,根据潜影贝总数不同而不同,决定了潜影贝生成的几率,
// 1 -> 100%
// 2 -> 80%
// 3 -> 60%
// 4 -> 40%
// 5 -> 20%
// 6 -> 0%
float var4 = (float)(var3 - 1) / 5.0F;
// 此处即是决定潜影贝是否生成成功
if (this.level.random.nextFloat() >= var4) {
// 生成一个新的潜影贝
Shulker var5 = (Shulker)EntityType.SHULKER.create(this.level);
// 下面4行设定潜影贝的颜色
DyeColor var6 = this.getColor();
if (var6 != null) {
var5.setColor(var6);
}
// 移动新潜影贝至原潜影贝的位置
var5.moveTo(var1);
// 将新潜影贝注册至当前level
this.level.addFreshEntity(var5);
}
}
}其实这里已经非常清楚了,但是还有一处相关的代码需要贴出来:
public boolean hurt(DamageSource var1, float var2) {
// hurt 是一个用于处理伤害的函数,var1是施加伤害的实体,var2是伤害数值,最终如果伤害成功,则返回true,否则返回false
Entity var3;
if (this.isClosed()) {
// 此处为潜影贝关闭时免疫箭矢伤害,和小黑类似
var3 = var1.getDirectEntity();
if (var3 instanceof AbstractArrow) {
return false;
}
}
if (!super.hurt(var1, var2)) {
// 如果伤害施加不成功,则返回false,此时不会生成新潜影贝
return false;
} else {
if ((double)this.getHealth() < (double)this.getMaxHealth() * 0.5D && this.random.nextInt(4) == 0) {
// 成功施加伤害,且潜影贝生命值少于50%,此时有20%的几率潜影贝会直接传送走且不会生成新潜影贝
this.teleportSomewhere();
} else if (var1.isProjectile()) {
var3 = var1.getDirectEntity();
if (var3 != null && var3.getType() == EntityType.SHULKER_BULLET) {
// 如果不满足上面的条件且施加伤害的实体是潜影贝的子弹,则会调用上面的函数
this.hitByShulkerBullet();
}
}
return true;
}
}之前说了,生成新潜影贝的条件之一是旧的潜影贝成功传送走,那么成功传送走的条件是什么呢?
protected boolean teleportSomewhere() {
if (!this.isNoAi() && this.isAlive()) {
// 当前位置
BlockPos var1 = this.blockPosition();
// 总共进行5次传送尝试
for(int var2 = 0; var2 < 5; ++var2) {
// 随机选取17x17x17范围内的一格
BlockPos var3 = var1.offset(8 - this.random.nextInt(17), 8 - this.random.nextInt(17), 8 - this.random.nextInt(17));
// 如果该位置大于最小建筑高度,且是空方块(空气方块),且不会有碰撞冲突问题
if (var3.getY() > this.level.getMinBuildHeight() && this.level.isEmptyBlock(var3) && this.level.getWorldBorder().isWithinBounds(var3) && this.level.noCollision(this, new AABB(var3))) {
// 在该位置上找一个可以附着的面
Direction var4 = this.findAttachableFace(var3);
if (var4 != null) {
// 如果成功找到,则首先尝试“下车”,再传送,设置朝向,播放传送音效登,仅有此时传送成功,返回true,其他情况均返回false
this.unRide();
this.entityData.set(DATA_ATTACH_FACE_ID, var4);
this.playSound(SoundEvents.SHULKER_TELEPORT, 1.0F, 1.0F);
this.entityData.set(DATA_ATTACH_POS_ID, Optional.of(var3));
this.entityData.set(DATA_PEEK_ID, (byte)0);
this.setTarget((LivingEntity)null);
return true;
}
}
}
return false;
} else {
return false;
}
}可以看到还是相当明白的,那么总结一下潜影贝的生成条件:
- 被潜影贝子弹击中,无论是该潜影贝自己发出的还是其他潜影贝发出的都可以
- 如果此时潜影贝生命值低于50%,则有20%机会直接传送走而不生成新潜影贝
- 处于打开状态,被潜影贝子弹击中,此时潜影贝会尝试传送走
- 成功传送走,条件比较复杂
- 如果满足了上述四个条件,则新潜影贝的生成机率和以潜影贝传送前位置为中心17x17x17范围内的潜影贝数量有关。用S代表潜影贝总数,其概率为
正常情况下S不会小于1,因为潜影贝只能传送到17x17x17范围内
理论上来说,生成效率主要取决于潜影贝的攻击频率,攻击频率越高,可能的效率就越高,类似ilmango的设计应该是相当高效的。总的来说,提高效率的思路无非是以下几点:
- 尽量快速清除生成的/传送走的潜影贝,最好使周围17x17x17范围内的仅存在一个潜影贝
- 尽量提高潜影贝成功传送的概率,需设计17x17x17范围内排布方式,假设该范围内有n个位置可传送,则传送成功的概率为
- 尽量保持潜影贝的血量在50%以上,这条影响不是很大,如果不满足,大约会降低20%的效率
- 尽量使潜影贝子弹成功击中
其中,4是必须要想办法满足的前提,且比较复杂,不细说。能动性比较高的主要是1和2,如果能保持17x17x17范围内仅存在一个潜影贝,那么1就不会影响效率。而2,假设17x17x17范围内有100个位置可以传送,则每次传送成功的概率约为10%,而大约在636个位置的时候,传送成功率达到了50%,也就是说,如果其他条件都一样,那么636个位置的效率会是100个位置的效率的5倍。636个位置如果不管排布方式,则其实并不是很难,因为每个方块有6个面,则放106个建筑方块就满足条件了,实际情况中如ilmongo的设计采用的是单平面可传送,那么每个方块只有一个面可以传送,此时需要636个方块作为潜在的传送位置。也就是一片53x12的墙,或者近似做6块10x12的墙,这在17x17x17的范围内并不是什么难事。
下图展示了传送成功概率随可传送位置数量变化的曲线,可以看到,可传送位置越多,提升就越不明显,当概率高于0.8以上时,就明显不值得在这里付出精力了。而且范围内总共也就4913个方块,要在一定条件下满足这么多可传送位置并不容易。即使按照每个方块提供6个可传送位置来算,范围内最多可传送位置也仅有4200个。而实际上这是根本不可能做到的事情,因为还需要做处死或者转移潜影贝的装置。