User Tools

Site Tools


zh_cn:tutorial:blockentity_modify_data

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
zh_cn:tutorial:blockentity_modify_data [2024/05/30 08:21] – [介绍] sjk1949zh_cn:tutorial:blockentity_modify_data [2025/04/01 12:48] (current) – [关于 NbtCompound 的注意事项] solidblock
Line 1: Line 1:
 ====== 修改方块实体数据 ====== ====== 修改方块实体数据 ======
  
-===== 介绍 =====+在之前的教程,我们创建了[[blockentity|方块实体]],但是这些方块实体太无聊了,没有任何数据。所以我们尝试给它添加一些数据,并定义了序列化和反序列化数据的方块。
  
-拥有一个与某个 ''方块'' 联,并持有一些数据的 ''方块实体'' 非常棒, 但是如何改变里面的数据?+===== 于 NbtCompound 注意事项 =====
  
-继续之前,你将需要个 [[zh_cn:tutorial:blocks|方块]] 和个 [[zh_cn:tutorial:blockentity|方块实体]],这该与下面代码形式类似+自从 1.21.5 开始,''NbtCompound'' 的相关方法被更改了。各 getter 方法返回的都是 ''Optional'' 对象,除非再指定一个参数以表示该项不存时的默认值。例如: 
 + 
 +<code java> 
 +// 没有指定默认值,将返回 Optional 
 +Optional<Integer> value = nbt.getInt("value"); // 注意不是 OptionalInt 
 + 
 +// 指定默认值,当对应字段不存在时返回这个默认值 
 +int value = nbt.getInt("value", 1000); 
 +</code> 
 + 
 +对于集合类型,如复合标签和列表,可以返回一个空的对象作为默认值,例如: 
 +<code java> 
 +// 没有指定默认值,将返回 Optional 
 +Optional<NbtCompound> config = nbt.getCompound("config"); 
 + 
 +// 当对应字段不存在时,返回空复合标签 
 +NbtCompound config = nbt.getCompoundOrEmpty("config"); 
 +</code> 
 + 
 +而在 1.21.5 之前,默认都会返回零值或者空白,如果需要指定默认值需要先自行用 ''contains'' 方法进行判断,例如: 
 +<code java> 
 +// 当对应字段不存在时返回 0。 
 +int value = nbt.getInt("value"); 
 + 
 +// 当对应字段不存在时返回空复合标签。 
 +NbtCompound config = nbt.getCompound("config"); 
 +</code> 
 + 
 +此外,自从 1.21.5 开始,整个 NBT 复合标签以及里面的字段,可以直接使用 [[codec]] 进行解码,例如: 
 +<code java> 
 +NbtCompound nbt = new NbtCompound(); 
 +nbt.put("Id", Identifier.CODEC, Identifier.of("tutorial", "example_id")); 
 +Optional<Identifier> id = nbt.get("Id", Identifier.CODEC); 
 +</code> 
 + 
 +<code java> 
 +NbtCompound nbt = new NbtCompound(); 
 +// 需要使用 RegistryOps,因为物品堆在编码解码时需要访问注册表内容 
 +nbt.copyFromCodec(ItemStack.MAP_CODEC, wrapperLookup.getOps(NbtOps.INSTANCE), new ItemStack(Items.WHEAT)); 
 +Optional<ItemStack> stack = nbt.decode(ItemStack.MAP_CODEC, wrapperLookup.getOps(NbtOps.INSTANCE)); 
 +</code> 
 + 
 +此外,从 1.21.5 开始,列表支持混合不同类型的元素,而在之前的版本中,混入不同类型的元素会导致报错。 
 + 
 +关于 NBT 变化的更多信息,请参见 [[https://fabricmc.net/2025/03/24/1215.html|Fabric for Minecraft 1.21.5]]。 
 +===== 序列化数据 ===== 
 + 
 +如果想在 ''BlockEntity'' 存储任何数据,必须保存加载它,否则数据只会在 ''BlockEntity'' 被加载时保持,而且每次你重进游戏时会重置。幸运的是,保存和加载非常简单——只需要覆盖 ''writeNbt()'' 和 ''readNbt()'' 即可。 
 + 
 +''writeNbt()'' 将会修改其参数 ''nbt'' 的内容,这个 ''nbt'' 包含了方块实体中的所有数据。该方法通常不会修改方块实体本身。方块实体数据将会存储在磁盘中并且如果您需要将 ''BlockEntity'' 数据与客户端同步,则会通过封包发送。 
 + 
 +在旧版本中,调用 ''super.writeNbt()'' 非常重要,因为方块实体的坐标及其方块实体类型 id 保存到 nbt 中。否则,您尝试保存的所有其他数据都将丢失,因为它与位置和 ''BlockEntityType'' 不相关。但是,最新版本中不是必要的,因为些数据会通过 ''createNbt()'' 方法处理。 
 + 
 +知道了这一点,下面的示例演示了如何将 ''BlockEntity'' 中的整数保存到标签中。在此示例中,整数保存在键 ''number'' 下——您可以将其替换为任何字符串,但是标签中的每个键只能有一个项,并且需要记住键以便以后读取数据。 
 + 
 +<code java 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 registries) { 
 +        // 将数字的当前值保存到 nbt 
 +        nbt.putInt("number", number); 
 + 
 +        super.writeNbt(nbt, registries); 
 +    } 
 +
 +</code> 
 + 
 +为了以后读取数据,您还需要覆盖 ''readNBT''。此方法与 ''writeNBT'' 相反——不会将数据保存到 ''NBTCompound'',而是您已经有了之前保存的 nbt 数据,使您可以检索所需的任何数据。该方法会修改方块实体本身,不会修改这个 ''nbt'' 参数。要检索我们之前保存的数字,请参见下面的示例。 
 + 
 +<code java> 
 +    // 反序列化方块实体 
 +    @Override 
 +    public void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registries) { 
 +        super.readNbt(nbt, registries); 
 +         
 +        number = nbt.getInt("number", 0); 
 +    } 
 +</code> 
 + 
 +要获取方块实体的 NBT 数据,调用 ''createNbt(registries)'',这会自动地处理一些数据,例如位置和组件。 
 + 
 +旧有版本中必须调用 ''super.readNbt()'' 和 ''super.writeNbt()'',因为有必要处理坐标等数据。在旧版本中,如果 ''createNbt'' 不存在,尝试 ''writeNbt(new NbtCompound())''。 
 + 
 +===== 将服务器数据同步至客户端 ===== 
 +数据通常是在服务器世界读取的。大多数数据都是客户端不需要知道的,例如客户端并不需要知道箱子和熔炉里面有什么,除非打开它。但对于某些方块实体,例如告示牌和旗帜,你需要将所有或者部分数据告知客户端,比如用于渲染。 
 + 
 +对于 1.17.1 及以下版本,请实现 Fabric API 中的 ''BlockEntityClientSerializable''。此接口提供了 ''fromClientTag'' 和 ''toClientTag'' 方法,其作用与前面讨论的 ''readNbt'' 和 ''writeNbt'' 方法基本相同,只是专门用于发送和接收客户端上的数据。你可以简单地在 ''fromClientTag'' 和 ''toClientTag'' 两个方法中调用 ''readNbt'' 和 ''writeNbt''。 
 + 
 +对于 1.18 及以上版本,请覆盖 ''toUpdatePacket'' 和 ''toInitialChunkDataNbt'' 
 +<code java> 
 +  @Nullable 
 +  @Override 
 +  public Packet<ClientPlayPacketListener> toUpdatePacket() { 
 +    return BlockEntityUpdateS2CPacket.create(this); 
 +  } 
 +  
 +  @Override 
 +  public NbtCompound toInitialChunkDataNbt() { 
 +    return createNbt(); 
 +  } 
 +</code>
  
 <code java> <code java>
