Skip to main content
Version: 1.21.4

方块(Blocks)

方块(Blocks)是 Minecraft 世界的核心组成部分。它们构成了所有地形、建筑以及机器。大多数情况下,如果你想开发一个模组(mod),你很可能会想要添加一些方块。本文将带你了解方块的创建过程,以及你可以用它们实现的一些功能。

一方块统治所有(One Block to Rule Them All)

在开始之前,有一点非常重要:游戏中每种方块(block)实际上只有一个实例。一个世界由数千个对同一个方块的引用组成,只是分布在不同的位置。换句话说,同一个方块会被多次显示出来。

因此,每个方块只应该被实例化一次,并且这个过程只会发生在注册表(registration)期间。方块注册完成后,你就可以根据需要使用注册得到的引用。

与大多数其他注册表(registry)不同,方块可以使用一种专门的 DeferredRegister 版本,叫做 DeferredRegister.BlocksDeferredRegister.Blocks 的作用基本等同于 DeferredRegister<Block>,但有一些细微的区别:

  • 它们通过 DeferredRegister.createBlocks("yourmodid") 创建,而不是常规的 DeferredRegister.create(...) 方法。
  • #register 方法返回一个 DeferredBlock<T extends Block>,它继承自 DeferredHolder<Block, T>。这里的 T 是我们要注册的方块类的类型。
  • 提供了一些用于注册方块的辅助方法。详见下文

现在,让我们注册自己的方块:

// BLOCKS 是一个 DeferredRegister.Blocks
public static final DeferredBlock<Block> MY_BLOCK = BLOCKS.register("my_block", registryName -> new Block(...));

方块注册完成后,所有对新 my_block 的引用都应该使用这个常量。例如,如果你想检查某个位置的方块是不是 my_block,代码大致如下:

level.getBlockState(position) // 返回给定世界(level)中指定位置处的方块状态(blockstate)
.is(MyBlockRegistrationClass.MY_BLOCK);

这种做法还有一个便利之处,就是可以直接用 block1 == block2 进行判断,而不用 Java 的 equals 方法(当然用 equals 也没问题,但实际上也是按引用比较的)。

danger

不要在注册(registration)之外调用 new Block()!一旦这样做,程序很可能会出错:

  • 方块必须在注册表(registry)解冻时创建。NeoForge 会为你解冻注册表,并在之后重新冻结,因此注册期间是你创建方块的唯一窗口。
  • 如果你在注册表冻结后尝试创建和/或注册方块,游戏会崩溃并报出 null 方块的错误,这会让人非常困惑。
  • 如果你仍然持有一个悬空(dangling)的方块实例,游戏在同步和保存时不会识别它,并会用空气(air)替换掉它。

创建方块(Creating Blocks)

如前所述,我们首先要创建自己的 DeferredRegister.Blocks

public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks("yourmodid");

基础方块(Basic Blocks)

对于不需要特殊功能的简单方块(比如圆石、木板等),可以直接使用 Block 类。在注册时,只需用一个 BlockBehaviour.Properties 参数来实例化 Block。这个 BlockBehaviour.Properties 参数可以通过 BlockBehaviour.Properties#of 创建,并可以通过调用其方法进行自定义。最重要的方法有:

  • setId - 设置方块的资源键(resource key)。
    • 每个方块必须设置此项,否则会抛出异常。
  • destroyTime - 决定方块被破坏所需的时间。
    • 石头的破坏时间是 1.5,泥土是 0.5,黑曜石是 50,基岩是 -1(不可破坏)。
  • explosionResistance - 决定方块的爆炸抗性。
    • 石头的爆炸抗性为 6.0,泥土为 0.5,黑曜石为 1,200,基岩为 3,600,000。
  • sound - 设置方块被敲击、破坏或放置时的声音。
    • 默认值为 SoundType.STONE。详细信息见 声音页面
  • lightLevel - 设置方块的发光等级。接受一个参数为 BlockState 的函数,返回值范围为 0 到 15。
    • 例如,荧石使用 state -> 15,火把使用 state -> 14
  • friction - 设置方块的摩擦系数(滑溜度)。
    • 默认值为 0.6。冰为 0.98。

例如,一个简单的实现如下所示:

//BLOCKS 是一个 DeferredRegister.Blocks
public static final DeferredBlock<Block> MY_BETTER_BLOCK = BLOCKS.register(
"my_better_block",
registryName -> new Block(BlockBehaviour.Properties.of()
.setId(ResourceKey.create(Registries.BLOCK, registryName))
.destroyTime(2.0f)
.explosionResistance(10.0f)
.sound(SoundType.GRAVEL)
.lightLevel(state -> 7)
));

