# 前言 - 真的很快很简单,真的
许多开发者对Minecraft中如何实现多方块结构感兴趣,鉴于国内相关知识的缺失,开发者们很难将自己的想法付诸实践,这里笔者将带领读者力求简单快速的构建自己的多方块结构。
鉴于本篇教程的基础性,我们并不想探究多方块结构中复杂的位置变换和数学计算,而仅仅是想用最快的方式上手并使用它们。所以在撰写过程中我们并不会深究某些方法的具体实现,而是将重点放在如何使用上,降低读者的阅读门槛。
本文编写使用环境: Minecraft 1.16.5 Forge 36.2.23 ZeroCore2 2.1.9
# 明确思路 - 你的下一个蓄水器何必是蓄水器
审视我们之前见到过的多方块结构,笔者这里想把他们分成两类。首先,例如RailCraft的多方块蓄水器,这种多方块结构往往需要玩家搭建一个立方体结构(这个结构的长宽高往往可以变化),然后在立方体结构上面添加自己需要的交互方块(例如蓄水器阀门)用来与外界交互,笔者希望把这种多方块结构称为立体型多方块结构。
我们似乎还能想到另外一些例子,例如OpenBlocks的蓄水器,可以单个放置使用,但玩家可以通过在旁边摆更多的蓄水器来扩展蓄水器的空间,这种结构往往具有一种扩展性,即围绕一个中心方块(我们第一次放的蓄水器就可以看做中心方块)进行扩展,以扩增原有的功能,笔者希望把这种多方块结构称为扩展性多方块结构。对于扩展型多方块结构,由Ph-苯开发的容积储存可以成为很好的学习例子。

这里笔者将着重介绍立体型多方块结构,当然,有的读者除上述的两种多方块结构外,还想到了例如Immersive Engineering的各类多方块结构,他们的形状并不是非常规则的立方体,这里笔者尚且不介绍这种结构,感兴趣的读者可以尝试阅读IE的源码。
对于立体型多方块结构,我们使用ZeroCore2中提供的Multiblock API进行实现
# 立体型多方块结构 - Powerful Tank
本节将带领读者开发一个多方块储罐,这个储罐利用Forge Energy构建一个多维异次元空间,使用能量立场对流体的分子结构进行压缩 这个多方块储罐可以储存无限桶流体,但需要消耗FE能量, 当能量不足时内部储存的所有流体将消失。
首先我们创建项目并添加所需的依赖,为了使用ZeroCore中Multiblock API,首先我们要引入ZeroCore2。利用ForgeGradle,我们无需手动下载。在build.gradle中的repositories和dependencies中加入以下条目,并且在mods.toml中添加依赖,之后点击IDE中的加载Gradle变更即可。
位于: build.gradle
repositories {
maven {
name = "zerono"
url = "https://maven.zerono.it/"
}
}
dependencies {
//替换成当前的Minecraft版本和ZeroCore2版本
implementation fg.deobf("it.zerono.mods.zerocore:ZeroCore2:1.16.5-2.1.9")
}
# 位于: resources\META-INF\mods.toml
[[dependencies.powerful_tank]]
modId="zerocore"
mandatory=true
versionRange="[1.16.5-2.1.9,)"
ordering="AFTER"
side="BOTH"
ZeroCore是由ZeroNoRyouki开发的一套API,其中包含了网络包、GUI、连接材质、矿物生成等功能,这里我们着重使用ZeroCore的多方块结构API。需要注意的是,本文面向高版本的ZeroCore2开发。
读者需要注意,本篇教程并不适用于Minecraft 1.12.2及以前的旧版ZeroCore,有需要的读者可以阅读原作者发到自己博客的开发教程: Zero CORE multiblock API tutorial,该教程已严重过时,仅适用于1.12.2及更早版本的ZeroCore。
由于原作者尚未针对ZeroCore2编写相适应的WIKI,本文是笔者根据ZeroNoRyouki自己的模组极限反应堆(Extreme Reactors)中相关内容总结理解而来,可能存在错误,欢迎批评指正。
想要创建一个多方块结构,在代码层面我们需要有如下考量,笔者把它们分为虚实两部分:
- 实:什么方块构成了这个多方块结构,这些方块需要提供什么功能。
- 虚:这个多方块结构整体有何功能,如何编写逻辑。
比如Powerful Tank这个例子中,对于实的部分,我们需要设计对应的能量和流体的接口方块,这些方块必然的需要创建对应的TileEntity(或称BlockEntity)来提供数据处理的支持。对于虚的部分,我们可能需要设计一个主控类负责管理能源和流体,这个类所描述的事物是多方块结构的整体效果,我们的设计中心应该在这个类上。
- 虚:这个多方块结构整体有何功能,如何编写逻辑。比如Powerful Tank这个例子中,我们可能需要设计一个MultiBlockTank类,负责管理能源和流体(储存多少流体,能量不足时清空流体),这个类所描述的事物是多方块结构的整体效果,我们的设计中心应该在这个类上。
在ZeroCore2中,主控类实际上被称作MultiblockController, 而组成多方块结构的部件,被称为MultiblockPart,需要注意的是,MultiblockPart继承自TileEntity,换言之,我们不应该在功能方块的XxxBlock类上做文章,而是在XxxTileEntity上做文章。
可能有读者对MultiblockController的说法有误解,这里的MultiblockController与我们在游戏中常见的XXX控制器是没有关联的,对于ZeroCore来说XXX控制器也算是多方块的部件,如果读者阅读极限反应堆的源码可以发现,反应堆控制器的TileEntity也继承自MultiblockPart。反应堆控制器中是不包含反应堆工作的任何逻辑的,相信理解了笔者写的虚和实关系的读者已经明白了。由此也可以看出,我们创建的多方块结构也是完全可以不包含XXX控制器的,在我们的例子中,笔者就并没有设计储罐控制器方块。
综合我们的考量,我们的代码结构应该是这样的

