Skip to main content
Version: 1.21.4

方块实体(Block Entities)

方块实体(Block Entities)允许你在 方块 上存储数据,适用于 方块状态 不适合的场景。尤其是当数据的可能选项数量无限时(比如物品栏),方块实体就非常有用。方块实体是静止的,并且绑定在某个方块上,但在其他方面与 实体 有许多相似之处,这也是其名称的由来。

note

如果你的方块只需要有限且数量较小(最多几百种)的状态,你应该优先考虑使用 方块状态

创建与注册方块实体(Creating and Registering Block Entities)

与实体类似,而不像方块,BlockEntity 类代表的是方块实体的实例,而不是 已注册 的单例对象。单例对象则由 BlockEntityType<?> 类表示。要创建一个新的方块实体,我们需要这两者。

让我们先创建自己的方块实体类:

public class MyBlockEntity extends BlockEntity {
public MyBlockEntity(BlockPos pos, BlockState state) {
super(type, pos, state);
}
}

你可能已经注意到,我们把一个未定义的变量 type 传递给了父类构造方法。暂时先不用管这个未定义变量,我们先来看注册流程。

注册 的方式与实体类似。我们需要创建相关的单例类 BlockEntityType<?> 的实例,并将其注册到方块实体类型注册表(Registry)中,如下所示:

public static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITY_TYPES =
DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, ExampleMod.MOD_ID);

public static final Supplier<BlockEntityType<MyBlockEntity>> MY_BLOCK_ENTITY = BLOCK_ENTITY_TYPES.register(
"my_block_entity",
// 方块实体类型
() -> new BlockEntityType<>(
// 用于构造方块实体实例的供应器(supplier)
MyBlockEntity::new,
// 可以拥有该方块实体的所有方块(可变参数)
// 这里假设被引用的方块已经作为 DeferredBlock<Block> 存在
MyBlocks.MY_BLOCK_1.get(), MyBlocks.MY_BLOCK_2.get()
)
);
note

请记得必须将 DeferredRegister 注册到 mod 事件总线 上!

现在我们已经有了方块实体类型,可以用它来替换之前留空的 type 变量了:

public class MyBlockEntity extends BlockEntity {
public MyBlockEntity(BlockPos pos, BlockState state) {
super(MY_BLOCK_ENTITY.get(), pos, state);
}
}
info

之所以采用这种看似复杂的设置流程,是因为 BlockEntityType 需要一个 BlockEntityType.BlockEntitySupplier<T extends BlockEntity>,本质上就是一个 BiFunction<BlockPos, BlockState, T extends BlockEntity>。因此,如果有一个可以直接用 ::new 引用的构造函数会非常方便。不过,我们还需要将构造好的方块实体类型传递给 BlockEntity 的唯一默认构造函数,因此我们需要在不同位置传递引用。

最后,我们需要修改与方块实体关联的方块类。这意味着我们不能直接将方块实体附加在简单的 Block 实例上,而是需要一个子类:

// 关键在于实现 EntityBlock 接口,并重写 #newBlockEntity 方法。
public class MyEntityBlock extends Block implements EntityBlock {
// 构造函数,调用父类构造。
public MyEntityBlock(BlockBehaviour.Properties properties) {
super(properties);
}

// 在这里返回我们自定义的方块实体的新实例。
@Override
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new MyBlockEntity(pos, state);
}
}

接下来,你当然需要在 方块注册 时,将该类作为类型使用:

public static final DeferredBlock<MyEntityBlock> MY_BLOCK_1 =
BLOCKS.register("my_block_1", () -> new MyEntityBlock( /* ... */ ));
public static final DeferredBlock<MyEntityBlock> MY_BLOCK_2 =
BLOCKS.register("my_block_2", () -> new MyEntityBlock( /* ... */ ));

数据存储(Storing Data)

BlockEntity 的主要用途之一就是存储数据。方块实体上的数据存储有两种方式:直接读写 NBT,或使用 数据附加(data attachments)。本节将介绍如何直接读写 NBT;关于数据附加,请参考相关链接文章。

info

数据附加的主要用途,顾名思义,是为现有的方块实体(如原版或其他模组提供的)附加数据。对于你自己模组的方块实体,推荐直接通过 NBT 进行保存和加载。

你可以通过 #loadAdditional#saveAdditional 方法,分别从 CompoundTag 读取和写入数据。当方块实体需要同步到磁盘或通过网络同步时,这些方法会被调用。

public class MyBlockEntity extends BlockEntity {
// 这里可以是你想要的任何类型的值,只要你能以某种方式将其序列化为 NBT 格式即可。
// 本例中我们使用 int 类型。
private int value;

public MyBlockEntity(BlockPos pos, BlockState state) {
super(MY_BLOCK_ENTITY.get(), pos, state);
}

// 在这里从传入的 CompoundTag 读取数据。
@Override
public void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
// 如果没有该键,则默认为 0。更多信息请参阅 NBT 相关文档。
this.value = tag.getInt("value");
}

// 在这里将数据保存到传入的 CompoundTag。
@Override
public void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
tag.putInt("value", this.value);
}
}

在这两个方法中,调用 super 非常重要,因为它会添加诸如位置等基础信息。标签名 idxyzNeoForgeData 以及 neoforge:attachments 已经被父类方法保留,因此你不应自行使用这些名称。

