转载

Forge 能量系统简述(二)

这一讲我们将达成两个目标:

  • 制造一个作为用电器的机器方块,且当实体生物站在该方块上时耗费能量为实体回血。
  • 使电池在右键方块时可以将自己的能量转移到特定方块,按住 Shift 右键则反过来。

添加方块

我们先编写一个最最基础的方块类,并为其指定材料、硬度、和爆炸抗性,同时为对应的物品指定创造模式物品栏:

@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
public class FEDemoMachineBlock extends Block  
{
    public static final String NAME = "fedemo:machine";

    @ObjectHolder(NAME)
    public static FEDemoMachineBlock BLOCK;

    @SubscribeEvent
    public static void onRegisterBlock(@Nonnull RegistryEvent.Register<Block> event)
    {
        FEDemo.LOGGER.info("Registering machine block ...");
        event.getRegistry().register(new FEDemoMachineBlock().setRegistryName(NAME));
    }

    @SubscribeEvent
    public static void onRegisterItem(@Nonnull RegistryEvent.Register<Item> event)
    {
        FEDemo.LOGGER.info("Registering machine item ...");
        event.getRegistry().register(new BlockItem(BLOCK, new Item.Properties().group(ItemGroup.MISC)).setRegistryName(NAME));
    }

    private FEDemoMachineBlock()
    {
        super(Block.Properties.create(Material.IRON).hardnessAndResistance(3));
    }
}

这里使用了 ObjectHolder 注解来使 Forge 自动注入对应的方块类型的实例。注意该注解的参数正是方块的注册名。

然后我们添加语言文件:

"block.fedemo.machine": "FE Heal Machine"

以及同名方块状态 JSON 文件( machine.json ):

{
  "variants": {
    "": {
      "model": "fedemo:block/machine"
    }
  }
}

该 JSON 文件指向同名材质描述文件。

我们创建 machine.json 文件,该文件的上一级目录名应为 block

{
  "parent": "block/cube_bottom_top",
  "textures": {
    "bottom": "block/furnace_top",
    "top": "fedemo:block/machine_top",
    "side": "fedemo:block/energy_side"
  }
}

该文件复用了熔炉的 JSON 材质,并引用了两张额外的材质( machine_top.pngenergy_side.png )。

在添加这两张材质的同时,我们不要忘了让 item 目录下的同名文件( machine.json )引用该 JSON:

{
  "parent": "fedemo:block/machine"
}

现在打开游戏。如一切顺利,方块和对应物品均应正常显示:

Forge 能量系统简述(二)

为方块添加方块实体

如果想要让方块存储复杂的数据,执行复杂的行为,方块实体( TileEntity )是必不可少的。更重要的一点是, TileEntity 本身实现了 ICapabilityProvider 接口,因此如果我们想要声明一个方块拥有能量,我们必须为该方块指定方块实体。

添加 TileEntity 前必须首先添加 TileEntityType 。和方块物品等类似, TileEntityType 本身也有注册事件,因此我们要监听这一事件并将 TileEntityType 的实例注册进去:

@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
public class FEDemoMachineTileEntity extends TileEntity  
{
    public static final String NAME = "fedemo:machine";

    @ObjectHolder(NAME)
    public static TileEntityType<FEDemoMachineTileEntity> TILE_ENTITY_TYPE;

    @SubscribeEvent
    public static void onRegisterTileEntityType(@Nonnull RegistryEvent.Register<TileEntityType<?>> event)
    {
        FEDemo.LOGGER.info("Registering machine tile entity type ...");
        event.getRegistry().register(TileEntityType.Builder.create(FEDemoMachineTileEntity::new, FEDemoMachineBlock.BLOCK).build(DSL.remainderType()).setRegistryName(NAME));
    }

    private FEDemoMachineTileEntity()
    {
        super(TILE_ENTITY_TYPE);
    }
}

除去注册名外,构造一个 TileEntityType 一共需要不少于三个参数:

  • create 方法的第一个参数代表方块实体的构造器,而后续参数代表能够和方块实体相容的方块类型(由于是变长参数,因此可传入多个),这里直接传入对应方块就好了。
  • build 方法的唯一参数代表方块实体 NBT 类型。该类型由 Mojang 官方的 DataFixer( com.mojang.datafixers )定义,这里直接取 DSL.remainderType() (代表未知类型)即可。

为方块实体添加 Capability

由于每个方块实体都分别对应一个 TileEntity 的实例,因此我们可以将数据直接以字段的方式存放在 TileEntity 中。唯一不同的是,为了让我们的数据能够映射到 NBT,我们需要同时实现 TileEntityreadwrite 两个方法:

