概述

Capability系统(又称能力系统)是Forge在Minecraft 1.8.9新添加的一项特性,旨在使得对于已有和新添加的对象的扩展属性变得更方便,而不是需要通过实现很多复杂的接口。这里我们以Minecraft 1.7.10版本中BuildCraft的泵(buildcraft.factory.TilePump)为例,查看其实现了哪些接口:

public class TilePump extends TileBuildCraft implements IHasWork, IFluidHandler, IRedstoneEngineReceiver, ILEDProvider {...}

public abstract class TileBuildCraft extends TileEntity implements IEnergyHandler, ISerializable {...}

一个BuildCraft的液体泵就实现了六个接口,分别代表六种不同的属性。Forge致力于解决的问题就是通过一个统一的接口,以解决implements后面长长的一串这件事。

Capability的另一个极大的作用就是:它还可以方便地扩展已有对象的属性,在实际开发中,我们经常希望给玩家(EntityPlayer)、方块(TileEntity)等添加一些新的属性,然而我们又不能通过直接修改的方式添加扩展。Forge提供的Capability系统可以非常方便地解决这一问题。这也是包含本节在内的“附加数据与同步”这一部分的所有章节讲述的主要内容。本节的主要内容就是把Capability系统应用于实体,尤其是玩家上。

创建Capability

在真正开始写代码之前,我们先来想一想这么一个Capability需要什么:

  • 一个独立的数据接口,这个接口通过定义的一系列方法声明这个Capability对应的数据
  • 序列化,在Minecraft中最适用于数据存储的序列化方案已经十分显然了——NBT标签
  • 对这一接口的一个默认实现,如果我们想要从一个NBT标签还原到一个Capability对象上,那么我们首先需要创建一个实例,然后再把NBT标签应用于此

这里作为演示我们使用“系统命令”一节中新添加的“/position”命令,并在每次获取玩家自身位置的时候存储历史记录,那么这里我们首先新建包com.github.ustc_zzzz.fmltutor.capability,并在其中创建一个IPositionHistory接口:

src/main/java/com/github/ustc_zzzz/fmltutor/capability/IPositionHistory.java:

package com.github.ustc_zzzz.fmltutor.capability;

import net.minecraft.util.Vec3;

public interface IPositionHistory
{
    public Vec3[] getHistories();

    public void setHistories(Vec3[] position);

    public void pushHistory(Vec3 position);
}

这个接口的三个方法的作用很简单,第一个用于获取所有历史记录,第二个用于直接设置所有历史记录,最后一个用于压入一个最新的数据。

现在我们来解决序列化这一问题,在包com.github.ustc_zzzz.fmltutor.capability下新建类CapabilityPositionHistory

src/main/java/com/github/ustc_zzzz/fmltutor/capability/CapabilityPositionHistory.java:

package com.github.ustc_zzzz.fmltutor.capability;

import net.minecraft.nbt.NBTBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.Vec3;
import net.minecraftforge.common.capabilities.Capability;

public class CapabilityPositionHistory
{
    public static class Storage implements Capability.IStorage<IPositionHistory>
    {
        @Override
        public NBTBase writeNBT(Capability<IPositionHistory> capability, IPositionHistory instance, EnumFacing side)
        {
            NBTTagList list = new NBTTagList();
            for (Vec3 vec3 : instance.getHistories())
            {
                NBTTagCompound compound = new NBTTagCompound();
                if (vec3 != null)
                {
                    compound.setDouble("x", vec3.xCoord);
                    compound.setDouble("y", vec3.yCoord);
                    compound.setDouble("z", vec3.zCoord);
                }
                list.appendTag(compound);
            }
            return list;
        }

        @Override
        public void readNBT(Capability<IPositionHistory> capability, IPositionHistory instance, EnumFacing side,
                NBTBase nbt)
        {
            NBTTagList list = (NBTTagList) nbt;
            Vec3[] histories = new Vec3[list.tagCount()];
            for (int i = 0; i < histories.length; ++i)
            {
                NBTTagCompound compound = list.getCompoundTagAt(i);
                if (!compound.hasNoTags())
                {
                    histories[i] = new Vec3(compound.getDouble("x"), compound.getDouble("y"), compound.getDouble("z"));
                }
            }
            instance.setHistories(histories);
        }
    }
}