首先,我们先创建代表多方块结构的主控类,在multiblock包中创建类MultiblockTank, 这个类需要继承AbstractCuboidMultiblockController。
因为继承自抽象类,我们需要补全他的所有抽象方法。
位于: powerful_tank/multiblock/MultiblockTank.java
public class MultiblockTank extends AbstractCuboidMultiblockController<MultiblockTank> {
}
读者可能发现, 除了Cuboid外,我们还可以选择Rectangular的主控。对于立方体结构,我们选择Cuboid,而平面的结构一般选择Rectangular,感兴趣的读者可以自己研究Rectangular版本。需要注意的是,在1.12.2版本Rectangular和Cuboid的概念并不是很明确,如果阅读过1.12.2文档的读者在这里可能感到疑惑,请以笔者的版本为准。
使用IDE自动填充所有抽象方法后,我们可以先关注这样几个方法
// 位于: powerful_tank/multiblock/MultiblockTank.java
@Override
// 这里定义了多方块结构中Part的最少数量,我们把储罐外壳也作为Part,所以3*3中空结构正好需要26个Part
protected int getMinimumNumberOfPartsForAssembledMachine() {
return 26;
}
// 多方块结构在X,Y,Z方向上的尺寸,ZeroCore允许创建可变大小的结构,这里我们想使用3*3的固定大小的结构,所以所有方向的尺寸均为3
@Override
protected int getMaximumXSize() {
return 3;
}
@Override
protected int getMinimumXSize() {
return 3;
}
@Override
protected int getMaximumZSize() {
return 3;
}
@Override
protected int getMinimumZSize() {
return 3;
}
@Override
protected int getMaximumYSize() {
return 3;
}
@Override
protected int getMinimumYSize() {
return 3;
}
// 这里我们需要判断某个非Part方块方块是否合法。
@Override
protected boolean isBlockGoodForInterior(@Nonnull World world, int i, int i1, int i2, @Nonnull IMultiblockValidator iMultiblockValidator) {
return world.getBlockState(new BlockPos(i, i1, i2)).getMaterial() == Material.AIR;
}
与isBlockGoodForInterior相似的一系列方法参与了多方块结构的判断,ZeroCore会在合适的时候调用他们来判断多方块结构是否成立。在后文我们还会看到关于多方块结构判断的更多细节。
这里的isBlockGoodForXXX的一系列方法,会在ZeroCore发现非Part方块以后执行,一般情况下我们不希望多方块结构中混入其他无关方块,所以所有的方法我们均返回false。但是这里我们希望多方块结构中空,所以我们重写了isBlockGoodForInterior,并且要求多方块结构内部为AIR。
之后我们来创建对应的Part,这里我们以外壳GlassWallBlock为例,读者可以采用相似的方法创建GlassEnergyPortBlock,GlassFluidPortBlock几个类。
在block包中创建类GlassWallBlock
// 位于: powerful_tank/block/GlassWallBlock.java
// 为了实现透明效果,我们继承自AbstractGlassBlock
public class GlassWallBlock extends AbstractGlassBlock {
public GlassWallBlock() {
// 为了实现透明效果,必须设置noOcclusion
super(Properties.of(Material.GLASS, MaterialColor.NONE).sound(SoundType.GLASS).noOcclusion());
this.setRegistryName("powerful_tank", "wall_block");
}
@Override
public boolean hasTileEntity(BlockState state) {
return true;
}
@Nullable
@Override
public TileEntity createTileEntity(BlockState state, IBlockReader world) {
return new GlassWallTileEntity();
}
}
之后我们来创建对应的GlassWallTileEntity,在包tileEntity中创建类GlassWallTileEntity,他需要继承自AbstractCuboidMultiblockPart,同时泛型类型为我们先前创建的MultiblockTank
// 位于: powerful_tank/tileentity/GlassWallTileEntity.java
public class GlassWallTileEntity extends AbstractCuboidMultiblockPart<MultiblockTank> {
public GlassWallTileEntity() {
// 待会我们要注册的TILE_ENTITY_TYPE
super(PowerfulTank.RegistryEvents.TILE_ENTITY_TYPE_WALL);
}
// 不允许Part被放置在多方块内部
@Override
public boolean isGoodForPosition(@Nonnull PartPosition partPosition, @Nonnull IMultiblockValidator iMultiblockValidator) {
return partPosition != PartPosition.Interior;
}
// 这里需要返回我们MultiblockTank类的实例
@Nonnull
@Override
public MultiblockTank createController() {
return new MultiblockTank(getCurrentWorld());
}
// 这里需要返回我们MultiblockTank类的Class
@Nonnull
@Override
public Class<MultiblockTank> getControllerType() {
return MultiblockTank.class;
}
}
上文我们提到了非Part方块的成立条件判断,对于Part的判断则是在这里的isGoodForPosition方法中。PartPosition是一个枚举类,读者可以自行根据自己需要的逻辑进行判断,这里我们只是不希望Part出现在结构内部,所以针对Interior进行了拒绝。
我们在主类中添加对应的注册,注意这里我们需要对三个方块的RenderType进行特殊的注册,因为我们希望这三者的渲染方式为CutoutMipped,感兴趣的读者可以阅读Boson的相关内容
// 位于: powerful_tank/PowerfulTank.java
@Mod("powerful_tank")
public class PowerfulTank {
// 这里我们让注册类作为主类的内部类
@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
public static class RegistryEvents {
public static final GlassWallBlock BLOCK_WALL = new GlassWallBlock();
public static final TileEntityType<GlassWallTileEntity> TILE_ENTITY_TYPE_WALL = TileEntityType.Builder.of(GlassWallTileEntity::new, BLOCK_WALL).build(DSL.remainderType());
@SubscribeEvent
public static void onBlocksRegistry(RegistryEvent.Register<Block> event) {
event.getRegistry().registerAll(BLOCK_WALL);
}
@SubscribeEvent
public static void onItemsRegistry(RegistryEvent.Register<Item> event) {
event.getRegistry().register(new BlockItem(BLOCK_WALL, new Item.Properties().tab(ItemGroup.TAB_BUILDING_BLOCKS)).setRegistryName("powerful_tank", "wall_block"));
}
@SubscribeEvent
public static void onTileEntitiesRegistry(RegistryEvent.Register<TileEntityType<?>> event) {
event.getRegistry().register(TILE_ENTITY_TYPE_WALL.setRegistryName("powerful_tank:wall"));
}
// 注意对于玻璃样式的材质,需要注册RenderType
@SubscribeEvent
public static void onRenderTypeRegistry(FMLClientSetupEvent event) {
event.enqueueWork(() -> {
RenderTypeLookup.setRenderLayer(BLOCK_WALL, RenderType.cutoutMipped());
});
}
}
}
相似的,读者应该补全相对于的其他Part的类和注册(EnergyPort和FluidPort),读者完成后,项目结构应该类似是这样:

