跳到主要内容

方块实体

Minecraft 使用名为方块实体(Block Entity)的机制来实现一些一般情况下方块做不到或很难做到的事情,例如

  • 持有更复杂的数据(物品、文本等)
  • 实现像熔炉那样持续不断地行为
  • 拥有奇妙的渲染特效

方块实体类型

Minecraft 中所有的方块实体均属于 BlockEntity 类型。我们首先需要创建我们自己的 BlockEntity 类。

src/main/java/org/teacon/xiaozhong/Xiaozhong.java
public static final class MyMachineEntity extends BlockEntity {
public MyMachineEntity(BlockEntityType<MyMachineEntity> type, BlockPos worldPosition, BlockState blockState) {
super(type, worldPosition, blockState);
}
}

然后我们需要注册一个 BlockEntityType 实例,它代表了「一种特定的方块实体」,指明了创建对应方块实体实例的工厂方法,及哪些方块允许持有这种方块实体。 BlockEntityType 的注册和方块及物品一样,由 Forge 托管,对应的注册系统为 ForgeRegistries.BLOCK_ENTITIES。 使用 DeferredRegister 注册 BlockEntityType

src/main/java/org/teacon/xiaozhong/Xiaozhong.java
// 注册 BlockEntityType
public static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITY_TYPES = DeferredRegister.create(ForgeRegistries.BLOCK_ENTITIES, "xiaozhong");
// 注册 xiaozhong:my_machine
public static final RegistryObject<BlockEntityType<MyMachienEntity>> MY_MACHINE_BLOCK_ENTITY;
MY_MACHINE_BLOCK_ENTITY = BLOCK_ENTITY_TYPES.register("my_machine",
() -> BlockEntityType.Builder.of(MyMachineEntity::new, MY_MACHINE.get()).build(DSL.remainderType()));
// 添加监听器
BLOCK_ENTITY_TYPES.register(FMLJavaModLoadingContext.get().getModEventBus());

注意到 MyMachineEntity::new 此时会报错。刚才提到的「方块实体实例的工厂方法」就是这个,然而原版要求「这个工厂方法只接受两个参数:BlockPosBlockState」。我们向 MyMachineEntity 添加额外构造器来解决此问题:

src/main/java/org/teacon/xiaozhong/Xiaozhong.java
public MyMachineEntity(BlockPos worldPosition, BlockState blockState) {
this(MY_MACHINE_BLOCK_ENTITY.get(), worldPosition, blockState);
}
信息

这个案例也展示了 DeferredRegistry 的一个优势:可以方便开发者打破这种出现循环依赖的情况。

BlockEntity 此时像是一个纯粹的数据容器:我们在我们的 MyMachineEntity 中可以随意添加新的成员字段,并视情况决定是否创建对应的访问器。

src/main/java/org/teacon/xiaozhong/Xiaozhong.java
public static final class MyMachineEntity extends BlockEntity {
public MyMachineEntity(BlockEntityType<MyMachineEntity> type, BlockPos worldPosition, BlockState blockState) {
super(type, worldPosition, blockState);
}

public MyMachineEntity(BlockPos worldPosition, BlockState blockState) {
this(MY_MACHINE_BLOCK_ENTITY.get(), worldPosition, blockState);
}

private int count = 0;
}

方块类

脱离方块的方块实体是不存在的。能容纳方块实体的方块需实现 EntityBlock 接口。实现这一接口需实现 newBlockEntity 方法。

Minecraft 原版为我们提供了 BaseEntityBlock 类方便我们实现:

src/main/java/org/teacon/xiaozhong/Xiaozhong.java
public static final DeferredRegister<Block> BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, "xiaozhong");

public static final RegistryObject<Block> MY_MACHINE;
MY_MACHINE = BLOCKS.register("my_machine",
() -> new MyMachine(BlockBehaviour.Properties.of(Material.METAL)));

BLOCKS.register(FMLJavaModLoadingContext.get().getModEventBus());

public static final class MyMachine extends BaseEntityBlock {
public MyMachine(Properties props) {
super(props);
}

@Nonnull
@Override
public RenderShape getRenderShape(@Nonnull BlockState state) {
// 注意这一方法的返回值被 BaseEntityBlock 覆盖成了 INVISIBLE,
// 这代表其无法使用 Minecraft 自带的方块模型的方式渲染。
// 为保证正常渲染应将这一方法的返回值覆盖回来。
return RenderShape.MODEL;
}

@Override
public BlockEntity newBlockEntity(@Nonnull BlockPos pos, @Nonnull BlockState state) {
// 这里返回我们刚刚定义的方块实体
return new MyMachineEntity(pos, state);
}
}

方块实体刻

默认情况下,方块实体并不具备跟随游戏刻刷新(亦即方块实体刻)的能力,若要获得此能力,方块实体所在的那个方块需要明确声明一个所谓的「Ticker」,亦即 BlockEntityTicker<?>。这通过覆盖 EntityBlockgetTicker 方法实现。

