This is an old revision of the document!
Table of Contents
添加方块实体
介绍
方块实体主要用于在方块内存储数据。创建之前,您需要一个方块。本教程将介绍 BlockEntity 类的创建及其注册。
创建方块实体
最简单的方块实体仅继承 BlockEntity
,并使用默认构造函数。这是完全有效的,但不会给予方块任何特殊功能。
- DemoBlockEntity.java
public class DemoBlockEntity extends BlockEntity { public DemoBlockEntity(BlockPos pos, BlockState state) { super(TutorialBlockEntityTypes.DEMO_BLOCK, pos, state); } }
请确保这个构造方法只接收这两个参数,否则我们后面写的方法引用 DemoBlockEntity::new
将会无效。这个 TutorialBlockEntityTypes.DEMO_BLOCK
字段稍后再写。
方块实体支持一系列方法以支持一些功能,例如与 NBT 之间的序列化和反序列化、提供物品栏等。本教程提供方块实体功能的最常见一些实现。
注册方块和方块实体
一旦创建了 BlockEntity
类,就需要注册它才起作用。这个过程的第一步是在我们的 TutorialBlockEntities
类中创建 BlockEntityType
对象,将 Block
和 BlockEntity
连接在一起。创建 Block
并保存到 TutorialBlocks
类的静态常量字段 DEMO_BLOCK
中。在本教程中,方块实体的 ID 是 tutorial:demo_block
。
BlockEntityType
应在类的初始化或 onInitialize
方法中注册,以确保在正确的时候注册。在这个例子中,我们在单独的类中注册(见 blocks)
- TutorialBlocks.java
public final class TutorialBlocks { [...] public static final DemoBlock DEMO_BLOCK = register("demo_block", new DemoBlock(AbstractBlock.Settings.create())); [...] }
- TutorialBlockEntityTypes.java
public class TutorialBlockEntityTypes { public static <T extends BlockEntityType<?>> T register(String path, T blockEntityType) { return Registry.register(Registries.BLOCK_ENTITY_TYPE, Identifier.of("tutorial", path), blockEntityType); } public static final BlockEntityType<DemoBlockEntity> DEMO_BLOCK = register( "demo_block", BlockEntityType.Builder.create(DemoBlockEntity::new, TutorialBlocks.DEMO_BLOCK).build() ); public static void initialize() { } }
记得要在 ModInitializer
里面引用 initialize
方法:
- ExampleMod.java
public class ExampleMod implements ModInitializer { [...] @Override public void onInitialize() { [...] TutorialBlockEntityTypes.initialize(); } }
对于旧版本,如果无法访问 BlockEntityType.Builder.create
,尝试 FabricBlockEntityTypeBuilder.create
。
这个方块实体类型定义了只有 TutorialBlocks.DEMO_BLOCK
可以拥有这个方块实体类型。如果你想要让方块实体类型支持更多方块,只需要将其添加到 BlockEntityType.Builder.create
的参数中即可。如果方法引用 DemoBlockEntity::new
无法解析,检查 DemoBlockEntity
的构造方法的参数是否正确。
将方块实体连接到方块
一旦创建并注册了 BlockEntityType
,就需要与之关联的方块。你可以继承 BlockWithEntity
,或者简单地实现 BlockEntityProvider
并覆盖 createBlockEntity
。每次放置方块,就会产生对应的方块实体。
- DemoBlock.java
public class DemoBlock extends BlockWithEntity { public DemoBlock(Settings settings) { super(settings); } @Override protected MapCodec<? extends DemoBlock> getCodec() { return createCodec(DemoBlock::new); } @Override public BlockEntity createBlockEntity(BlockPos pos, BlockState state) { return new DemoBlockEntity(pos, state); } @Override protected BlockRenderType getRenderType(BlockState state) { return BlockRenderType.MODEL; } }
覆盖getRenderType
为因为 BlockWithEntity
会默认让它不可见。
序列化数据
如果您想在 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, RegistryWrapper.WrapperLookup wrapper) { // 将数字的当前值保存到 nbt nbt.putInt("number", number); super.writeNbt(nbt, wrapper); } }
为了以后读取数据,您还需要覆盖 readNBT
。此方法与 writeNBT
相反——不会将数据保存到 NBTCompound
,而是您已经有了之前保存的 nbt 数据,使您可以检索所需的任何数据。该方法会修改方块实体本身,不会修改这个 nbt
参数。与 writeNbt
一样,必须调用 super.readNbt
,并且您将需要使用相同的键来检索保存的数据。要检索我们之前保存的数字,请参见下面的示例。
// 反序列化方块实体 @Override public void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup wrapper) { super.readNbt(nbt, wrapper); 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<ClientPlayPacketListener> toUpdatePacket() { return BlockEntityUpdateS2CPacket.create(this); } @Override public NbtCompound toInitialChunkDataNbt() { return createNbt(); }
警告: 需要调用 world.updateListeners(pos, state, state, Block.NOTIFY_LISTENERS);
来触发数据的同步,否则客户端不会知道方块实体已经改变。
方块实体刻
刻是指方块在每一刻(每 1/20 秒)运行些什么。要让方块有刻,通常需要使用 Block
中的 getTicker
,链接回到 BlockEntity
中的静态方法 tick
。参考下面关于刻的常见的实现。
在你的 Block
类中:
- DemoBlock.java
public class DemoBlock extends BlockWithEntity { [...] @Override public <T extends BlockEntity> BlockEntityTicker<T> getTicker(World world, BlockState state, BlockEntityType<T> type) { // 如果只需要在服务器上有刻,确保检查 world.isClient return validateTicker(type, ExampleMod.DEMO_BLOCK_ENTITY, DemoBlockEntity::tick); }
在你的 BlockEntity
类中:
- DemoBlockEntity.java
public class DemoBlockEntity extends BlockEntity { [...] public static void tick(World world, BlockPos pos, BlockState state, DemoBlockEntity be) { [...] } }
下一步
现在,您应该拥有自己的 BlockEntity
,可以以各种方式扩展以适应您的需求。注册了 BlockEntityType
,并用它来将 Block
和 BlockEntity
类连接在一起。然后,继承了 BlockWithEntity
并使用了接口 BlockEntityProvider
以提供 BlockEntity
的新实例。
你也学习了如何为它添加课刻。下一步,可以尝试对方块实体进行一些复杂的操作,例如: