# 你的第一台机器
**警告:在阅读本教程之前,请确保你掌握了对NBT的基础操作**
**难度分级:☆☆☆☆**
正如上一章所提到的那样,很多时候我们希望让游戏中的方块能够**拥有存储自定义数据的能力**,并且能够让方块也**像实体一样随着世界的刷新(tick)做出相应的变化**。而一般的Minecraft方块并不具有这样的功能,他们一旦被创建以后基本就变成了一个**静态**的对象,无法再做出更多的内部修改。因此,Minecraft为我们提供了一种名为 **方块实体(TileEntity,~~或BlockEntity~~)** 的对象来让方块能够拥有类似**实体**的功能。
**PS:由于方块实体会随着世界的更新同步刷新,在世界中大量生成方块实体会大量占用系统资源,造成严重的卡顿。因此在实际开发中,我们应当尽可能的避免创建大量的方块实体。**
## 自定义方块实体类 难度分级:☆☆☆
由于方块实体的功能较为复杂,我们需要自定义新的方块实体类来实现我们想要的功能。所有的方块实体类必须继承`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){
//每一次更新时该方块实体需要完成的逻辑代码
}
}
~~~
## 注册自定义方块实体 难度分级:☆☆
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();
}
...
}
~~~
## 一些细节问题 ~~难度分级: ☆~~
现在你已经成功创建了一个完整的方块实体。但当在游戏中放下该方块时,你会发现方块模型变成了**透明**。这是因为**BlockContainer类**中将**方块模型渲染级别**设置为了**不可见(Invisible)**,因此我们还需要通过覆写 **getRenderType()** 函数来将其还原成标准的方块模型渲染级别。
这里直接使用EOK中的代码来进行示范:
~~~
public class BlockElementaryResearchTable extends BlockContainer implements IHasModel {
...
@Override
public EnumBlockRenderType getRenderType(IBlockState state) {
return EnumBlockRenderType.MODEL;
}
...
}
~~~
- 0.引子
- 基础篇 - 1.构建开发环境
- 基础篇 - 2.主类和代理
- 基础篇 - 3.创建一个物品
- 基础篇 - 4.创建一个方块
- 基础篇 - 4.1自定义方块模型
- 基础篇 - 5.初探事件系统
- 基础篇 - 6.Capability系统
- 基础篇 - 7.创建一个方块实体
- 基础篇 - 8.你的第一个GUI
- 基础篇 - 9.网络与通讯
- 进阶篇 - 0.更复杂的Mod
- 进阶篇 - 1.Minecraft渲染原理
- 进阶篇 - 2.更复杂的GUI
- 进阶篇 - 3.物品进阶
- 高级篇 - 1.深入探索OpenGL
- 高级篇 - 1.1OpenGL颜色渲染
- 高级篇 - 1.2OpenGL光照系统
- 高级篇 - 2.Minecraft贴图加载原理