====== 添加方块实体 ====== ===== 介绍 ===== 方块实体主要用于在方块内存储数据。创建之前,您需要一个[[zh_cn:tutorial:blocks|方块]]。本教程将介绍BlockEntity类的创建及其注册。 ===== 创建一个方块实体 ===== 最简单的方块实体仅继承 ''BlockEntity'',并使用默认构造函数。 这是完全有效的,但不会给予方块任何特殊功能。 public class DemoBlockEntity extends BlockEntity { public DemoBlockEntity(BlockPos pos, BlockState state) { super(ExampleMod.DEMO_BLOCK_ENTITY, pos, state); } } 请确保这个构造方法只接收这两个参数,否则我们后面写的方法引用 ''DemoBlockEntity::new'' 将会无效。这个 ''ExampleMod.DEMO_BLOCK_ENTITY'' 字段稍后再写。 您可以简单地向此准系统类添加变量,或实现诸如 ''Tickable'' 和 ''Inventory'' 之类的接口以添加更多功能。''Tickable'' 提供了一个单独的 ''tick()'' 方法,该方法将会在世界加载的每一刻调用一次。而 ''Inventory'' 则允许您的方块实体自动进行交互,例如漏斗——稍后可能会有专门针对此接口的单独教程。 ===== 注册你的方块实体 ===== 一旦创建了 ''BlockEntity'' 类,您将需要对其进行注册以使其起作用。 此过程的第一步是创建一个 ''BlockEntityType'',它将 ''Block'' 和 ''BlockEntity'' 链接在一起。假设您的 ''Block'' 已创建并保存到了静态常量字段 ''DEMO_BLOCK'' 中,则将在下面的行中创建匹配的 ''BlockEntityType''。在本教程中,方块实体的ID是 ''tutorial:demo_block_entity''。 ''BlockEntityType'' 应在类的初始化或 ''onInitialize'' 方法中注册,以确保它在正确的时候注册。 public static final BlockEntityType DEMO_BLOCK_ENTITY = Registry.register( Registries.BLOCK_ENTITY_TYPE, new Identifier("tutorial", "demo_block_entity"), FabricBlockEntityTypeBuilder.create(DemoBlockEntity::new, DEMO_BLOCK).build() ); 这个方块实体类型定义了只有 ''DEMO_BLOCK'' 可以拥有这个方块实体类型。如果你想要让方块实体类型支持更多方块,只需要将其添加到 ''FabricBlockEntityTypeBuilder.create'' 的参数中即可。如果方法引用 ''DemoBlockEntity::new'' 无法解析,检查 ''DemoBlockEntity'' 的构造方法的参数是否正确。 ==== 将方块实体连接到方块 ==== 一旦创建并注册了 ''BlockEntityType'',就可以在 ''Block'' 类中简单地实现 ''BlockEntityProvider''。每次放置方块,就会产生对应的方块实体。 public class DemoBlock extends Block implements BlockEntityProvider { [...] @Override public BlockEntity createBlockEntity(BlockPos pos, BlockState state) { return new DemoBlockEntity(pos, state); } } ===== 序列化数据 ===== 如果您想在 ''BlockEntity'' 存储任何数据,必须保存和加载它,否则数据只会在''BlockEntity''被加载时保持,而且每次你重进游戏时会重置。幸运的是,保存和加载非常简单——只需要覆盖 ''writeNbt()'' 和 ''readNbt()'' 即可。 ''writeNbt()'' 将会修改其参数 ''nbt'' 的内容,这个 ''nbt'' 包含了方块实体中的所有数据。该方法通常不会修改方块实体本身。方块实体数据将会存储在磁盘中,并且如果您需要将 ''BlockEntity'' 数据与客户端同步,则会通过封包发送。调用 ''super.writeNbt()'' 非常重要,因为方块实体的坐标及其方块实体类型 id 保存到 nbt 中。否则,您尝试保存的所有其他数据都将丢失,因为它与位置和 ''BlockEntityType'' 不相关。 知道了这一点,下面的示例演示了如何将 ''BlockEntity'' 中的整数保存到标签中。在此示例中,整数保存在键 ''number'' 下——您可以将其替换为任何字符串,但是标签中的每个键只能有一个项,并且需要记住该键以便以后读取数据。 public class DemoBlockEntity extends BlockEntity { // 储存数字的当前值 private int number = 7; public DemoBlockEntity(BlockPos pos, BlockState state) { super(ExampleMod.DEMO_BLOCK_ENTITY, pos, state); } // 序列化方块实体 @Override public void writeNbt(NbtCompound nbt) { // Save the current value of the number to the tag nbt.putInt("number", number); super.writeNbt(nbt) } } 为了以后读取数据,您还需要覆盖 ''readNBT''。此方法与 ''writeNBT'' 相反——不会将数据保存到 ''NBTCompound'',而是您已经有了之前保存的 nbt 数据,使您可以检索所需的任何数据。该方法会修改方块实体本身,不会修改这个 ''nbt'' 参数。与 ''writeNbt'' 一样,必须调用 ''super.readNbt'',并且您将需要使用相同的键来检索保存的数据。要检索我们之前保存的数字,请参见下面的示例。 // 反序列化方块实体 @Override public void readNbt(NbtCompound nbt) { super.readNbt(nbt); number = nbt.getInt("number"); } 一旦实现了 ''writeNbt'' 和 ''readNbt'' 方法,您只需要确保在合适的时候调用即可。每当您的 ''BlockEntity'' 数据发生更改并需要保存时,请调用 ''markDirty()''。这会将方块所在的区块标记为dirty,在世界下次保存时强制调用 ''writeNbt'' 方法。原则上,只要修改了 ''BlockEntity'' 类中的任何一个自定义变量,就只需要简单调用 ''markDirty'',否则当你退出并重进世界后,这个方块实体依然会是没有修改过的。 ===== 将服务器数据同步至客户端 ===== 数据通常是在服务器世界读取的。大多数数据都是客户端不需要知道的,例如客户端并不需要知道箱子和熔炉里面有什么,除非打开它。但对于某些方块实体,例如告示牌和旗帜,你需要将所有或者部分数据告知客户端,比如用于渲染。 对于 1.17.1 及以下版本,请实现 Fabric API 中的 ''BlockEntityClientSerializable''。此接口提供了 ''fromClientTag'' 和 ''toClientTag'' 方法,其作用与前面讨论的 ''readNbt'' 和 ''writeNbt'' 方法基本相同,只是专门用于发送和接收客户端上的数据。你可以简单地在 ''fromClientTag'' 和 ''toClientTag'' 两个方法中调用 ''readNbt'' 和 ''writeNbt''。 对于 1.18 及以上版本,请覆盖 ''toUpdatePacket'' 和 ''toInitialChunkDataNbt'': @Nullable @Override public Packet toUpdatePacket() { return BlockEntityUpdateS2CPacket.create(this); } @Override public NbtCompound toInitialChunkDataNbt() { return createNbt(); } **警告**: 需要调用 ''world.updateListeners(pos, state, state, Block.NOTIFY_LISTENERS);'' 来触发数据的同步,否则客户端不会知道方块实体已经改变。 ===== 方块实体刻 ===== 1.17 添加了静态的刻,然后你就可以实现 ''Tickable'' 接口。对于需要计划刻的方块,你只需要使用 ''Block'' 中的 ''getTicker'',链接回到 ''Block Entity''。参考下面关于刻的一个常见的实现。 在你的 ''Block'' 类中: public class DemoBlock extends BlockWithEntity { [...] @Override public BlockRenderType getRenderType(BlockState state) { // 由于继承了BlockWithEntity,这个默认为INVISIBLE,所以我们需要更改它! return BlockRenderType.MODEL; } @Override public BlockEntityTicker getTicker(World world, BlockState state, BlockEntityType type) { return checkType(type, ExampleMod.DEMO_BLOCK_ENTITY, (world1, pos, state1, be) -> DemoBlockEntity.tick(world1, pos, state1, be)); } 在你的 ''BlockEntity'' 类中: public class DemoBlockEntity extends BlockEntity { public DemoBlockEntity(BlockPos pos, BlockState state) { super(ExampleMod.DEMO_BLOCK_ENTITY, pos, state); } public static void tick(World world, BlockPos pos, BlockState state, DemoBlockEntity be) { [...] } } ===== 概览 ===== 现在,您应该拥有自己的 ''BlockEntity'',可以以各种方式扩展以适应您的需求。 您注册了 ''BlockEntityType'',并用它来将 ''Block'' 和 ''BlockEntity'' 类连接在一起。 然后,您在 ''Block'' 类中实现了 ''BlockEntityProvider'',并使用该接口提供了新 ''BlockEntity'' 的实例。 最后,您学习了如何将数据保存到 ''BlockEntity'' 中,以及如何检索以备后用。