跳到主要内容
版本:1.21.6 - 1.21.8

方块(Blocks)

方块是 Minecraft 世界的基石。它们构成了所有的地形、结构和机器。如果您有兴趣制作模组(Mod),那么您很可能想要添加一些方块。本页面将指导您如何创建方块,以及您可以用它们做的一些事情。

一统万物的方块(One Block to Rule Them All)

在我们开始之前,理解游戏中的每种方块都只有一个实例是非常重要的。一个世界由成千上万个对该方块在不同位置的引用组成。换句话说,同一个方块只是被显示了很多次。

因此,一个方块应该只被实例化一次,那就是在注册(registration)期间。一旦方块被注册,您就可以根据需要来使用该注册引用。

与大多数其他注册表(Registries)不同,方块可以使用一个特殊版本的 DeferredRegister,称为 DeferredRegister.BlocksDeferredRegister.Blocks 基本上就像一个 DeferredRegister<Block>,但有一些细微差别:

  • 它们通过 DeferredRegister.createBlocks("yourmodid") 创建,而不是常规的 DeferredRegister.create(...) 方法。
  • #register 返回一个 DeferredBlock<T extends Block>,它扩展了 DeferredHolder<Block, T>T 是我们正在注册的方块类的类型。
  • 有一些用于注册方块的辅助方法。更多详情请参见下文(below)

那么现在,让我们注册我们的方块:

//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)中给定位置(position)放置的方块状态(blockstate)
.is(MyBlockRegistrationClass.MY_BLOCK);

这种方法还有一个方便的效果,即 block1 == block2 可以正常工作,并且可以用来替代 Java 的 equals 方法(当然,使用 equals 仍然有效,但由于它是通过引用比较的,所以没有意义)。

危险

请勿在注册之外调用 new Block()!一旦您这样做,事情可能会且将会出错:

  • 方块必须在注册表(Registries)未冻结时创建。NeoForge 会为您解冻注册表,稍后再冻结它们,因此注册是您创建方块的时间窗口。
  • 如果您尝试在注册表再次冻结时创建和/或注册方块,游戏将会崩溃并报告一个 null 方块,这可能会非常令人困惑。
  • 如果您仍然设法获得一个悬空的方块实例,游戏在同步和保存时将无法识别它,并用空气(Air)替换它。

创建方块(Creating Blocks)

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

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

基础方块(Basic Blocks)

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

  • setId - 设置方块的资源键(Resource Key)。
    • 必须为每个方块设置;否则,将抛出异常。
  • destroyTime - 确定方块被破坏所需的时间。
    • 石头(Stone)的破坏时间(destroy time)为 1.5,泥土(Dirt)为 0.5,黑曜石(Obsidian)为 50,基岩(Bedrock)为 -1(不可破坏)。
  • explosionResistance - 确定方块的爆炸抗性。
    • 石头的爆炸抗性(explosion resistance)为 6.0,泥土为 0.5,黑曜石为 1,200,基岩为 3,600,000。
  • sound - 设置方块在被敲击、破坏或放置时发出的声音。
  • lightLevel - 设置方块的亮度等级(light emission)。接受一个带有 BlockState 参数的函数,该函数返回一个介于 0 到 15 之间的值。
    • 例如,荧石(Glowstone)使用 state -> 15,火把(Torch)使用 state -> 14
  • friction - 设置方块的摩擦系数(光滑度)。
    • 默认值为 0.6。冰(Ice)使用 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 类。

备注

重要的是要理解,世界中的方块与物品栏(Inventory)中的方块不是一回事。在物品栏中看起来像方块的东西实际上是 BlockItem,这是一种特殊的物品(item)类型,在使用时会放置一个方块。这也意味着像创造模式标签页(creative tab)或最大堆叠数(max stack size)这样的东西是由对应的 BlockItem 处理的。

BlockItem 必须与方块分开注册。这是因为方块不一定需要一个物品,例如,如果它不打算被收集(就像火(Fire)的情况一样)。

更多功能(More Functionality)