更多文档请参见 BlockBehaviour.Properties 的源码。如需更多示例,或者想了解 Minecraft 中所用的具体参数,请参考 Blocks 类。

note

需要理解的是,世界中的方块和物品栏中的“方块”并不是同一个东西。物品栏中看起来像方块的其实是 BlockItem,这是一种特殊的 物品,使用时可以放置方块。这也意味着,像创造模式标签或最大堆叠数量之类的属性,是由对应的 BlockItem 控制的。

BlockItem 必须与方块分开注册。这是因为方块不一定需要对应的物品,例如火这种方块就不能被收集,所以没有对应的物品。

更多功能(More Functionality)

直接使用 Block 只能创建非常基础的方块。如果你想要添加更多功能,比如玩家交互或者自定义碰撞箱(hitbox),就需要自定义一个继承自 Block 的类。Block 类有许多可以重写(override)的方法,用于实现不同的行为;更多信息可以参考 BlockBlockBehaviourIBlockExtension 这几个类。常见的方块用例可以参考下方的 方块的使用 部分。

如果你想要制作带有不同变体的方块(比如有下半块、上半块和双层变体的台阶),应该使用 方块状态(blockstates)。最后,如果你需要一个可以存储额外数据的方块(比如可以存储物品栏的箱子),则应使用 方块实体(block entity)。这里的经验法则是:如果你的状态数量是有限且相对较少的(最多几百种状态),使用方块状态(blockstates);如果你的状态数量是无限或接近无限的,则使用方块实体(block entity)。

方块类型(Block Types)

方块类型是用于序列化和反序列化方块对象的 MapCodec。这个 MapCodec 通过 BlockBehaviour#codec 设置,并注册到方块类型注册表(block type registry)。目前,它唯一的用途是在生成方块列表报告时使用。每个 Block 的子类都应该创建一个对应的方块类型。例如,FlowerBlock#CODEC 代表了大多数花类方块的类型,而它的子类 WitherRoseBlock 则有一个单独的方块类型。

如果你的方块子类只接收 BlockBehaviour.Properties 作为参数,那么可以用 BlockBehaviour#simpleCodec 来创建对应的 MapCodec

// 针对某个方块子类
public class SimpleBlock extends Block {
public SimpleBlock(BlockBehavior.Properties properties) {
// ...
}

@Override
public MapCodec<SimpleBlock> codec() {
return SIMPLE_CODEC.get();
}
}

// 在某个注册类中
public static final DeferredRegister<MapCodec<? extends Block>> REGISTRAR = DeferredRegister.create(BuiltInRegistries.BLOCK_TYPE, "yourmodid");

public static final Supplier<MapCodec<SimpleBlock>> SIMPLE_CODEC = REGISTRAR.register(
"simple",
() -> BlockBehaviour.simpleCodec(SimpleBlock::new)
);

如果你的方块子类包含更多参数,那么应该使用 RecordCodecBuilder#mapCodec 来创建 MapCodec,并为 BlockBehaviour.Properties 参数传入 BlockBehaviour#propertiesCodec

// 对于某个 Block 子类
public class ComplexBlock extends Block {
public ComplexBlock(int value, BlockBehavior.Properties properties) {
// ...
}

@Override
public MapCodec<ComplexBlock> codec() {
return COMPLEX_CODEC.get();
}

public int getValue() {
return this.value;
}
}

// 在某个注册类中
public static final DeferredRegister<MapCodec<? extends Block>> REGISTRAR = DeferredRegister.create(BuiltInRegistries.BLOCK_TYPE, "yourmodid");

public static final Supplier<MapCodec<ComplexBlock>> COMPLEX_CODEC = REGISTRAR.register(
"simple",
() -> RecordCodecBuilder.mapCodec(instance ->
instance.group(
Codec.INT.fieldOf("value").forGetter(ComplexBlock::getValue),
BlockBehaviour.propertiesCodec() // 代表 BlockBehavior.Properties 参数
).apply(instance, ComplexBlock::new)
)
);
note

虽然目前方块类型(block types)基本上还未被广泛使用,但预计随着 Mojang 继续推进以 codec 为核心的架构,方块类型在未来会变得更加重要。

DeferredRegister.Blocks 辅助方法(helpers)

我们已经在上文讨论了如何创建一个 DeferredRegister.Blocks,以及它返回的是 DeferredBlock。现在,让我们看看这个专用的 DeferredRegister 还提供了哪些其他实用工具。首先来看 #registerBlock

public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks("yourmodid");

