方块实体
当方块状态不适合表示数据时,方块实体可以为方块存储额外信息。典型例子是物品栏,其数据组合几乎没有固定上限。方块实体不能移动,并与一个方块绑定,但在其他方面与实体有许多相似之处,因此得名。
提示 如果方块只有数量有限、规模较小的状态(最多几百种),可以考虑使用方块状态。
创建和注册方块实体
与实体相似、与方块不同,BlockEntity 类表示实际的方块实体实例,并非注册到注册表中的单例对象。对应的单例由 BlockEntityType<?> 表示。新增方块实体时,两者都需要。
先创建方块实体类:
public class MyBlockEntity extends BlockEntity {
public MyBlockEntity(BlockPos pos, BlockState state) {
super(type, pos, state);
}
}
这里向父类构造函数传入的 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",
// The block entity type, created using a builder.
() -> BlockEntityType.Builder.of(
// The supplier to use for constructing the block entity instances.
MyBlockEntity::new,
// A vararg of blocks that can have this block entity.
// This assumes the existence of the referenced blocks as DeferredBlock<Block>s.
MyBlocks.MY_BLOCK_1.get(), MyBlocks.MY_BLOCK_2.get()
)
// Build using null; vanilla does some datafixer shenanigans with the parameter that we don't need.
.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 实例,而需要创建子类:
// The important part is implementing the EntityBlock interface and overriding the #newBlockEntity method.
public class MyEntityBlock extends Block implements EntityBlock {
// Constructor deferring to super.
public MyEntityBlock(BlockBehaviour.Properties properties) {
super(properties);
}
// Return a new instance of our block entity here.
@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;数据附件的用法请参阅对应文章。
说明 数据附件主要用于向已有方块实体附加数据,例如原版或其他 Mod 提供的方块实体。对于自己 Mod 中的方块实体,建议直接通过 NBT 保存和加载数据。
分别通过 #loadAdditional 和 #saveAdditional 从 CompoundTag 读取、写入数据。方块实体同步到磁盘或通过网络同步时,会调用这些方法。
public class MyBlockEntity extends BlockEntity {
// This can be any value of any type you want, so long as you can somehow serialize it to NBT.
// We will use an int for the sake of example.
private int value;
public MyBlockEntity(BlockPos pos, BlockState state) {
super(MY_BLOCK_ENTITY.get(), pos, state);
}
// Read values from the passed CompoundTag here.
@Override
public void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
// Will default to 0 if absent. See the NBT article for more information.
this.value = tag.getInt("value");
}
// Save values into the passed CompoundTag here.
@Override
public void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
tag.putInt("value", this.value);
}
}
两个方法都必须调用父类实现,因为父类会处理位置等基本信息。id、x、y、z、NeoForgeData 和 neoforge:attachments 是父类方法保留的标签名,不要自行使用。
与普通字段一样,你可以随时修改这些值。不过,要让游戏保存修改,必须在修改后调用 #setChanged(),将方块实体所在区块标记为脏区块,即需要保存的区块。Minecraft 只会保存标记为脏的区块;如果不调用这个方法,保存时可能会跳过该方块实体。
每刻更新器
方块实体的另一种常见用途是每刻更新,通常会结合存储的数据使用。每刻更新是指在每个游戏刻执行一些代码。要实现它,需要重写 EntityBlock#getTicker,返回一个 BlockEntityTicker。这个函数式接口可以理解为接受世界、位置、方块状态、方块实体四个参数的 consumer:
// Note: The ticker is defined in the block, not the block entity. However, it is good practice to
// keep the ticking logic in the block entity in some way, for example by defining a static #tick method.
public class MyEntityBlock extends Block implements EntityBlock {
// other stuff here
// We use a second method here due to generic conversions
// If extending `BaseEntityBlock`, this method is also available there as a protected static method
private static <E extends BlockEntity, A extends BlockEntity> @Nullable BlockEntityTicker<A> createTickerHelper(
BlockEntityType<A> type, BlockEntityType<E> checkedType, BlockEntityTicker<? super E> ticker
) {
return checkedType == type ? (BlockEntityTicker<A>) ticker : null;
}
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> type) {
// You can return different tickers here, depending on whatever factors you want. A common use case would be
// to return different tickers on the client or server, only tick one side to begin with,
// or only return a ticker for some blockstates (e.g. when using a "my machine is working" blockstate property).
return createTickerHelper(type, MY_BLOCK_ENTITY.get(), MyBlockEntity::tick);
}
}
public class MyBlockEntity extends BlockEntity {
// other stuff here
// The signature of this method matches the signature of the BlockEntityTicker functional interface.
public static void tick(Level level, BlockPos pos, BlockState state, MyBlockEntity blockEntity) {
// Whatever you want to do during ticking.
// For example, you could change a crafting progress value or consume power here.
}
}
注意,#tick 确实会在每个游戏刻执行。应尽量避免在其中进行大量复杂计算,例如只每隔若干刻计算一次,或缓存计算结果。
同步
方块实体逻辑通常运行在服务端,因此需要将必要信息告知客户端。可以在区块加载时同步、在方块更新时同步,或使用自定义数据包。通常应只同步必要信息,避免无谓地占用网络。
区块加载时同步
每次从网络或磁盘读取区块时,都会加载区块并使用这套同步机制。要发送数据,需要重写以下方法:
public class MyBlockEntity extends BlockEntity {
// ...
// Create an update tag here. For block entities with only a few fields, this can just call #saveAdditional.
@Override
public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
CompoundTag tag = new CompoundTag();
saveAdditional(tag, registries);
return tag;
}
// Handle a received update tag here. The default implementation calls #loadAdditional here,
// so you do not need to override this method if you don't plan to do anything beyond that.
@Override
public void handleUpdateTag(CompoundTag tag, HolderLookup.Provider registries) {
super.handleUpdateTag(tag, registries);
}
}
方块更新时同步
方块更新时可以使用这套机制。方块更新必须手动触发,但通常比区块同步处理得更及时。
public class MyBlockEntity extends BlockEntity {
// ...
// Create an update tag here, like above.
@Override
public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
CompoundTag tag = new CompoundTag();
saveAdditional(tag, registries);
return tag;
}
// Return our packet here. This method returning a non-null result tells the game to use this packet for syncing.
@Override
public Packet<ClientGamePacketListener> getUpdatePacket() {
// The packet uses the CompoundTag returned by #getUpdateTag. An alternative overload of #create exists
// that allows you to specify a custom update tag, including the ability to omit data the client might not need.
return ClientboundBlockEntityDataPacket.create(this);
}
// Optionally: Run some custom logic when the packet is received.
// The super/default implementation forwards to #loadAdditional.
@Override
public void onDataPacket(Connection connection, ClientboundBlockEntityDataPacket packet, HolderLookup.Provider registries) {
super.onDataPacket(connection, packet, registries);
// Do whatever you need to do here.
}
}
要实际发送数据包,必须在服务端调用 Level#sendBlockUpdated(BlockPos pos, BlockState oldState, BlockState newState, int flags) 触发更新通知。位置参数应使用方块实体的位置,可通过 BlockEntity#getBlockPos 获取。两个方块状态参数都可以使用方块实体当前位置的状态,通过 BlockEntity#getBlockState 获取。flags 是更新掩码,其含义与 Level#setBlock 中相同。
自定义数据包
使用专用的更新数据包,可以在需要时自行发送同步信息。这种方式最灵活,也最复杂,因为需要设置网络处理逻辑。通过 PacketDistributor#sendToPlayersTrackingChunk 可以向所有正在追踪该方块实体所在区块的玩家发送数据包。详情见网络通信。
注意 必须进行有效性检查,因为消息到达玩家时,
BlockEntity可能已被销毁或替换。还应通过Level#hasChunkAt检查区块是否已加载。
来源: NeoForged/Documentation(1.21.1)。中文翻译依据提交
4a680cdd4d15。