之后我们来为能量接口和流体接口添加承载能量和流体的能力,即添加Capability。
对于Capability感兴趣的读者,这里笔者推荐阅读Boson的相关部分。
对于实现流体能力感兴趣的读者,这里笔者推荐阅读Harbinger的相关部分。
对于实现能量能力感兴趣的读者,这里笔者推荐阅读Forge 能量系统简述。
首先,在GlassEnergyPortTileEntity中加入以下内容,为他添加能量储存
// 位于: powerful_tank/tileentity/GlassEnergyPortTileEntity.java
// 因为我们对能量的需求比较简单,我们这里没有自己手动实现IEnergyHandler,而是直接利用了Forge实现好的EnergyStorage类
public final EnergyStorage energyStorage = new EnergyStorage(2_500_000, 2_500_000);
// 在1.16.5中,我们只需要重写getCapability,而不需要重写hasCapability了
// 这里需要注意,我们应该重写的是包含两个参数Capability<T>和Direction的方法,而不是仅有一个参数的方法,这里读者要注意
@Nonnull
@Override
public <T> LazyOptional<T> getCapability(@Nonnull Capability<T> cap, @Nullable Direction side) {
if (cap == CapabilityEnergy.ENERGY) {
return LazyOptional.of(() -> energyStorage).cast();
}
return super.getCapability(cap, side);
}
// 读者不要忘记储存和读取TileEntity内部的数据,这里我们把储存的能量储存在energy标签中
@Override
public void syncDataFrom(@Nonnull CompoundNBT data, @Nonnull SyncReason syncReason) {
super.syncDataFrom(data, syncReason);
energyStorage.receiveEnergy(data.getInt("energy"), false);
}
@Nonnull
@Override
public CompoundNBT syncDataTo(CompoundNBT data, @Nonnull SyncReason syncReason) {
data.putInt("energy", energyStorage.getEnergyStored());
return super.syncDataTo(data, syncReason);
}
// 这个方法并不是必要的,但是方便我们一会查看容器内的能量和流体储存
public String getEnergyText() {
int cost = 0;
// 注意处理多方块结构的存在性问题
if (getMultiblockController().isPresent() && isMachineAssembled()) {
// 一会我们来补充这个方法
cost = getMultiblockController().get().getCost();
}
return String.format("Energy: %d / %d FE, Cost: %d FE/t", energyStorage.getEnergyStored(), energyStorage.getMaxEnergyStored(), cost);
}
此时进入游戏,我们的能量接口已经支持了各个遵循Forge标准的模组的能量管线。