直接使用 Block 只允许非常基础的方块。如果您想添加功能,如玩家交互(Player Interaction)或不同的碰撞箱(hitbox),则需要一个继承自 Block 的自定义类。Block 类有许多可以被重写以执行不同操作的方法;有关更多信息,请参阅类 BlockBlockBehaviourIBlockExtension。另请参阅下面的使用方块(Using blocks)部分,了解方块的一些最常见用例。

如果您想制作一个具有不同变体(variants)的方块(想想一个具有底部(bottom)、顶部(top)和双层(double)变体的台阶(slab)),您应该使用方块状态(blockstates)。最后,如果您想要一个存储额外数据的方块(想想一个存储其物品栏的箱子(chest)),应该使用方块实体(block entity)。这里的经验法则是:如果您有有限且合理数量的状态(最多几百个状态),请使用方块状态;如果您有无限或接近无限数量的状态,请使用方块实体。

方块类型(Block Types)

方块类型是用于序列化和反序列化方块对象的 MapCodecs。这个 MapCodec 通过 BlockBehaviour#codec 设置,并注册(registered)到方块类型注册表(block type registry)。目前,它唯一的用途是在生成方块列表报告(block list report)时使用。每个 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

// 对于某个方块子类
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)
)
);
备注

尽管方块类型目前基本上未被使用,但随着 Mojang 继续朝着以编解码器(Codec)为中心的结构发展,预计它会变得更加重要。

DeferredRegister.Blocks 辅助方法(DeferredRegister.Blocks helpers)

我们已经讨论了如何创建 DeferredRegister.Blocks 如前所述(above),以及它返回 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, // 属性将被传入的工厂(factory)。
BlockBehaviour.Properties.of() // 要使用的属性。
);

如果您想使用 Block::new,可以完全省略工厂:

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

这与前一个示例完全相同,但稍微短一些。当然,如果您想使用 Block 的子类而不是 Block 本身,您将不得不使用前一种方法。

资源(Resources)

如果您注册了方块并将其放置在世界中,您会发现它缺少诸如纹理(texture)之类的东西。这是因为纹理(textures)等是由 Minecraft 的资源系统处理的。在 Minecraft 中添加新方块时,您应该编写或生成(generate)以下文件:

对于以上所有内容,也请参考类似的原版(Vanilla)方块的文件和数据生成器(Data Generators)。

使用方块(Using Blocks)

方块很少被直接用来做事情。事实上,Minecraft 中最常见的两个操作——获取某个位置的方块,以及在某个位置设置一个方块——使用的是方块状态(BlockState),而不是方块。一般的设计方法是让方块定义行为,但行为实际上通过方块状态来运行。因此,BlockState 经常作为参数传递给 Block 的方法。有关如何使用方块状态以及如何从方块获取方块状态的更多信息,请参阅使用方块状态(Using Blockstates)

在多种情况下,Block 的不同方法会在不同时间被使用。以下子部分列出了最常见的与方块相关的工作流程。除非另有说明,否则所有方法都在逻辑两端(logical sides)被调用,并且应该在两端返回相同的结果。

放置方块(Placing a Block)

方块的放置逻辑是从 BlockItem#useOn(或其某个子类的实现,例如用于睡莲(lily pads)的 PlaceOnWaterBlockItem)调用的。有关游戏如何到达该处的更多信息,请参阅右键单击物品(Right-Clicking Items)。实际上,这意味着一旦右键单击了某个 BlockItem(例如一个圆石物品),就会调用此行为。

  • 会检查几个先决条件,例如您不在旁观模式(spectator mode),该方块所需的所有功能标志(feature flags)都已启用,或者目标位置不在世界边界(world border)之外。如果这些检查中至少有一个失败,则流程结束。
  • 对当前位于尝试放置方块的位置的方块调用 BlockBehaviour#canBeReplaced。如果返回 false,则流程结束。此处返回 true 的突出情况是高大的草(tall grass)或雪层(snow layers)。
  • 调用 Block#getStateForPlacement。在这里,根据上下文(包括位置、旋转以及放置方块的侧面等信息),可以返回不同的方块状态。这对于例如可以放置在不同方向的方块很有用。
  • 使用上一步获得的方块状态调用 BlockBehaviour#canSurvive。如果返回 false,则流程结束。
  • 方块状态通过 Level#setBlock 调用设置到世界(Level)中。
    • 在那个 Level#setBlock 调用中,会调用 BlockBehaviour#onPlace
  • 调用 Block#setPlacedBy

