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 参数代表是否为模拟行为。

extractEnergyreceiveEnergy 各自均接收一个 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.jsonbattery4.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