企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# 你的第一台机器 **警告:在阅读本教程之前,请确保你掌握了对NBT的基础操作** **难度分级:☆☆☆☆** 正如上一章所提到的那样,很多时候我们希望让游戏中的方块能够**拥有存储自定义数据的能力**,并且能够让方块也**像实体一样随着世界的刷新(tick)做出相应的变化**。而一般的Minecraft方块并不具有这样的功能,他们一旦被创建以后基本就变成了一个**静态**的对象,无法再做出更多的内部修改。因此,Minecraft为我们提供了一种名为 **方块实体(TileEntity,~~或BlockEntity~~)** 的对象来让方块能够拥有类似**实体**的功能。 **PS:由于方块实体会随着世界的更新同步刷新,在世界中大量生成方块实体会大量占用系统资源,造成严重的卡顿。因此在实际开发中,我们应当尽可能的避免创建大量的方块实体。** ## 自定义方块实体类 &nbsp;&nbsp; 难度分级:☆☆☆ 由于方块实体的功能较为复杂,我们需要自定义新的方块实体类来实现我们想要的功能。所有的方块实体类必须继承`net.minecraft.tileentity.TileEntity`类,在这里你可以完成该方块实体的绝大部分逻辑。 ### **数据的存储** 原版的方块通过直接与区块NBT进行交互来完成对方块实体数据的存储。但正如上一章所提到的那样,对于Forge 1.8之后版本的模组开发者来说,对一个游戏对象的数据存储最好的方式便是通过**Capability系统**,Forge也自动为原版的TileEntity类实现了**ICapabilitySerializable**接口以方便开发者实现对方块实体Capability的读写。 对于一个TileEntity来说,一个最常见的需求便是对**物品**进行存储。Forge在1.8.9之后的版本中为TileEntity新增了一种名为**IItemHandler**的Capability以方便开发者对TileEntity进行物品的读写。一个常见的对TileEntity的IItemHandler系统进行交互的代码结构如下: ~~~ @Override public boolean hasCapability(Capability<?> capability, @Nullable EnumFacing facing) { if(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY.equals(capability)){ return true; } return super.hasCapability(capability, facing); } @Nullable @Override public <T> T getCapability(Capability<T> capability, @Nullable EnumFacing facing) { if(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY.equals(capability)){ //返回你的IItemHandler变量 } return super.getCapability(capability, facing); } ~~~ 在这里我们需要返回一个IItemHandler系统下的自定义变量。Forge本身为我们实现了一种该系统下的变量类型,即**ItemStackHandler**,这一实现可以表征多个物品槽的内含物品栈。在自定义方块实体类中,我们可以直接新建该变量类型的自定义变量。 ~~~ protected ItemStackHandler 变量名 = new ItemStackHandler(); ~~~ 同时,这里还有一个方块独有的概念,即**朝向面(EnumFacing)**。对于一个方块来说,有 **东(East)西(West)南(South)北(North)上(Up)下(Down)** 六个朝向面,分别对应该方块对于游戏世界中的六种朝向。EnumFacing可以用来对**方块自动旋转**,**方块实体自动化**等操作进行方向判断。 在EOK中,对**基础研究台(ElementaryResearchTable)** 这一方块实体的交互代码如下: ~~~ public class TEElementaryResearchTable extends TileEntity ... { //“纸”物品槽 protected ItemStackHandler paperSlot = new ItemStackHandler(); //“笔”物品槽 protected ItemStackHandler penSlot = new ItemStackHandler(); @Override public boolean hasCapability(Capability<?> capability, @Nullable EnumFacing facing) { if(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY.equals(capability)){ return true; } return super.hasCapability(capability, facing); } @Nullable @Override public <T> T getCapability(Capability<T> capability, @Nullable EnumFacing facing) { if(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY.equals(capability)){ @SuppressWarnings("unchecked") //定义朝下的一面对应“纸”物品槽,朝上的一面对应“笔墨”物品槽 T result = (T) (facing == EnumFacing.DOWN ? paperSlot : penSlot); return result; } return super.getCapability(capability, facing); } } ~~~ 和上一章的操作类似,在我们对Capability进行操作时,我们需要通过**NBT**来对自定义数据进行**序列化**和**反序列化**。对于物品存储来说,这里的代码较为固定,我们直接使用EOK中**基础研究台**的代码进行解释: ~~~ public class TEElementaryResearchTable extends TileEntity ... { ... @Override public void readFromNBT(NBTTagCompound compound) { super.readFromNBT(compound); //反序列化NBT中“纸”物品槽的数据 this.paperSlot.deserializeNBT(compound.getCompoundTag("paperSlot")); //反序列化NBT中“笔墨”物品槽的数据 this.toolSlot.deserializeNBT(compound.getCompoundTag("penSlot")); } @Override public NBTTagCompound writeToNBT(NBTTagCompound compound) { super.writeToNBT(compound); //将“纸”物品槽的数据存入NBT compound.setTag("paperSlot", this.paperSlot.serializeNBT()); //将“笔墨”物品槽的数据存入NBT compound.setTag("penSlot", this.toolSlot.serializeNBT()); //将封装好的NBT变量传入上层代码进行进一步操作 return super.writeToNBT(compound); } } ~~~ ### **方块实体的即时更新** 为了实现方块实体的即时更新,你需要为你的自定义方块实体类实现`net.minecraft.util.ITickable`接口。 ~~~ public class 自定义方块实体类名 extends TileEntity implements ITickable ~~~ 实现了该接口后,方块实体会随着世界进行以 **1游戏刻(Tick,1Tick = 0.05s)** 为单位的即时更新。实现改接口需要我们覆写**update()** 函数,一般的代码结构如下: ~~~ @Override public void update() { //判断所在游戏端是否为服务端,isRemote意为“是否是被远程操控的”,因此world.isRemote=true为客户端,false为服务端 if(!this.world.isRemote){ //每一次更新时该方块实体需要完成的逻辑代码 } } ~~~ ## 注册自定义方块实体 &nbsp;&nbsp; 难度分级:☆☆ TileEntity的注册在Forge中并没有单独的事件类型,需要通过 **GameRegistry.registerTileEntity()** 方法来对其进行注册。该方法要求我们传入**自定义方块实体类的class**以及一个独立标识符(这里为ResourceLocation)。在EOK中,对于**基础研究台(ElementaryResearchTable)** 这一方块实体的注册代码如下: ~~~ @Mod.EventBusSubscriber public class RegistryHandler { ... @SubscribeEvent public static void onBlockRegister(RegistryEvent.Register<Block> event){ ... GameRegistry.registerTileEntity(TEElementaryResearchTable.class, new ResourceLocation(EOK.MODID, "te_elementary_research_table")); } ... } ~~~ **需要注意的是,EOK中将方块实体的注册一起写到了方块注册的事件方法里,从代码运行上来看并没有什么问题,但其实这是一种不符合规范的写法,因为TileEntity的注册和方块注册事件并无关联。一个标准的写法应当是单独新建一个TileEntityHandler类,在其中单独新建一个注册方法,并将该方法在模组主类或代理类的相应位置调用。~~EOK这里这么写主要是因为懒(雾)~~** ## 方块对TileEntity的绑定 对于一个自定义方块而言,如果Forge检测到其实现了**ITileEntityProvider接口**,Forge便会将其标记为**含有TileEntity的方块**。由于实现ITileEntityProvider接口之后还需要进行一系列十分繁琐的操作,Forge提供了一个名为**BlockContainer**的方块子类并**带上了ITileEntityProvider接口**,因此对于含有TileEntity的方块,我们可以将其方块类直接继承`net.minecraft.block.BlockContainer`。由于BlockContainer只是带上了ITileEntityProvider接口而并没有对其进行默认实现,因此我们还需要通过覆写 **createNewTileEntity()** 方法并返回**自定义TileEntity类**来将该方块与对应的TileEntity建立关联。 例如在EOK中对于**基础研究台(ElementaryResearchTable)** 这一方块的方块实体关联代码如下: ~~~ public class BlockElementaryResearchTable extends BlockContainer implements IHasModel { ... @Nullable @Override public TileEntity createNewTileEntity(World worldIn, int meta) { return new TEElementaryResearchTable(); } ... } ~~~ ## 一些细节问题 &nbsp;&nbsp; ~~难度分级: ☆~~ 现在你已经成功创建了一个完整的方块实体。但当在游戏中放下该方块时,你会发现方块模型变成了**透明**。这是因为**BlockContainer类**中将**方块模型渲染级别**设置为了**不可见(Invisible)**,因此我们还需要通过覆写 **getRenderType()** 函数来将其还原成标准的方块模型渲染级别。 这里直接使用EOK中的代码来进行示范: ~~~ public class BlockElementaryResearchTable extends BlockContainer implements IHasModel { ... @Override public EnumBlockRenderType getRenderType(IBlockState state) { return EnumBlockRenderType.MODEL; } ... } ~~~