Skip to content

方块实体

字数
2627 字
阅读时间
11 分钟

方块实体(Block Entities)

方块实体允许在方块状态不适合的情况下存储数据。这在数据选项无限的情况下尤其有用,例如物品栏。方块实体是静止的并绑定到一个方块,但在其他方面与实体有许多相似之处,因此得名。

注意:如果你的方块有有限且合理数量的可能状态(最多几百个),你可能需要考虑使用方块状态。


创建和注册方块实体

与实体不同,BlockEntity​类表示方块实体实例,而不是注册的单例对象。单例通过BlockEntityType<?>​类表示。我们需要两者来创建一个新的方块实体。

首先,创建我们的方块实体类:

java
public class MyBlockEntity extends BlockEntity {
    public MyBlockEntity(BlockPos pos, BlockState state) {
        super(type, pos, state);
    }
}

你可能已经注意到,我们向super​构造函数传递了一个未定义的变量type​。让我们暂时保留这个未定义的变量,转而进行注册。

注册的方式与实体类似。我们创建关联的单例类BlockEntityType<?>​的实例,并将其注册到方块实体类型注册表中,如下所示:

java
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​变量:

java
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​的简单实例上,而是需要一个子类:

java
// 重要的部分是实现 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);
    }
}

然后,你当然需要在方块注册中使用此类作为类型:

java
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​读取和写入数据。这些方法在方块实体同步到磁盘或网络时调用。

java
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)的消费者,如下所示:

java
// 注意:刻是在方块中定义的,而不是方块实体中。然而,将刻逻辑以某种方式保留在方块实体中是一个好习惯,例如通过定义一个静态的 #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 刻计算一次,或缓存结果。


同步

方块实体逻辑通常在服务器上运行。因此,我们需要告诉客户端我们在做什么。有三种方法可以实现这一点:在区块加载时、在方块更新时,或使用自定义数据包。你通常只应在必要时同步信息,以免不必要地堵塞网络。

在区块加载时同步

每次从网络或磁盘读取区块时,都会加载区块(并因此使用此方法)。要在此处发送你的数据,你需要重写以下方法:

java
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);
    }
}

在方块更新时同步

此方法在方块更新发生时使用。方块更新必须手动触发,但通常比区块同步处理得更快。

java
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​检查区块是否已加载。

贡献者

The avatar of contributor named as 小飘 小飘

文件历史