这里为了方便起见,我们新建了一个可能没什么用的类,把我们接下来可能用到的类以内部类的形式包裹起来。

现在我们可以开始研究一下这个内部类了。

public static class Storage implements Capability.IStorage<IPositionHistory>

首先,Forge规定我们实现Capability.IStorage接口以声明对一个类的序列化。我们同时也可以看到,这个接口声明了两个方法,其中名为writeNBT的方法用于把对象序列化(Serialize)成一个NBT标签,而名为readNBT的方法正好把这一操作逆过来(Deserialize)。

在本节中作为演示,作者把一个IPositionHistory序列化成了一个NBTTagListwriteNBTreadNBT的实现都很简单,作者这里就不再解释了。

最后还有一个问题,也就是默认实现了,我们在其中新建一个内部类Implementation并使其实现我们刚刚声明的IPositionHistory接口:

src/main/java/com/github/ustc_zzzz/fmltutor/capability/CapabilityPositionHistory.java(部分):

    public static class Implementation implements IPositionHistory
    {
        private Vec3[] histories = new Vec3[5];

        @Override
        public Vec3[] getHistories()
        {
            return histories.clone();
        }

        @Override
        public void setHistories(Vec3[] position)
        {
            histories = position.clone();
        }

        @Override
        public void pushHistory(Vec3 position)
        {
            for (int i = 1; i < histories.length; ++i)
            {
                histories[i - 1] = histories[i];
            }
            histories[histories.length - 1] = position;
        }
    }

三件事都办完了,就可以注册这个Capability了。

注册和获取Capability

我们在包com.github.ustc_zzzz.fmltutor.capability下新建类CapabilityLoader

src/main/java/com/github/ustc_zzzz/fmltutor/capability/CapabilityLoader.java:

package com.github.ustc_zzzz.fmltutor.capability;

import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.CapabilityInject;
import net.minecraftforge.common.capabilities.CapabilityManager;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;

public class CapabilityLoader
{
    @CapabilityInject(IPositionHistory.class)
    public static Capability<IPositionHistory> positionHistory;

    public CapabilityLoader(FMLPreInitializationEvent event)
    {
        CapabilityManager.INSTANCE.register(IPositionHistory.class, new CapabilityPositionHistory.Storage(),
                CapabilityPositionHistory.Implementation.class);
    }
}

然后扔进CommonProxypreInit阶段

src/main/java/com/github/ustc_zzzz/fmltutor/common/CommonProxy.java(部分):

    public void preInit(FMLPreInitializationEvent event)
    {
        new ConfigLoader(event);
        new CapabilityLoader(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);
    }

注册Capability的核心自然就是这段代码,也就是CapabilityManager类的register方法:

src/main/java/com/github/ustc_zzzz/fmltutor/capability/CapabilityLoader.java(部分):

        CapabilityManager.INSTANCE.register(IPositionHistory.class, new CapabilityPositionHistory.Storage(),
                CapabilityPositionHistory.Implementation.class);

第一个参数传入代表Capability的接口的class,第二个参数传入Capability.IStorage的实现,也就是用于Capability的序列化,最后一个参数传入默认实现的class,用于产生一个默认实现。

然后我们怎么获取到Capability呢?上面的方法没有办法直接获取到已经注册的Capability,不过Forge采取了一种被称为“依赖注入”的手段,即为想要获取到Capability的字段和方法添加@CapabilityInject注解,Forge就会完成剩下的工作:

src/main/java/com/github/ustc_zzzz/fmltutor/capability/CapabilityLoader.java(部分):

    @CapabilityInject(IPositionHistory.class)
    public static Capability<IPositionHistory> positionHistory;

首先我们需要声明一个被标识为static的Capability字段(Field),然后使用@CapabilityInject注解它,Forge会在preInit阶段和init阶段之间自动为其赋值为注册的Capability。

另外一种可行的做法如下:

    private static Capability<IPositionHistory> positionHistory;

    @CapabilityInject(IPositionHistory.class)
    public static Capability<IPositionHistory> setPositionHistory(Capability<IPositionHistory> capability)
    {
        positionHistory = capability;
    }

此时Forge就会调用这一方法,并传入注册的Capability,我们就可以做更多的事情。

扩展已有对象的Capability

