概述
很多时候,只有十六种BlockState的方块是不够用的,我们可能需要存储接近无穷多的数据(比如原版的箱子),还要时不时地进行自动数据更新(比如原版的熔炉),这个时候,TileEntity就要派到用场了,虽然它的名字和实体差不多,但是它主要是用于方块上的。和方块本身不同,世界上的TileEntity是另外单独存储的。
本部分作者将通过创建和注册TileEntity、设置TileEntity的自动数据更新,以及TileEntity中数据的存储等方式,带领读者对TileEntity有一个初步的认识,并带领读者接触到一种用于存储物品并和其他方块(如漏斗等)进行物品传输交互的,Forge在Minecraft 1.8.9新加入的机制:IItemHandler
。这一系统被设计成用于取代1.8及之前不够方便的IInventory
和ISidedInventory
系统。
不过在这里作者提醒的一点是,因为TileEntity会在游戏逻辑中被额外处理到,所以在游戏中放置大量TileEntity,尤其是可以自动更新的TileEntity,将会对游戏性能带来不可估量的拖慢后果。
创建和注册TileEntity
新建包com.github.ustc_zzzz.fmltutor.tileentity
,并在其中新建一个类TileEntityMetalFurnace
,使其继承TileEntity
类。这里的TileEntity
类的子类,就是我们要用来管理上一部分我们创建的新的方块的:
src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityMetalFurnace.java:
package com.github.ustc_zzzz.fmltutor.tileentity;
import net.minecraft.tileentity.TileEntity;
public class TileEntityMetalFurnace extends TileEntity
{
// TODO
}
现在我们注册这个TileEntity,我们在包com.github.ustc_zzzz.fmltutor.tileentity
下新建一个类TileEntityLoader
:
src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityLoader.java:
package com.github.ustc_zzzz.fmltutor.tileentity;
import com.github.ustc_zzzz.fmltutor.FMLTutor;
import net.minecraft.tileentity.TileEntity;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.fml.common.registry.GameRegistry;
public class TileEntityLoader
{
public TileEntityLoader(FMLPreInitializationEvent event)
{
registerTileEntity(TileEntityMetalFurnace.class, "MetalFurnace");
}
public void registerTileEntity(Class<? extends TileEntity> tileEntityClass, String id)
{
GameRegistry.registerTileEntity(tileEntityClass, FMLTutor.MODID + ":" + id);
}
}
我们在preInit
阶段,通过调用GameRegistry
类的registerTileEntity
方法,传入TileEntity对应的class实例以注册:
src/main/java/com/github/ustc_zzzz/fmltutor/common/CommonProxy.java(部分):
public void preInit(FMLPreInitializationEvent event)
{
new ConfigLoader(event);
new CreativeTabsLoader(event);
new FluidLoader(event);
new ItemLoader(event);
new BlockLoader(event);
new OreDictionaryLoader(event);
new PotionLoader(event);
new EntityLoader(event);
new TileEntityLoader(event);
}
创建带有TileEntity的方块
能够让Minecraft辨认出一个方块含有TileEntity
的方式是让这个方块继承一个名为ITileEntityProvider
的接口,实际上还可能需要做一些处理。不过幸运的是,Minecraft提供了一种名为BlockContainer
的类,这个类是Block
类的子类,并带上了ITileEntityProvider
接口。我们可以继承这个类而不是Block
类以解决问题:
src/main/java/com/github/ustc_zzzz/fmltutor/block/BlockMetalFurnace.java(部分):
public class BlockMetalFurnace extends BlockContainer
当然这个类只是带上了ITileEntityProvider
的接口,而没有真正实现它,我们这里还是需要手动实现这个名为createNewTileEntity
的方法:
src/main/java/com/github/ustc_zzzz/fmltutor/block/BlockMetalFurnace.java(部分):
@Override
public TileEntity createNewTileEntity(World worldIn, int meta)
{
return new TileEntityMetalFurnace();
}
现在似乎没有什么问题,除了打开游戏后我们发现所有放置在地上的金属熔炉都消失了。
这是因为BlockContainer
类覆写了Block
类的getRenderType
方法,这个方法返回一个整数以表示该方块渲染的类型,Block
类默认为3,表示按照blockstates
文件的指示以最普通的方式渲染方块,然而BlockContainer
类覆写了这个方法,使其返回-1,表示根本不渲染,本意是方便我们自己编写OpenGL手动渲染,然而这里我们需要手动覆写这一方法,使其仍然返回3:
src/main/java/com/github/ustc_zzzz/fmltutor/block/BlockMetalFurnace.java(部分):
@Override
public int getRenderType()
{
return 3;
}
创建新的物品存储
现在我们讲到的,就是刚刚已经提到的IItemHandler
了,此处我们需要覆写TileEntity
类的两个方法:
@Override
public boolean hasCapability(Capability<?> capability, EnumFacing facing)
{
if (CapabilityItemHandler.ITEM_HANDLER_CAPABILITY.equals(capability))
{
return true;
}
return super.hasCapability(capability, facing);
}
@Override
public <T> T getCapability(Capability<T> capability, EnumFacing facing)
{
if (CapabilityItemHandler.ITEM_HANDLER_CAPABILITY.equals(capability))
{
// return your IItemHandler
}
return super.getCapability(capability, facing);
}
IItemHandler
的使用和Forge在Minecraft 1.8.9中引入的一种崭新的,名为Capability
的系统息息相关,这一系统为已有事物(比如原版的事物)添加属性提供了极大的便利,不过这里我们不需要深入了解这是怎么一回事,只需要这么做就可以了:当第一个参数为CapabilityItemHandler.ITEM_HANDLER_CAPABILITY
的时候,hasCapability
方法返回true
,而getCapability
方法返回的,就是表征第二个参数表示的方向(上下东南西北)对应的IItemHandler
(比如熔炉的上面、下面、和侧面各表征三种类型的物品存储)。
可能读者读到这里还是不容易理解,这里为了演示,教程提供了两种IItemHandler
,每个IItemHandler
存放一个物品槽:
src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityMetalFurnace.java(部分):
protected int burnTime = 0;
protected ItemStackHandler upInventory = new ItemStackHandler();
protected ItemStackHandler downInventory = new ItemStackHandler();
@Override
public boolean hasCapability(Capability<?> capability, EnumFacing facing)
{
if (CapabilityItemHandler.ITEM_HANDLER_CAPABILITY.equals(capability))
{
return true;
}
return super.hasCapability(capability, facing);
}
@Override
public <T> T getCapability(Capability<T> capability, EnumFacing facing)
{
if (CapabilityItemHandler.ITEM_HANDLER_CAPABILITY.equals(capability))
{
@SuppressWarnings("unchecked")
T result = (T) (facing == EnumFacing.DOWN ? downInventory : upInventory);
return result;
}
return super.getCapability(capability, facing);
}
(这里作者添加了一个整数待用。)
现在,这个熔炉的上面和侧面表征的是同一种IItemHandler
,而下面表征的是另一种IItemHandler
。
通过上面的源代码我们还可以注意到,Forge已经为我们提供了一种IItemHandler
的实现,也就是ItemStackHandler
。这一实现可以表征一个提供若干个槽,其中每个槽都存放一种物品(一个ItemStack
)的物品存储。Forge还提供了一个名为EmptyHandler
的类用于表征一个槽都没有的物品存储。
如果读者有开发过较早版本(1.8/1.7.10或更早)的Minecraft对应的Forge Mod的话,可能会对继承复杂而又麻烦的IInventory
接口以表征一个物品存储的方式有着比较深刻的印象。Forge同样提供了两个类,分别名为InvWrapper
和SidedInvWrapper
,用于包装IInventory
以实现一个IItemHandler
,如果代码是从较早版本迁移而来,那么就可以用这两个类方便地提供对IItemHandler
的支持,如果是崭新的代码,那么就没有必要了。
ItemStackHandler
还有一个传入一个整数的构造方法,以表征存放物品的槽的数量。如果使用不传参的构造方法默认为一个槽,就像前面的代码展示的那样。
数据存储
TileEntity提供了两个分别名为readFromNBT
和writeToNBT
的方法,这两个方法的用法和实体的对应两个方法一样,都传入一个NBT标签以进行数据存储。不过有一点需要指明的是,ItemStackHandler
类本身提供了serializeNBT
和deserializeNBT
方法,这使得对物品存储数据的读写变得异常方便,如果读者有开发过较早版本(1.8/1.7.10或更早)的Minecraft对应的Forge Mod的话,应该体验过手写这两个方法以读写物品存储数据的痛苦吧:
src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityMetalFurnace.java(部分):
@Override
public void readFromNBT(NBTTagCompound compound)
{
super.readFromNBT(compound);
this.upInventory.deserializeNBT(compound.getCompoundTag("UpInventory"));
this.downInventory.deserializeNBT(compound.getCompoundTag("DownInventory"));
this.burnTime = compound.getInteger("BurnTime");
}
@Override
public void writeToNBT(NBTTagCompound compound)
{
super.writeToNBT(compound);
compound.setTag("UpInventory", this.upInventory.serializeNBT());
compound.setTag("DownInventory", this.downInventory.serializeNBT());
compound.setInteger("BurnTime", this.burnTime);
}
NBT标签的数据存储极为复杂,不过幸运的是,在源代码中我们不需要考虑那么多,只需要使用NBTTagCompound
的一串set开头的设置方法、和一串get开头的获取方法就可以了。这里教程只是简要运用了一下NBT标签,更为详细的了解,可以参见这里。
自动数据更新
我们需要实现ITickable
接口,这个接口提供了一个名为update
的方法:
src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityMetalFurnace.java(部分):
public class TileEntityMetalFurnace extends TileEntity implements ITickable
然后我们需要实现这个方法,注意这个方法客户端和服务端往往都会调用,所以一般情况下我们会使用!this.worldObj.isRemote
判断,以保证我们想要自动更新的内容只作用在服务端:
src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityMetalFurnace.java(部分):
@Override
public void update()
{
if (!this.worldObj.isRemote)
{
// TODO
}
}
本节的稍后我们会补充这个方法。
Minecraft在检查到TileEntity的时候,会去检查这个TileEntity是不是实现了ITickable
接口。如果实现了,就每一gametick(0.05秒)调用一次这个方法。
获取TileEntity和物品存储
一般情况下,一个BlockContainer
还会覆写onBlockActivated
方法,以实现右键方块的行为,一般都是打开GUI什么的。不过因为本部分不负责讲解GUI,所以这里我们给玩家发送一个消息以显示两个物品存储中的物品:
src/main/java/com/github/ustc_zzzz/fmltutor/block/BlockMetalFurnace.java(部分):
@Override
public boolean onBlockActivated(World worldIn, BlockPos pos, IBlockState state, EntityPlayer playerIn,
EnumFacing side, float hitX, float hitY, float hitZ)
{
if (!worldIn.isRemote)
{
TileEntityMetalFurnace te = (TileEntityMetalFurnace) worldIn.getTileEntity(pos);
IItemHandler up = te.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, EnumFacing.UP);
IItemHandler down = te.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, EnumFacing.DOWN);
String msg = String.format("Up: %s, Down: %s", up.getStackInSlot(0), down.getStackInSlot(0));
playerIn.addChatComponentMessage(new ChatComponentText(msg));
}
return true;
}
首先,World
类有一个名为getTileEntity
的方法,用于获取特定方块位置的TileEntity。
然后,我们使用了TileEntity
类的getCapability
方法,并在第一个参数传入CapabilityItemHandler.ITEM_HANDLER_CAPABILITY
以获取IItemHandler
。因为这里表征上面和下面的两种IItemHandler
不太一样,所以我们传入的第二个参数也不同。
IItemHandler
有两个方法,分别名为getSlots
和getStackInSlot
,前者用于获取物品槽的数量,而后者用于获取特定序数的槽。因为这里两个物品存储都只有一个槽,所以这里直接就传入0了。
此外,一个BlockContainer
往往还会覆写breakBlock
方法,以保证方块被打碎的时候内容物会以掉落的形式出现:
src/main/java/com/github/ustc_zzzz/fmltutor/block/BlockMetalFurnace.java(部分):
@Override
public void breakBlock(World worldIn, BlockPos pos, IBlockState state)
{
TileEntityMetalFurnace te = (TileEntityMetalFurnace) worldIn.getTileEntity(pos);
IItemHandler up = te.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, EnumFacing.UP);
IItemHandler down = te.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, EnumFacing.DOWN);
for (int i = up.getSlots() - 1; i >= 0; --i)
{
if (up.getStackInSlot(i) != null)
{
Block.spawnAsEntity(worldIn, pos, up.getStackInSlot(i));
((IItemHandlerModifiable) up).setStackInSlot(i, null);
}
}
for (int i = down.getSlots() - 1; i >= 0; --i)
{
if (down.getStackInSlot(i) != null)
{
Block.spawnAsEntity(worldIn, pos, down.getStackInSlot(i));
((IItemHandlerModifiable) down).setStackInSlot(i, null);
}
}
super.breakBlock(worldIn, pos, state);
}
这里我们获取到TileEntity里的两个IItemHandler
,并进行遍历,使用spawnAsEntity
把其中的物品以掉落的形式出现,然后清空。
清空的过程我们就要说到IItemHandlerModifiable
接口了,ItemStackHandler
类同时实现了这个接口,也就是其中的setStackInSlot
方法,这个方法的作用就是设置特定序数的槽中的物品。
设置TileEntity和物品存储
现在就是设置自动更新逻辑的时刻了!这里因为没有GUI所以不好演示,就做一个简单的每过一段时间从上面表征的物品存储(后面简称UP)取出一个物品放入下面表征的物品存储(后面简称DOWN)好了:
src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityMetalFurnace.java(部分):
@Override
public void update()
{
if (!this.worldObj.isRemote)
{
ItemStack itemStack = upInventory.extractItem(0, 1, true);
IBlockState state = this.worldObj.getBlockState(pos);
if (itemStack != null && downInventory.insertItem(0, itemStack, true) == null)
{
this.worldObj.setBlockState(pos, state.withProperty(BlockMetalFurnace.BURNING, Boolean.TRUE));
int burnTotalTime = 200;
switch (state.getValue(BlockMetalFurnace.MATERIAL))
{
case IRON:
burnTotalTime = 150;
break;
case GOLD:
burnTotalTime = 100;
break;
}
if (++this.burnTime >= burnTotalTime)
{
this.burnTime = 0;
itemStack = upInventory.extractItem(0, 1, false);
downInventory.insertItem(0, itemStack, false);
this.markDirty();
}
}
else
{
this.worldObj.setBlockState(pos, state.withProperty(BlockMetalFurnace.BURNING, Boolean.FALSE));
}
}
}
我们先来看IItemHandler
类作者还没有讲到的两个方法,extractItem
和insertItem
:
ItemStack insertItem(int slot, ItemStack stack, boolean simulate);
ItemStack extractItem(int slot, int amount, boolean simulate);
我们先从extractItem
开始,这个方法的作用是从对应的物品槽获取物品:
- 第一个参数的作用是指定物品槽的序数
- 第二个参数的作用是指定想要获取的物品数量
- 第三个参数的作用是设置是否为模拟行为,设置为真或者假不影响返回值,但如果设置为假,如果成功,物品槽里的物品就真的减少了,否则物品槽里的物品数量不会发生变化
这个方法返回真正获取到的物品(因为原先的物品槽可能没有物品或者物品数量不能满足第二个参数传入的数量,如果没有物品则返回null
)。
然后是insertItem
方法,这个方法的作用是向对应的物品槽塞入物品:
- 第一个参数的作用是指定物品槽的序数
- 第二个参数的作用是指定想要塞入的物品
- 第三个参数的作用是设置是否为模拟行为,设置为真或者假不影响返回值,但如果设置为假,如果成功,物品槽里的物品就真的增加了,否则物品槽里的物品数量不会发生变化
这个方法返回塞入后剩下的物品(如果被全部塞入了就会返回null
,如果塞入的物品类型和物品槽中的物品类型不符那么就会原样返回塞入的物品,如果塞满了就返回剩下没能塞进去的物品)。
不过不管是哪一个方法,有一点是需要万分注意的,也就是这个方法返回的ItemStack
应该当作只读实例,也就是说,不应该修改返回值的内容,如果想要使用,请复制这个ItemStack
。
现在我们可以设计一下算法了:
- 从UP模拟获取一个物品并模拟塞入DOWN中
- 如果UP没取出来物品,或者DOWN没塞入物品,则执行第3步,否则执行第4步
- 设置炉子的状态为不工作,更新结束
- 设置炉子的状态为工作中
- 燃烧时间(上面添加待用的
burnTime
)加一 - 获取对应炉子的燃烧时间
- 如果燃烧时间没到,则更新结束,否则继续执行
- 把燃烧时间置零
- 从UP真实获取一个物品并真实塞入DOWN中
- 标记TileEntity为等待保存状态,更新结束
上面的算法是和上面的源代码一一对应的,这里只需要把markDirty
方法(第十步)讲一下,还有设置炉子状态(第三步和第四步)的一点需要注意的地方。
markDirty
方法用于设置这个TileEntity里的数据发生了变动,这样游戏才会知道这个TileEntity的数据发生的变动,在保存的时候才不会跳过这一个TileEntity,否则游戏会在保存的时候跳过这一个TileEntity而不去保存变动的数据。
实际上目前这个炉子还是有问题的,就是当炉子的方块状态被设置的时候,TileEntity会被清空重置,这时候我们就需要考虑到TileEntity
类的shouldRefresh
方法了:
src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityMetalFurnace.java(部分):
@Override
public boolean shouldRefresh(World world, BlockPos pos, IBlockState oldState, IBlockState newState)
{
return oldState.getBlock() != newState.getBlock();
}
这个方法会在世界更新TileEntity所在位置的方块状态时调用,默认的判定是oldState
和newState
不相等时替换,然而这里我们需要更新方块状态以表示炉子是否工作,所以这里只判定方块是否相同。
现在我们可以在炉子的周围加装漏斗来测试物品的放置情况,打开游戏试试吧~