转载

Forge 能量系统简述(三)

在这一讲我们将制造一个作为发电机的机器方块:

  • 该方块收集太阳能作为能量来源。
  • 该方块能够向周围方块输出能量。

添加方块和方块实体

以下是方块类的基础实现:

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

    @ObjectHolder(NAME)
    public static FEDemoGeneratorBlock BLOCK;

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

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

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

    @Override
    public boolean hasTileEntity(@Nonnull BlockState state)
    {
        return true;
    }

    @Override
    public TileEntity createTileEntity(@Nonnull BlockState state, @Nonnull IBlockReader world)
    {
        return FEDemoGeneratorTileEntity.TILE_ENTITY_TYPE.create();
    }
}

以下是方块实体类的基础实现:

@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
public class FEDemoGeneratorTileEntity extends TileEntity implements ITickableTileEntity  
{
    public static final String NAME = "fedemo:generator";

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

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

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

方块和方块实体类的实现和上一讲针对用电器的实现大同小异。

然后我们指定方块状态 JSON( generator.json ):

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

接下来是描述方块材质的同名 JSON( generator.json ):

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

以及描述方块对应物品的同名 JSON( generator.json ):

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

相较上一讲,我们额外添加了 generator_top.png 作为发电机顶部的新材质。

最后我们补充语言文件( en_us.json ):

"block.fedemo.generator": "FE Energy Generator"

打开游戏就可以看到效果了:

Forge 能量系统简述(三)

为方块实体实现 Capability

我们仍然使用一个 int 字段存储方块实体的能量,并将其通过 readwrite 方法和 NBT 映射:

private int energy = 0;

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

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

然后我们基于此实现我们自己的 LazyOptional<IEnergyStorage> 和基于能量的 Capability 实现:

private final LazyOptional<IEnergyStorage> lazyOptional = LazyOptional.of(() -> new IEnergyStorage()  
{
    @Override
    public int receiveEnergy(int maxReceive, boolean simulate)
    {
        return 0;
    }

    @Override
    public int extractEnergy(int maxExtract, boolean simulate)
    {
        int energy = this.getEnergyStored();
        int diff = Math.min(energy, maxExtract);
        if (!simulate)
        {
            FEDemoGeneratorTileEntity.this.energy -= diff;
        }
        return diff;
    }

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

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

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

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

@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);
}

这里的实现和上一讲针对用电器的实现类似,唯一的不同之处在于:发电机的电量应该是只出不进的。注意 canReceivereceiveEnergy 两个方法的返回值。

为方块实体实现功能

我们既然希望方块收集太阳能,那我们自然是希望方块实体所存储的能量随时间递增。这需要我们让我们的方块实体每 tick 执行一段代码,原版 Minecraft 为我们提供了 ITickableTileEntity 接口。我们只需要让我们的类在继承 TileEntity 的同时实现这一接口即可:

public class FEDemoGeneratorTileEntity extends TileEntity implements ITickableTileEntity  
{
    // ...

    @Override
    public void tick()
    {
        if (this.world != null && !this.world.isRemote)
        {
            this.generateEnergy(this.world);
            this.transferEnergy(this.world);
        }
    }

    private void generateEnergy(@Nonnull World world)
    {
        // TODO
    }

    private void transferEnergy(@Nonnull World world)
    {
        // TODO
    }

    // ...
}

我们先从 generateEnergy 方法的实现开始:

private void generateEnergy(@Nonnull World world)  
{
    if (world.getDimension().hasSkyLight())
    {
        int light = world.getLightFor(LightType.SKY, this.pos.up()) - world.getSkylightSubtracted();
        this.energy = Math.min(192_000, this.energy + 10 * Math.max(0, light - 10));
    }
}

表达式 world.getLightFor(LightType.SKY, this.pos.up()) - world.getSkylightSubtracted() 返回的是当前方块上方的天空亮度值,不超过 15。它的下一行代码规定了亮度和能量的映射关系:亮度不超过 10 时不增加 FE,超过 10 后每增加 1 每 tick 相应增加 10 FE,亮度为 15 时为 50 FE。最后别忘了不要让能量值超过能够存储的最大值。

然后我们实现 transferEnergy 方法。

能量的主动输出

我们希望实现发电机和用电器相邻时传输能量的功能,但仅仅为两个机器实现能量相关的 Capability 是远远不够的:计算机程序不是物理定律,不会出现自然而然的能量流动,换言之,我们需要手写能量流动的相关代码。那么这段代码到底应该是“发电机主动输出能量”,还是“用电器主动吸收能量”呢?答案是显然的:我们应该让发电机控制能量的流动,因此,我们需要让我们的发电机对应的方块实体每 tick 自动搜寻附近的方块实体,并分别注入能量。

我们现在来实现 transferEnergy 方法:

private final Queue<Direction> directionQueue = Queues.newArrayDeque(Direction.Plane.HORIZONTAL);

private void transferEnergy(@Nonnull World world)  
{
    this.directionQueue.offer(this.directionQueue.remove());
    for (Direction direction : this.directionQueue)
    {
        TileEntity tileEntity = world.getTileEntity(this.pos.offset(direction));
        if (tileEntity != null)
        {
            tileEntity.getCapability(CapabilityEnergy.ENERGY, direction.getOpposite()).ifPresent(e ->
            {
                if (e.canReceive())
                {
                    int diff = Math.min(500, this.energy);
                    this.energy -= e.receiveEnergy(diff, false);
                }
            });
        }
    }
}

方法还是相对简单的:通过遍历水平方向的所有相邻方块,然后逐个注入能量,一次最多注入 500 FE。注意在获取相邻方块时,需要获取的是相反的方向(例如对于东侧的方块,注入能量时应该从该方块的西侧注入),也就是对 Direction 调用 getOpposite 方法并取其返回值。

唯一可能令人费解的是这一行:

this.directionQueue.offer(this.directionQueue.remove());

通过 directionQueue 字段的声明我们可以注意到,我们把该队列的第一个元素取出放到了最后一个元素的位置,这是为什么呢?

我们思考一下如何不这么做会发生什么:

  • 首先找到北侧的方块并注入能量。
  • 然后找到东侧的方块并注入能量。
  • 接着找到南侧的方块并注入能量。
  • 最后找到西侧的方块并注入能量。

我们可以注意到,如果只是平凡地遍历,那么北侧的方块将永远拥有最大的优先级。如果我们每 tick 只能产出 50 FE 能量,但北侧的方块一次可以吸收 200 FE 的能量,那势必会导致能量会全部被北侧的方块吸走。因此,我们为了雨露均沾,必须每次注入能量时人为调整能量的优先级。当然了,可以考虑的实现有很多,这里读者可以尽情地发挥自己的想象力。

现在打开游戏,能量应能正常收集并传输了。

Forge 能量系统简述(三)

代码清单

这一部分添加的文件有:

src/main/java/com/github/ustc_zzzz/fedemo/block/FEDemoGeneratorBlock.java
src/main/java/com/github/ustc_zzzz/fedemo/tileentity/FEDemoGeneratorTileEntity.java
src/main/resources/assets/fedemo/blockstates/generator.json
src/main/resources/assets/fedemo/models/block/generator.json
src/main/resources/assets/fedemo/models/item/generator.json
src/main/resources/assets/fedemo/textures/block/generator_top.png

这一部分修改的文件有:

  • src/main/resources/assets/fedemo/lang/en_us.json
原文  https://blog.ustc-zzzz.net/forge-energy-demo-3/
正文到此结束
Loading...