1. ItemStack

你应该注意到了,Item 类下没有一个字段代表数量,换言之它没有数量的概念。它实际上只是一个物品种类的标记而已。想想看:

  • 一颗钻石和一组钻石都是钻石,只是数量不同。
  • 一块铁锭和一块金锭的数量相同,但它们不是同一种物品。
  • 一颗苹果和六颗苹果都一样能吃。

1.1. 享元(Flyweight Pattern)

Item 代表了物品的“类型”。两颗钻石因为“都是钻石”所以能叠加在一起。两块不同的金属锭因为类型不同所以不能叠加在一起。 有数量概念的是 ItemStack,一个 ItemStack 的实例代表了“一堆特定物品,数量不定”。Minecraft 中玩家背包里的东西都是 ItemStack。玩家手持的也是 ItemStack。在此基础上,ItemStack 还有携带一个 NBT 标签的能力,这允许 ItemStack 携带更多的自定义数据。
Minecraft 在这里采用了享元的概念:一种特定物品只对应一个 Item 实例。换言之,在 Modding 的过程中,我们可以直接通过 == 来判断两个物品是否为同一个种类。

1.2. Item 是什么?

既然上文说到了 Item 实际上是指类型,那么不妨先来探讨一下物品的“类型”究竟是什么。考虑这么几个例子:

  • 一把快用坏的铁镐和一把刚做出来的铁镐都一样能挖石头,一样都需要用铁锭而不是金锭修复,虽然修复完好无损的铁镐没什么意义。
  • 不论你给命名牌改什么名,他们仍然是命名牌,只是你对着某个生物(实体)使用的时候会给予他们不同的名字。
  • 土豆不会因为你持有的数量而改变它的营养价值,也不会因为数量而改变它在熔炉里烤一下的产量。
  • 附魔书可以携带单种任意等级的任意魔咒,但它是附魔书的本质不会因为携带的魔咒或魔咒等级不同而改变。

一言以蔽之,不论 ItemStack 携带的信息如何改变,处理他们的逻辑都不会随之而转移。这些“逻辑”包括:

  • 使用/放置(左键/右键)的行为
  • 在客户端上显示的名称及 Tooltip
  • 合成配方
  • et cetera

实际上 Item 类下大多数方法、字段和上面的性质是对应的。 由此我们可以看到,所谓 Item,其实是对所有玩家通过 ItemStack 互动行为处理机制的逻辑所存放的类。就这一点来说,它的确符合享元模式的一般使用动机:

Use sharing to support large numbers of fine-grained objects efficiently.

- "Design Patterns: Elements of Reusable Object-Oriented Software"

所有 ItemStack 在被实例化的时候必定指定了一个 Item 实例作为它的类型,相应的,该 ItemStack 的所有行为 (右键、挖掘方块、攻击实体、渲染、……) 都是交给 ItemStack.getItem() 返回的那个 Item 对象来处理的(Item.onItemRightClickItem.onItemUseFinishItem.hitEntityItem.onBlockDestroyedItem.canHarvestBlock、……)。也因此,刚才提到的方法的参数中都会包含一个 ItemStack 实例——它代表了该物品的具体状态

其实,通过覆写这些方法,读者当然可以实现绝大部分自己想要的功能——但是十分麻烦,而且对其他mod的兼容性也会受影响(其实就是在造轮子)。

在 4.2 中将会介绍一些 Item 类的子类以及 Item 类其他的一些特性,以进一步阐述如何实现 Item 类。不过在此之前,请读者确保自己已经对上面所说的东西充分理解了。

1.3. “空”

上文提到“所有 ItemStack 在被实例化的时候必定指定了一个 Item 实例作为它的类型”,但实际上有一个例外:ItemStack 类下的常量字段 EMPTY 初始化时没有使用 Item 实例,而是用了 null。相应地,它代表了“空”的状态,即这个 ItemStack 实质上什么都没有;更准确地说,它相当于持有 Items.AIR(即“空气”)这个物品,这在 Minecraft 的世界中等价于“无”。
代表“空”的状态的不止这一个情况。当数量为非正数(小于等于零)的时候,这个 ItemStack 也会被认为是“空”的。构造 ItemStack 对象时使用 Items.AIR 效果也一样。

有鉴于对“空”的状态的明确定义,ItemStack 的引用在 Minecraft 中被看作是 non-null 的。换言之,任何出现 ItemStack 的地方都应视作“不可能为 null”。 至于判断给定 ItemStack 是不是空的,我们只需要调用 isEmpty() 就行了。

ItemStack stack = ...;
if (!stack.isEmpty()) {
    // 仅当物品非空时继续操作
}