在这一讲我们将制造一个作为发电机的机器方块:
以下是方块类的基础实现:
@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"
打开游戏就可以看到效果了:
我们仍然使用一个 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; } 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(); 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 的能量,那势必会导致能量会全部被北侧的方块吸走。因此,我们为了雨露均沾,必须每次注入能量时人为调整能量的优先级。当然了,可以考虑的实现有很多,这里读者可以尽情地发挥自己的想象力。
现在打开游戏,能量应能正常收集并传输了。
这一部分添加的文件有:
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