用户:MashKJo/1.12.2模组开发教程/8.NBT系统的使用
NBT是什么?首先,我们先引用一段Minecraft Wiki上对NBT的介绍。
另一种玩家更熟悉的是字符串形式的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; }
最后
- 对NBT对象的构造一定要发生于逻辑服务端正在运行的时候,否则逻辑客户端肯定会抛NPE并崩游戏。且只有ItemStack的NBT是会自动双端同步的,其他地方的NBT若想完成同步,需要手写双端通信的代码,有的基于覆写若干方法,还有的基于Forge提供的双端通信系统。
- NBTTagCompound中存储的额外信息,如果不是给Minecraft原版的代码用的,而是为你自己的Mod的逻辑用的,那么存储的额外信息所对应的NBTBase对象,所对应的键名,并无特别要求。那为什么存储附魔信息的必须是一个NBTTagList,并存于ItemStack的附加NBT中,对应的键名必须为“ench”呢?因为这是Minecraft原版读取ItemStack中附魔信息的相关代码所决定的。如果你放进NBT中的信息是给你自己的Mod用的,那键名任你取,就没有什么要求了。不过你还是最好以你的ModName为名称开个子标签,再写入你自己的数据,这是为了防止别的模组也向该NBT中添加额外数据,且键名发生冲突的情况。
注释与外部链接
- ↑ 方块实体是个只体现在代码层面的概念,用于给方块存储理论上无限的数据,它的生成与销毁是伴随着特定方块的,这块内容会在后面讲到。