方块

方块

方块是 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。