那我们如何才能知道,一个对象有着若干个Capability呢?这里用到的就是ICapabilitySerializable接口了,这一接口要求我们做两件事:

  • 声明了hasCapabilitygetCapability两个方法,用于查看是否拥有这一Capability和获取对应Capability储存的对象
  • 声明了serializeNBTdeserializeNBT两个方法,用于把这么一个ICapabilitySerializable和一个NBT标签相对应

我们看看Forge为原版的哪些类实现了这个接口呢?一共有三个:EntityTileEntityItemStack

如果我们继承了一个新的实体或者TileEntity,一种常见的扩展Capability的方法如下:

    @Override
    public boolean hasCapability(Capability<?> capability, EnumFacing facing)
    {
        if (CapabilityLoader.positionHistory.equals(capability))
        {
            return true;
        }
        return super.hasCapability(capability, facing);
    }

    @Override
    public <T> T getCapability(Capability<T> capability, EnumFacing facing)
    {
        if (CapabilityLoader.positionHistory.equals(capability))
        {
            // Do Something. 
        }
        return super.getCapability(capability, facing);
    }

也就是我们覆写了这两个方法以达到添加我们想要的Capability的目的。不过——这段代码看起来似曾相识?

这是“TileEntity与数据更新”一节的一段代码:

src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityMetalFurnace.java(部分):

    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正是以Capability的形式存储的,其对应的Capability正是CapabilityItemHandler.ITEM_HANDLER_CAPABILITY

hasCapabilitygetCapability两个方法还顺带了一个EnumFacing参数,对于有着六个面的TileEntity类是有作用的,对于其他的EntityItemStack等类是没有意义的。

但是本节的主要内容是体现Capability系统对于已有对象的属性扩展的优势,而这些已有对象,比如EntityPlayer是没有办法修改的。不过还好,Forge为我们提供了AttachCapabilitiesEvent,可以用于添加Capaility。那么我们监听这个事件:

src/main/java/com/github/ustc_zzzz/fmltutor/common/EventLoader.java(部分):

    @SubscribeEvent
    public void onAttachCapabilitiesEntity(AttachCapabilitiesEvent.Entity event)
    {
        if (event.getEntity() instanceof EntityPlayer)
        {
            // TODO
        }
    }

AttachCapabilitiesEvent提供了一个方法addCapability用于为已有的对象扩展新的Capability。它有三个子类,分别为AttachCapabilitiesEvent.TileEntityAttachCapabilitiesEvent.EntityAttachCapabilitiesEvent.Item,分别用于TileEntityEntity、和ItemStack,并会在它们的构造方法调用时触发这个事件。由于这里我们只想扩展玩家的Capability,所以这里我们只监听了AttachCapabilitiesEvent.Entity事件,并判断其是不是玩家。

Forge在这里使用的是一种名为“责任链模式”的设计模式,具体是这样的:

  • 首先该ICapabilitySerializable,这里就是作为玩家的实体,先判断自己是否拥有这种Capability,如果有,便直接处理并返回(上面的覆写hasCapabilitygetCapability两个方法)
  • 如果没有,便挨个检查在AttachCapabilitiesEvent中添加的ICapabilitySerializable,如果有ICapabilitySerializable能够接管这一Capability,就由这个ICapabilitySerializable接着解决这一问题

那么我们首先实现一个ICapabilitySerializable,在CapabilityPositionHistory内新建一个ProviderPlayer并实现这个接口:

src/main/java/com/github/ustc_zzzz/fmltutor/capability/CapabilityPositionHistory.java:

    public static class ProviderPlayer implements ICapabilitySerializable<NBTTagCompound>
    {
        private IPositionHistory histories = new Implementation();
        private IStorage<IPositionHistory> storage = CapabilityLoader.positionHistory.getStorage();

        @Override
        public boolean hasCapability(Capability<?> capability, EnumFacing facing)
        {
            return CapabilityLoader.positionHistory.equals(capability);
        }

        @Override
        public <T> T getCapability(Capability<T> capability, EnumFacing facing)
        {
            if (CapabilityLoader.positionHistory.equals(capability))
            {
                @SuppressWarnings("unchecked")
                T result = (T) histories;
                return result;
            }
            return null;
        }

        @Override
        public NBTTagCompound serializeNBT()
        {
            NBTTagCompound compound = new NBTTagCompound();
            compound.setTag("histories", storage.writeNBT(CapabilityLoader.positionHistory, histories, null));
            return compound;
        }

        @Override
        public void deserializeNBT(NBTTagCompound compound)
        {
            NBTTagList list = (NBTTagList) compound.getTag("histories");
            storage.readNBT(CapabilityLoader.positionHistory, histories, null, list);
        }
    }