private int energy = 0;

@Override
public void read(@Nonnull CompoundNBT compound)  
{
    this.energy = compound.getInt("MachineEnergy");
    super.read(compound);
}

@Nonnull
@Override
public CompoundNBT write(@Nonnull CompoundNBT compound)  
{
    compound.putInt("MachineEnergy", this.energy);
    return super.write(compound);
}

readwrite 两个方法反映的分别是方块实体的反序列化和序列化两个过程。一个 TileEntity 通过这两个方法实现了和 NBT 复合标签的映射。

现在我们来实现 getCapability 方法。在上面的内容中我们提到过, TileEntity 本身实现了 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) && side.getAxis().isHorizontal();
    return isEnergy ? this.lazyOptional.cast() : super.getCapability(cap, side);
}

注意相较物品,我们的 getCapability 方法在判断时额外判定了传入的是否为水平朝向(东南西北)。通过这种方法我们可以设定输入输出能量相较朝向的限制,在这里我们直接禁止了能量在上下两个朝向的交互。

然后我们构造 LazyOptional<IEnergyStorage> 的实例:

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)
        {
            FEDemoMachineTileEntity.this.energy += diff;
        }
        return diff;
    }

    @Override
    public int extractEnergy(int maxExtract, boolean simulate)
    {
        return 0;
    }

    @Override
    public int getEnergyStored()
    {
        return Math.max(0, Math.min(this.getMaxEnergyStored(), FEDemoMachineTileEntity.this.energy));
    }

    @Override
    public int getMaxEnergyStored()
    {
        return 192_000;
    }

    @Override
    public boolean canExtract()
    {
        return false;
    }

    @Override
    public boolean canReceive()
    {
        return true;
    }
});

和基于物品的实现,基于方块实体的实现有以下几点不同:

  • 直接通过修改 energy 字段调整能量。
  • getMaxEnergyStored 返回的是最大存储能量,这里设置为 192000
  • 由于是作为用电器的机器,所以能量是只进不出的。注意 canExtractextractEnergy 两个方法的返回值。

为方块实现具体功能

为了更方便地调整方块实体的能量,我们为方块实体类添加一个 heal 方法用于回血,一次回复 0.1 点(约一秒一颗心):

public void heal(@Nonnull LivingEntity entity)  
{
    int diff = Math.min(this.energy, 100);
    if (diff > 0)
    {
        this.energy -= diff;
        entity.heal((float) diff / 1_000);
    }
}

若想判断实体是否接触了方块,我们需要利用方块的 onEntityCollision 方法。原版 Minecraft 会在实体进入方块所处区域时触发该方法,我们覆盖 Block 类的这一方法即可:

@Override
@SuppressWarnings("deprecation")
public void onEntityCollision(@Nonnull BlockState state, @Nonnull World world, @Nonnull BlockPos pos, @Nonnull Entity entity)  
{
    if (!world.isRemote && entity instanceof LivingEntity)
    {
        LivingEntity livingEntity = (LivingEntity) entity;
        if (livingEntity.getHealth() < livingEntity.getMaxHealth())
        {
            TileEntity tileEntity = world.getTileEntity(pos);
            if (tileEntity instanceof FEDemoMachineTileEntity)
            {
                ((FEDemoMachineTileEntity) tileEntity).heal(livingEntity);
            }
        }
    }
}

在上面的方法里我们主要检查了四件事,如果四件事均满足我们便调用方块实体类的 heal 方法:

!world.isRemote
entity instanceof LivingEntity
livingEntity.getHealth() < livingEntity.getMaxHealth()
tileEntity instanceof FEDemoMachineTileEntity

最后,为了让我们的实体进入方块所处区域,我们需要重新定义碰撞箱,不能让碰撞箱占满整个方块:

@Nonnull
@Override
@SuppressWarnings("deprecation")
public VoxelShape getCollisionShape(@Nonnull BlockState state, @Nonnull IBlockReader world, @Nonnull BlockPos pos, @Nonnull ISelectionContext context)  
{
    return Block.makeCuboidShape(0, 0, 0, 16, 15, 16);
}

代码很简单,只是让高度也就是 Y 轴从 16 变成了 15 而已,X 轴和 Z 轴方向都没有变。

为物品实现具体功能

现在进入到这一讲的最后一步,也就是实现电池右键方块的行为。原版 Minecraft 会在物品右键方块时调用 Item 类的 onItemUse 方法,因此我们可以通过覆盖这一方法实现相应行为:

