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
。程序就是通过判断一个 Item
的 food
属性是否为空,来判断这个 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
,我们看构造函数中还能设置一些参数,它们分别是干什么的呢?
duration
(durationIn
就是duration
的传入参数),用整型描述**效果持续时间**;amplifier
,用整型描述**效果等级**;ambient
,是否是范围效果;splash
,是否展示效果粒子;- ……;
也就是说,MC 将效果和出现效果的概率解耦,放在使用到效果的地方进行指定。
1.5.B.2 Fuel
& IForgeItem
你可能会很奇怪,为什么在 Item
类中并没有找到 getBurnTime
这个方法啊?为什么说实现它就能作为燃料了呢?
实际上,Forge 中的 Item
类型除了继承了一些工具类以外,还 implements 了 IForgeItem
的 Forge 底层接口。这个接口内有更多的描述 Item
属性的方法,它们大多数都使用 default
关键字修饰,直接在接口中实现了,这就是为什么你在 Item
中看不到的原因。
除了 getBurnTime
以外,还有一些方法例如 isSheid
、canDisableSheid
之类的方法,可以让我们定义一些类似盾牌特性的物品。更多有意思的方法请参见反汇编的 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
在食用时的声音呢?”
诸如此类的问题可能很难从网上直接找答案,而又羞于向社区里的大佬提问,那么查看相关类型的源码可能对你有很大帮助。