# 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 难度:☆☆☆☆☆
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的同步,我们将在下一章中进行介绍。**
- 0.引子
- 基础篇 - 1.构建开发环境
- 基础篇 - 2.主类和代理
- 基础篇 - 3.创建一个物品
- 基础篇 - 4.创建一个方块
- 基础篇 - 4.1自定义方块模型
- 基础篇 - 5.初探事件系统
- 基础篇 - 6.Capability系统
- 基础篇 - 7.创建一个方块实体
- 基础篇 - 8.你的第一个GUI
- 基础篇 - 9.网络与通讯
- 进阶篇 - 0.更复杂的Mod
- 进阶篇 - 1.Minecraft渲染原理
- 进阶篇 - 2.更复杂的GUI
- 进阶篇 - 3.物品进阶
- 高级篇 - 1.深入探索OpenGL
- 高级篇 - 1.1OpenGL颜色渲染
- 高级篇 - 1.2OpenGL光照系统
- 高级篇 - 2.Minecraft贴图加载原理