@Nonnull
@Override
public ActionResultType onItemUse(@Nonnull ItemUseContext context)  
{
    World world = context.getWorld();
    if (!world.isRemote)
    {
        TileEntity tileEntity = world.getTileEntity(context.getPos());
        if (tileEntity != null)
        {
            Direction side = context.getFace();
            tileEntity.getCapability(CapabilityEnergy.ENERGY, side).ifPresent(e ->
            {
                this.transferEnergy(context, e);
                this.notifyPlayer(context, e);
            });
        }
    }
    return ActionResultType.SUCCESS;
}

private void notifyPlayer(@Nonnull ItemUseContext context, @Nonnull IEnergyStorage target)  
{
    PlayerEntity player = context.getPlayer();
    if (player != null)
    {
        String msg = target.getEnergyStored() + " FE / " + target.getMaxEnergyStored() + " FE";
        player.sendMessage(new StringTextComponent(msg).applyTextStyle(TextFormatting.GRAY));
    }
}

private void transferEnergy(@Nonnull ItemUseContext context, @Nonnull IEnergyStorage target)  
{
    // TODO
}
getCapability
transferEnergy
notifyPlayer

我们现在实现 transferEnergy 方法:

private void transferEnergy(@Nonnull ItemUseContext context, @Nonnull IEnergyStorage target)  
{
    context.getItem().getCapability(CapabilityEnergy.ENERGY).ifPresent(e ->
    {
        if (context.isPlacerSneaking())
        {
            if (target.canExtract())
            {
                int diff = e.getMaxEnergyStored() - e.getEnergyStored();
                e.receiveEnergy(target.extractEnergy(diff, false), false);
            }
        }
        else
        {
            if (target.canReceive())
            {
                int diff = target.receiveEnergy(e.getEnergyStored(), true);
                target.receiveEnergy(e.extractEnergy(diff, false), false);
            }
        }
    });
}

我们获取了物品本身对应的 IEnergyStorage 后,判断玩家是否按下 Shift。

接下来进入到了两个分支。我们先从第一个分支,也就是玩家按下 Shift 取出能量开始看:

if (target.canExtract())  
{
    int diff = e.getMaxEnergyStored() - e.getEnergyStored();
    e.receiveEnergy(target.extractEnergy(diff, false), false);
}

一个重要的问题是取出多少能量。很明显,为了达成“能取多少取多少”的目标,我们需要划定一个可以承受的上限,这个上限自然是电池还可以容纳的能量。

我们计算出数值后存放到 diff 变量下,然后我们调用方块实体的 extractEnergy 方法以及和物品相关的 receiveEnergy 方法就可以了。

现在我们来看第二个分支,也就是玩家不按下 Shift 存入能量:

if (target.canReceive())  
{
    int diff = target.receiveEnergy(e.getEnergyStored(), true);
    target.receiveEnergy(e.extractEnergy(diff, false), false);
}

整段实现和取出能量类似,但具体上仍有细微的差别。除了存取能量的身份对调外,我们还要注意一点: diff 变量的值为什么不是 e.getEnergyStored()

我们当然要贯彻“能存多少存多少”的目标,因此这里的上限自然应该是电池内部已存储的能量,但这里涉及到存取能量的细微差别:如果目标能量超过了方块实体能够取出的范围,那我们只能从方块实体中取出比目标要少的能量;但如果目标能量超出了方块实体能够存入的范围,我们是真的能从电池中取出目标能量的。因此,如果把高于承受能力的能量强行存入方块实体,这只会导致多出来的能量浪费,所以说我们在实际操作前必须模拟存入一次(调用 receiveEnergy 方法并将 simulate 设为 true ),从而确定方块实体的实际承受能力,这样才能按需存入能量。

以下是打开游戏后的显示结果。

Forge 能量系统简述(二)

代码清单

这一部分添加的文件有:

src/main/java/com/github/ustc_zzzz/fedemo/block/FEDemoMachineBlock.java
src/main/java/com/github/ustc_zzzz/fedemo/tileentity/FEDemoMachineTileEntity.java
src/main/resources/assets/fedemo/blockstates/machine.json
src/main/resources/assets/fedemo/models/block/machine.json
src/main/resources/assets/fedemo/models/item/machine.json
src/main/resources/assets/fedemo/textures/block/energy_side.png
src/main/resources/assets/fedemo/textures/block/machine_top.png

这一部分修改的文件有:

src/main/java/com/github/ustc_zzzz/fedemo/item/FEDemoBatteryItem.java
src/main/resources/assets/fedemo/lang/en_us.json
原文  https://blog.ustc-zzzz.net/forge-energy-demo-2/
正文到此结束
Loading...