Skip to main content

2.3 方块的其他属性

2.3.1 被爆破行为

你是否好奇过,多个 TNT 方块放在一起时,引爆其中一个是如何引发连锁反应的?

实际上,通过实现父类 Block 的方法 onBlockExploded,就能控制在方块受到爆炸影响后的行为,例如连锁爆炸、播放自定义音频,等等:

@Override
public void onBlockExploded(World world, BlockPos pos, Explosion explosion) {
world.createExplosion(null, pos.getX(), pos.getY(), pos.getZ(), 4.0F, true, Explosion.Mode.DESTROY);
super.wasExploded(world, pos, explosion);
}

在 forge 中,唯一和方块、爆炸有关的的分别位于 IForgeBlock 接口以及 Block 类的自定义方法(AbstractBlock 中没有):

注意到虚函数的默认实现是:当方块被爆炸波及后,先将方块设置为 “和空气的默认方块状态一样” 的状态(等价于破坏了当前方块),然后调用 Block 对象自己定义的 onBlockExploded 方法。这个方法默认方法体为空(什么都不做),所有我们才能通过重写 OnBlockExploded 来自定义被爆破时的行为。

2.3.1-EX 拓展:世界事件处理(World 类)

注意到一个好玩的类:World,我们看到了 World 至少有爆炸(explode)、控制方块状态属性的方法(setBlockState),两种方法。感兴趣的同学可以检查它的源码,上面能看到超多 World 的接口,例如:

  • isRemote():查看当前世界对象是在 client 侧还是 server 侧;

  • isValidXZPosition(BlockPos pos):查询当前方块位置是否是合法的 X / Z 轴位置,可以看到合法范围在正负 30000000;

  • isValid() & isValidPosition():两个函数用途不同:

    • 前者检查世界中的自然生成物 / 放置物的位置是否合法,高度最大 256,最小 0;判断逻辑是 isValidXZPosition() && !isYOutOfBounds()
    • 后者检查这个位置是否在世界中,或者说这个坐标是否合理。这个合理的位置不一定可以放置方块;高度最大 20000000;判断逻辑是 isValidXZPosition() && isValidYPosition()
  • getChunk():按参数信息获取指定区块。其中有一个有趣的重载函数,能告诉我们 Minecraft 的区块划分方法:

  • setBlockState() / getBlockState():设置 / 获取方块状态;

  • isDaytime() / isNightTime():获取当前日夜间模式;

    注意到 DimensionType 类型,其中对世界维度的特性进行定义,例如环境光、是否有固定的日夜时间。MC Forge 对 OverworldThe Nether(地狱)、The End(末地)有 vanilla 定义;

  • playSound():播放指定音频,我们可以通过这个方法底层播放声音(与物品或其他对象相关的声音建议在对于的类中定义);

  • addParticle()@OnlyIn(Dist.CLIENT)):渲染指定粒子效果。关于粒子效果我们会在后面详细叙述;

  • add/set/get/removeTileEntity() / addTileEntities() / tickBlockEntities() / guardEntityTick():和 Block Entity(Tile Entity)相关的接口,用于按策略处理这个世界中的所有 Block Entity 的活动。我们将在后面叙述 Block Entity;

  • createExplosion():创建爆炸对象并且爆炸。这里顺带注意到我们之前遇到的参数类型 Explosion 类型。

    注意到 Explosion 类型的构造函数全部由 @OnlyIn(Dist.CLIENT) 修饰,意味着爆炸对象创建完全在 Client 端完成。

    大致了解一下 Explosion 类型的构造函数是传入爆炸的位置、引发实体、爆炸半径、爆炸种类(损坏、彻底破坏);

    创建好 Explosion 对象后,就进行以下 3 步:

    1. 执行 forge event factory 中注册的 onExplosionStart,一旦返回的是 true 代表不进行本次爆炸的效果(立即返回);
    2. 否则执行 Explosion#doExplosionA(),这步的作用是计算需要破坏的方块;
    3. 下一步执行 Explosion#doExplosionB(),这步的作用是产生爆炸后的音效、粒子效果、掉落物;

    最终这个 createExplosion 都会返回创建的 Explosion 对象;

  • isBlockPresent(BlockPos):检查指定位置是否存在方块,先检查 Y 轴高度是否在能生成方块的位置,再检查所在区块是否存在(可以看出,Minecraft 中管理方块的生成是以区块为单位的);

  • isDirectionSolid() / isTopSolid():通过 BlockState 检查指定方块的指定方向的 碰撞箱 是否覆盖该方块表面。关于方块的碰撞箱的定义,我们将在 “模型” 一节继续讨论;

  • getSeaLevel():获取世界水平面高度,默认是 Y = 63

  • getStrongPower() / getRedstonePower() / getRedstonePowerFromNeighbors()isSidePowered() / isBlockPowered():前 3 个方法用于处理世界的红石信号。如果不能理解含义,请阅读 Chapter 4 红石的相关内容。

    • getStrongPower(BlockPos) 计算获取指定位置上的方块所收到最强的红石信号(因为可能多个面都收到红石信号);
    • getRedstonePower(BlockPos, Direction):检查指定方块的红石信号(考虑是否为弱充能或强充能);
    • getRedstonePowerFromNeighbors(BlockPos):检查当前方块的相邻方块中最大红石信号值;
  • get/setThunderStrength() / get/setRainStrength():获取当前世界的雷暴 / 降水强度;

    注意到一个有趣的现象:降水等级是使用 [this.prevRainingStrength, this.rainingStrength] 间斜率为 delta 的线性插值计算。

    而雷暴等级则使用线性插值 乘以 降水等级计算。

  • isRaining() / isRainingAt(BlockPos) / isThundering():查询当前世界(指定位置)是否存在雷暴/降水;