破坏方块(Breaking a Block)

破坏方块要复杂一些,因为它需要时间。该过程大致可分为三个阶段:"开始(Initiating)"、"挖掘(Mining)"和"实际破坏(Actually breaking)"。

  • 当左键被点击时,进入"开始"阶段。
  • 现在,需要按住左键,进入"挖掘"阶段。此阶段的方法会在每个游戏刻(tick)调用。
  • 如果"继续"阶段没有被中断(通过松开左键)并且方块被破坏,则进入"实际破坏"阶段。

或者对于更喜欢伪代码的人来说:

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

以下子部分将这些阶段进一步分解为实际的方法调用。有关游戏如何从左键点击到达此流程的信息,请参阅左键单击物品(Left-Clicking an Item)

"开始"阶段(The "Initiating" Stage)

  • 检查几个先决条件,例如您不在旁观模式,主手中的 ItemStack 所需的所有功能标志都已启用,或者所讨论的方块不在世界边界之外。如果这些检查中至少有一个失败,则流程结束。
  • 触发 PlayerInteractEvent.LeftClickBlock。如果事件被取消,流程结束。
    • 注意,当事件在客户端被取消时,不会向服务器发送数据包,因此服务器上不会运行任何逻辑。
    • 但是,在服务器上取消此事件仍将导致客户端代码运行,这可能导致不同步(desync)!
  • 调用 BlockBehaviour#attack

"挖掘"阶段(The "Mining" Stage)

  • 触发 PlayerInteractEvent.LeftClickBlock。如果事件被取消,流程将转到"结束(finishing)"阶段。
    • 注意,当事件在客户端被取消时,不会向服务器发送数据包,因此服务器上不会运行任何逻辑。
    • 但是,在服务器上取消此事件仍将导致客户端代码运行,这可能导致不同步!
  • 调用 BlockBehaviour#getDestroyProgress 并将其添加到内部破坏进度计数器(destroy progress counter)。
    • BlockBehaviour#getDestroyProgress 返回一个介于 0 和 1 之间的浮点值,表示每个游戏刻破坏进度计数器应增加多少。
  • 进度覆盖(裂纹纹理)相应更新。
  • 如果破坏进度大于 1.0(即完成,即方块应该被破坏),则退出"挖掘"阶段并进入"实际破坏"阶段。

"实际破坏"阶段(The "Actually Breaking" Stage)

  • 调用 Item#canDestroyBlock。如果返回 false(确定方块不应被破坏),流程将转到"结束"阶段。
  • 如果方块是 GameMasterBlock 的实例,则调用 Player#canUseGameMasterBlocks。这决定了玩家是否有能力破坏仅限创造模式(GameMaster)的方块。如果为 false,流程将转到"结束"阶段。
  • 仅服务器端(Server-only): 调用 Player#blockActionRestricted。这决定了当前玩家是否可以破坏该方块。如果为 true,流程将转到"结束"阶段。
  • 仅服务器端: 触发 BlockEvent.BreakEvent。如果被取消,流程将转到"结束"阶段。初始的取消状态由上述三个方法决定。
  • 调用 Block#playerWillDestroy
  • 仅服务器端: 调用 IBlockExtension#canHarvestBlock。这决定了方块是否可以被收获(harvested),即被破坏并掉落物品。如果 Player#preventsBlockDrops 返回 true,则忽略此检查。
    • 仅服务器端: 如果 IBlockExtension#canHarvestBlock 没有被重写且未调用其父类(super call),则触发 PlayerEvent.HarvestCheck。如果 HarvestCheck#canHarvest 返回 false,则不会调用 Block#playerDestroy,从而阻止任何资源或经验值掉落。
  • 调用 IBlockExtension#onDestroyedByPlayer。如果返回 false,流程将转到"结束"阶段。
    • 通过一个以 Blocks.AIR.defaultBlockState() 或当前记录的流体(logged fluid)作为方块状态参数的 Level#setBlock 调用,将方块状态从世界中移除。
      • 在那个 Level#setBlock 调用中,会调用 Block#onRemove
    • 如果 IBlockExtension#onDestroyedByPlayer 返回 true,则调用 Block#destroy
  • 仅服务器端: 如果先前对 IBlockExtension#canHarvestBlockIBlockExtension#onDestroyedByPlayer 的调用返回 true,则调用 Block#playerDestroy
    • 仅服务器端: 调用 Block#dropResources。这决定了方块被挖掘时掉落什么,包括经验值。
      • 仅服务器端: 触发 BlockDropsEvent。如果事件被取消,则方块破裂时不会掉落任何东西。否则,BlockDropsEvent#getDrops 中的每个 ItemEntity 都会被添加到当前世界。此外,如果 getDroppedExperience 大于 0,则调用 Block#popExperience
        • 仅服务器端: 调用 IBlockExtension#getExpDrop,并通过 EnchantmentHelper#processBlockExperience 增强。这是在可能被修改之前为 BlockDropsEvent#getDroppedExperience 设置的初始值。
  • 仅服务器端: 如果在上述过程中的任何时候用于挖掘方块的物品损坏了,则触发 PlayerDestroyItemEvent