当然,你可能还希望设置其他字段的值,而不仅仅是使用默认值。你可以像操作普通字段一样自由设置这些值。然而,如果你希望游戏能够保存这些更改,必须在修改后调用 #setChanged() 方法,这会将方块实体所在的区块标记为“已脏”(即需要被保存)。如果你没有调用该方法,Minecraft 的存档系统可能会跳过该方块实体,因为它只会保存被标记为已脏的区块。

Tickers

方块实体(Block Entity)另一个非常常见的用途,通常会结合存储的数据使用,就是“滴答”(Ticking)。滴答指的是每个游戏刻(tick)执行某段代码。这可以通过重写 EntityBlock#getTicker 并返回一个 BlockEntityTicker 实现。BlockEntityTicker 本质上是一个带有四个参数(level、position、blockstate 和 block entity)的消费者,示例如下:

// 注意:ticker(计时器)定义在方块(block)中,而不是方块实体(block entity)中。不过,推荐的做法是
// 仍然将 tick(计时)逻辑以某种方式放在方块实体中,例如通过定义一个静态的 #tick 方法。
public class MyEntityBlock extends Block implements EntityBlock {
// 其它内容

@SuppressWarnings("unchecked") // 由于泛型的原因,这里需要进行未经检查的类型转换。
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> type) {
// 你可以根据任意条件返回不同的 ticker。常见的用法包括:
// 在客户端或服务端返回不同的 ticker,只在一侧进行 tick,
// 或者仅在某些 blockstate(如 "我的机器正在工作" 的方块状态属性)下返回 ticker。
return type == MY_BLOCK_ENTITY.get() ? (BlockEntityTicker<T>) MyBlockEntity::tick : null;
}
}

public class MyBlockEntity extends BlockEntity {
// 其它内容

// 这个方法的签名与 BlockEntityTicker 函数式接口的签名一致。
public static void tick(Level level, BlockPos pos, BlockState state, MyBlockEntity blockEntity) {
// 这里可以实现你希望在 tick 时执行的任何逻辑。
// 例如,你可以在这里修改合成进度值或消耗能量。
}
}

请注意,#tick 方法实际上会在每一 tick 被调用。因此,如果可以的话,应尽量避免在这里进行大量复杂计算, 例如通过每 X tick 才计算一次,或者缓存结果来优化性能。

同步(Syncing)

方块实体(block entity)逻辑通常在服务端运行。因此,我们需要告知客户端我们所做的更改。一般有三种方式可以实现同步(sync): 在区块(chunk)加载时、在方块更新时,或通过自定义数据包(custom packet)。通常只有在确有必要时才进行同步,以避免不必要地占用网络带宽。

区块加载时同步(Syncing on Chunk Load)

每当区块从网络或磁盘读取时,就会被加载(因此会用到此方法)。如果你需要在此时发送数据,需要重写以下方法:

public class MyBlockEntity extends BlockEntity {
// ...

// 在这里创建一个更新标签(update tag)。对于只有少数字段的方块实体,可以直接调用 #saveAdditional。
@Override
public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
CompoundTag tag = new CompoundTag();
saveAdditional(tag, registries);
return tag;
}

// 在这里处理收到的更新标签。默认实现会调用 #loadAdditional,
// 因此如果你不需要做额外的处理,无需重写此方法。
@Override
public void handleUpdateTag(CompoundTag tag, HolderLookup.Provider registries) {
super.handleUpdateTag(tag, registries);
}
}

方块更新时同步(Syncing on Block Update)

此方法在每当方块更新发生时被调用。方块更新必须手动触发,但通常比区块(chunk)同步处理得更快。

public class MyBlockEntity extends BlockEntity {
// ...

// 在这里创建一个更新标签(update tag),与上文类似。
@Override
public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
CompoundTag tag = new CompoundTag();
saveAdditional(tag, registries);
return tag;
}

// 在这里返回我们的数据包。此方法返回非空结果时,游戏会使用该数据包进行同步。
@Override
public Packet<ClientGamePacketListener> getUpdatePacket() {
// 该数据包使用 #getUpdateTag 返回的 CompoundTag。#create 还有一个重载版本,
// 允许你指定自定义的更新标签,可以省略客户端不需要的数据。
return ClientboundBlockEntityDataPacket.create(this);
}

// 可选:当数据包被接收时运行一些自定义逻辑。
// 父类/默认实现会转发到 #loadAdditional。
@Override
public void onDataPacket(Connection connection, ClientboundBlockEntityDataPacket packet, HolderLookup.Provider registries) {
super.onDataPacket(connection, packet, registries);
// 在这里执行你需要的任何操作。
}
}

要实际发送数据包,必须在服务端通过调用 Level#sendBlockUpdated(BlockPos pos, BlockState oldState, BlockState newState, int flags) 触发一次更新通知。参数 position 应为方块实体的位置,可通过 BlockEntity#getBlockPos 获取。两个 blockstate 参数都可以填写该方块实体位置的 blockstate,可通过 BlockEntity#getBlockState 获取。最后,flags 参数是一个更新掩码,与 Level#setBlock 中用法相同。

使用自定义数据包(Custom Packet)

通过使用专用的更新数据包,你可以在需要时自行发送数据包。这是最灵活但也最复杂的方式,因为它需要你设置网络处理器(network handler)。你可以通过 PacketDistrubtor#sendToPlayersTrackingChunk 向所有追踪该方块实体的玩家发送数据包。更多信息请参见 网络通信 部分。

caution

请务必做好安全性检查,因为消息到达玩家时,BlockEntity 可能已经被销毁或替换。你还应通过 Level#hasChunkAt 检查区块是否已加载。