方块实体
当方块状态不适合存储数据时,可以使用方块实体在方块上保存数据,尤其是物品栏这类取值可能性无法用有限状态列举的数据。方块实体固定在一个方块位置,不会移动,但其他方面与实体有许多相似之处,因此得名。
提示 如果方块可能的状态数量有限且较少,最多只有几百种,可以考虑改用方块状态。
创建和注册方块实体
与实体相似、与方块不同,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.
() -> new BlockEntityType<>(
// The supplier to use for constructing the block entity instances.
MyBlockEntity::new,
// An optional value that, when true, only allows players with OP permissions
// to load NBT data (e.g. placing a block item)
false,
// 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()
)
);
提示 记得将
DeferredRegister注册到 Mod 事件总线!
有了方块实体类型,就可以用它替换前面保留的 type 变量:
public class MyBlockEntity extends BlockEntity {
public MyBlockEntity(BlockPos pos, BlockState state) {
super(MY_BLOCK_ENTITY.get(), pos, state);
}
}
说明 这个设置过程看起来有些绕,是因为
BlockEntityType需要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 的主要用途之一是保存数据。方块实体有两种数据存储方式:读写值输入输出,或使用数据附件。本节介绍值输入输出;数据附件请参阅链接文档。
说明 数据附件主要用于向已有方块实体附加数据,例如原版或其他 Mod 提供的方块实体。自己的 Mod 所定义的方块实体,优先直接通过值输入输出保存和加载数据。
分别通过 #loadAdditional 和 #saveAdditional 方法,可以从值输入输出读取数据或向其中写入数据。方块实体保存到磁盘或经网络同步时,会调用这些方法。
public class MyBlockEntity extends BlockEntity {
// This can be any value of any type you want, so long as you can somehow serialize it to the value I/O.
// 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 ValueInput here.
@Override
public void loadAdditional(ValueInput input) {
super.loadAdditional(input);
// Will default to 0 if absent. See the ValueIO article for more information.
this.value = input.getIntOr("value", 0);
}
// Save values into the passed ValueOutput here.
@Override
public void saveAdditional(ValueOutput output) {
super.saveAdditional(output);
output.putInt("value", this.value);
}
}
两个方法中都必须调用父类实现,因为它会添加位置等基本信息。id、x、y、z、NeoForgeData 和 neoforge:attachments 这些名称由父类方法保留,自己的数据不应使用它们。
当然,你可以像操作普通字段一样修改这些值,而不是始终使用默认值。但如果希望游戏保存修改,之后必须调用 #setChanged(),将方块实体所在区块标记为脏,也就是需要保存。不调用该方法时,保存过程可能跳过这个方块实体,因为 Minecraft 的保存系统只保存标记为脏的区块。
移除方块实体
有时需要在方块实体被移除时导出所存数据,例如玩家破坏方块时掉落其物品栏内容。此类逻辑应在 BlockEntity#preRemoveSideEffects 中处理。默认情况下,如果方块实体实现了 Container,就会掉落所存物品。
public class MyBlockEntity extends BlockEntity {
@Override
public void preRemoveSideEffects(BlockPos pos, BlockState state) {
super.preRemoveSideEffects(pos, state);
// Perform any remaining export logic on removal here.
}
}
警告 带有
Block.UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS标志移除方块时,不会调用这个方法。克隆命令和严格模式下放置结构时通常会使用这一标志。
如果相邻方块需要知道方块实体被破坏,例如物品栏通过比较器输出红石信号,应在方块类中重写 BlockBehaviour#affectNeighborsAfterRemoval。输出红石信号的方块实体通常会在这里调用 Containers#updateNeighboursAfterDestroy。
public class MyEntityBlock extends Block implements EntityBlock {
@Override
protected void affectNeighborsAfterRemoval(BlockState state, ServerLevel level, BlockPos pos, boolean movedByPiston) {
// Handle whatever logic you want to execute on the surrounding neighbors
Containers.updateNeighboursAfterDestroy(state, level, pos);
}
}
刻更新器
另一个常见用途是执行刻更新,通常会与保存的数据配合使用。刻更新是指每个游戏刻执行一些代码。重写 EntityBlock#getTicker 并返回 BlockEntityTicker 即可实现;后者基本相当于接受世界、位置、方块状态和方块实体四个参数的消费者:
// 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 #saveWithoutMetadata.
@Override
public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
return this.saveWithoutMetadata(registries);
}
// Handle a received update tag here. The default implementation calls #loadWithComponents here,
// so you do not need to override this method if you don't plan to do anything beyond that.
@Override
public void handleUpdateTag(ValueInput input) {
super.handleUpdateTag(input);
}
}
方块更新时同步
每次发生方块更新时都会使用这种方式。方块更新必须手动触发,但通常比区块同步处理得更快。
public class MyBlockEntity extends BlockEntity {
// ...
// Create an update tag here, like above.
@Override
public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
return this.saveWithoutMetadata(registries);
}
// 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 #loadWithComponents.
@Override
public void onDataPacket(Connection connection, ValueInput input) {
super.onDataPacket(connection, input);
// 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(26.1)。中文翻译依据提交
4a680cdd4d15。