方块实体简介

方块实体

BlockEntity 类似于绑定到方块的简化 Entity,用于存储动态数据、执行基于刻的任务以及动态渲染。原版例子包括箱子物品栏、熔炉烧炼逻辑和信标范围效果;Mod 中还有采石机、分拣机、管道和显示器等更复杂的例子。

注册的 BlockEntityType 提供工厂与可附加方块,EntityBlock.newBlockEntity 返回运行时实例;数据改变时调用 setChanged 标记待保存,世界保存时写入。

原创说明图,依据 Forge 1.21.x 官方文档 绘制。

注意:方块实体并非万能方案,错误使用可能导致卡顿。尽可能避免使用。

注册

方块实体动态创建和移除,因此本身不是注册表对象。要创建 BlockEntity,需要继承该类;另外注册一个对象,以方便创建和引用动态对象的类型。对于方块实体,这种类型称为 BlockEntityType。

BlockEntityType 可以像其他注册对象一样注册。可通过 BlockEntityType$Builder#of 创建构建器,它接收两个参数:一个接收 BlockPos 和 BlockState、用于创建关联方块实体实例的 BlockEntityType$BlockEntitySupplier,以及该方块实体可附加到的 Block 可变参数列表。调用 BlockEntityType$Builder#build 构建类型,参数是 DataFixer 中引用该注册对象的类型安全 Type。Mod 可以选择不使用 DataFixer,因此可传入 null。

// 对于某个 DeferredRegister<BlockEntityType<?>> REGISTER
public static final RegistryObject<BlockEntityType<MyBE>> MY_BE = REGISTER.register("mybe", () -> BlockEntityType.Builder.of(MyBE::new, validBlocks).build(null));

// 在 BlockEntity 子类 MyBE 中
public MyBE(BlockPos pos, BlockState state) {
  super(MY_BE.get(), pos, state);
}

创建 BlockEntity

要创建方块实体并附加到方块,必须在 Block 子类上实现 EntityBlock 接口。实现 EntityBlock#newBlockEntity(BlockPos, BlockState),返回方块实体的新实例。

存储数据

要保存数据,重写以下两个方法:

BlockEntity#saveAdditional(CompoundTag tag)
BlockEntity#load(CompoundTag tag)

包含方块实体的 LevelChunk 从标签加载或保存到标签时会调用这些方法,用它们读取和写入方块实体类中的字段。

注意:每当数据变化时,需要调用 BlockEntity#setChanged;否则保存世界时可能跳过包含该方块实体的区块。

重要:务必调用 super 方法!标签名 id、x、y、z、ForgeData 和 ForgeCaps 由父类方法保留。

让方块实体按刻更新

如果需要按刻更新的方块实体,例如跟踪烧炼进度,必须在 EntityBlock 中实现并重写 EntityBlock#getTicker(Level, BlockState, BlockEntityType)。可以根据逻辑端实现不同的 ticker,也可以只实现一个通用 ticker。无论哪种方式,都必须返回 BlockEntityTicker。它是函数式接口,因此可以直接传入代表 ticker 的方法:

// 在某个 Block 子类中
@Nullable
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> type) {
  return type == MyBlockEntityTypes.MYBE.get() ? MyBlockEntity::tick : null;
}

// 在 MyBlockEntity 中
public static void tick(Level level, BlockPos pos, BlockState state, MyBlockEntity blockEntity) {
  // 处理逻辑
}

此方法每刻调用,因此应避免在其中进行复杂计算。如果可能,应每隔 X 刻执行一次更复杂的计算。每秒刻数可能低于 20,但不会更高。

将数据同步到客户端

有三种方式:区块加载时同步、方块更新时同步,以及使用自定义网络消息。

在 LevelChunk 加载时同步

需要重写:

BlockEntity#getUpdateTag()
IForgeBlockEntity#handleUpdateTag(CompoundTag tag)

第一个方法收集应发送给客户端的数据,第二个处理数据。如果方块实体数据不多,可以使用存储数据一节中的方法。

重要:同步过多或无用数据会造成网络拥塞。应只在客户端需要时发送其所需信息,优化网络使用。例如,更新标签通常不需要发送方块实体的物品栏,因为可以通过其 AbstractContainerMenu 同步。

在方块更新时同步

此方法稍复杂,但只需重写两三个方法。简单示例:

@Override
public CompoundTag getUpdateTag() {
  CompoundTag tag = new CompoundTag();
  // 将数据写入标签
  return tag;
}

@Override
public Packet<ClientGamePacketListener> getUpdatePacket() {
  // 将从 #getUpdateTag 获取标签
  return ClientboundBlockEntityDataPacket.create(this);
}

// 可以重写 IForgeBlockEntity#onDataPacket。默认会交给 #load。

静态构造方法 ClientboundBlockEntityDataPacket#create 接收方块实体,以及一个可选的从方块实体获取 CompoundTag 的函数;默认使用 BlockEntity#getUpdateTag。

发送数据包前,必须在服务端发出更新通知:

Level#sendBlockUpdated(BlockPos pos, BlockState oldState, BlockState newState, int flags)

pos 应为方块实体的位置。oldState 和 newState 可以传入该位置的当前 BlockState。flags 是位掩码,应包含 2,以将变化同步到客户端。更多信息及其他标志参见 Block;标志 2 等价于 Block#UPDATE_CLIENTS。

使用自定义网络消息同步

这种方式可能最复杂,但通常最优化,因为可以确保只同步需要的数据。尝试之前应先阅读网络通信,特别是 SimpleImpl。创建自定义网络消息后,可通过 SimpleChannel#send(PacketDistributor$PacketTarget, MSG) 将其发送给加载了该方块实体的所有用户。

警告:务必执行安全检查:消息到达玩家时,方块实体可能已经被销毁或替换!还应使用 Level#hasChunkAt(BlockPos) 检查区块是否已加载。

原文:BlockEntities。