MCBBS Wiki欢迎您共同参与编辑!在参与编辑之前请先阅读Wiki方针

如果在编辑的过程中遇到了什么问题,可以去讨论板提问。

为了您能够无阻碍地参与编辑 未验证/绑定过邮箱的用户,请尽快绑定/验证

MCBBS Wiki GitHub群组已上线!

您可以在回声洞中发表吐槽!

服务器状态监控。点击进入

本站由MCBBS用户自行搭建,与MCBBS及东银河系漫游指南(北京)科技有限公司没有从属关系。点此了解 MCBBS Wiki 不是什么>>

用户:MashKJo/1.12.2模组开发教程/8.NBT系统的使用

来自MCBBS Wiki
跳到导航 跳到搜索

NBT是什么?首先,我们先引用一段Minecraft Wiki上对NBT的介绍。

NBT(二进制命名标签NamedBinary Tags格式是一种由众多的标签所组成的树状数据结构。在Minecraft中,其被广泛用于向存档文件中存储数据。所有的标签都有一个独立的数字ID和名称,以及一个负载。

另一种玩家更熟悉的是字符串形式的NBT,通常在命令里使用。这种格式常被称为SNBT(字符串化的二进制命名标签,Stringified NBT)。

字符串形式的NBT很像序列化之后的JsonElement,实际上NBT系统的设计确实和Gson有许多相似之处。

所有NBT类的基类是NBTBase类,而NBTBase的继承树如下:

\-NBTBase
  \-NBTPrimitive
    |-NBTTagByte
    |-NBTTagDouble
    |-NBTTagFloat
    |-NBTTagInt
    |-NBTTagLong
    |-NBTTagShort
  |-NBTTagByteArray
  |-NBTTagCompound
  |-NBTTagEnd
  |-NBTTagIntArray
  |-NBTTagList
  |-NBTTagLongArray
  |-NBTTagString

除去NBTPrimitive的所有NBTBase的子类,都覆写了toString方法,借此你可以轻松得到某个NBTBase实例的字符串形式,也即用于命令系统中的那种NBT的形式。

我们可以通过NBTBase下的static createNewByType(byte id)来创建新的NBTBase实例,具体类型由传入的byte幻数决定,Forge在Constants类下提供了相应的常量,用这个方法的时候直接传入Forge提供的常量即可。不过我们一般也可以自己手动new一个就行。

我们该用什么?

虽然那个NBTBase的继承树很长,但是我们几乎在99.9%的情况下,都只会用到NBTTagCompound和NBTTagList。前者是NBT复合标签,可以在里面再嵌套NBTBase对象;后者则是经过简单封装的专门用于存储NBTBase的ArrayList。

NBTPrimitive的子类、NBTTagByteArray、NBTTagIntArray、NBTTagLongArray和NBTTagString都封装了一些很常用的类型,那为什么要对它们进行封装呢?这些都是为NBTTagCompound和NBTTagList服务的。至于NBTTagEnd,它用于标记一个NBTTagCompound的结束,它的toString方法则返回END。正常情况下,我们不该直接构造NBTTagEnd,因为Minecraft在处理NBT数据时会自动给NBTTagCompound末尾加上一个NBTTagEnd的。一般也很少直接构造NBTPrimitive的子类、NBTTagByteArray、NBTTagIntArray、NBTTagLongArray和NBTTagString,对它们的操作一般是封装在NBTTagCompound中的setter和getter中的。

在什么时候我们会需要操作NBT?

那得看Minecraft中什么游戏元素的额外信息是以NBT格式保存的。实际上以NBT格式保存的游戏元素的基类都实现了接口INBTSerializable<? extends NBTBase>,它们包括但不限于:ItemStack、方块实体(TileEntity)[1]、实体(Entity)、世界附加数据(WorldSavedData)、村庄(Village)等等。另外,Forge引入的Capability系统,其数据的持久化,也是依托于NBT格式的。INBTSerializable的泛型参数,代表游戏元素序列化之后产生的NBTBase的实际类型。

具体应用

NBTTagCompound

NBTTagCompound的底层实现,实际上是依托于一个非静态HashMap<String, NBTBase>字段 —— 这就是为何Minecraft要把某些常用的类型进行封装,毕竟设定成HashMap<String, Object>总不合适吧?

之前说过,该类有若干getter和setter供我们使用。如setString这一setter,该setter会根据传入的value构造出一个NBTTagString,再把传入的key和这个NBTTagString放到那个hashMap中。所以这也印证了前文所述——我们不需要直接new那些封装性质的NBTBase子类。

此外,该类还有诸多好用的方法,如hasKey、removeTag等等。

NBTTagList

NBTTagList的使用很简单,它本质上就是对ArrayList<NBTBase>的简单封装,我们可以通过appendTag、removeTag这两个方法来完成对某个NBTTagList中保存的NBTBase对象的增添或移除。

另外,NBTTagList类实现了Iterable<NBTBase>接口,这意味着我们可以用foreach循环来便捷地遍历一个NBTTagList。

实例

前文说过,ItemStack的信息实际上是以NBT格式存储的。比如一把附魔了耐久三的满耐久钻石镐,其字符串形式的NBT标签应该是这样的:

{id:"minecraft:diamond_pickaxe", Count:1b, Damage:0s, tag:{ench:[{id:34s, lvl:3s}]}}

注意:前文所述的ItemStack的附加NBT标签,实际上在这个例子中,对应"tag"这一键名对应的复合标签——附魔的信息被存储在该复合标签中,这也印证了前面的说法。

虽然我们可以通过ItemStack的构造方法和addEnchantment方法构造出这么一个ItemStack,再调用serializeNBT方法即可得到该SNBT标签所对应的NBTTagCompound对象,不过笔者这里还是用代码手动构造一个,以帮助读者更好地理解:如何在代码层面构造NBTTagCompound。

public NBTTagCompound get(){
   NBTTagCompound nbt = new NBTTagCompound();
   nbt.setString("id", "minecraft:diamond_pickaxe");
   nbt.setByte("Count", (byte)1);
   nbt.setShort("Damage", (short)0);
   NBTTagCompound tag = new NBTTagCompound();
   NBTTagList enchList = new NBTTagList();
   NBTTagCompound enchTag = new NBTTagCompound();
   enchTag.setShort("id", (short)Enchantment.getEnchantmentID(Enchantments.UNBREAKING));
   enchTag.setShort("lvl", (short)3);
   enchList.appendTag(enchTag);
   tag.setTag("ench", enchList);
   nbt.setTag("tag", tag);
   return nbt;
}

最后

  1. 对NBT对象的构造一定要发生于逻辑服务端正在运行的时候,否则逻辑客户端肯定会抛NPE并崩游戏。且只有ItemStack的NBT是会自动双端同步的,其他地方的NBT若想完成同步,需要手写双端通信的代码,有的基于覆写若干方法,还有的基于Forge提供的双端通信系统。
  2. NBTTagCompound中存储的额外信息,如果不是给Minecraft原版的代码用的,而是为你自己的Mod的逻辑用的,那么存储的额外信息所对应的NBTBase对象,所对应的键名,并无特别要求。那为什么存储附魔信息的必须是一个NBTTagList,并存于ItemStack的附加NBT中,对应的键名必须为“ench”呢?因为这是Minecraft原版读取ItemStack中附魔信息的相关代码所决定的。如果你放进NBT中的信息是给你自己的Mod用的,那键名任你取,就没有什么要求了。不过你还是最好以你的ModName为名称开个子标签,再写入你自己的数据,这是为了防止别的模组也向该NBT中添加额外数据,且键名发生冲突的情况。

注释与外部链接

  1. 方块实体是个只体现在代码层面的概念,用于给方块存储理论上无限的数据,它的生成与销毁是伴随着特定方块的,这块内容会在后面讲到。