概述

很多时候,只有十六种BlockState的方块是不够用的,我们可能需要存储接近无穷多的数据(比如原版的箱子),还要时不时地进行自动数据更新(比如原版的熔炉),这个时候,TileEntity就要派到用场了,虽然它的名字和实体差不多,但是它主要是用于方块上的。和方块本身不同,世界上的TileEntity是另外单独存储的。

本部分作者将通过创建和注册TileEntity、设置TileEntity的自动数据更新,以及TileEntity中数据的存储等方式,带领读者对TileEntity有一个初步的认识,并带领读者接触到一种用于存储物品并和其他方块(如漏斗等)进行物品传输交互的,Forge在Minecraft 1.8.9新加入的机制:IItemHandler。这一系统被设计成用于取代1.8及之前不够方便的IInventoryISidedInventory系统。

不过在这里作者提醒的一点是,因为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同样提供了两个类,分别名为InvWrapperSidedInvWrapper,用于包装IInventory以实现一个IItemHandler,如果代码是从较早版本迁移而来,那么就可以用这两个类方便地提供对IItemHandler的支持,如果是崭新的代码,那么就没有必要了。

ItemStackHandler还有一个传入一个整数的构造方法,以表征存放物品的槽的数量。如果使用不传参的构造方法默认为一个槽,就像前面的代码展示的那样。

数据存储

TileEntity提供了两个分别名为readFromNBTwriteToNBT的方法,这两个方法的用法和实体的对应两个方法一样,都传入一个NBT标签以进行数据存储。不过有一点需要指明的是,ItemStackHandler类本身提供了serializeNBTdeserializeNBT方法,这使得对物品存储数据的读写变得异常方便,如果读者有开发过较早版本(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有两个方法,分别名为getSlotsgetStackInSlot,前者用于获取物品槽的数量,而后者用于获取特定序数的槽。因为这里两个物品存储都只有一个槽,所以这里直接就传入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类作者还没有讲到的两个方法,extractIteminsertItem

ItemStack insertItem(int slot, ItemStack stack, boolean simulate);

ItemStack extractItem(int slot, int amount, boolean simulate);

我们先从extractItem开始,这个方法的作用是从对应的物品槽获取物品:

  • 第一个参数的作用是指定物品槽的序数
  • 第二个参数的作用是指定想要获取的物品数量
  • 第三个参数的作用是设置是否为模拟行为,设置为真或者假不影响返回值,但如果设置为假,如果成功,物品槽里的物品就真的减少了,否则物品槽里的物品数量不会发生变化

这个方法返回真正获取到的物品(因为原先的物品槽可能没有物品或者物品数量不能满足第二个参数传入的数量,如果没有物品则返回null)。

然后是insertItem方法,这个方法的作用是向对应的物品槽塞入物品:

  • 第一个参数的作用是指定物品槽的序数
  • 第二个参数的作用是指定想要塞入的物品
  • 第三个参数的作用是设置是否为模拟行为,设置为真或者假不影响返回值,但如果设置为假,如果成功,物品槽里的物品就真的增加了,否则物品槽里的物品数量不会发生变化

这个方法返回塞入后剩下的物品(如果被全部塞入了就会返回null,如果塞入的物品类型和物品槽中的物品类型不符那么就会原样返回塞入的物品,如果塞满了就返回剩下没能塞进去的物品)。

不过不管是哪一个方法,有一点是需要万分注意的,也就是这个方法返回的ItemStack应该当作只读实例,也就是说,不应该修改返回值的内容,如果想要使用,请复制这个ItemStack

现在我们可以设计一下算法了:

  1. 从UP模拟获取一个物品并模拟塞入DOWN中
  2. 如果UP没取出来物品,或者DOWN没塞入物品,则执行第3步,否则执行第4步
  3. 设置炉子的状态为不工作,更新结束
  4. 设置炉子的状态为工作中
  5. 燃烧时间(上面添加待用的burnTime)加一
  6. 获取对应炉子的燃烧时间
  7. 如果燃烧时间没到,则更新结束,否则继续执行
  8. 把燃烧时间置零
  9. 从UP真实获取一个物品并真实塞入DOWN中
  10. 标记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所在位置的方块状态时调用,默认的判定是oldStatenewState不相等时替换,然而这里我们需要更新方块状态以表示炉子是否工作,所以这里只判定方块是否相同。

现在我们可以在炉子的周围加装漏斗来测试物品的放置情况,打开游戏试试吧~

results matching ""

    No results matching ""