方块实体

方块实体

当方块状态不适合存储数据时,可以使用方块实体在方块上保存数据,尤其是物品栏这类取值可能性无法用有限状态列举的数据。方块实体固定在一个方块位置,不会移动,但其他方面与实体有许多相似之处,因此得名。

提示 如果方块可能的状态数量有限且较少,最多只有几百种,可以考虑改用方块状态。

创建和注册方块实体

与实体相似、与方块不同,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。