在这一讲我们将制造一个作为发电机的机器方块:
- 该方块收集太阳能作为能量来源。
- 该方块能够向周围方块输出能量。
添加方块和方块实体
以下是方块类的基础实现:
@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"
打开游戏就可以看到效果了:
为方块实体实现 Capability
我们仍然使用一个 int
字段存储方块实体的能量,并将其通过 read
和 write
方法和 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;
if (diff != 0)
{
FEDemoGeneratorTileEntity.this.markDirty();
}
}
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);
}
这里的实现和上一讲针对用电器的实现类似,唯一的不同之处在于:发电机的电量应该是只出不进的。注意 canReceive
和 receiveEnergy
两个方法的返回值。
为方块实体实现功能
我们既然希望方块收集太阳能,那我们自然是希望方块实体所存储的能量随时间递增。这需要我们让我们的方块实体每 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();
int diff = Math.min(192_000 - this.energy, 10 * Math.max(0, light - 10));
if (diff != 0)
{
this.energy += diff;
this.markDirty();
}
}
}
表达式 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 = e.receiveEnergy(Math.min(500, this.energy), false);
if (diff != 0)
{
this.energy -= diff;
this.markDirty();
}
}
});
}
}
}
方法还是相对简单的:通过遍历水平方向的所有相邻方块,然后逐个注入能量,一次最多注入 500 FE。注意在获取相邻方块时,需要获取的是相反的方向(例如对于东侧的方块,注入能量时应该从该方块的西侧注入),也就是对 Direction
调用 getOpposite
方法并取其返回值。
唯一可能令人费解的是这一行:
this.directionQueue.offer(this.directionQueue.remove());
通过 directionQueue
字段的声明我们可以注意到,我们把该队列的第一个元素取出放到了最后一个元素的位置,这是为什么呢?
我们思考一下如何不这么做会发生什么:
- 首先找到北侧的方块并注入能量。
- 然后找到东侧的方块并注入能量。
- 接着找到南侧的方块并注入能量。
- 最后找到西侧的方块并注入能量。
我们可以注意到,如果只是平凡地遍历,那么北侧的方块将永远拥有最大的优先级。如果我们每 tick 只能产出 50 FE 能量,但北侧的方块一次可以吸收 200 FE 的能量,那势必会导致能量会全部被北侧的方块吸走。因此,我们为了雨露均沾,必须每次注入能量时人为调整能量的优先级。当然了,可以考虑的实现有很多,这里读者可以尽情地发挥自己的想象力。
现在打开游戏,能量应能正常收集并传输了。
代码清单
这一部分添加的文件有:
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