方块实体
方块实体(Block Entities)
方块实体允许在方块状态不适合的情况下存储数据。这在数据选项无限的情况下尤其有用,例如物品栏。方块实体是静止的并绑定到一个方块,但在其他方面与实体有许多相似之处,因此得名。
注意:如果你的方块有有限且合理数量的可能状态(最多几百个),你可能需要考虑使用方块状态。
创建和注册方块实体
与实体不同,BlockEntity
类表示方块实体实例,而不是注册的单例对象。单例通过BlockEntityType<?>
类表示。我们需要两者来创建一个新的方块实体。
首先,创建我们的方块实体类:
public class MyBlockEntity extends BlockEntity {
public MyBlockEntity(BlockPos pos, BlockState state) {
super(type, pos, state);
}
}
你可能已经注意到,我们向super
构造函数传递了一个未定义的变量type
。让我们暂时保留这个未定义的变量,转而进行注册。
注册的方式与实体类似。我们创建关联的单例类BlockEntityType<?>
的实例,并将其注册到方块实体类型注册表中,如下所示:
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",
// 方块实体类型,使用构建器创建。
() -> BlockEntityType.Builder.of(
// 用于构造方块实体实例的供应商。
MyBlockEntity::new,
// 可以具有此方块实体的方块的变长参数。
// 假设引用的方块是 DeferredBlock<Block>。
MyBlocks.MY_BLOCK_1.get(), MyBlocks.MY_BLOCK_2.get()
)
// 使用 null 构建;原版对参数进行了一些数据修复操作,我们不需要。
.build(null)
);
现在我们有了方块实体类型,我们可以用它替换之前留下的type
变量:
public class MyBlockEntity extends BlockEntity {
public MyBlockEntity(BlockPos pos, BlockState state) {
super(MY_BLOCK_ENTITY.get(), pos, state);
}
}
注意:这种设置过程之所以令人困惑,是因为BlockEntityType.Builder#of
期望一个BlockEntityType.BlockEntitySupplier<T extends BlockEntity>
,这基本上是一个BiFunction<BlockPos, BlockState, T extends BlockEntity>
。因此,能够直接引用构造函数::new
是非常有益的。然而,我们还需要将构造的方块实体类型传递给BlockEntity
的默认且唯一的构造函数,因此我们需要稍微传递一些引用。
最后,我们需要修改与方块实体关联的方块类。这意味着我们将无法将方块实体附加到Block
的简单实例上,而是需要一个子类:
// 重要的部分是实现 EntityBlock 接口并重写 #newBlockEntity 方法。
public class MyEntityBlock extends Block implements EntityBlock {
// 构造函数委托给 super。
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( /* ... */ ));
存储数据
BlockEntity
的主要目的之一是存储数据。方块实体上的数据存储可以通过两种方式实现:直接读写NBT,或使用数据附件。本节将介绍直接读写NBT;有关数据附件,请参阅相关文章。
注意:数据附件的主要目的是,顾名思义,将数据附加到现有的方块实体上,例如原版或其他模组提供的方块实体。对于你自己的模组的方块实体,直接保存和加载NBT是首选。
可以使用#loadAdditional
和#saveAdditional
方法分别从CompoundTag
读取和写入数据。这些方法在方块实体同步到磁盘或网络时调用。
public class MyBlockEntity extends BlockEntity {
// 这可以是任何类型的任何值,只要你能以某种方式将其序列化为NBT。
// 我们将使用一个整数作为示例。
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
很重要,因为它添加了基本信息,例如位置。标签名称id
、x
、y
、z
、NeoForgeData
和neoforge:attachments
由super
方法保留,因此你不应自己使用它们。
当然,你会想要设置其他值,而不仅仅是使用默认值。你可以像任何其他字段一样自由地这样做。但是,如果你希望游戏保存这些更改,你必须在之后调用#setChanged()
,这将标记方块实体的区块为脏(需要保存)。如果你不调用此方法,方块实体可能会在保存过程中被跳过,因为 Minecraft 的保存系统只保存标记为脏的区块。
刻(Tickers)
方块实体的另一个非常常见的用途,通常与一些存储的数据结合使用,是刻(ticking)。刻意味着每游戏刻执行一些代码。这是通过重写EntityBlock#getTicker
并返回一个BlockEntityTicker
来实现的,BlockEntityTicker
基本上是一个具有四个参数(level、position、blockstate 和 block entity)的消费者,如下所示:
// 注意:刻是在方块中定义的,而不是方块实体中。然而,将刻逻辑以某种方式保留在方块实体中是一个好习惯,例如通过定义一个静态的 #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) {
// 你可以根据你想要的任何因素返回不同的刻。一个常见的用例是
// 在客户端或服务器上返回不同的刻,只在一侧开始刻,
// 或仅为某些方块状态返回刻(例如,当使用“我的机器正在工作”方块状态属性时)。
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
方法实际上每刻都会被调用。因此,如果可以,你应该避免在这里进行大量复杂的计算,例如每 X 刻计算一次,或缓存结果。
同步
方块实体逻辑通常在服务器上运行。因此,我们需要告诉客户端我们在做什么。有三种方法可以实现这一点:在区块加载时、在方块更新时,或使用自定义数据包。你通常只应在必要时同步信息,以免不必要地堵塞网络。
在区块加载时同步
每次从网络或磁盘读取区块时,都会加载区块(并因此使用此方法)。要在此处发送你的数据,你需要重写以下方法:
public class MyBlockEntity extends BlockEntity {
// ...
// 在这里创建一个更新标签。对于只有几个字段的方块实体,这可以只调用 #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);
}
}
在方块更新时同步
此方法在方块更新发生时使用。方块更新必须手动触发,但通常比区块同步处理得更快。
public class MyBlockEntity extends BlockEntity {
// ...
// 在这里创建一个更新标签,如上所述。
@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);
}
// 可选:在接收到数据包时运行一些自定义逻辑。
// super/默认实现转发到 #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)
触发更新通知。位置应该是方块实体的位置,可通过BlockEntity#getBlockPos
获取。两个方块状态参数可以是方块实体位置的方块状态,可通过BlockEntity#getBlockState
获取。最后,flags
参数是更新掩码,如Level#setBlock
中使用的。
使用自定义数据包
通过使用专用的更新数据包,你可以在需要时自己发送数据包。这是最灵活但也是最复杂的变体,因为它需要设置网络处理器。你可以通过使用PacketDistrubtor#sendToPlayersTrackingChunk
向所有跟踪方块实体的玩家发送数据包。有关更多信息,请参阅网络部分。
警告:进行安全检查很重要,因为当消息到达玩家时,BlockEntity
可能已经被销毁/替换。你还应通过Level#hasChunkAt
检查区块是否已加载。