Skip to main content

1.5 食物 与 燃料

Part A. 实战

食物就是一种特殊的 Item,也继承于 Item 来创建,只是比普通 Item 多几个属性(包括专门的类 Food 作为属性)。举个例子:

public class ObsidianApple extends Item {
public ObsidianApple() {
// 定义食物的属性,使用 Food 类
super(new Properties()
.group(ModGroup.MAIN_GROUP)
.food(
new Food.Builder()
.hunger(20)
.saturation(10)
.effect(() -> new EffectInstance(Effects.POISON, 10 * 20, 1), 0.5f)
.build()
)
);
}
}

其中,Food::Builder() 静态方法初始化 Food.Builder 类实例,用来在 Food 属性对象生成前设置好参数,支持 saturation()hunger()effect() 方法。

需要说明的是,其中 effect() 方法的第一参数Supplier<EffectInstance>第二参数触发的概率EffectInstance 类的初始化方法第一参数是 Effects 类的枚举量(含有 MC 中几乎所有效果),第二参数是效果持续的游戏 Tick 时间,第三参数是对应的药水等级。

一个游戏刻 Tick 就是主程序循环一次的时间,固定是 0.05 s;例如 3*20 就是 3 秒。

最后的 Food.Builder::build() 方法将 Food.Builder 及其中的设置构造为 Food 类的实例,可以在初始化物品时对 Properties 对象使用 food() 方法指定(和 group() 一样)。

接下来的物品注册、模型和材质都与普通 Item 相差无几。

那么,如何定义燃料呢?实不相瞒,燃料没有独立的类型,只要实现了以下接口的 Item 就能作为燃料(放入熔炉中):

public int getBurnTime(ItemStack itemStack, @Nullable IRecipeType<?> recipeType);	// 单位: tick

小贴士:一般普通物品像原矿石,平均熔炼时间(smelt time)为 200 tick(即 10s),而一个煤炭的 burn time 是 1600 ticks,大概能熔炼 8 个原矿石。

所以理论上,你甚至可以定义一个食物的 burn time,这样食物就能作为燃料了!快来试一试吧(doge)

注:在本节的 Part B 中,有介绍在切石机、营火等其他器械上定义不同的 burn time 的行为,感兴趣可以查看。

Part B. 理论:Food & Fuel

1.5.B.1 Food & Effect

如果你想知道 Forge 是如何抽象这些实现的,我们不妨看看 Forge 对 Item 的源码:

注意到 Item 类型中有个 foodProperties 可空属性,类型就是 Food。程序就是通过判断一个 Itemfood 属性是否为空,来判断这个 Item 是否为食物的。

我们上面创建一个食物的类型,也就是通过继承于 Item 时,加入了 foodProperties 属性,就相当于创建了一个食物类型的 Item

这是软件工程原理中的 “优先聚合而非继承” 的原则。

我们把目光转移到 Food 类型上来,Food 类型中究竟放了什么东西?

Food 这个类相当简单,总共 100 行代码,以下是部分的代码:

注意到 Food 类型使用软件工程原理中的 “建造者模式”(Builder)进行构造,内置了 Builder 类型。经常使用 lombok 框架的小伙伴可能很熟悉这种方法(@Builder)。

这里 Forge 建议使用 builder 对 Food 进行构造,我们一般构造:

  • Builder.nutrition(...),营养价值。每个食物都拥有自己固定的营养价值(nutritional value)。营养价值是回复饱和度(saturation)与饥饿值(Hunger)的比值;

  • Builder.saturationMod(...),饱和度,对应构造 Food.saturationModifier 属性;

  • Builder.effect(...),使用效果,对应构造 Food.effects 属性。

    这是向 Food 类中添加了效果对(Pair<Supplier, Float>),一个效果对的第一项是生成效果实例的 supplier(因为需要动态生成实例,所以使用 supplier),第二项是出现效果的概率(Float),最后使用一个列表来保存这些效果,表示可能可以触发多个效果。很好理解对吧?

  • Builder.alwaysEat(...),饱了也可以吃的属性,对应构造 Food.canAlwaysEat 属性。

  • ……(更多请自己查看源码)

我们还想看看 EffectInstance 是如何表示效果的。

首先很显然,EffectInstance 中的 Effect 类型的 portion 属性指明了效果的类型(对效果的自定义我们以后再讨论),我们这里就讨论如何利用程序中已有的效果来生成效果实例。

程序已有的效果存在 Effects.* 中,你可以使用 IDE 来提示查看。

除了 Effect,我们看构造函数中还能设置一些参数,它们分别是干什么的呢?

  • durationdurationIn 就是 duration 的传入参数),用整型描述**效果持续时间**;
  • amplifier,用整型描述**效果等级**;
  • ambient是否是范围效果
  • splash是否展示效果粒子
  • ……;

也就是说,MC 将效果和出现效果的概率解耦,放在使用到效果的地方进行指定。

1.5.B.2 Fuel & IForgeItem

你可能会很奇怪,为什么在 Item 类中并没有找到 getBurnTime 这个方法啊?为什么说实现它就能作为燃料了呢?

实际上,Forge 中的 Item 类型除了继承了一些工具类以外,还 implements 了 IForgeItem 的 Forge 底层接口。这个接口内有更多的描述 Item 属性的方法,它们大多数都使用 default 关键字修饰,直接在接口中实现了,这就是为什么你在 Item 中看不到的原因。

除了 getBurnTime 以外,还有一些方法例如 isSheidcanDisableSheid 之类的方法,可以让我们定义一些类似盾牌特性的物品。更多有意思的方法请参见反汇编的 Java 代码(Forge 源码)。

其中 getBurnTime 的第二参数是 IRecipeType<?>,它不仅仅被用在 getBurnTime 这里,还会被用在各种加工 item 的地方。可以简单理解,这个类的作用是指明当前的 item 作为原料究竟在哪个地方被处理

比如,作为熔炉的原料?营火的?烟熏器的?爆炸熔炉的?切石机的?工作台的?

例如,如果你向熔炉加了煤炭作燃料、生鸡肉作原料,那么此时就会调用生鸡肉对象的 getBurnTime 方法,传入的 ItemStack 是 煤炭对象所在的 stack,IRecipeType 是 “smelting”(熔炉熔炼)类型。预先定义的对象如下:

注:crafting 表示原料在工作台、smelting 表示原料在熔炉、blasting 表示原料在爆炸熔炉、smoking 表示原料在烟炉、campfile_cooking 表示原料在营火、smithing 表示原料在锻造台、stonecutting 表示原料在切石机……

显然在 getBurnTime 函数中,传入的参数不会是 crafting / stonecutting / smithing

这就在告诉我们,学习 forge 的时候,如果网络上找不到相关资料,那一定要善于查看源码。比如,“如何定义一个对火焰免疫的 Item 呢?”、“如何定义一个不可被破坏的 Item 呢?”、“如果定义一个 Item 在食用时的声音呢?”

诸如此类的问题可能很难从网上直接找答案,而又羞于向社区里的大佬提问,那么查看相关类型的源码可能对你有很大帮助。