Table of Contents

修改方块实体数据

在之前的教程,我们创建了方块实体,但是这些方块实体太无聊了,没有任何数据。所以我们尝试给它添加一些数据,并定义了序列化和反序列化数据的方块。

序列化数据

如果想在 BlockEntity 存储任何数据,必须保存和加载它,否则数据只会在 BlockEntity 被加载时保持,而且每次你重进游戏时会重置。幸运的是,保存和加载非常简单——只需要覆盖 writeNbt()readNbt() 即可。

writeNbt() 将会修改其参数 nbt 的内容,这个 nbt 包含了方块实体中的所有数据。该方法通常不会修改方块实体本身。方块实体数据将会存储在磁盘中,并且如果您需要将 BlockEntity 数据与客户端同步,则会通过封包发送。

在旧版本中,调用 super.writeNbt() 非常重要,因为方块实体的坐标及其方块实体类型 id 保存到 nbt 中。否则,您尝试保存的所有其他数据都将丢失,因为它与位置和 BlockEntityType 不相关。但是,最新版本中不是必要的,因为这些数据会通过 createNbt() 方法处理。

知道了这一点,下面的示例演示了如何将 BlockEntity 中的整数保存到标签中。在此示例中,整数保存在键 number 下——您可以将其替换为任何字符串,但是标签中的每个键只能有一个项,并且需要记住该键以便以后读取数据。

DemoBlockEntity.java
public class DemoBlockEntity extends BlockEntity {
 
    // 储存数字的当前值
    private int number = 7;
 
    public DemoBlockEntity(BlockPos pos, BlockState state) {
        super(TutorialBlockEntityTypes.DEMO_BLOCK, pos, state);
    }
 
    // 序列化方块实体
    @Override
    public void writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup wrapper) {
        // 将数字的当前值保存到 nbt
        nbt.putInt("number", number);
 
        super.writeNbt(nbt, wrapper);
    }
}

为了以后读取数据,您还需要覆盖 readNBT。此方法与 writeNBT 相反——不会将数据保存到 NBTCompound,而是您已经有了之前保存的 nbt 数据,使您可以检索所需的任何数据。该方法会修改方块实体本身,不会修改这个 nbt 参数。要检索我们之前保存的数字,请参见下面的示例。

    // 反序列化方块实体
    @Override
    public void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup wrapper) {
        super.readNbt(nbt, wrapper);
 
        number = nbt.getInt("number");
    }

要获取方块实体的 NBT 数据,调用 createNbt(registryLookup),这会自动地处理一些数据,例如位置和组件。

旧有版本中必须调用 super.readNbt()super.writeNbt(),因为有必要处理坐标等数据。在旧版本中,如果 createNbt 不存在,尝试 writeNbt(new NbtCompound())

将服务器数据同步至客户端

数据通常是在服务器世界读取的。大多数数据都是客户端不需要知道的,例如客户端并不需要知道箱子和熔炉里面有什么,除非打开它。但对于某些方块实体,例如告示牌和旗帜,你需要将所有或者部分数据告知客户端,比如用于渲染。

对于 1.17.1 及以下版本,请实现 Fabric API 中的 BlockEntityClientSerializable。此接口提供了 fromClientTagtoClientTag 方法,其作用与前面讨论的 readNbtwriteNbt 方法基本相同,只是专门用于发送和接收客户端上的数据。你可以简单地在 fromClientTagtoClientTag 两个方法中调用 readNbtwriteNbt

对于 1.18 及以上版本,请覆盖 toUpdatePackettoInitialChunkDataNbt

  @Nullable
  @Override
  public Packet<ClientPlayPacketListener> toUpdatePacket() {
    return BlockEntityUpdateS2CPacket.create(this);
  }
 
  @Override
  public NbtCompound toInitialChunkDataNbt() {
    return createNbt();
  }
public class DemoBlockEntity extends BlockEntity {
 
    public int number = 0;
 
    public DemoBlockEntity(BlockPos pos, BlockState state) {
        super(ExampleMod.DEMO_BLOCK_ENTITY, pos, state);
    }
 