此外,你可以根据 Level 是在逻辑服务器上还是逻辑客户端上来返回不同的 Ticker。通常我们在客户端不需要特别的逻辑,因此我们会在 level.isClientSide() 返回 true 时,令 getTicker 返回 null

首先在 MyMachineEntity 中创建静态方法 tick

src/main/java/org/teacon/xiaozhong/Xiaozhong.java
private int count = 0;

public static void tick(Level level, BlockPos pos, BlockState state, MyMachineEntity entity) {
// 计数。每一个游戏刻中,游戏都会调用一次 tick 方法,所以我们只需要在这里让 count 自增 1 即可。
entity.count += 1;
// 当计数超过 100 时……
if (entity.count > 100) {
// 首先重置计数
entity.count = 0;
// 检查 Level 实例是否存在,以及是不是在服务器上
if (level != null && !level.isClientSide()) {
// 然后寻找半径 5 方块以内的玩家,最后的 false 表示无视创造或观察模式的玩家
var player = level.getNearestPlayer(pos.getX(), pos.getY(), pos.getZ(), 5.0, false);
// 如果找到了这样的玩家……
if (player != null) {
// 向这名玩家的聊天栏发送问候语。
player.sendSystemMessage(Component.translatable("chat.xiaozhong.welcome"));
}
}
}
}

然后在我们的方块类中,覆写 getTicker 方法:

src/main/java/org/teacon/xiaozhong/Xiaozhong.java
@Nullable
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> blockEntityType) {
// BaseEntityBlock 提供了 createTickerHelper 帮助你生成 BlockEntityTicker 的实例,这里直接调用并传入 tick 方法引用即可。
return level.isClientSide() ? null : createTickerHelper(blockEntityType, MY_MACHINE_BLOCK_ENTITY.get(), MyMachineEntity::tick);
}

以下演示了一个最基础方块实体的实现及的注册流程——没有相应的语言文件,没有模型,什么都没有,甚至没有为该方块注册对应的物品:

src/main/java/org/teacon/xiaozhong/Xiaozhong.java
package org.teacon.xiaozhong;

import com.mojang.datafixers.DSL;
import net.minecraft.Util;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.TranslatableComponent;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.BaseEntityBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.RenderShape;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.Material;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.RegistryObject;

import javax.annotation.Nonnull;

@Mod("xiaozhong")
public class Xiaozhong {
public static final DeferredRegister<Block> BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, "xiaozhong");
public static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITY_TYPES = DeferredRegister.create(ForgeRegistries.BLOCK_ENTITY_TYPES, "xiaozhong");

public static final RegistryObject<Block> MY_MACHINE;
public static final RegistryObject<BlockEntityType<MyMachineEntity>> MY_MACHINE_BLOCK_ENTITY;

static {
MY_MACHINE = BLOCKS.register("my_machine",
() -> new MyMachine(BlockBehaviour.Properties.of(Material.METAL)));
MY_MACHINE_BLOCK_ENTITY = BLOCK_ENTITY_TYPES.register("my_machine",
() -> BlockEntityType.Builder.of(MyMachineEntity::new, MY_MACHINE.get()).build(DSL.remainderType()));
}

public Xiaozhong() {
BLOCKS.register(FMLJavaModLoadingContext.get().getModEventBus());
BLOCK_ENTITY_TYPES.register(FMLJavaModLoadingContext.get().getModEventBus());
}

public static final class MyMachine extends BaseEntityBlock {
public MyMachine(Properties props) {
super(props);
}

@Nonnull
@Override
public RenderShape getRenderShape(@Nonnull BlockState state) {
return RenderShape.MODEL;
}

@Override
public BlockEntity newBlockEntity(@Nonnull BlockPos pos, @Nonnull BlockState state) {
return new MyMachineEntity(pos, state);
}

@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, @Nonnull BlockState state, @Nonnull BlockEntityType<T> blockEntityType) {
return level.isClientSide() ? null : createTickerHelper(blockEntityType, MY_MACHINE_BLOCK_ENTITY.get(), MyMachineEntity::tick);
}
}

public static final class MyMachineEntity extends BlockEntity {
public MyMachineEntity(BlockEntityType<MyMachineEntity> type, BlockPos worldPosition, BlockState blockState) {
super(type, worldPosition, blockState);
}

public MyMachineEntity(BlockPos worldPosition, BlockState blockState) {
this(MY_MACHINE_BLOCK_ENTITY.get(), worldPosition, blockState);
}

private int count = 0;

public static void tick(Level level, BlockPos pos, BlockState state, MyMachineEntity entity) {
entity.count += 1;
if (entity.count > 100) {
entity.count = 0;
if (level != null && !level.isClientSide()) {
var player = level.getNearestPlayer(pos.getX(), pos.getY(), pos.getZ(), 5.0, false);
if (player != null) {
player.sendSystemMessage(Component.translatable("chat.xiaozhong.welcome"));
}
}
}
}
}
}

这个方块实体的效果是,每 100 个游戏刻检查是否有玩家距离该方块实体距离不到 5 方块,若有,随机抽一位向其打招呼。

打开游戏,执行 /setblock ~ ~-1 ~ xiaozhong:my_machine 看看效果吧~