概述
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
序列化成了一个NBTTagList
。writeNBT
和readNBT
的实现都很简单,作者这里就不再解释了。
最后还有一个问题,也就是默认实现了,我们在其中新建一个内部类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);
}
}
然后扔进CommonProxy
的preInit
阶段:
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
接口了,这一接口要求我们做两件事:
- 声明了
hasCapability
和getCapability
两个方法,用于查看是否拥有这一Capability和获取对应Capability储存的对象 - 声明了
serializeNBT
和deserializeNBT
两个方法,用于把这么一个ICapabilitySerializable
和一个NBT标签相对应
我们看看Forge为原版的哪些类实现了这个接口呢?一共有三个:Entity
、TileEntity
、ItemStack
。
如果我们继承了一个新的实体或者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
。
hasCapability
和getCapability
两个方法还顺带了一个EnumFacing
参数,对于有着六个面的TileEntity
类是有作用的,对于其他的Entity
、ItemStack
等类是没有意义的。
但是本节的主要内容是体现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.TileEntity
、AttachCapabilitiesEvent.Entity
、AttachCapabilitiesEvent.Item
,分别用于TileEntity
、Entity
、和ItemStack
,并会在它们的构造方法调用时触发这个事件。由于这里我们只想扩展玩家的Capability,所以这里我们只监听了AttachCapabilitiesEvent.Entity
事件,并判断其是不是玩家。
Forge在这里使用的是一种名为“责任链模式”的设计模式,具体是这样的:
- 首先该
ICapabilitySerializable
,这里就是作为玩家的实体,先判断自己是否拥有这种Capability,如果有,便直接处理并返回(上面的覆写hasCapability
和getCapability
两个方法) - 如果没有,便挨个检查在
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
进行序列化。
然后我们使用AttachCapabilitiesEvent
的addCapability
方法注册掉它:
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的方式十分简单,直接调用hasCapability
和getCapability
两个方法就行了。作为本节教程的演示,我们把系统命令的代码修改一下:
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
命令,就可以看到历史记录已经储存起来了。