    @Override
    public void writeNbt(NbtCompound nbt) {
        nbt.putInt("number", number);
 
        super.writeNbt(nbt);
    }
 
    @Override
    public void readNbt(NbtCompound nbt) {
        super.readNbt(nbt);
 
        number = nbt.getInt("number");
    }
}

确保 number 的作用域是 public,因为我们将在 DemoBlock 类中改变它的值。你也可以实现 getter 以及 setter 方法,不过我们这里为了简便就直接暴露字段。

修改数据

这将会在右键点击方块的位置获得 BlockEntity 并且如果它的类型是 DemoBlockEntity,它的 number 属性会增加并且会发送一条消息给玩家。

DemoBlock.class
public class DemoBlock extends BlockWithEntity {
 
    [...]
 
    @Override
    public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, BlockHitResult hit) {
        if (!world.isClient){
            BlockEntity blockEntity = world.getBlockEntity(pos);
            if (blockEntity instanceof DemoBlockEntity demoBlockEntity) {
                demoBlockEntity.number++;
                player.sendMessage(Text.literal("数字是" + demoBlockEntity.number));
                demoBlockEntity.markDirty();
            }
        }
 
        return ActionResult.SUCCESS;
    }
}

每当 BlockEntity 数据发生更改并需要保存时,请调用 markDirty()。这会将方块所在的区块标记为 dirty,在世界下次保存时强制调用 writeNbt 方法。原则上,只要修改了 BlockEntity 类中的任何一个自定义变量,就只需要简单调用 markDirty,否则当你退出并重进世界后,这个方块实体依然会是没有修改过的。

如果起要客户端知识更新(例如,需要用于渲染,或者在没有按 Ctrl 的情况下拾取物品),则在 markDirty 的调用后再加一行,这样客户端也会知道方块实体被改变了:

                world.updateListeners(pos, state, state, 0);

使用数据组件

自从 1.20.5 开始,还可以使用数据组件来存储数据,如果要将数据写入到物品堆,则有必要。你还是需要写 readNbtwriteNbt 方块以正确保存方块实体。

在单独的 TutorialDataComponentTypes 类中创建数据组件。关于注册数据组件的更多信息,可见 Fabric Docs 页面

TutorialDataComponentTypes.java
public class TutorialDataComponentTypes {
  public static final ComponentType<Integer> NUMBER = register("number", builder -> builder
      .codec(Codec.INT)
      .packetCodec(PacketCodecs.INTEGER));
 
  public static <T> ComponentType<T> register(String path, UnaryOperator<ComponentType.Builder<T>> builderOperator) {
    return Registry.register(Registries.DATA_COMPONENT_TYPE, Identifier.of("tutorial", path), builderOperator.apply(ComponentType.builder()).build());
  }
 
  public static void initialize() {
  }
}

记得有 ModInitializer 中引用这个方块:

ExampleMod.java
public class ExampleMod implements ModInitializer {
  [...]
 
  @Override
  public static void onInitialize() {
    [..]
    TutorialDataComponentTypes.initialize();
  }
}

然后在方块实体中:

DemoBlockEntity.java
  @Override
  protected void readComponents(ComponentsAccess components) {
    super.readComponents(components);
    this.number = components.getOrDefault(ExampleMod.NUMBER, 0);
  }
 
  @Override
  protected void addComponents(ComponentMap.Builder componentMapBuilder) {
    super.addComponents(componentMapBuilder);
    componentMapBuilder.add(ExampleMod.NUMBER, number);
  }
 
  @Override
  public void removeFromCopiedStackNbt(NbtCompound nbt) {
    nbt.remove("number");
  }

removeFromCopiedStackNbt 的用途是,复制物品堆时,因为数据组件已经被复制,所以 NBT 就不再需要了。如果拾取物品(按下 Ctrl 的同时按下鼠标中键),组件会转移到物品堆。如果需要在不按下 Ctrl 的情况下就转移这些组件(就像原版旗帜的行为),请看 blockentity_sync_itemstack