2.3.2 植物种植

那么植物是如何判定可以种植在哪些方块上的呢?我们只需要重写 vanilla Block#canSustainPlant 方法,就能决定哪些方块可以种植在自定义的 Block 类型上了。

可以看到 Block 类原生对植物能否种植的判断比较复杂,不过判断的是 vanilla block,因此新加入的方块几乎什么植物都不能种植。

事实上,植物在 Minecraft 中也作为一种方块存在,它们都实现了 IPlantable 接口,只不过它们的模型各有差别。

IPlantable 接口只有两个方法 getPlantTypegetPlant,前者返回预定义的 PlantType,后者返回方块状态 BlockState

所以在 canSustainBlock 的实现中,我们只需要取出 plantable 植物对应的方块,判断这个方块是否就是指定的已注册的植物,根据情况返回 boolean 值,就能实现方块是否允许种植特定植物的特性了。举例:

@Override
public boolean canSustainPlant(BlockState state, IBlockReader world, BlockPos pos, Direction facing, net.minecraftforge.common.IPlantable plantable) {
Block plantBlock = plantable.getPlant(world, pos).getBlock();
// Can sustain cactus.
if (plantBlock == Blocks.CACTUS) {
return true;
} else {
// Use vanilla logic, which is always TRUE.
return super.canSustainPlant(state, world, pos, facing, plantable);
}
}

2.3.3 随机时刻行为

在一些情况下,方块需要随机时刻渲染 / 执行相应逻辑,这可能是庄稼成长,或者是杂草蔓延,等等,详细信息参见 wiki - random ticks

如果我们需要方块具有随机行为,我们可以重写 AbstractBlock#randomTick 方法(已弃用,可以重写 AbstractBlockState#randomTick 方法):

@Override
public void randomTick(BlockState state, ServerWorld worldIn, BlockPos pos, Random random) {
BlockState aboveState = worldIn.getBlockState(pos.up());
if (aboveState.isAir()) {
// set block & update (flag = 1)
worldIn.setBlockState(pos.up(), Blocks.CACTUS.getDefaultState(), 1);
}
}

并且,务必指定传入的 AbstractBlock.Properties 设置 tickRandomly(),否则 main loop 永远不会调用这个方法。

如果我们需要调试随机时刻行为的效果,我们可以在进入世界时执行 /gamerule randomTickSpeed 9999 来加快随机时刻速度以供调试;

2.3.4 含水方块

参见 5.1 节。