Line 17: Line 126:
    
     @Override     @Override
-    public void writeNbt(NbtCompound nbt) {+    public void writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registries) {
         nbt.putInt("number", number);         nbt.putInt("number", number);
    
-        super.writeNbt(nbt);+        super.writeNbt(nbt, registries);
     }     }
          
     @Override     @Override
-    public void readNbt(NbtCompound nbt) { +    public void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registries) { 
-        super.readNbt(nbt);+        super.readNbt(nbt, registries);
    
-        number = nbt.getInt("number");+        // 对于 1.21.5 之前的版本,请使用 nbt.getInt("number"); 
 +        number = nbt.getInt("number", 0);
     }     }
 } }
 </code> </code>
  
-确保 ''number'' 的作用域是 **public**,因为我们将在 ''DemoBlock'' 类中改变它的值。你也可以实现 getter 以及 setter 方法。+确保 ''number'' 的作用域是 **public**,因为我们将在 ''DemoBlock'' 类中改变它的值。你也可以实现 getter 以及 setter 方法,不过我们这里为了简便就直接暴露字段
  
-===== From Block's onUse() =====+===== 修改数据 =====
  
-这将会在右键点击方块的位置获得 ''方块实体'' 并且如果它的类型是 ''DemoBlockEntity'',它的 ''number'' 属性会增加并且会发送一条消息给玩家。+这将会在右键点击方块的位置获得 ''BlockEntity'' 并且如果它的类型是 ''DemoBlockEntity'',它的 ''number'' 属性会增加并且会发送一条消息给玩家。
  
