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