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

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

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

MCBBS Wiki GitHub群组已上线!

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

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

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

用户:MashKJo/1.12.2模组开发教程/6.ItemStack和Meta-hack

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

前文说过,Item这一类型遵循享元设计模式——由此可以看出,每一个Item实例代表的其实是一个特定的物品类型。

那么读者肯定就要问了:那我们在游戏里,跟背包里的物品打交道的时候,不仅能得到背包里每个格子里的物品的种类信息,还能得到数量的信息啊!但是Item类中似乎没有代表堆叠数量的字段,这是什么回事呢?所以由此可见,Item实例所能提供的信息还是不够多。因此,实际上玩家在世界中,在99%的情况下,是和ItemStack对象而非Item打交道。

ItemStack包含的信息

一个ItemStack中所包含的信息有:物品种类(Item)、堆叠数量(int)、meta值(int)和附加NBT(NBTTagCompound)。物品种类和堆叠数量都不难理解,至于这个meta到底什么含义?这会在本节的最后一部分揭晓的,还请读者耐心看下去。至于附加NBT,这个熟悉原版命令的读者应该比较了解,在游戏中它一般是以一种类似JSON文件的格式出现的,即字符串形式的NBT,这块的内容可以参考Minecraft Wiki的有关条目;但在代码层面,NBT有着另一套用法。关于NBT的知识,后面会有专门一节来进行讲解。

ItemStack有着8种构造方法,其中有4种是我们会经常用到的:(Item item)(Item item, int count)(Item item, int count, int meta)(Item item, int count, int meta, @Nullable NBTTagCompound nbt)。还有3种构造方法是把刚才所述的前3种构造方法的第1个形参换成了Block,实际上这3种含Block的构造方法会自动获取传入的方块的物品形式(ItemBlock),再调用那3个含Item的构造方法——没错,我们认知中的拿在玩家手中的方块,实际上是方块的物品形式;被放置在世界中的方块,才是真正的方块。

还有个构造方法,只传入一个NBTTagCompound,这是怎么回事呢?实际上MC在序列化(Serialize)物品数据时,会先把ItemStack的物品类型、堆叠数量、meta值和附加NBT汇总起来,形成一个新的最终NBT,再进行存储操作;而这个构造器实际上就是执行了把最终NBT解成ItemStack的流程。

警告:ItemStack的附加NBT有可能为null,除非你调用含附加NBT的构造方法构造ItemStack,或调用setTagCompound显式指定附加NBT,亦或者这个ItemStack已经通过附加NBT存储了一些数据。所以用附加NBT前要先判空,否则大概率会抛NullPointerException崩游戏。

ItemStack对象的使用

ItemStack对象和Item不同,它是随建随用的。永远不要尝试去继承ItemStack类,因为这么做没有意义——事实上你也不可能去继承它,因为它是一个final class。

在设计物品的功能时,我们自然而然地会有给物品添加附加数据的需求。首先先明确一点——永远不要让附加数据以Item实例的实例变量的形式存在,因为Item代表的是物品类型,牵一发而动全身,数据的变化会影响所有物品类型为该Item实例的ItemStack——所以你应该把目光放在ItemStack的那几个字段上。能存储额外信息的字段,其实只有两个:Meta和附加NBT。但是除非你要存储的数据是该物品的损害值/耐久值,否则,通常不推荐用meta存储额外数据,因为它本身不是什么复杂的引用类型变量,只是一个int整数罢了,而且它的使用颇有些hack的味道(甚至于1.13开始它直接被抹去了)。因此,还是把目光放在附加NBT上为好。

另外,读者如果翻阅过ItemStack类的源代码,可能会发现该类中有个特殊的静态字段:EMPTY。它其实是代表“空”的ItemStack,即对应的Item为null的情况——实际上它被赋予的值就是new ItemStack((Item)null),如果对ItemStack.EMPTY调用getItem方法的话,却不会返回null,而是会返回Items.AIR(没错,这其中有一个三目运算符在判空)。