挖掘速度(Mining Speed)

挖掘速度是根据方块的硬度(hardness)、所用工具(tool)的速度以及几个实体属性(attributes)根据以下规则计算的:

// 这将返回工具的挖掘速度,如果手持物品为空、不是工具或不适用于正在被破坏的方块,则返回 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.HASTE) || player.hasEffect(MobEffects.CONDUIT_POWER)) {
int haste = player.hasEffect(MobEffects.HASTE)
? player.getEffect(MobEffects.HASTE).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.MINING_FATIGUE)) {
destroySpeed *= switch (player.getEffect(MobEffects.MINING_FATIGUE).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);
}
// 如果玩家试图在半空中破坏一个方块,则使玩家的挖掘速度减慢 5 倍。
if (!player.onGround()) {
destroySpeed /= 5;
}
destroySpeed = /* PlayerEvent.BreakSpeed 事件在此处触发,允许模组作者进一步修改此值。 */;
return destroySpeed;

可以参考 Player#getDestroySpeed 中的确切代码。

刻处理(Ticking)

刻处理(Ticking)是一种机制,每 1/20 秒或 50 毫秒("一个游戏刻")更新(刻处理)游戏的部分内容。方块提供了在不同情况下被调用的不同刻处理方法。

服务器刻处理与刻调度(Server Ticking and Tick Scheduling)

BlockBehaviour#tick 在两种情况下被调用:通过默认的随机刻处理(random ticking)(见下文),或通过计划刻(scheduled ticks)。计划刻可以通过 Level#scheduleTick(BlockPos, Block, int) 创建,其中 int 表示延迟。这在原版中有多种用途,例如,大型垂滴叶(big dripleaf)的倾斜机制严重依赖于此系统。其他重要的使用者包括各种红石组件。

客户端刻处理(Client Ticking)

Block#animateTick 仅在客户端调用,每帧一次。这里是客户端专用行为发生的地方,例如火把粒子生成。

天气刻处理(Weather Ticking)

天气刻处理由 Block#handlePrecipitation 处理,独立于常规刻处理运行。它只在服务器端调用,只有当某种形式的降雨或降雪时,有 1/16 的几率调用。例如,这用于在降雨或降雪期间填充的炼药锅(cauldron)。

随机刻处理(Random Ticking)

随机刻系统独立于常规刻处理运行。必须通过方块的 BlockBehaviour.Properties 调用 BlockBehaviour.Properties#randomTicks() 方法来启用随机刻。这使得方块能够成为随机刻处理机制的一部分。

随机刻每个游戏刻在一个区块(chunk)中的设定数量的方块上发生。该设定数量由 randomTickSpeed 游戏规则(gamerule)定义。在其默认值 3 的情况下,每个游戏刻,从区块中选择 3 个随机方块。如果这些方块启用了随机刻处理,则会调用它们各自的 BlockBehaviour#randomTick 方法。

随机刻处理在 Minecraft 中被广泛应用于各种机制,例如植物生长、冰和雪融化,或铜氧化。