方块
方块是 Minecraft 世界的基础,构成了地形、建筑和机器。制作 Mod 时,通常也会需要添加方块。本页介绍方块的创建方式,以及可以为方块实现的功能。
每种方块只有一个实例
首先要理解:游戏中每种方块始终只有一个实例。世界中不同位置存储的是对这个实例的成千上万个引用,换句话说,同一个方块对象被显示了很多次。
因此,方块只应在注册期间实例化一次,之后按需使用注册得到的引用。
与多数注册表不同,方块可以使用专用的 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", registryName -> new 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 创建,并通过方法配置,常用方法包括:
setId:设置方块的资源键。- 每个方块都必须设置,否则会抛出异常。
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",
registryName -> new Block(BlockBehaviour.Properties.of()
//highlight-start
.setId(ResourceKey.create(Registries.BLOCK, registryName))
.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.get();
}
}
// In some registration class
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 创建 codec,并以 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.get();
}
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 Supplier<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 便捷方法
上文已经说明它的创建方式及返回的 DeferredBlock。下面看专用注册器的其他工具,先从 #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()
// The ID must be set on the block
.setId(ResourceKey.create(Registries.BLOCK, registryName))
)
);
// Same as above, except that the block properties are supplied separately.
// setId is also called internally on the properties object.
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 supplied properties to use.
);
// Same as above, except that the `Properties#of` is supplied and operated upon.
// setId is also called internally on the properties object.
public static final DeferredBlock<Block> EXAMPLE_BLOCK = BLOCKS.registerBlock(
"example_block",
Block::new, // The factory that the properties will be passed into.
props -> props // A unary operator of the properties to use.
);
若使用 Block::new,可以完全省略工厂参数:
public static final DeferredBlock<Block> EXAMPLE_BLOCK = BLOCKS.registerSimpleBlock(
"example_block",
() -> BlockBehaviour.Properties.of() // The supplied properties to use.
);
public static final DeferredBlock<Block> EXAMPLE_BLOCK = BLOCKS.registerSimpleBlock(
"example_block",
props -> props // A unary operator of the properties to use.
);
这与上一示例完全等价,只是更简短。如果使用的是 Block 子类,而非 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。
破坏方块
破坏方块需要时间,因此更加复杂,大体分为“开始”“挖掘”和“实际破坏”三个阶段。
- 按下鼠标左键时进入“开始”阶段。
- 持续按住左键,进入“挖掘”阶段。这一阶段的方法每 tick 都会调用。
- 如果未松开左键中断挖掘,并且达到破坏条件,就进入“实际破坏”阶段。
用伪代码表示如下:
leftClick();
initiatingStage();
while (leftClickIsBeingHeld()) {
miningStage();
if (blockIsBroken()) {
actuallyBreakingStage();
break;
}
}
下面进一步说明各阶段调用的实际方法。游戏如何从左键点击进入此流程,见左键使用物品。
“开始”阶段
- 检查各种前提,例如玩家不是旁观者、主手
ItemStack所需特性标志已启用、目标方块位于世界边界内等。任一失败则终止。 - 触发
PlayerInteractEvent.LeftClickBlock,若被取消则终止。- 客户端取消事件时,不会向服务端发送数据包,因此服务端不执行相应逻辑。
- 只在服务端取消时,客户端代码仍会运行,可能导致不同步!
- 调用
BlockBehaviour#attack。
“挖掘”阶段
- 触发
PlayerInteractEvent.LeftClickBlock,若取消则转入“收尾”阶段。- 客户端取消时不会发包,服务端也不会执行逻辑。
- 仅在服务端取消仍会让客户端代码运行,可能造成不同步。
- 调用
BlockBehaviour#getDestroyProgress,将结果加入内部破坏进度计数器。- 返回 0 到 1 的浮点值,表示每 tick 增加多少破坏进度。
- 更新破坏进度叠加层,即裂纹纹理。
- 进度大于 1.0(已完成,应破坏方块)时,退出“挖掘”,进入“实际破坏”。
“实际破坏”阶段
- 调用
Item#canDestroyBlock,若返回false,表示不应破坏,转入“收尾”。 - 若方块是
GameMasterBlock,调用Player#canUseGameMasterBlocks,判断玩家是否有权破坏仅创造模式可用的方块。为false则转入“收尾”。 - 仅服务端:调用
Player#blockActionRestricted,判断玩家是否被限制破坏方块。为true则转入“收尾”。 - 仅服务端:触发
BlockEvent.BreakEvent,若被取消则转入“收尾”。初始取消状态由以上三个方法决定。 - 调用
Block#playerWillDestroy。 - 仅服务端:调用
IBlockExtension#canHarvestBlock,判断能否收获方块,也就是破坏后产生掉落物。若Player#preventsBlockDrops返回 true,则忽略这个结果。- 仅服务端:只要重写
IBlockExtension#canHarvestBlock时没有省略 super 调用,就会触发PlayerEvent.HarvestCheck。若HarvestCheck#canHarvest返回false,就不调用Block#playerDestroy,从而阻止资源和经验掉落。
- 仅服务端:只要重写
- 仅服务端:调用
Item#mineBlock。 - 调用
IBlockExtension#onDestroyedByPlayer。返回false则转入“收尾”。- 调用
Level#setBlock,以Blocks.AIR.defaultBlockState()或当前位置含有的流体状态为参数,移除方块状态。- 该调用内部调用
Block#onRemove。
- 该调用内部调用
- 如果
IBlockExtension#onDestroyedByPlayer返回true,则调用Block#destroy。
- 调用
- 仅服务端:如果前面的
IBlockExtension#canHarvestBlock和IBlockExtension#onDestroyedByPlayer均返回true,调用Block#playerDestroy。- 仅服务端:调用
Block#dropResources,确定挖掘产生的掉落物和经验。- 仅服务端:触发
BlockDropsEvent,若取消则不掉落任何东西;否则将BlockDropsEvent#getDrops中的每个ItemEntity加入当前世界。若getDroppedExperience大于 0,还会调用Block#popExperience。- 仅服务端:调用
IBlockExtension#getExpDrop,并由EnchantmentHelper#processBlockExperience进一步处理。这是BlockDropsEvent#getDroppedExperience在可能被修改前的初始值。
- 仅服务端:调用
- 仅服务端:触发
- 仅服务端:调用
- 仅服务端:如果挖掘物品在上述过程中损坏,触发
PlayerDestroyItemEvent。
挖掘速度
挖掘速度根据方块硬度、所用工具的速度,以及若干实体属性计算,规则如下:
// This will return the tool's mining speed, or 1 if the held item is either empty, not a tool,
// or not applicable for the block being broken.
float destroySpeed = item.getDestroySpeed(blockState);
// If we have an applicable tool, add the minecraft:mining_efficiency attribute as an additive modifier.
if (destroySpeed > 1) {
destroySpeed += player.getAttributeValue(Attributes.MINING_EFFICIENCY);
}
// Apply effects from haste or 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;
}
// Apply slowness effect.
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;
};
}
// Add the minecraft:block_break_speed attribute as a multiplicative modifier.
destroySpeed *= player.getAttributeValue(Attributes.BLOCK_BREAK_SPEED);
// If the player is underwater, apply the underwater mining speed penalty multiplicatively.
if (player.isEyeInFluid(FluidTags.WATER)) {
destroySpeed *= player.getAttributeValue(Attributes.SUBMERGED_MINING_SPEED);
}
// If the player is trying to break a block in mid-air, make the player mine 5 times slower.
if (!player.onGround()) {
destroySpeed /= 5;
}
destroySpeed = /* The PlayerEvent.BreakSpeed event is fired here, allowing modders to further modify this value. */;
return destroySpeed;
具体实现可参考 Player#getDestroySpeed。
Tick 更新
Tick 是每 1/20 秒(50 毫秒)更新游戏某些部分的机制。方块提供多种更新方法,调用方式各不相同。
服务端 tick 与计划 tick
BlockBehaviour#tick 通过计划 tick 调用。可使用 Level#scheduleTick(BlockPos, Block, int) 安排 tick,其中 int 表示延迟。原版很多地方都会使用,例如大型垂滴叶的倾斜机制,以及各种红石元件。
客户端 tick
Block#animateTick 仅在客户端每帧调用,用于火把粒子生成等客户端行为。
天气 tick
天气更新由 Block#handlePrecipitation 处理,独立于常规 tick。它只在服务端、有降水时以 1/16 的概率调用,例如炼药锅在降雨或降雪时积水。
随机 tick
随机 tick 独立于常规更新。调用方块属性的 BlockBehaviour.Properties#randomTicks() 后,该方块才会参与随机 tick。
每个 tick 会从区块中随机选取一定数量的方块,该数量由 randomTickSpeed 游戏规则确定,默认值为 3。若选中的方块已启用随机 tick,就调用其 BlockBehaviour#randomTick 方法。
随机 tick 用于植物生长、冰雪融化、铜氧化等许多机制。
来源: NeoForged/Documentation(26.1)。中文翻译依据提交
4a680cdd4d15。