那么,Items.AIR又是何方神圣呢?望文生义,它似乎是空气方块的物品形式?唔,这个解释,对,也不对。因为所有作为方块的物品形式的物品,都是ItemBlock类的实例,但Items.AIR是ItemAir类的实例,ItemAir类却是直接继承了Item,并未继承ItemBlock——所以我们可以说,空气方块不存在对应的物品——这也是为什么在几乎所有Minecraft版本中我们均不能获得空气物品(除了极个别快照版本)。但另一方面,ItemAir类的构造方法接受一个Block参数,在Item类的registerItems这一静态方法中,Items.AIR又被这样赋了值:

registerItemBlock(Blocks.AIR, new ItemAir(Blocks.AIR));

Mojang的意思似乎又是:Items.AIR就是Blocks.AIR的物品形式,着实让人大惑不解。更何况既然都给ItemAir取名为“空气物品”了,又何必多此一举让一个Block传入进去呢?笔者对此属实是不能理解,只能理解为Items.AIR纯粹是Mojang用于占位的一个Item罢了,恰巧“无”的概念在Minecraft中和空气是相近的。

此外,下面列出了一些ItemStack类常用的方法,这些方法在实际开发中通常会很实用:

  • void addEnchantment(Enchantment ench, int level):用于给某个ItemStack添加一个固定等级的附魔,注意ItemStack类中并没有一个非静态的Map<Enchantment, Integer>字段来保存ItemStack对象的附魔情况,所以这个方法是怎么实际发挥作用的呢?答案是附魔信息存在了附加NBT里。
  • ItemStack copy():用于构造出一个各方面都与该ItemStack对象一样的另一个不同的ItemStack对象——只要读者懂得Java中关于变量、引用、对象的知识,就知道这个方法是用来干啥的
  • ItemStack setStackDisplayName(String displayName)void clearCustomName():设定或移除某个ItemStack的自定义名称——实际效果跟把某个ItemStack放在铁砧里重命名是一样的,自定义展示名这个参数同样是储存在附加NBT中的。注意!这里不能做本地化处理——实际上你做了也没用,比如你给这个自定义名称设定一个本地化键名“tutorial_mod.custom_name”,并在语言文件中写好相应的本地化文本,你在游戏中看到的这个ItemStack只会显示未本地化的原始键名
  • boolean isEmpty():判断该ItemStack是否为空,即是否和ItemStack.EMPTY“相等”——实际上这个词用得不好,因为ItemStack类并未覆写equals方法,实际上这里实际的判断是先判断this == ItemStack.EMPTY,再判断当前对象的物品类型

此外我们也可以看看Item类中那些未讲到的getter了。实际上这些getter很多都有传入一个ItemStack参数,所以如果你想实现Item属性的高级控制,你可以不用那些简单的setter,而是通过覆写这些getter,结合传入的ItemStack的信息来操作返回结果,借此我们可以实现一些很酷炫的效果。

以及你可能已经发现了,ItemStack类中有很多方法是和Item重合的,这些方法一般都通过getItem方法来代理给ItemStack对象对应的Item来做。

Meta-hack

那么,笔者终于要给读者解惑:ItemStack的meta字段,到底是什么意思??

好,这个字段,它的实际名称,为itemDamage,意思为“物品的损害值”,什么叫损害值?举个例子,一个满耐久的铁镐,其损害值为0;而如果用了它一次,且它挖掉的这个方块具有让它损耗耐久的功能(不能是TNT这种方块),那么它之后的损害值就变为了1——这下明白了吧?一个物品的最大损害值,实际上数值上等于它的最大耐久值。通过Item类的setMaxDamage方法可以控制物品的最大损害值。

然而,事情远没有这么简单。itemDamage这一字段,它的含义是会变的——由对应Item实例的boolean hasSubtypes字段控制,该字段默认为false。

