方块
方块是 Minecraft 世界的基本组成部分,构成地形、建筑和机器。开发 Mod 时,你很可能也希望添加自己的方块。本文介绍方块的创建过程,以及可以为它们实现的一些行为。
每种方块只有一个实例
开始之前,需要理解:游戏中每一种方块始终只有一个实例。世界中不同位置的成千上万个方块,实际上都是对同一个方块对象的引用;同一个方块被显示了许多次。
因此,方块只能在注册阶段实例化一次。注册后,按需使用已注册的引用即可。
与大多数注册表不同,方块可以使用 DeferredRegister 的专用版本 DeferredRegister.Blocks。它基本上相当于 DeferredRegister<Block>,但有一些区别:
- 使用
DeferredRegister.createBlocks("yourmodid")创建,而不是常规的DeferredRegister.create(...)。 #register返回DeferredBlock<T extends Block>,它继承自DeferredHolder<Block, T>。其中T是所注册方块的类。- 提供一些注册方块的辅助方法,见下文。
下面注册一个方块:
//BLOCKS is a DeferredRegister.Blocks
public static final DeferredBlock<Block> MY_BLOCK = BLOCKS.register("my_block", () -> new Block(...));
注册后,所有对新方块 my_block 的引用都应使用这个常量。例如,检查某个位置是否为 my_block:
level.getBlockState(position) // returns the blockstate placed in the given level (world) at the given position
//highlight-next-line
.is(MyBlockRegistrationClass.MY_BLOCK);
这种方式也使得 block1 == block2 可以直接使用,代替 Java 的 equals。当然,equals 也能使用,但由于它同样按引用比较,因此没有额外意义。
重要警告 不要在注册阶段之外调用
new Block()!否则会产生问题:
- 必须在注册表未冻结时创建方块。NeoForge 会先解冻注册表,之后再冻结,因此注册阶段就是创建方块的时间窗口。
- 如果注册表重新冻结后才尝试创建或注册方块,游戏会崩溃,并可能报告一个令人困惑的
null方块错误。- 即使设法保留了一个游离的方块实例,游戏在同步和保存时也无法识别它,会将其替换为空气。
创建方块
如前文所述,先创建 DeferredRegister.Blocks:
public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks("yourmodid");
基础方块
圆石、木板这类不需要特殊功能的方块,可以直接使用 Block 类。注册时,为构造函数传入 BlockBehaviour.Properties。通过 BlockBehaviour.Properties#of 创建属性对象,然后调用相应方法调整。最重要的方法包括:
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 is a DeferredRegister.Blocks
public static final DeferredBlock<Block> MY_BETTER_BLOCK = BLOCKS.register(
"my_better_block",
() -> new Block(BlockBehaviour.Properties.of()
//highlight-start
.destroyTime(2.0f)
.explosionResistance(10.0f)
.sound(SoundType.GRAVEL)
.lightLevel(state -> 7)
//highlight-end
));
更多选项请查看 BlockBehaviour.Properties 源代码。若要参考更多示例或原版使用的具体数值,可以查看 Blocks 类。
提示 世界中的方块与物品栏中的方块并不相同。物品栏里看起来像方块的对象,实际是
BlockItem,即一种使用后会放置方块的特殊物品。因此,创造模式物品栏分类、最大堆叠数量等属性由对应的BlockItem处理。
BlockItem必须独立于方块注册,因为方块不一定需要对应物品。例如,火这类不打算让玩家收集的方块就没有相应物品。
更多功能
直接使用 Block 只能创建最基础的方块。若要实现玩家交互或不同碰撞箱等功能,需要继承 Block 创建自己的类。Block 提供了大量可重写的方法,详情见 Block、BlockBehaviour 和 IBlockExtension。一些常见用途也会在下文的使用方块中介绍。
如果同一方块需要多个变体,例如台阶的下半、上半和双层形态,应使用方块状态。如果方块需要保存额外数据,例如箱子的物品栏,应使用方块实体。一般而言,数量有限且较少的状态(最多几百种)适合方块状态;无限或接近无限的数据组合适合方块实体。
方块类型
方块类型是用于序列化和反序列化方块对象的 MapCodec。通过 BlockBehaviour#codec 指定,并注册到方块类型注册表。在此文档版本中,它仅在生成方块列表报告时使用。每个 Block 子类都应创建自己的方块类型。例如,FlowerBlock#CODEC 表示大多数花的方块类型,其子类 WitherRoseBlock 则使用独立类型。
如果方块子类只接受 BlockBehaviour.Properties 参数,可以使用 BlockBehaviour#simpleCodec 创建 MapCodec:
// For some block subclass
public class SimpleBlock extends Block {
public SimpleBlock(BlockBehavior.Properties properties) {
// ...
}
@Override
public MapCodec<SimpleBlock> codec() {
return SIMPLE_CODEC.value();
}
}
// In some registration class
public static final DeferredRegister<MapCodec<? extends Block>> REGISTRAR = DeferredRegister.create(BuiltInRegistries.BLOCK_TYPE, "yourmodid");
public static final DeferredHolder<MapCodec<? extends Block>, MapCodec<SimpleBlock>> SIMPLE_CODEC = REGISTRAR.register(
"simple",
() -> simpleCodec(SimpleBlock::new)
);
如果构造函数还有其他参数,应使用 RecordCodecBuilder#mapCodec,并用 BlockBehaviour#propertiesCodec 表示 BlockBehaviour.Properties 参数:
// For some block subclass
public class ComplexBlock extends Block {
public ComplexBlock(int value, BlockBehavior.Properties properties) {
// ...
}
@Override
public MapCodec<ComplexBlock> codec() {
return COMPLEX_CODEC.value();
}
public int getValue() {
return this.value;
}
}
// In some registration class
public static final DeferredRegister<MapCodec<? extends Block>> REGISTRAR = DeferredRegister.create(BuiltInRegistries.BLOCK_TYPE, "yourmodid");
public static final DeferredHolder<MapCodec<? extends Block>, MapCodec<ComplexBlock>> COMPLEX_CODEC = REGISTRAR.register(
"simple",
() -> RecordCodecBuilder.mapCodec(instance ->
instance.group(
Codec.INT.fieldOf("value").forGetter(ComplexBlock::getValue),
BlockBehaviour.propertiesCodec() // represents the BlockBehavior.Properties parameter
).apply(instance, ComplexBlock::new)
);
);
提示 在此文档版本中,方块类型的实际用途较少。原文预计,随着 Mojang 继续采用以 codec 为中心的结构,它们将逐渐变得更重要。
DeferredRegister.Blocks 辅助方法
前文介绍了 DeferredRegister.Blocks 的创建方法,以及它返回 DeferredBlock 的特点。这个专用注册器还提供其他便捷工具,先看 #registerBlock:
public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks("yourmodid");
public static final DeferredBlock<Block> EXAMPLE_BLOCK = BLOCKS.registerBlock(
"example_block",
Block::new, // The factory that the properties will be passed into.
BlockBehaviour.Properties.of() // The properties to use.
);
它会将属性参数传给指定的方块工厂,通常就是构造函数。内部效果相当于 BLOCKS.register("example_block", () -> new Block(BlockBehaviour.Properties.of()))。
如果使用的工厂是 Block::new,还可以省略工厂参数:
public static final DeferredBlock<Block> EXAMPLE_BLOCK = BLOCKS.registerSimpleBlock(
"example_block",
BlockBehaviour.Properties.of() // The properties to use.
);
这与前一个示例完全相同,只是更简短。如果使用的是 Block 子类,则仍需要前一种方式。
资源
将注册好的方块放入世界后,你会发现它还没有纹理等内容。这是因为纹理等内容由 Minecraft 的资源系统管理。添加新方块时,需要编写或生成以下文件:
以上内容都可以参考相似原版方块的资源文件与数据生成器。
使用方块
实际操作中很少直接对方块对象执行行为。例如,Minecraft 中最常见的两个操作——读取某个位置的方块、设置某个位置的方块——使用的都是方块状态。整体设计是由方块定义行为,再通过方块状态调用行为。因此,Block 的许多方法都会接受 BlockState 参数。状态的具体用法,以及如何从方块获取状态,见使用方块状态。
在一些流程中,Block 的多个方法会在不同时间依次调用。下面列出最常见的方块处理流程。除非另有说明,这些方法会在两个逻辑端调用,且应在两端返回相同结果。
放置方块
放置逻辑从 BlockItem#useOn 开始,或从子类中的对应实现开始,例如睡莲使用的 PlaceOnWaterBlockItem。游戏如何进入这一步,见交互流程。实际效果是,右键使用圆石等 BlockItem 时,会执行以下行为:
- 检查前置条件,例如玩家不处于旁观模式、方块所需的功能标记均已启用、目标位置位于世界边界内。任意检查失败都会结束流程。
- 对目标位置当前的方块调用
BlockBehaviour#canBeReplaced。返回false时结束流程。草丛、积雪层等是通常会返回true的例子。 - 调用
Block#getStateForPlacement。它根据上下文返回合适的方块状态;上下文包括位置、旋转和放置面等信息,适合用于可朝不同方向放置的方块。 - 以上一步获取的状态调用
BlockBehaviour#canSurvive。返回false时结束流程。 - 通过
Level#setBlock将方块状态写入世界。- 在这次调用中,还会调用
BlockBehaviour#onPlace。
- 在这次调用中,还会调用
- 调用
Block#setPlacedBy。
破坏方块
破坏方块需要时间,因此过程更复杂,可大致分为“开始”“挖掘”“实际破坏”三个阶段。
- 按下鼠标左键,进入“开始”阶段。
- 持续按住左键,进入“挖掘”阶段。此阶段的方法每个游戏刻都会调用。
- 如果没有松开左键中断过程,且方块被挖掘完成,就进入“实际破坏”阶段。
可用以下伪代码表示:
leftClick();
initiatingStage();
while (leftClickIsBeingHeld()) {
miningStage();
if (blockIsBroken()) {
actuallyBreakingStage();
break;
}
}
下面进一步列出各阶段实际调用的方法。
“开始”阶段
- 仅客户端:触发
InputEvent.InteractionKeyMappingTriggered,按键为鼠标左键,使用主手。事件被取消则结束流程。 - 检查前置条件,例如玩家不处于旁观模式、主手
ItemStack需要的功能标记已启用、方块位于世界边界内。任一检查失败则结束流程。 - 触发
PlayerInteractEvent.LeftClickBlock。被取消则结束流程。- 如果客户端取消事件,就不会发送数据包,服务端也不会执行相应逻辑。
- 仅在服务端取消事件,客户端代码仍会执行,可能导致不同步。
- 调用
Block#attack。
“挖掘”阶段
- 触发
PlayerInteractEvent.LeftClickBlock。被取消时,流程转入结束处理。- 客户端取消事件后不会发送数据包,服务端不会执行相应逻辑。
- 仅在服务端取消仍会让客户端代码执行,可能造成不同步。
- 调用
Block#getDestroyProgress,将结果累加到内部破坏进度计数器。- 该方法返回 0~1 的浮点数,表示每刻增加多少破坏进度。
- 根据进度更新裂纹贴图。
- 进度大于 1.0 时,表示挖掘完成,退出“挖掘”阶段,进入“实际破坏”阶段。
“实际破坏”阶段
- 调用
Item#canAttackBlock。返回false表示不应破坏方块,转入结束处理。 - 如果方块是
GameMasterBlock,调用Player#canUseGameMasterBlocks,判断玩家能否破坏创造模式专用方块。返回false则转入结束处理。 - 仅服务端:调用
Player#blockActionRestricted,判断当前玩家是否被限制破坏此方块。返回true则转入结束处理。 - 仅服务端:触发
BlockEvent.BreakEvent。如果事件被取消,或getExpToDrop返回 -1,则转入结束处理。初始取消状态由前三个方法决定。- 仅服务端:触发
PlayerEvent.HarvestCheck。如果HarvestCheck#canHarvest返回false,或传入破坏事件的BlockState为 null,事件初始经验值设为 0。 - 仅服务端:如果
PlayerEvent.HarvestCheck#canHarvest返回true,调用IBlockExtension#getExpDrop,将结果传给BlockEvent.BreakEvent#getExpToDrop,供后续使用。
- 仅服务端:触发
- 仅服务端:调用
IBlockExtension#canHarvestBlock,判断能否采集方块,即破坏时能否产生掉落物。 - 调用
IBlockExtension#onDestroyedByPlayer。返回false则转入结束处理。在此方法中:- 调用
Block#playerWillDestroy。 - 调用
Level#setBlock,传入Blocks.AIR.defaultBlockState(),移除世界中的方块状态。- 在这次调用中执行
Block#onRemove。
- 在这次调用中执行
- 调用
- 调用
Block#destroy。 - 仅服务端:如果此前
IBlockExtension#canHarvestBlock返回true,调用Block#playerDestroy。- 仅服务端:调用
Block#dropResources,决定挖掘后的掉落物。- 仅服务端:触发
BlockDropsEvent。如果取消事件,则不产生任何掉落物;否则,将BlockDropsEvent#getDrops中的每个ItemEntity加入当前世界。
- 仅服务端:触发
- 仅服务端:调用
- 仅服务端:如果此前
IBlockExtension#getExpDrop返回值大于 0,以该结果调用Block#popExperience。
游戏刻更新
游戏刻更新是一种周期性更新机制,通常每 1/20 秒,即 50 毫秒,执行一刻。方块提供了多种以不同方式调用的更新方法。
服务端更新与计划刻
BlockBehaviour#tick 通过计划刻调用。使用 Level#scheduleTick(BlockPos, Block, int) 可以安排计划刻,其中整数参数表示延迟。原版许多地方使用了这套系统,例如大型垂滴叶的倾斜机制,以及多种红石元件。
客户端更新
Block#animateTick 仅在客户端调用,用于产生客户端效果,例如火把粒子。原文将其描述为每帧调用。
天气更新
天气更新由 Block#handlePrecipitation 处理,与常规更新独立。它只在服务端、存在雨雪等降水时,以 1/16 的概率调用。例如,炼药锅在下雨或下雪时积水或积雪,就使用了这种机制。
随机刻
随机刻系统独立于常规更新。需要通过方块的 BlockBehaviour.Properties#randomTicks() 启用,方块才会参与随机刻机制。
每个游戏刻会在区块中随机选择一定数量的方块,数量由游戏规则 randomTickSpeed 决定。默认值为 3,表示每刻随机选择 3 个方块。如果被选中的方块启用了随机刻,就调用它的 BlockBehaviour#randomTick。
Minecraft 中许多机制都使用随机刻,例如植物生长、冰雪融化和铜氧化。
来源: NeoForged/Documentation(1.21.1)。中文翻译依据提交
4a680cdd4d15。