同样的,这里我们为GlassFluidPortTileEntity添加流体支持
// 位于: powerful_tank/tileentity/GlassFluidPortTileEntity.java
// 这里我们也没有手动实现IFluidHandler,而是使用了Forge实现好的FluidTank,并且做了小更改
public final FluidTank fluidTank = new FluidTank(Integer.MAX_VALUE) {
@Override
public int fill(FluidStack resource, FluidAction action) {
// 不构成多方块就想填入流体?
return super.fill(GlassFluidPortTileEntity.this.isMachineAssembled() ? resource : FluidStack.EMPTY, action);
}
@Nonnull
@Override
public FluidStack drain(int maxDrain, FluidAction action) {
// 不构成多方块就想取出流体?
return super.drain(GlassFluidPortTileEntity.this.isMachineAssembled() ? maxDrain : 0, action);
}
};
@Nonnull
@Override
public <T> LazyOptional<T> getCapability(@Nonnull Capability<T> cap, @Nullable Direction side) {
if (cap == CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY) {
return LazyOptional.of(() -> fluidTank).cast();
}
return super.getCapability(cap, side);
}
@Override
public void syncDataFrom(@Nonnull CompoundNBT data, @Nonnull SyncReason syncReason) {
super.syncDataFrom(data, syncReason);
fluidTank.readFromNBT(data);
}
@Nonnull
@Override
public CompoundNBT syncDataTo(@Nonnull CompoundNBT data, @Nonnull SyncReason syncReason) {
fluidTank.writeToNBT(data);
return super.syncDataTo(data, syncReason);
}
// 这个方法并不是必要的,但是方便我们一会查看容器内的能量和流体储存
public String getFluidText() {
FluidStack fluidStack = fluidTank.getFluid();
int cost = 0;
if (getMultiblockController().isPresent() && isMachineAssembled()) {
cost = getMultiblockController().get().getCost();
}
return String.format("Fluid(%s): %d / %s mb, Cost: %d FE/t", I18n.get(fluidStack.getTranslationKey()), fluidStack.getAmount(), "INFINITE", cost);
}
为了限制流体只在多方块成立时可以存取,我们这里重写了FluidTank的两个方法。
对于多方块结构的Part,我们可以调用isMachineAssembled来得知多方块结构是否完整,同时也可以调用getMultiblockController来获得到多方块结构自身。这里可能需要读者处理一下Optional问题。
此时进入游戏,我们的流体接口也可以接上其他模组的流体管道了,但此时流体应该还流不进去,因为我们没有构成完整的多方块结构。

