跳到主要内容
版本:1.21.5

方块(Blocks)

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

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

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

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

与大多数其他注册表不同,方块可以使用一个专门版本的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)中给定位置的方块状态(blockstate)
.is(MyBlockRegistrationClass.MY_BLOCK);

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

危险

不要在注册之外调用new Block()!一旦你这样做,事情就会并且将会出错:

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

创建方块(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 - 设置方块被敲击、破坏或放置时发出的声音。
  • 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类。

备注

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

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

更多功能(More Functionality)

直接使用Block只允许非常基础的方块。如果你想添加功能,比如玩家交互或不同的碰撞箱,需要一个扩展Block的自定义类。Block类有许多可以重写的方法来做不同的事情;更多信息请参见BlockBlockBehaviourIBlockExtension类。另请参见下面的使用方块(Using blocks)部分,了解方块的一些最常见用例。

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

方块类型(Block Types)

方块类型是用于序列化和反序列化方块对象的MapCodecs。这个MapCodec通过BlockBehaviour#codec设置并注册(registered)到方块类型注册表。目前,它的唯一用途是在生成方块列表报告时。每个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继续向以编解码器为中心的结构发展,预计未来会变得更加重要。

DeferredRegister.Blocks辅助方法

我们已经讨论了如何创建DeferredRegister.Blocksabove,以及它返回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))
)
);

// 与上面相同,只是方块属性是急切构造的。
// 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本身,则必须使用前一个方法。

资源

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

对于以上所有内容,还应参考类似原版方块的文件和数据生成器。

使用方块(Using Blocks)

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

在几种情况下,Block的多个方法在不同时间被使用。以下小节列出了最常见的方块相关流程。除非另有说明,否则所有方法都在两个逻辑侧调用,并且应在两侧返回相同的结果。

放置方块(Placing a Block)

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

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

破坏方块(Breaking a Block)

破坏方块稍微复杂一些,因为它需要时间。该过程大致可分为三个阶段:“启动”、“挖掘”和“实际破坏”。

  • 当点击鼠标左键时,进入“启动”阶段。
  • 现在,需要按住鼠标左键,进入“挖掘”阶段。此阶段的方法每刻调用一次。
  • 如果“持续”阶段未被中断(通过释放鼠标左键)并且方块被破坏,则进入“实际破坏”阶段。

或者对于那些更喜欢伪代码的人:

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

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

"启动"阶段(The "Initiating" Stage)

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

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

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

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

  • 调用Item#canDestroyBlock。如果返回false(确定方块不应被破坏),流程移动到“完成”阶段。
  • 如果方块是GameMasterBlock的实例,则调用Player#canUseGameMasterBlocks。这确定玩家是否有能力破坏仅限创造模式的方块。如果false,流程移动到“完成”阶段。
  • 仅限服务器:调用Player#blockActionRestricted。这确定当前玩家是否不能破坏方块。如果true,流程移动到“完成”阶段。
  • 仅限服务器:触发BlockEvent.BreakEvent。如果取消,流程移动到“完成”阶段。初始取消状态由上述三个方法确定。
  • 调用Block#playerWillDestroy
  • 仅限服务器:调用IBlockExtension#canHarvestBlock。这确定方块是否可以收获,即破坏时掉落物品。如果Player#preventsBlockDrops返回true,则忽略此方法。
    • 仅限服务器:如果IBlockExtension#canHarvestBlock未被重写而不调用其超类,则触发PlayerEvent.HarvestCheck。如果HarvestCheck#canHarvest返回false,则不会调用Block#playerDestroy,防止任何资源或经验掉落。
  • 调用IBlockExtension#onDestroyedByPlayer。如果返回false,流程移动到“完成”阶段。
    • 通过Level#setBlock调用将方块状态从世界中移除,使用Blocks.AIR.defaultBlockState()或当前记录的流体作为方块状态参数。
      • 在该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)

挖掘速度根据以下规则从方块的硬度、使用的tool速度以及几个实体attributes计算:

// 这将返回工具的挖掘速度,如果持有的物品为空、不是工具或不适用于正在破坏的方块,则返回1。
float destroySpeed = item.getDestroySpeed(blockState);
// 如果我们有适用的工具,将minecraft:mining_efficiency属性作为加法修饰符添加。
if (destroySpeed > 1) {
destroySpeed += player.getAttributeValue(Attributes.MINING_EFFICIENCY);
}
// 应用急迫或潮涌核心效果。
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;
}
// 应用挖掘疲劳效果。
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)

刻更新是一种每1/20秒或50毫秒(“一刻”)更新(刻)游戏部分的机制。方块提供不同的刻更新方法,以不同方式调用。

服务器刻更新和刻调度(Server Ticking and Tick Scheduling)

BlockBehaviour#tick在两种情况下被调用:要么通过默认的随机刻更新(random ticking)(见下文),要么通过计划刻。计划刻可以通过Level#scheduleTick(BlockPos, Block, int)创建,其中int表示延迟。原版在各种地方使用此系统,例如,大型垂滴叶的倾斜机制严重依赖此系统。其他突出的使用者是各种红石组件。

客户端刻更新(Client Ticking)

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

天气刻更新(Weather Ticking)

天气刻更新由Block#handlePrecipitation处理,独立于常规刻更新运行。它仅在服务器上调用,仅当以某种形式下雨时,有1/16的几率。例如,在雨或雪期间填充的炼药锅使用此功能。

随机刻更新(Random Ticking)

随机刻系统独立于常规刻更新运行。必须通过调用BlockBehaviour.Properties#randomTicks()方法在方块的BlockBehaviour.Properties中启用随机刻。这使方块成为随机刻机制的一部分。

随机刻每刻在区块中一定数量的方块上发生。该数量通过randomTickSpeed游戏规则定义。其默认值为3,每刻从区块中选择3个随机方块。如果这些方块启用了随机刻,则调用它们各自的BlockBehaviour#randomTick方法。

随机刻更新在Minecraft中被广泛使用,例如植物生长、冰和雪融化或铜氧化。