ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
# Forge能力系统 **~~zijing_233:我觉得这篇教程不应该放在前面?~~** **难度分级:☆☆☆☆☆☆** 在很多时候,我们需要为游戏中的**实体,方块,玩家等**添加某种属性或能力(例如饱食度,口渴值,电量,所含物品等)。在原版MC及以往的Forge版本中,我们需要通过封装一系列接口类,并分别实现他们才能实现这样的效果,步骤十分繁琐。自Minecraft 1.8起,Forge通过引入**Capability系统(能力系统)**,使得开发者为游戏内容扩展属性的过程得以大大简化,本章我们将对该系统的运用进行一个基本的阐述。 ## NBT(二进制命名标签) 难度:☆ **NBT(Named Binary Tag)** 格式为Minecraft用于向文件中存储数据的一种**存储格式**。NBT格式以**树形结构并配以许多标签的形式**存储数据,每一个标签在数据树中都是一个独立的部分。标签的第一个字节为**标签类型(ID)** ,其后两字节为**存储名称的长度**,之后以**UTF-8格式字符串**的方式存储标签。 下表所示为目前MC版本中二进制命名标签格式中所有13个已知的标签类型: | ID | 标签类型 | 标签描述 | 标签类型备注 | 数据范围 | | --- | --- | --- | --- | --- | --- | | 0 | TAG\_**End** | 用于标记复合标签的结尾。本标签无任何名称所以只有一个零字节。 | 无 | N/A | | 1 | TAG\_**Byte** | 有正负的整值数据类型,通常用于布尔表达式。 | 1字节 / 8位,有正负 | -128 到 127 | | 2 | TAG\_**Short** | 有正负的整值数据类型。 | 2字节 / 16位,有正负,字节序:**Big Endian字节序** | -32,768 到 32,767 | | 3 | TAG\_**Int** | 有正负的整值数据类型。 | 4 字节 / 32 位,有正负,字节序:**Big Endian字节序** | -2,147,483,648 到 2,147,483,647 | | 4 | TAG\_**Long** | 有正负的整值数据类型。| 8 字节 / 64 位,有正负,**Big Endian字节序** | -9,223,372,036,854,775,808 到9,223,372,036,854,775,807 | | 5 | TAG\_**Float** | 有正负的浮点数据类型。 | 4 字节 / 32 位,有正负,**Big Endian字节序**,IEEE 754-2008标准,binary32。| 标准单精度浮点型范围,最大值约为3.4*1038 | | 6 | TAG\_**Double** | 有正负的浮点数据类型。 | 8 字节 / 64 位,有正负,**Big Endian字节序**,IEEE 754-2008标准,binary64。| 标准双精度浮点型范围,最大值约为1.8*10308 | | 7 | TAG\_**Byte**\_**Array** | 数组。 | 可理解为TAG_Int及TAG_Byte的合集。| 每一个属均为Byte型的数据范围(取决于JVM版本) | 8 | TAG\_**String** | 以UTF-8格式编码的字符串 | 前2个字节(TAG_Short)存储字符串字符的长度。然后是以UTF-8标准存储的字符串,因为拥有长度,因此没有空结束符。 | 32,767 字节 | | 9 | TAG\_**List** | 一系列没有重复标签ID和标签名称的集合。 | 第1个字节(TAG_Byte)存储列表标签类型的ID,接下来的4个字节(TAG_Int)存储列表的大小,接下来的字节将存储列表标签类型的对应信息。 | List中最大元素个数为2,147,483,639。**注:List与Compound的嵌套不能超过512层否则可能会爆栈。** | | 10 | TAG\_**Compound** | 一系列完整的标签信息,包括ID、名称以及辅助信息等。任意两个标签都不会有相同的名称。 | 标签的完整形式,需要附加TAG\_End | 没有额外的标签个数限制,但**嵌套不能超过512层否则可能会爆栈。** | 11 | TAG\_**Int**\_**Array** | 存储TAG\_Int的数组。 | 前4个字节(TAG\_Int)用于存储数组的大小,接着存储所有的数组数据。占用存储空间: 4+4\**数组大小 Byte*。 | 数组大小不超过2,147,483,639或2,147,483,647(取决于JVM) | | 12 | TAG\_**Long**\_**Array** | 存储TAG\_Long的数组。 | 前4个字节(TAG\_Int)用于存储数组的大小,接着存储所有的数组数据。占用存储空间: 4+8\**数组大小 Byte*。 | 数组大小不超过2,147,483,639或2,147,483,647(取决于JVM) | ~~看起来很简单对吧?让我们继续~~ 在Minecraft中有大量的内容都使用NBT进行存储,它们被保存在一系列以 **.dat** 结尾的文件中(例如level.dat,villages.dat,[player].dat),Capability同样也可以使用该存储方式进行数据存储与交互 **(但Capability不仅仅可以使用NBT存储信息)**。 关于NBT数据格式的更多介绍可前往[Minecraft Gamepedia]([https://minecraft.gamepedia.com/NBT\_format](https://minecraft.gamepedia.com/NBT_format))阅读。 ## 创造属于你自己的Capability &nbsp;&nbsp;难度:☆☆☆☆☆ Forge本身提供了四种Capability,分别为`IItemHandler`,`IFluidHandler`(`IFluidHandlerItem `),`IEnergyStorage `,`IAnimationStateMachine`(**注:动画状态机**)。它们的作用将会在后续的章节中依次介绍。在很多情况下,我们还需要为游戏中的内容附加新的属性,这时候我们就需要创建属于我们自己的Capability。 ### **创建数据接口** 首先我们需要声明新定义的Capability中包含的数据类型及存取方法,这时我们便需要封装一个新的**接口**。这个接口至少应当具有**存储数据**和**读取数据**两种方法。例如在EOK中,对于 **“玩家清醒度”** 这一Capability的数据接口代码如下: ~~~ public interface IConsciousness { public double getConsciousnessValue(); public void setConsciousnessValue(double conV); } ~~~ ### **序列化数据** 由于自定义的Capability数据可能会有各种各样不同的数据类型,我们需要通过一种标准将其统一化并放入游戏内容中。这时我们便可以使用上文所介绍的**NBT数据格式**进行存取。首先我们封装一个新的类,并在其中创建一个**Storage内部类**。为了实现数据序列化,Forge要求该类实现以**你的数据接口类型**为泛型标识的**Capability.IStorage**接口。该接口需要我们覆写 **writeNBT(将数据序列化成NBT)** 及 **readNBT(将NBT中的数据反序列化)** 两个方法。在这两个方法中,我们分别需要完成**对NBT的封装并返回**和**对NBT的解析并调用数据接口中的方法将数据传回**。 这里说的十分抽象,让我们用实际代码来做一定的解释。例如在EOK中,对于 **“玩家清醒度”** 这一Capability的Storage类代码如下: ~~~ public class CapabilityConsciousness { //Storage类 public static class Storage implements Capability.IStorage<IConsciousness>{ @Nullable @Override public NBTBase writeNBT(Capability<IConsciousness> capability, IConsciousness instance, EnumFacing side) { //创建一个新的NBT标签 NBTTagCompound compound = new NBTTagCompound(); //将清醒度值存入NBT的“consciousness”标签中 compound.setDouble("consciousness", instance.getConsciousnessValue()); //将该NBT标签返回上级代码 return compound; } @Override public void readNBT(Capability<IConsciousness> capability, IConsciousness instance, EnumFacing side, NBTBase nbt) { //将传入的NBTBase强制转换为NBTTagCompound类型方便处理 NBTTagCompound compound =(NBTTagCompound) nbt; //设置存储读取数据变量的初值防止报错 double conV = 100.0D; //判断是否有所需要的标签 if(compound.hasKey("consciousness")) { //读取标签 conV = compound.getDouble("consciousness"); } //调用数据接口类中的方法将读取的数据设置进自定义的数据结构中 instance.setConsciousnessValue(conV); } } } ~~~ ### **设置默认实现方法** 封装完数据后,我们需要设置**数据接口**的默认实现方法。我们创建一个新的内部类**Implementation**,并实现我们自定义的数据接口。通常情况下这个类中起到**传递数据**的作用,因此我们只需要在方法中对值进行相应的**写入和读出**即可 **(如有其它特殊的传递操作也需要写在这里)** 。例如EOK对于**玩家清醒度**这一Capability的Implementation代码如下: ~~~ public static class Implementation implements IConsciousness{ private double conV = 100.0D; @Override public double getConsciousnessValue() { return this.conV; } @Override public void setConsciousnessValue(double conV) { this.conV = conV; } } ~~~ ### **注册Capability** 和物品/方块一样,,我们需要将Capability注册进游戏中。首先我们新建一个以**我们的自定义数据接口**为**泛型标识**的Capability变量,并通过 **@CapabilityInject** 将其与我们**封装好的Capability定义类**进行关联。例如在EOK中对于**玩家清醒度**的变量定义代码为: ~~~ public class CapabilityHandler { @CapabilityInject(IConsciousness.class) public static Capability<IConsciousness> capConsciousness; } ~~~ 随后我们需要调用**CapabilityManager实例**的**register**方法将该Capability注册进**Forge的Capability管理器**中。该方法要求我们分别传入**数据接口的Class**,**Storage类的Class**,以及**Implementation类的Class**。同时,我们还需要在**游戏启动**时触发该register指令,所以我们还需要将其写入**主类**的**PreInitialization**阶段中。例如EOK对于**玩家清醒度**的注册过程代码如下: ~~~ public class CapabilityHandler { ... public static void setupCapabilities(){ CapabilityManager.INSTANCE.register(IConsciousness.class, new CapabilityConsciousness.Storage(), CapabilityConsciousness.Implementation.class); } } ~~~ ~~~ public class EOK { @EventHandler public void preInit(FMLPreInitializationEvent event) { ... CapabilityHandler.setupCapabilities(); } } ~~~ ### **将Capability附着于游戏对象中** 现在我们拥有了一个自己的Capability,但它还没有和任何一个游戏中的内容关联,因此我们还需要将其关联至游戏的现有内容中。Forge为我们提供了三种可与Capability关联的游戏对象:**实体(Entity),方块实体(TileEntity)以及物品栈(ItemStack)**。要扩展一个已有游戏对象的Capability,我们需要监听并改写Forge的**AttachCapabilitiesEvent**事件。该事件可以使用三种泛型,分别对应可供关联的游戏对象,即`AttachCapabilitiesEvent<Entity>`,`AttachCapabilitiesEvent<TileEntity>`以及`AttachCapabilitiesEvent.<Item>`**(该处的Item即指ItemStack)**。 ### **Capability责任链** 在游戏过程中,如果某段代码需要用到一个游戏对象的Capability时,Forge会首先检测**该对象本身**是否有该Capability,如果没有,则Forge会检查所有在**AttachCapabilitiesEvent**中关联的Capability,如果有对应的**ICapabilityProvider**,则Forge便会将执行权移交给该Capability,这便是Forge的**Capability责任链**。 #### **将Capability扩展到玩家上** 在Minecraft中,玩家(EntityPlayer)也被认为是一种实体(Entity),因此我们通过`AttachCapabilitiesEvent<Entity>`来完成对玩家的Capability扩展。由于Forge在检测时需要该Capability实现**ICapabilityProvider接口**,因此我们还需要为我们的Capability增加一个**ProvidePlayer**类以实现该接口。由于我们使用了NBT进行数据序列化,因此我们还需要实现**ICapabilitySerializable**接口(ICapabilitySerializable接口自身实现了ICapabilityProvider接口,因此实现了ICapabilitySerializable接口就不用再单独实现ICapabilityProvider了)。该接口还需要我们指定**NBT标签的类型**,在这里即为**NBTTagCompound泛型**。 实现ICapabilitySerializable接口需要我们覆写4个函数:**hasCapability ,getCapability ,serializeNBT 以及deserializeNBT**。前两个函数用于**查看一个游戏对象是否拥有这一Capability**和**获取对应Capability储存的对象**,后两个函数的作用是**把这么一个实现了ICapabilitySerializable的对象和一个NBT标签相对应**。 这里说的十分抽象,我们直接使用EOK **“玩家清醒度”** 这一Capability的相关代码来做一定的解释。 ~~~ public class CapabilityConsciousness { ... public static class ProvidePlayer implements ICapabilitySerializable<NBTTagCompound>, ICapabilityProvider { //创建一个已经默认实现了的consciousness实例 private IConsciousness consciousness = new Implementation(); //获取该Capability的Storage结构 private Capability.IStorage<IConsciousness> storage = CapabilityHandler.capConsciousness.getStorage(); //判断目前Forge在该游戏对象上检测到的Capability是否为该Capability @Override public boolean hasCapability(@Nonnull Capability<?> capability, @Nullable EnumFacing facing) { return CapabilityHandler.capConsciousness.equals(capability); } //获取该游戏对象的Capability @Nullable @Override public <T> T getCapability(@Nonnull Capability<T> capability, @Nullable EnumFacing facing) { //判断获取到的Capability是否为该Capability if(CapabilityHandler.capConsciousness.equals(capability)){ @SuppressWarnings("unchecked") //将consciousness变量强制转为一个通用泛型并返回(玄学代码,如果理解不了可以暂时留着,但要求会用) T result = (T) consciousness; return result; } return null; } //将Capability中的IConsiousness数据结构序列化为一个NBT @Override public NBTTagCompound serializeNBT() { //新建一个NBTTagCompound标签 NBTTagCompound compound = new NBTTagCompound(); //从该Capability的Storage中取出consciousness数据结构并将其写入compound的“consciousness”标签中 compound.setTag("consciousness", storage.writeNBT(CapabilityHandler.capConsciousness, consciousness, null)); //将处理好的NBT标签传回上层函数做进一步处理 return compound; } //从NBT中读取IConsciousness类型的数据并将其放入consciousness变量中 @Override public void deserializeNBT(NBTTagCompound nbt) { //取出NBT中标签为"consciousness"的对应数据 NBTTagCompound compound = nbt.getCompoundTag("consciousness"); //从compound中读取出Capability数据并将其放入consciousness变量中 storage.readNBT(CapabilityHandler.capConsciousness, consciousness, null, compound); } } ~~~ 实现了ICapabilityProvider以后,我们就可以通过`AttachCapabilitiesEvent<Entity>`事件中的**addCapability方法**来完成对玩家的Capability扩展了。由于每个玩家的Capability不尽相同,我们还需要为每一种Capability指定一个**独立的标识符**(这里为**ResourceLocation**)。EOK对于**玩家清醒度**这一Capability的扩展代码如下: ~~~ public class AnotherEventHandler { @SubscribeEvent public void onAttachCapabilitiesEntity(AttachCapabilitiesEvent<Entity> event){ if(event.getObject() instanceof EntityPlayer){ ICapabilitySerializable<NBTTagCompound> providerConsciousness = new CapabilityConsciousness.ProvidePlayer(); ... event.addCapability(new ResourceLocation(EOK.MODID + ":" + "consciousness"), providerConsciousness); ... } } } ~~~ 现在我们已经将自己的Capability扩展到了玩家实体上,但由于每次在玩家重生时,游戏都会**按照游戏本身的规范**克隆出一个新的玩家实体,因此我们也需要通过监听**PlayerEvent.Clone事件**来使玩家在重生时自动将之前的自定义Capability一并复制过去。我们使用EOK中对于**玩家清醒度**这一Capability的克隆进行演示: ~~~ public class AnotherEventHandler { ... @SubscribeEvent public void onPlayerClone(PlayerEvent.Clone event){ //创建一个新的玩家清醒度Capability变量 Capability<IConsciousness> capabilityConsciousness = CapabilityHandler.capConsciousness; //获取新创建的Capability变量中的Storage空间 Capability.IStorage<IConsciousness> storageConsciousness = capabilityConsciousness.getStorage(); ... //如果原玩家实体带有对应的Capability if(event.getOriginal().hasCapability(capabilityConsciousness, null) && event.getEntityPlayer().hasCapability(capabilityConsciousness, null)){ //将原玩家实体对应Capability中的NBT数据取出 NBTBase nbt = storageConsciousness.writeNBT(capabilityConsciousness, event.getOriginal().getCapability(capabilityConsciousness, null), null); //将NBT数据放入克隆出的新玩家实体的Capability中 storageConsciousness.readNBT(capabilityConsciousness, event.getEntityPlayer().getCapability(capabilityConsciousness, null), null, nbt); } ... } } } ~~~ #### **将Capability扩展到其他实体上** //待完成 #### **将Capability扩展到物品栈上** //待完成 #### **调用Capability对游戏对象的自定义数据进行读写** 当我们完成了对一个游戏对象的Capability扩展后,我们就可以在任何我们需要的地方进行Capability的调用。一个最简单的Capability调用结构如下: ~~~ if(某个游戏对象.hasCapability(CapabilityHandler.自定义Capability变量名, 游戏对象的EnumFacing(除了TileEntity其他都应当设置为null))) { 新建数据接口变量 = (数据接口类型) 游戏对象.getCapability(CapabilityHandler.自定义Capability变量名, 游戏对象的EnumFacing(除了TileEntity其他都应当设置为null)); 数据接口变量.数据接口中的对应方法(); } ~~~ 例如在EOK中,将**玩家清醒度**这一Capability显示到玩家GUI上使用到了如下代码: ~~~ @SideOnly(Side.CLIENT) public class PlayerVitalSigns { ... @SubscribeEvent public void render(RenderGameOverlayEvent.Pre event){ ... EntityPlayerSP player = Minecraft.getMinecraft().player; double conV = 0.0D; ... if(player.hasCapability(CapabilityHandler.capConsciousness, null)) { IConsciousness consciousness = (IConsciousness) player.getCapability(CapabilityHandler.capConsciousness, null); conV = consciousness.getConsciousnessValue(); } } } ~~~ **需要注意的是,由于Forge不会自动同步服务端和客户端的自定义Capability信息,因此在调用Capability时必须调用相应游戏端的对应游戏对象。如果直接仿照以上代码在客户端中直接调用服务端中的Capability,会报出NullPointerException异常。关于该如何进行游戏端的远程交互以及完成对Capability的同步,我们将在下一章中进行介绍。**