方块实体

方块实体

当方块状态不适合表示数据时,方块实体可以为方块存储额外信息。典型例子是物品栏,其数据组合几乎没有固定上限。方块实体不能移动,并与一个方块绑定,但在其他方面与实体有许多相似之处,因此得名。

提示 如果方块只有数量有限、规模较小的状态(最多几百种),可以考虑使用方块状态。

创建和注册方块实体

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