Forge 能量系统简述(一)
虽然夹杂着众多争议,但 Forge 最终仍然决定在 1.10.2 加入官方的能量系统,并一直将其延续到现在。该系统参考了 CoFH 团队的众多设计,因此和在此前已经拥有鼎鼎大名的 Redstone Flux 能量系统有着众多的相似之处。
在本系列教程中,各位读者将走入 Forge 能量系统所带来的奇妙世界。由于本文将使用 Minecraft 1.14.4 和 Forge 28.2.4 进行讲解,因此如果读者想要顺畅阅读本教程,那么有一些要求是需要满足的:
- 已能够相对熟练地使用 Java 8 编写代码和设计程序。
- 已能够基于 Minecraft 1.14.4 和 Forge 添加简单的方块或物品。
本系列教程相关源代码:FEDemo.zip(82.5 KiB)。
废话不多说,我们开始吧。
准备工作
我们决定将 ModID 起名为 fedemo
,以下是 META-INF/mods.toml
文件:
modLoader="javafml"
loaderVersion="[28,)"
[[mods]]
modId="fedemo"
version="${file.jarVersion}"
displayName="FE Demonstration Mod"
description="Demonstration for Forge Energy"
[[dependencies.fedemo]]
modId="forge"
mandatory=true
versionRange="[28.2,)"
ordering="NONE"
side="BOTH"
[[dependencies.fedemo]]
modId="minecraft"
mandatory=true
versionRange="[1.14.4]"
ordering="NONE"
side="BOTH"
以下是主类,非常简洁:
@Mod("fedemo")
public class FEDemo
{
public static final Logger LOGGER = LogManager.getLogger(FEDemo.class);
}
本系列教程的所有 Java 代码均在 com.github.ustc_zzzz.fedemo
包下。
Capability 系统
Capability 系统是 Forge 能量系统的基石。
Capability 系统对原版游戏元素和第三方行为(大多数情况下和 Mod 有关)实施了一定程度的解耦合。具体来说,Mod 开发者可通过调用 getCapability
方法获取并操纵特定的第三方行为。getCapability
方法由 ICapabilityProvider
接口声明,而 Forge 为很多游戏元素都实现了这一接口,比如我们耳熟能详的物品堆叠(ItemStack
)、实体(Entity
)、方块实体(TileEntity
)等。
CapabilityDispatcher
是一类特殊的 ICapabilityProvider
,因为它可以存有多个 ICapabilityProvider
。刚才我们提到的物品堆叠、实体、方块实体等游戏元素,内部都存在一个由 Forge 提供的 CapabilityDispatcher
,这使得我们向已有的游戏元素添加 ICapabilityProvider
成为可能。
我们来看 getCapability
方法的声明:
@Nonnull <T> LazyOptional<T> getCapability(@Nonnull Capability<T> cap, Direction side);
getCapability
方法的第一个参数代表的是特定的 Capability,我们可以通过 CapabilityEnergy.ENERGY
来拿到它,从而为实现 Forge 能量系统铺路;getCapability
方法的第二个参数代表一个方向,在和方块实体打交道的时候我们用得着。
为物品添加 Capability
我们决定制作一个存储 FE 的电池。我们先编写一个最最基础的物品类,并为其指定创造模式物品栏和最大物品数量:
@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
public class FEDemoBatteryItem extends Item
{
public static final String NAME = "fedemo:battery";
@SubscribeEvent
public static void onRegisterItem(@Nonnull RegistryEvent.Register<Item> event)
{
FEDemo.LOGGER.info("Registering battery item ...");
event.getRegistry().register(new FEDemoBatteryItem().setRegistryName(NAME));
}
private FEDemoBatteryItem()
{
super(new Item.Properties().maxStackSize(1).group(ItemGroup.MISC));
}
}
Forge 为 Item
额外追加了 initCapabilities
方法,这个方法的返回值是 ICapabilityProvider
,我们需要覆盖这个方法:
@Override
public ICapabilityProvider initCapabilities(@Nonnull ItemStack stack, CompoundNBT nbt)
{
return new ICapabilityProvider()
{
private LazyOptional<IEnergyStorage> lazyOptional; // TODO
@Nonnull
@Override
public <T> LazyOptional<T> getCapability(@Nonnull Capability<T> cap, Direction side)
{
boolean isEnergy = Objects.equals(cap, CapabilityEnergy.ENERGY);
return isEnergy ? this.lazyOptional.cast() : LazyOptional.empty();
}
};
}
我们注意到了 LazyOptional
的存在。LazyOptional
类由 Forge 提供,本质和 Java 的 Optional 类似,不过其内部的实例只在用到的时候才会加载。我们在参数为 CapabilityEnergy.ENERGY
的时候返回一个预先准备好的 LazyOptional
,否则便返回一个 LazyOptional.empty()
。
CapabilityEnergy.ENERGY
的类型是 Capability<IEnergyStorage>
,因此我们要实现的也是 IEnergyStorage
。
物品能量的具体实现
ItemStack
存储三种数据:物品类型、数量、和 NBT。很明显,我们的物品能量只能放在 NBT 里。
我们会用到以下五个方法操纵 NBT:
hasTag
:检查一个ItemStack
是否拥有 NBT。getTag
:返回一个ItemStack
的 NBT,如果不存在则返回null
。getOrCreateTag
:返回一个ItemStack
的 NBT,如果不存在则为其创建一个。putInt
:设置 NBT 复合标签的特定整数值。getInt
:获取 NBT 复合标签的特定整数值,如果值不存在或者不是整数则返回0
。
很好。以下是具体实现:
private final LazyOptional<IEnergyStorage> lazyOptional = LazyOptional.of(() -> new IEnergyStorage()
{
@Override
public int receiveEnergy(int maxReceive, boolean simulate)
{
int energy = this.getEnergyStored();
int diff = Math.min(this.getMaxEnergyStored() - energy, maxReceive);
if (!simulate)
{
stack.getOrCreateTag().putInt("BatteryEnergy", energy + diff);
}
return diff;
}
@Override
public int extractEnergy(int maxExtract, boolean simulate)
{
int energy = this.getEnergyStored();
int diff = Math.min(energy, maxExtract);
if (!simulate)
{
stack.getOrCreateTag().putInt("BatteryEnergy", energy - diff);
}
return diff;
}
@Override
public int getEnergyStored()
{
if (stack.hasTag())
{
int energy = Objects.requireNonNull(stack.getTag()).getInt("BatteryEnergy");
return Math.max(0, Math.min(this.getMaxEnergyStored(), energy));
}
return 0;
}
@Override
public int getMaxEnergyStored()
{
return 48_000;
}
@Override
public boolean canExtract()
{
return true;
}
@Override
public boolean canReceive()
{
return true;
}
});
代码有点复杂,我们一个方法一个方法拆开看。
canReceive
代表是否能输入能量,这里我们让它返回true
。canExtract
代表是否能输出能量,这里我们也让它返回true
。getMaxEnergyStored
代表内部能够存储的最大能量,这里我们设定在48000
。getEnergyStored
代表内部存储的实际能量,这里我们通过物品的 NBT 读取能量。extractEnergy
代表实施输出能量的行为,其中simulate
参数代表是否为模拟行为。receiveEnergy
代表实施输入能量的行为,其中simulate
参数代表是否为模拟行为。
extractEnergy
和 receiveEnergy
各自均接收一个 int
作为参数,并生成 int
作为返回值。其中,作为参数传入的 int
代表期望输入输出的能量,而作为返回值的 int
代表实际输入输出的能量。这两个参数都非常重要,希望读者能够加以注意。
杂项
我们可以向 en_us.json
这一语言文件里添加一项:
"item.fedemo.battery": "FE Battery"
我们还可以通过覆盖 addInformation
方法添加额外的提示文本:
@Override
@OnlyIn(Dist.CLIENT)
public void addInformation(@Nonnull ItemStack stack, World world, @Nonnull List<ITextComponent> tooltip, @Nonnull ITooltipFlag flag)
{
stack.getCapability(CapabilityEnergy.ENERGY).ifPresent(e ->
{
String msg = e.getEnergyStored() + " FE / " + e.getMaxEnergyStored() + " FE";
tooltip.add(new StringTextComponent(msg).applyTextStyle(TextFormatting.GRAY));
});
}
我们也可以通过覆盖 fillItemGroup
方法为创造模式物品栏添加多个物品,分别对应 0 FE,12000 FE,24000 FE,36000 FE,和 48000 FE:
@Override
public void fillItemGroup(@Nonnull ItemGroup group, @Nonnull NonNullList<ItemStack> items)
{
if (this.isInGroup(group))
{
IntStream.rangeClosed(0, 4).forEach(i ->
{
ItemStack stack = new ItemStack(this);
stack.getCapability(CapabilityEnergy.ENERGY).ifPresent(e ->
{
int energy = e.getMaxEnergyStored() / 4 * i;
e.receiveEnergy(energy, false);
items.add(stack);
});
});
}
}
到目前为止,我们已经解决了除材质外的所有问题了。
材质
我们为电池绘制了五种材质,分别对应电量空到电量满等五种情况,也恰好对应创造模式物品栏的五个物品:
默认情况自然是电量空,那我们怎么映射剩下的四种情况呢?原版 Minecraft 为我们提供了 Item Property Override 机制,该机制使得根据 NBT 动态调整材质成为可能。
欲使用 Item Property Override,我们只需在构造方法中添加下面一句:
this.addPropertyOverride(new ResourceLocation("energy"), (stack, world, entity) ->
{
LazyOptional<IEnergyStorage> lazyOptional = stack.getCapability(CapabilityEnergy.ENERGY);
return lazyOptional.map(e -> (float) e.getEnergyStored() / e.getMaxEnergyStored()).orElse(0.0F);
});
这样我们的物品就有了一个名为 energy
的属性,我们在描述材质的 JSON 文件(应为 battery.json
)写下:
{
"parent": "item/generated",
"textures": {
"layer0": "fedemo:item/battery"
},
"overrides": [
{
"predicate": {
"energy": 0.125
},
"model": "fedemo:item/battery1"
},
{
"predicate": {
"energy": 0.375
},
"model": "fedemo:item/battery2"
},
{
"predicate": {
"energy": 0.625
},
"model": "fedemo:item/battery3"
},
{
"predicate": {
"energy": 0.875
},
"model": "fedemo:item/battery4"
}
]
}
注意和普通 JSON 相比,该文件额外多出了 override
部分,其中 predicate
判定的是当前属性值是否不小于提供的值,因此我们在该 JSON 中将 energy
划分为了五档,从而应对五种可能的情况。
接下来我们只需要完善 battery1.json
到 battery4.json
就可以了。以下是 battery4.json
的全部内容:
{
"parent": "item/generated",
"textures": {
"layer0": "fedemo:item/battery4"
}
}
以下是打开游戏后的显示结果。
代码清单
这一部分添加的文件有:
src/main/java/com/github/ustc_zzzz/fedemo/FEDemo.java
src/main/java/com/github/ustc_zzzz/fedemo/item/FEDemoBatteryItem.java
src/main/resources/pack.mcmeta
src/main/resources/META-INF/mods.toml
src/main/resources/assets/fedemo/lang/en_us.json
src/main/resources/assets/fedemo/models/item/battery.json
src/main/resources/assets/fedemo/models/item/battery1.json
src/main/resources/assets/fedemo/models/item/battery2.json
src/main/resources/assets/fedemo/models/item/battery3.json
src/main/resources/assets/fedemo/models/item/battery4.json
src/main/resources/assets/fedemo/textures/item/battery.png
src/main/resources/assets/fedemo/textures/item/battery1.png
src/main/resources/assets/fedemo/textures/item/battery2.png
src/main/resources/assets/fedemo/textures/item/battery3.png
src/main/resources/assets/fedemo/textures/item/battery4.png