但如果,我们用setHasSubtypes(true)来改变了这一字段呢?那么itemDamage,也即meta,它就不能用于指代损害值了,而是用于区分共享同一个Item实例的不同种物品了。这个技巧,有什么好处呢?一是为了防止物品占用过多的数字ID——虽然Forge的注册表不要求我们显式指定数字ID,但不代表数字ID就不存在了,它仍然存在于Minecraft底层之中,直到1.13它才彻底消失;而在1.12.2时期,数字ID是会被Forge动态分配的。二是在某些实现中它确实很符合人的惯常思维,也确实很好用,比如MC中有很多与颜色相关的物品,先写一个辅助类EnumDyeColor,再通过meta的值进行统筹分配颜色,确实是个不错的设计。然而为什么这玩意叫“Meta-hack”呢?那是因为Mojang不知道怎么想的,非得把物品的损害值和类型序数这两个概念耦合在一个字段中——这不是吃饱了撑的吗?所以实际上一个Item实例并不能同时拥有类型序数和耐久值,除非其中有一个被存在附加NBT中(实际上最好把itemDamage设定为类型序数,这样在设定物品模型的时候方便——setCustomModelResourceLocation那块正好传入一个meta值,不过把类型序数存进附加NBT也不是不行,写个Property Override就可以,这个内容后面会讲到的)。实际上meta这个概念在1.13直接被废除了,物品的耐久也被并入了附加NBT中。

而且,使用Meta-hack时,通常需要覆写Item类的一些getter,通过switch(stack.getMetadata())来控制同一个Item实例不同meta的ItemStack的各自的unlocalizedName、maxStackSize等参数。

而且还有一个问题:如何让带合适meta的ItemStack体现在创造模式物品栏中呢?答案是覆写Item类的getSubItems方法,向传入的NonNullList<ItemStack>中添加特定的ItemStack即可。注意这个方法不需要设定hasSubtypes为true才能起效果,因此这个方法也不是专门为meta-hack准备的——因为ItemStack里除了meta还有附加NBT啊,所以理论上你可以让陈列在创造模式物品栏中的ItemStack带上一定的附魔——暮色森林中的迷宫破坏者,在创造模式物品栏中,就是自带附魔的,原理就是这个。

另外,由于ItemStack对象的使用方法是随建随用,因此你甚至可以往这个NonNullList<ItemStack>中塞2个各方面都完全相同的2个ItemStack。

说教得差不多了,来段示例代码吧(这里笔者设定了:在创造模式物品栏中展示meta为0 - 3的该物品。

src/main/java/net/tutorial_mod/item/ItemMultiMetadata.java:

package net.tutorial_mod.item;

import net.minecraft.creativetab.CreativeTabs;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.util.NonNullList;
import net.tutorial_mod.TutorialMod;

public class ItemMultiMetadata extends Item{
   public ItemMultiMetadata(){
       this.setRegistryName(TutorialMod.MODID, "multimetadata_item");
       this.setCreativeTab(CreativeTabs.MISC);
       this.setHasSubtypes(true);
       this.setNoRepair();
   }

   @Override
   public String getUnlocalizedName(ItemStack stack){
       return "item.multimetadata_item_" + stack.getMetadata();
   }

   @Override
   public void getSubItems(CreativeTabs tab, NonNullList<ItemStack> items)
   {
       if (this.isInCreativeTab(tab))
       {
           for(int i = 0; i < 4; i ++)
           {
               items.add(new ItemStack(this, 1, i));
           }
       }
   }
}

最后,再说两点吧:

  1. 对于物品而言(猜猜看笔者为什么要特意强调“对于物品”),meta值最大为short类型的上限:65535。
  2. 除非你真的有特殊需求,否则,不要轻易用这个Meta-hack,因为它的实现莫名其妙的,且在1.13就会失效。那么有什么代替的方案吗?也简单,实际上原版就有这样的案例——ItemBucket类,原版的空桶、水桶、岩浆桶都是通过new这个类构造出来的(注意牛奶桶不在此列),因为ItemBucket的构造方法中有个Block参数,借此控制桶中到底装的是啥——空桶在被构造时,传入的是空气方块——合理吧?所以你更应该修改你的Item子类的构造方法,加点形参用于控制不同的变种,再多new几次这个类就行了,犯不着一定要用Meta-hack。