最后我们来补全MultiblockTank的逻辑。在MultiblockTank中添加下面的内容
// 位于: powerful_tank/multiblock/MultiblockTank.java
private EnergyStorage energyStorage;
private FluidTank fluidTank;
// 为了让多方块结构找到两个接口,我们添加了这样一个私有方法,我们可以利用_connectedParts成员来获得所有连接的Part
private void findPorts() {
for (IMultiblockPart<MultiblockTank> connectedPart : _connectedParts) {
energyStorage = null;
fluidTank = null;
// 获得位于各个Part的实例,这种手法很常见
if (connectedPart instanceof GlassEnergyPortTileEntity)
energyStorage = ((GlassEnergyPortTileEntity) connectedPart).energyStorage;
if (connectedPart instanceof GlassFluidPortTileEntity)
fluidTank = ((GlassFluidPortTileEntity) connectedPart).fluidTank;
}
}
// 当多方块结构的区块被卸载后重新加载时,会调用这个方法
@Override
protected void onMachineRestored() {
// 我们希望这个时候重新搜索一次Part
findPorts();
}
// 当多方块结构形成时,会调用这个方法
@Override
protected void onMachineAssembled() {
// 这应该是第一次寻找Part
findPorts();
}
// 最终判定多方块结构是否成立的方法,下文会予以介绍
@Override
protected boolean isMachineWhole(@Nonnull IMultiblockValidator validatorCallback) {
findPorts();
if (energyStorage != null && fluidTank != null)
return super.isMachineWhole(validatorCallback);
else
//validatorCallback.setLastError(······);
return false;
}
这里笔者详细介绍一下ZeroCore是如何判断多方块结构是否成立的。读者如果查看父类的isMachineWhole方法大概可以猜出,这个方法对于Part类方块,会利用我们之前提供的isGoodForPosition方法来辨别该方块是否合法,而对于其他方块,则会调用isBlockGoodForXXX的一系列方法来判断是否合法。
我们要做的,便是重写isMachineWhole,添加我们自己所需的判断条件。例如我们上文调用findPorts方法,只有两个接口都存在,才会进一步判断多方块结构是否成立。
这里细心的读者可能对validatorCallback抱有疑问,我们上文多次见到这个参数,且都是在与结构成立判断的相关方法中存在,我们可以猜测这个参数可能与结构判断有关。
其实,validatorCallback是负责处理多方块结构不成立时的错误信息的,当结构出现不符合的情况时,我们希望给予一个错误信息来让玩家知道如何正确摆放多方块机器。在阅读父类isMachineWhole方法中,我们发现ZeroCore已经针对一些常见的问题处理了错误文本,我们只需关心自己的逻辑判断中的问题,并给予提示就可以了。
这里读者需要补全setLastError并且给予错误字符串,这里的字符串是支持lang提供的国际化的,读者可以自行设置。如果希望显示最后一个错误,可以调用validatorCallback.getLastError()方法,下文我们会看到相关的例子。
之后,我们需要添加核心逻辑,相信读者应该对ITickableTileEntity略知一二,他会在每一个游戏刻执行TileEntity内的代码,正是这个功能给Minecraft中无数科技模组的机器提供了可能。相似的,多方块结构也有类似的方法。
对于多方块结构,updateServer方法会在逻辑服务端每一个游戏刻被执行一次,所以请用对待tick方法的态度对待他,尽量减小对服务器性能的影响。
// 位于: powerful_tank/multiblock/MultiblockTank.java
@Override
protected boolean updateServer() {
if (energyStorage != null && fluidTank != null) {
// 每一个tick都扣除指定的能量,energyStorage实例我们上文已经获取到了
if (energyStorage.getEnergyStored() > 0) energyStorage.extractEnergy(getCost(), false);
// 能量归零且有流体存在
if (energyStorage.getEnergyStored() == 0 && fluidTank.getFluidAmount() != 0) {
fluidTank.setFluid(FluidStack.EMPTY);
}
return true;
}
return false;
// 这里,如果
}
// 根据内部保存的流体数量计算耗能,这里对于1000桶流体大概消耗251RF/t
public int getCost() {
return (int) Math.pow(fluidTank.getFluidAmount() / 1000.0, 0.8);
}
读者可能注意到updateServer具有返回值,这个返回值意味着是否需要数据同步。我们操作了储存在世界中的TileEntity中相关数据的值,所以我们在值发生改变的时候返回true。类似于TileEntity中的markDirty方法
现在进入游戏,我们的多方块结构已经能够正常运作了,相信阅读到了这里,读者已经对如何开发一个简单的多方块机器有了自己的理解。我们这里还会补全一些外围方法,来让机器更人性化。首先,我们希望多方块结构在没有构成时告知我们错误的原因。我们编辑GlassWallBlock类并且重写use方法
// 位于: powerful_tank/block/GlassWallBlock.java
@Nonnull
@Override
public ActionResultType use(@Nonnull BlockState pState, World pLevel, @Nonnull BlockPos pPos, @Nonnull PlayerEntity pPlayer, @Nonnull Hand pHand, @Nonnull BlockRayTraceResult pHit) {
if (!pLevel.isClientSide && pHand == Hand.MAIN_HAND) {
GlassWallTileEntity te = (GlassWallTileEntity) pLevel.getBlockEntity(pPos);
if (te != null && te.getMultiblockController().isPresent() && !te.isMachineAssembled() && pPlayer.isShiftKeyDown()) {
// 对于可能存在的错误信息,我们使用Optional来处理,调用validationError.getChatMessage()就可以得到本地化后的信息,方便发给玩家。
Optional<ValidationError> errorOptional = te.getMultiblockController().get().getLastError();
errorOptional.ifPresent(validationError -> pPlayer.sendMessage(validationError.getChatMessage(), PowerfulTank.POWERFUL_TANK_UUID));
return ActionResultType.SUCCESS;
}
}
return super.use(pState, pLevel, pPos, pPlayer, pHand, pHit);

玩家可能还想要知道此时储存的能量和流体数量,我们也添加相关方法,编辑GlassEnergyPortBlock和GlassFluidPortBlock类
// 位于: powerful_tank/block/GlassEnergyPortBlock.java
@Nonnull
@Override
public ActionResultType use(@Nonnull BlockState pState, World pLevel, @Nonnull BlockPos pPos, @Nonnull PlayerEntity pPlayer, @Nonnull Hand pHand, @Nonnull BlockRayTraceResult pHit) {
if (!pLevel.isClientSide && pHand == Hand.MAIN_HAND) {
GlassEnergyPortTileEntity te = (GlassEnergyPortTileEntity) pLevel.getBlockEntity(pPos);
// 只有在多方块结构形成时,才希望读取内部储存的能量
if (te != null && te.isMachineAssembled()) {
// 这里调用了我们之前写过的getEnergyText方法
pPlayer.sendMessage(new StringTextComponent(te.getEnergyText()), PowerfulTank.POWERFUL_TANK_UUID);
return ActionResultType.SUCCESS;
}
}
return super.use(pState, pLevel, pPos, pPlayer, pHand, pHit);
}
// 位于: powerful_tank/block/GlassFluidPortBlock.java
@Nonnull
@Override
public ActionResultType use(@Nonnull BlockState pState, World pLevel, @Nonnull BlockPos pPos, @Nonnull PlayerEntity pPlayer, @Nonnull Hand pHand, @Nonnull BlockRayTraceResult pHit) {
if (!pLevel.isClientSide && pHand == Hand.MAIN_HAND) {
GlassFluidPortTileEntity te = (GlassFluidPortTileEntity) pLevel.getBlockEntity(pPos);
// 只有在多方块结构形成时,才希望读取内部储存的流体
if (te != null && te.isMachineAssembled()) {
// 这里调用了我们之前写过的getFluidText方法
pPlayer.sendMessage(new StringTextComponent(te.getFluidText()), PowerfulTank.POWERFUL_TANK_UUID);
return ActionResultType.SUCCESS;
}
}
return super.use(pState, pLevel, pPos, pPlayer, pHand, pHit);
}

至此,我们已经完成了一个基于ZeroCore的立体型多方块结构。
这里笔者还希望补充一些附加思考,读者可以选择性阅读:
善于逻辑的读者可能看出,这里笔者的逻辑实现方式略显Dirty,读者可能认为,FluidTank这一成员不应是流体接口所拥有的,而是整个多方块结构MultiblockTank所应该拥有的。即拥有储存流体能力的并不应该是流体接口,而是整个多方块储罐。
对于这一问题的答案,读者毋庸置疑是正确的,这一成员确实应该移至MultiblockTank中,这里笔者的确处理的并不是很妥当。但如若读者仅是单纯将它移动至MultiblockTank,并且对Capability的处理是从MultiblockTank中获取(可能需要是否为空的判断),这样的行为也是不正确的。因为Capability的本意是某一方块具有某一能力,这一能力不应该从别处引用。如果读者这样尝试,会得到一个结果,当重新进入存档时,所有连接的管道均会断开,因为读取存档的时候,各个Mod获取我们流体接口的Capability是在多方块结构形成之前,这时我们无法获取到对应的MultiblockTank实例,也就无法获取到对应的FluidTank(即Capability本身)。
那对于这一问题如何处理呢,如果感兴趣的读者阅读极限反应堆代码可以了解到,原作者是使用一种Forwarder的想法来处理的,即其他模组访问到的不是FluidTank本身,而是一个Forwarder,这个Forwarder负责处理对于多方块机器的连接。从逻辑角度考虑,对于我们的流体接口,他所含有的能力应该是 连接至多方块结构中的FluidTank 的能力,而不是具有FluidTank的能力,在逻辑层面编写一个Forwarder也是合理的,对此感兴趣的读者可以自行阅读,并且尝试改进笔者的代码。其中一个例子

需要注意的是如果改进以后,FluidTank的数据持久化就需要由我们的MultiblockTank来完成,可喜的是,MultiblockTank也提供了对应的syncDataFrom和syncDataTo方法,这有助于我们储存多方块结构整体的数据,也就是这里我们需要储存的FluidTank。
这里笔者还希望添加一些上文例子中没有包含到的一些方法的补充:
对于MultiBlockController,还有一些相关的方法读者可能会用到
- onPartAdded 当Part并入Multiblock的时候会被执行,如果读者用一个列表来储存很多Part以作备用,这里可以向列表中添加Part
- onPartRemoved 同理,这里可以从列表中移除某些Part
- onMachinePaused 因为一些特殊原因(比如区块卸载)导致多方块结构无法正常使用时,会调用这个方法
- onMachineRestored 区块重新加载时会调用这个方法
- onMachineDisassembled 当多方块结构被破坏时会调用这个方法(例如爆炸或者被玩家挖掉)
- onAssimilate 当有另外一个结构与当前结构合并时,会调用这个方法,参数中会给出另外一个结构的Controller, 开发者应该在这里接管对方的所有数据(例如把对面的能量输入到自己里面)
- onAssimilated 与上一个相似的,如果自己被合并了,会调用这个方法,通常是清理一些之前的数据。这两个功能一般很少会用到,可以参考极限反应堆的相关代码
对于MultiblockPart,读者可以自行查阅。
# 小结与后记
经过本节的学习,读者应该学会了如何利用ZeroCore快速建立一个属于自己的多方块结构,同时也了解了多方块的各个部件间是如何与主控类建立联系的,这种思路将始终贯穿各种方式开发的各种多方块结构中,相信读者阅读本文后,对进一步阅读并理解IE和容积储存等Mod多方块结构的设计思路有很大帮助。同时这里笔者也针对读者可能产生的想法进行了解读,这些问题和相应的解决方案也是笔者行文时产生并解决的。这里笔者始终希望读者明白,在模组开发的过程中,你所遇到的需求和解决方案并不总是前人总结好的经验,更多的是在阅读前人代码中自行理解和解读出来的。如何让读者尝试去解读,让读者有自己为自己写指南的能力,这正是笔者行文的初衷,也是之前举二反三系列的初衷。
至此,读者可以自行根据需求开发自己的多方块结构,或者依靠本节的经验进一步深挖多方块结构的魅力。
本文代码已开源:Github - Powerful Tank
# 参考资料
ExtremeReactors2: https://github.com/ZeroNoRyouki/ExtremeReactors2
Forge 能量系统简述: https://www.mcbbs.net/forum.php?mod=viewthread&tid=1034965
Boson 1.16 Modding Tutorial: https://boson.v2mcdev.com/
Harbinger: https://harbinger.covertdragon.team/
CapacityStorage: https://github.com/Phoupraw/CapacityStorage
ZERONOMODS(过时): http://zerono.it/zerocore-multiblock-api-tutorial/
ZeroTest(过时): https://github.com/ZeroNoRyouki/ZeroTest
