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 小飘 小飘

文件历史

布局切换

调整 VitePress 的布局样式,以适配不同的阅读习惯和屏幕环境。

全部展开
使侧边栏和内容区域占据整个屏幕的全部宽度。
全部展开,但侧边栏宽度可调
侧边栏宽度可调,但内容区域宽度不变,调整后的侧边栏将可以占据整个屏幕的最大宽度。
全部展开,且侧边栏和内容区域宽度均可调
侧边栏宽度可调,但内容区域宽度不变,调整后的侧边栏将可以占据整个屏幕的最大宽度。
原始宽度
原始的 VitePress 默认布局宽度

页面最大宽度

调整 VitePress 布局中页面的宽度,以适配不同的阅读习惯和屏幕环境。

调整页面最大宽度
一个可调整的滑块,用于选择和自定义页面最大宽度。

内容最大宽度

调整 VitePress 布局中内容区域的宽度,以适配不同的阅读习惯和屏幕环境。

调整内容最大宽度
一个可调整的滑块,用于选择和自定义内容最大宽度。

聚光灯

支持在正文中高亮当前鼠标悬停的行和元素,以优化阅读和专注困难的用户的阅读体验。

ON开启
开启聚光灯。
OFF关闭
关闭聚光灯。

聚光灯样式

调整聚光灯的样式。

置于底部
在当前鼠标悬停的元素下方添加一个纯色背景以突出显示当前鼠标悬停的位置。
置于侧边
在当前鼠标悬停的元素旁边添加一条固定的纯色线以突出显示当前鼠标悬停的位置。