public static final DeferredBlock<Block> EXAMPLE_BLOCK = BLOCKS.register(
"example_block", registryName -> new Block(
BlockBehaviour.Properties.of()
// 必须在方块上设置 ID
.setId(ResourceKey.create(Registries.BLOCK, registryName))
)
);

// 与上面相同,只是方块属性会被提前(eagerly)构造。
// setId 也会在属性对象内部被调用。
public static final DeferredBlock<Block> EXAMPLE_BLOCK = BLOCKS.registerBlock(
"example_block",
Block::new, // 用于接收属性参数的工厂方法。
BlockBehaviour.Properties.of() // 要使用的属性。
);

如果你想用 Block::new,可以完全省略工厂方法:

public static final DeferredBlock<Block> EXAMPLE_BLOCK = BLOCKS.registerSimpleBlock(
"example_block",
BlockBehaviour.Properties.of() // 要使用的属性。
);

这与前一个例子完全相同,但写法更简洁。当然,如果你想注册的是 Block 的子类而不是 Block 本身,就需要用前面的方法。

资源(Resources)

如果你注册了自己的方块(block)并将其放置到世界中,你会发现它缺少诸如纹理等内容。这是因为 [纹理][textures] 以及其他资源,是由 Minecraft 的资源系统负责管理的。要为方块应用纹理,你需要提供一个 [模型][model] 和一个 [方块状态文件(blockstate file)][bsfile],用来将方块与纹理及形状关联起来。建议阅读这些链接的文章以获取更多信息。

## 使用方块(Using Blocks) \{#using-blocks}

在实际开发中,很少直接操作方块(block)本身。事实上,Minecraft 里最常见的两种操作 —— 获取某个位置的方块,以及在某个位置设置方块 —— 用的都是方块状态(blockstate),而不是方块对象。整体的设计思路是:方块定义行为(behavior),但这些行为实际是通过方块状态(blockstate)来执行的。因此,`BlockState` 经常作为参数传递给 `Block` 的方法。关于方块状态的用法,以及如何从方块获取方块状态,详见 [使用方块状态][usingblockstates]。

在多种场景下,`Block` 的多个方法会在不同阶段被调用。以下小节列出了最常见的与方块相关的处理流程。除非特别说明,所有方法会在逻辑的两侧(客户端和服务端)都被调用,并且应当返回相同的结果。

### 放置方块(Placing a Block) \{#placing-a-block}

方块的放置逻辑是从 `BlockItem#useOn` 方法(或其某个子类的实现,比如用于睡莲的 `PlaceOnWaterBlockItem`)中开始调用的。关于游戏是如何进入这一步的,详见 [右键物品][rightclick]。实际上,这意味着只要右键点击了某个 `BlockItem`(例如圆石方块物品),就会触发下面的行为。

- 首先会进行一系列前置条件检查,例如:玩家是否处于旁观者模式(spectator mode),方块所需的所有功能标志(feature flags)是否已启用,目标位置是否在世界边界内等。如果有任何一项检查未通过,流程直接终止。
- 会调用 `BlockBehaviour#canBeReplaced` 方法,判断当前目标位置的方块是否可以被替换。如果返回 `false`,流程终止。常见会返回 `true` 的情况有高草丛或积雪层等可被替换的方块。
- 调用 `Block#getStateForPlacement` 方法。这里会根据上下文(包括位置、旋转角度、方块放置的朝向等信息)返回不同的方块状态。这对于可以朝不同方向放置的方块非常有用。
- 用上一步获得的方块状态调用 `BlockBehaviour#canSurvive` 方法。如果返回 `false`,流程终止。
- 通过 `Level#setBlock` 方法将方块状态设置到世界中。
- 在 `Level#setBlock` 调用过程中,会触发 `BlockBehaviour#onPlace` 方法。
- 调用 `Block#setPlacedBy` 方法。

### 破坏方块(Breaking a Block) \{#breaking-a-block}

破坏方块的流程稍微复杂一些,因为它需要一定的时间。整个过程大致可以分为三个阶段:“开始破坏(initiating)”、“挖掘中(mining)”和“实际破坏(actually breaking)”。
- 当按下鼠标左键时,进入“启动(initiating)”阶段。
- 此时需要持续按住鼠标左键,进入“挖掘(mining)”阶段。**该阶段的方法会在每一刻(tick)被调用。**
- 如果“持续(continuing)”阶段没有被中断(比如松开鼠标左键),并且方块被破坏,则进入“实际破坏(actually breaking)”阶段。

如果你更喜欢伪代码,可以参考下面:

```java
leftClick();
initiatingStage();
while (leftClickIsBeingHeld()) {
miningStage();
if (blockIsBroken()) {
actuallyBreakingStage();
break;
}
}

下面的小节会进一步将这些阶段拆解为实际的方法调用。关于游戏如何从左键点击进入这一流程的详细信息,请参见左键点击物品

“启动(Initiating)”阶段

  • 首先会进行一系列前置条件检查,例如你是否处于旁观者模式(spectator mode)、主手中的 ItemStack 是否启用了所有所需的特性标志(feature flags)、以及目标方块是否在世界边界(world border)内。如果这些检查中有任何一项不通过,流程就会终止。
  • 触发 PlayerInteractEvent.LeftClickBlock 事件。如果事件被取消,流程也会终止。
    • 注意,如果该事件在客户端被取消,则不会向服务器发送任何数据包(packet),因此服务器端不会执行任何逻辑。
    • 但是,如果该事件在服务器端被取消,客户端的代码依然会运行,这可能导致客户端与服务器状态不同步(desync)!
  • 调用 Block#attack 方法。

“挖掘(Mining)”阶段

  • 触发 PlayerInteractEvent.LeftClickBlock 事件。如果事件被取消,流程会进入“完成(finishing)”阶段。
    • 注意,如果该事件在客户端被取消,则不会向服务器发送任何数据包,因此服务器端不会执行任何逻辑。
    • 但是,如果该事件在服务器端被取消,客户端的代码依然会运行,这可能导致客户端与服务器状态不同步!
  • 调用 Block#getDestroyProgress 方法,并将其返回值累加到内部的破坏进度计数器中。
    • Block#getDestroyProgress 返回一个介于 0 和 1 之间的浮点数,表示每一刻(tick)应当增加多少破坏进度。
  • 相应地更新进度覆盖层(即方块裂纹贴图)。
  • 如果破坏进度大于 1.0(即已完成,也就是方块应该被破坏),则退出“挖掘”阶段,进入“实际破坏(actually breaking)”阶段。

“实际破坏(Actually Breaking)”阶段

  • 调用 Item#canAttackBlock 方法。如果返回 false(即判断该方块不应被破坏),流程将进入“结束”阶段。
  • 如果方块是 GameMasterBlock 的实例,则会调用 Player#canUseGameMasterBlocks 方法。这会判断玩家是否有权限破坏仅限创造模式的方块。如果返回 false,流程将进入“结束”阶段。
  • 仅限服务端:调用 Player#blockActionRestricted 方法。该方法判断当前玩家是否被限制破坏该方块。如果返回 true,流程将进入“结束”阶段。
  • 仅限服务端:触发 BlockEvent.BreakEvent 事件。如果事件被取消或 getExpToDrop 返回 -1,流程将进入“结束”阶段。事件的初始取消状态由上述三个方法决定。
    • 仅限服务端:触发 PlayerEvent.HarvestCheck 事件。如果 canHarvest 返回 false 或传入破坏事件的 BlockState 为 null,则该事件的初始经验值为 0。
    • 仅限服务端:如果 PlayerEvent.HarvestCheck#canHarvest 返回 true,则调用 IBlockExtension#getExpDrop 方法。该值会传递给 BlockEvent.BreakEvent#getExpToDrop,供后续流程使用。
  • 仅限服务端:调用 IBlockExtension#canHarvestBlock 方法。该方法判断方块是否可被收获(即破坏后是否掉落物品)。
  • 调用 IBlockExtension#onDestroyedByPlayer 方法。如果返回 false,流程将进入“结束”阶段。在 IBlockExtension#onDestroyedByPlayer 方法调用过程中:
    • 调用 Block#playerWillDestroy 方法。
    • 通过调用 Level#setBlock 方法,并将 Blocks.AIR.defaultBlockState() 作为 blockstate 参数,从世界中移除该方块状态。
      • Level#setBlock 方法调用过程中,会调用 Block#onRemove 方法。
  • 调用 Block#destroy 方法。
  • 仅限服务端:如果之前对 IBlockExtension#canHarvestBlock 的调用返回了 true,则调用 Block#playerDestroy 方法。
    • 仅限服务端:调用 Block#dropResources 方法。该方法决定挖掘方块时会掉落哪些物品。
      • 仅限服务端:触发 BlockDropsEvent 事件。如果该事件被取消,则方块破坏时不会掉落任何物品。否则,BlockDropsEvent#getDrops 返回的每个 ItemEntity 都会被添加到当前世界中。
  • 仅限服务端:如果之前 IBlockExtension#getExpDrop 调用返回值大于 0,则调用 Block#popExperience 方法,并传入该返回值。

挖掘速度(Mining Speed)

挖掘速度由方块的硬度、所用 工具 的速度,以及多个实体 属性 按照如下规则共同决定:

// 这将返回工具的挖掘速度;如果手持物为空、不是工具,或不适用于当前方块,则返回 1。
float destroySpeed = item.getDestroySpeed(blockState);
// 如果我们有合适的工具,则将 minecraft:mining_efficiency 属性作为加法修饰器加入。
if (destroySpeed > 1) {
destroySpeed += player.getAttributeValue(Attributes.MINING_EFFICIENCY);
}
// 应用急迫(haste)或潮涌能量(conduit power)效果。
if (player.hasEffect(MobEffects.DIG_SPEED) || player.hasEffect(MobEffects.CONDUIT_POWER)) {
int haste = player.hasEffect(MobEffects.DIG_SPEED)
? player.getEffect(MobEffects.DIG_SPEED).getAmplifier()
: 0;
int conduitPower = player.hasEffect(MobEffects.CONDUIT_POWER)
? player.getEffect(MobEffects.CONDUIT_POWER).getAmplifier()
: 0;
int amplifier = Math.max(haste, conduitPower);
destroySpeed *= 1 + (amplifier + 1) * 0.2f;
}
// 应用挖掘缓慢(slowness)效果。
if (player.hasEffect(MobEffects.DIG_SLOWDOWN)) {
destroySpeed *= switch (player.getEffect(MobEffects.DIG_SLOWDOWN).getAmplifier()) {
case 0 -> 0.3F;
case 1 -> 0.09F;
case 2 -> 0.0027F;
default -> 8.1E-4F;
};
}
// 将 minecraft:block_break_speed 属性作为乘法修饰器加入。
destroySpeed *= player.getAttributeValue(Attributes.BLOCK_BREAK_SPEED);
// 如果玩家在水下,则乘以水下挖掘速度惩罚。
if (player.isEyeInFluid(FluidTags.WATER)) {
destroySpeed *= player.getAttributeValue(Attributes.SUBMERGED_MINING_SPEED);
}
// 如果玩家在空中尝试破坏方块,则挖掘速度变为原来的五分之一。
if (!player.onGround()) {
destroySpeed /= 5;
}
destroySpeed = /* 这里会触发 PlayerEvent.BreakSpeed 事件,允许模组开发者进一步修改该值。 */;
return destroySpeed;

此代码的完整实现可参考 Player#getDestroySpeed

Ticking(刻/周期更新)

Ticking(周期更新)是一种机制,每隔 1 / 20 秒(即 50 毫秒,“一刻”)会对游戏的部分内容进行一次更新(tick)。方块(block)提供了多种 ticking 方法,这些方法会在不同的情况下被调用。

服务器端 Ticking 与 Tick 调度(Server Ticking and Tick Scheduling)

BlockBehaviour#tick 会在两种情况下被调用:一种是默认的 随机 tick(见下文),另一种是通过调度 tick(scheduled ticks)。调度 tick 可以通过 Level#scheduleTick(BlockPos, Block, int) 创建,其中 int 参数表示延迟的 tick 数。原版(vanilla)在多个场景中使用了这一机制,例如大型垂滴叶(big dripleaves)的倾斜机制就高度依赖该系统。其他典型用例还包括各种红石(redstone)元件。

客户端 Ticking(Client Ticking)

Block#animateTick 只会在客户端(client)上、每一帧(frame)调用一次。这里可以实现仅限客户端的行为,例如火把粒子的生成等。

天气 Ticking(Weather Ticking)

天气相关的 tick 由 Block#handlePrecipitation 方法(方法)处理,并且其运行是独立于常规 tick 的。该方法只会在服务器端调用,且仅在出现某种形式的降雨时才会触发,触发概率为 1/16。比如,雨天或下雪时能够装满水的炼药锅(cauldron)就是利用了这一机制。

随机 Tick(Random Ticking)

随机 tick 系统同样是独立于常规 tick 运行的。要启用随机 tick,必须在方块的 BlockBehaviour.Properties 属性中调用 BlockBehaviour.Properties#randomTicks() 方法。这将使该方块加入到随机 tick 机制中。

在每一次 tick 时,区块(chunk)中的一定数量的方块会被选中进行随机 tick。这个数量由 randomTickSpeed 游戏规则(gamerule)决定。默认值为 3,即每 tick 会从区块中随机选择 3 个方块。如果这些方块启用了随机 tick,则会调用它们对应的 BlockBehaviour#randomTick 方法。

Minecraft 中许多机制都用到了随机 tick,比如植物生长、冰雪融化、铜锈蚀等。