这一讲我们将达成两个目标:
我们先编写一个最最基础的方块类,并为其指定材料、硬度、和爆炸抗性,同时为对应的物品指定创造模式物品栏:
@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.png
和 energy_side.png
)。
在添加这两张材质的同时,我们不要忘了让 item
目录下的同名文件( machine.json
)引用该 JSON:
{ "parent": "fedemo:block/machine" }
现在打开游戏。如一切顺利,方块和对应物品均应正常显示:
如果想要让方块存储复杂的数据,执行复杂的行为,方块实体( 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()
(代表未知类型)即可。 由于每个方块实体都分别对应一个 TileEntity
的实例,因此我们可以将数据直接以字段的方式存放在 TileEntity
中。唯一不同的是,为了让我们的数据能够映射到 NBT,我们需要同时实现 TileEntity
的 read
和 write
两个方法:
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); }
read
和 write
两个方法反映的分别是方块实体的反序列化和序列化两个过程。一个 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
。 canExtract
和 extractEnergy
两个方法的返回值。 为了更方便地调整方块实体的能量,我们为方块实体类添加一个 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
),从而确定方块实体的实际承受能力,这样才能按需存入能量。
以下是打开游戏后的显示结果。
这一部分添加的文件有:
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