-<code java> +<code java DemoBlock.class
-public class DemoBlock extends Block implements BlockEntityProvider {+public class DemoBlock extends BlockWithEntity {
  
     [...]     [...]
  
     @Override     @Override
-    public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) {+    public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, BlockHitResult hit) {
         if (!world.isClient){         if (!world.isClient){
             BlockEntity blockEntity = world.getBlockEntity(pos);             BlockEntity blockEntity = world.getBlockEntity(pos);
-            if (blockEntity instanceof DemoBlockEntity){ +            if (blockEntity instanceof DemoBlockEntity demoBlockEntity) {
-                DemoBlockEntity demoBlockEntity = (DemoBlockEntity) blockEntity;+
                 demoBlockEntity.number++;                 demoBlockEntity.number++;
-                player.sendMessage(Text.literal("Number is... "+demoBlockEntity.number), false); +                player.sendMessage(Text.literal("数字是" + demoBlockEntity.number)); 
-                 +                demoBlockEntity.markDirty();
-                return ActionResult.SUCCESS;+
             }             }
         }         }
                  
-        return ActionResult.PASS;+        return ActionResult.SUCCESS;
     }     }
 } }
 </code> </code>
 +
 +每当 ''BlockEntity'' 数据发生更改并需要保存时,请调用 ''markDirty()''。这会将方块所在的区块标记为 dirty,在世界下次保存时强制调用 ''writeNbt'' 方法。原则上,只要修改了 ''BlockEntity'' 类中的任何一个自定义变量,就只需要简单调用 ''markDirty'',否则当你退出并重进世界后,这个方块实体依然会是没有修改过的。
 +
 +如果起要客户端知识更新(例如,需要用于渲染,或者在没有按 ''Ctrl'' 的情况下拾取物品),则在 ''markDirty'' 的调用后再加一行,这样客户端也会知道方块实体被改变了:
 +<code java>
 +                world.updateListeners(pos, state, state, 0);
 +</code>
 +
 +===== 使用数据组件 =====
 +
 +自从 1.20.5 开始,还可以使用数据组件来存储数据,如果要将数据写入到物品堆,则有必要。你还是需要写 ''readNbt'' 和 ''writeNbt'' 方块以正确保存方块实体。
 +
 +在单独的 ''TutorialDataComponentTypes'' 类中创建数据组件。关于注册数据组件的更多信息,可见 [[https://docs.fabricmc.net/zh_cn/develop/items/custom-data-components|Fabric Docs 页面]]。
 +
 +<code java 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() {
 +  }
 +}
 +</code>
 +
 +记得有 ''ModInitializer'' 中引用这个方块:
 +<code java ExampleMod.java>
 +public class ExampleMod implements ModInitializer {
 +  [...]
 +  
 +  @Override
 +  public static void onInitialize() {
 +    [..]
 +    TutorialDataComponentTypes.initialize();
 +  }
 +}
 +</code>
 +
 +然后在方块实体中:
 +
 +<code java 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");
 +  }
 +</code>
 +
 +''removeFromCopiedStackNbt'' 的用途是,复制物品堆时,因为数据组件已经被复制,所以 NBT 就不再需要了。如果拾取物品(按下 ''Ctrl'' 的同时按下鼠标中键),组件会转移到物品堆。如果需要在不按下 ''Ctrl'' 的情况下就转移这些组件(就像原版旗帜的行为),请看 [[blockentity_sync_itemstack]]。
zh_cn/tutorial/blockentity_modify_data.1717057306.txt.gz · Last modified: 2024/05/30 08:21 by sjk1949