上面的这个ICapabilitySerializable,提供了一个IPositionHistory,并使用刚刚我们注册的Capability对应的Capability.IStorage进行序列化。

然后我们使用AttachCapabilitiesEventaddCapability方法注册掉它:

src/main/java/com/github/ustc_zzzz/fmltutor/common/EventLoader.java(部分):

    @SubscribeEvent
    public void onAttachCapabilitiesEntity(AttachCapabilitiesEvent.Entity event)
    {
        if (event.getEntity() instanceof EntityPlayer)
        {
            ICapabilitySerializable<NBTTagCompound> provider = new CapabilityPositionHistory.ProviderPlayer();
            event.addCapability(new ResourceLocation(FMLTutor.MODID + ":" + "position_history"), provider);
        }
    }

第一个参数需要传入一个ResourceLocation,作为ICapabilitySerializable的唯一标识符,这里我们使用的是"fmltutor:position_history",第二个参数就是需要传入的ICapabilitySerializable

这里有一点需要稍稍注意一下,就是玩家(EntityPlayer)在死亡或从末界回到主世界时,会产生一个新的EntityPlayer,但是Capability对应的数据不会被复制到这个新的EntityPlayer中,也就是会重置,所以我们需要监听PlayerEvent.Clone这一事件以把数据复制到新的EntityPlayer里:

src/main/java/com/github/ustc_zzzz/fmltutor/common/EventLoader.java(部分):

    @SubscribeEvent
    public void onPlayerClone(net.minecraftforge.event.entity.player.PlayerEvent.Clone event)
    {
        Capability<IPositionHistory> capability = CapabilityLoader.positionHistory;
        IStorage<IPositionHistory> storage = capability.getStorage();

        if (event.original.hasCapability(capability, null) && event.entityPlayer.hasCapability(capability, null))
        {
            NBTBase nbt = storage.writeNBT(capability, event.original.getCapability(capability, null), null);
            storage.readNBT(capability, event.entityPlayer.getCapability(capability, null), null, nbt);
        }
    }

使用Capability

使用Capability的方式十分简单,直接调用hasCapabilitygetCapability两个方法就行了。作为本节教程的演示,我们把系统命令的代码修改一下:

src/main/java/com/github/ustc_zzzz/fmltutor/command/CommandPosition.java(部分):

    @Override
    public void processCommand(ICommandSender sender, String[] args) throws CommandException
    {
        if (args.length > 1)
        {
            throw new WrongUsageException("commands.position.usage");
        }
        else
        {
            EntityPlayerMP entityPlayerMP = args.length > 0 ? CommandBase.getPlayer(sender, args[0])
                    : CommandBase.getCommandSenderAsPlayer(sender);
            Vec3 pos = entityPlayerMP.getPositionVector();

            if (entityPlayerMP == sender && entityPlayerMP.hasCapability(CapabilityLoader.positionHistory, null))
            {
                sender.addChatMessage(new ChatComponentTranslation("commands.position.history"));
                IPositionHistory histories = entityPlayerMP.getCapability(CapabilityLoader.positionHistory, null);
                for (Vec3 vec3 : histories.getHistories())
                {
                    if (vec3 != null)
                    {
                        sender.addChatMessage(new ChatComponentText(vec3.toString()));
                    }
                }
                histories.pushHistory(pos);
            }

            sender.addChatMessage(new ChatComponentTranslation("commands.position.success", entityPlayerMP.getName(),
                    pos, entityPlayerMP.worldObj.provider.getDimensionName()));
        }
    }

最后补充一下语言文件:

src/main/resources/assets/fmltutor/lang/en_US.lang(部分):

commands.position.usage=/position [player]
commands.position.history=Position query history:
commands.position.success=The position of %1$s is %2$s in world %3$s

src/main/resources/assets/fmltutor/lang/zh_CN.lang(部分):

commands.position.usage=/position [玩家]
commands.position.history=位置查询历史:
commands.position.success=玩家 %1$s 处于名为 %3$s 的世界,其坐标为 %2$s

打开游戏,多次输入/position命令,就可以看到历史记录已经储存起来了。

results matching ""

    No results matching ""