能力系统
能力(Capability)可以动态、灵活地公开功能,而不必直接实现大量接口。通常,每种能力以接口的形式提供一种功能。
原创说明图,依据 Forge 1.21.x 官方文档 绘制。
Forge 为 BlockEntity、Entity、ItemStack、Level 和 LevelChunk 添加了能力支持。可以通过事件附加能力,也可以在自己实现的对象中重写能力方法来公开它们。下面详细介绍。
Forge 提供的能力
Forge 提供三种能力:IItemHandler、IFluidHandler 和 IEnergyStorage。
IItemHandler提供处理物品栏槽位的接口,可用于方块实体(箱子、机器等)、实体(额外玩家槽位、生物物品栏或背包)及物品堆叠(便携背包等)。它以方便自动化的系统取代旧的Container和WorldlyContainer。IFluidHandler提供处理流体容器的接口,也可用于方块实体、实体或物品堆叠。IEnergyStorage提供处理能量容器的接口,可用于方块实体、实体或物品堆叠。它基于 TeamCoFH 的 RedstoneFlux API。
使用已有能力
如前所述,方块实体、实体和物品堆叠通过 ICapabilityProvider 接口实现能力提供者功能。该接口添加了 #getCapability 方法,可查询关联提供者对象所具有的能力。
获取能力时,需要引用其唯一实例。对于 IItemHandler,能力主要保存在 ForgeCapabilities#ITEM_HANDLER 中,也可以通过 CapabilityManager#get 获取其他实例引用:
public static final Capability<IItemHandler> ITEM_HANDLER = CapabilityManager.get(new CapabilityToken<>(){});
调用 CapabilityManager#get 会为对应类型提供一个非空能力。匿名 CapabilityToken 让 Forge 在保留软依赖机制的同时,获取正确能力所需的泛型信息。
重要:即使能力始终非空,也不意味着该能力本身已经可用或注册。可通过
Capability#isRegistered检查。
#getCapability 的第二个参数类型为 Direction,可用于请求特定面的实例。如果传入 null,可以认为请求来自方块内部,或来自方向没有意义的地方(例如另一个维度);此时请求的是不区分方向的通用能力实例。返回类型为传入能力声明类型对应的 LazyOptional;对于物品处理能力,即 LazyOptional<IItemHandler>。如果特定提供者没有该能力,则返回空的 LazyOptional。
公开能力
要公开能力,首先需要底层能力类型的实例。每个持有能力的对象都应分配独立实例,因为该能力很可能与其所属对象绑定。
对于 IItemHandler,默认实现使用 ItemStackHandler,其构造函数有可选参数用于指定槽位数量。不过应避免依赖这些默认实现一定存在:能力系统的目的就是防止能力不存在时发生加载错误,因此实例化应置于检查能力是否已注册的条件之后(参见上一节 CapabilityManager#get 的说明)。
拥有能力接口实例后,需要通过重写 #getCapability 通知能力系统的使用者,并提供接口引用的 LazyOptional。比较传入的能力实例与要公开的能力。如果机器按查询方向提供不同槽位,可以检查 side 参数。对于实体和物品堆叠,可以忽略此参数,但方向也可以作为上下文,例如 Direction#UP 表示玩家头盔槽位,或 Direction#WEST 表示熔炉的输入槽位。不要忘记回退到 super,否则已有的附加能力会停止工作。
在提供者生命周期结束时,必须通过 LazyOptional#invalidate 使能力失效。对于自己拥有的方块实体和实体,可在 #invalidateCaps 中使其失效;对于不归自己所有的提供者,应向 AttachCapabilitiesEvent#addListener 传入负责失效处理的 runnable。
// 在你的 BlockEntity 子类中
LazyOptional<IItemHandler> inventoryHandlerLazyOptional;
// 提供实例(例如 () -> inventoryHandler)
// 确保延迟初始化,只在需要时初始化
inventoryHandlerLazyOptional = LazyOptional.of(inventoryHandlerSupplier);
@Override
public <T> LazyOptional<T> getCapability(Capability<T> cap, Direction side) {
if (cap == ForgeCapabilities.ITEM_HANDLER) {
return inventoryHandlerLazyOptional.cast();
}
return super.getCapability(cap, side);
}
@Override
public void invalidateCaps() {
super.invalidateCaps();
inventoryHandlerLazyOptional.invalidate();
}
提示:如果某个对象只公开一种能力,可以使用
Capability#orEmpty替代 if/else:
@Override
public <T> LazyOptional<T> getCapability(Capability<T> cap, Direction side) {
return ForgeCapabilities.ITEM_HANDLER.orEmpty(cap, inventoryHandlerLazyOptional);
}
Item 是特殊情况,因为其能力提供者保存在 ItemStack 上。应通过 Item#initCapabilities 附加提供者,由其在堆叠的生命周期内持有能力。
强烈建议在代码中直接检查能力,而不是依赖映射或其他数据结构:许多对象每刻都可能进行能力检查,必须尽可能快,以免拖慢游戏。
附加能力
如前所述,可使用 AttachCapabilitiesEvent 将能力附加到已有提供者、Level 和 LevelChunk。所有可提供能力的对象使用同一事件,该事件有五种有效的泛型类型:
AttachCapabilitiesEvent<Entity>:仅对实体触发。AttachCapabilitiesEvent<BlockEntity>:仅对方块实体触发。AttachCapabilitiesEvent<ItemStack>:仅对物品堆叠触发。AttachCapabilitiesEvent<Level>:仅对维度触发。AttachCapabilitiesEvent<LevelChunk>:仅对维度区块触发。
泛型类型不能比上述类型更具体。例如,要向 Player 附加能力,必须订阅 AttachCapabilitiesEvent<Entity>,再在附加前判断提供的对象是否为 Player。
在所有情况下,事件的 #addCapability 方法可将能力附加到目标对象。加入列表的是能力提供者,而非能力本身;提供者可以只从特定方向返回能力。提供者只需实现 ICapabilityProvider;如果能力需要持久存储数据,还可以实现 ICapabilitySerializable<T extends Tag>,它除了返回能力,还提供标签保存和加载函数。
有关如何实现 ICapabilityProvider,请参阅上文“公开能力”。
创建自己的能力
能力可通过 RegisterCapabilitiesEvent 或 @AutoRegisterCapability 注册。
RegisterCapabilitiesEvent
向 #register 方法传入能力类型的类即可注册能力。该事件在模组事件总线上处理。
@SubscribeEvent
public void registerCaps(RegisterCapabilitiesEvent event) {
event.register(IExampleCapability.class);
}
@AutoRegisterCapability
在能力类型上添加 @AutoRegisterCapability 注解即可注册:
@AutoRegisterCapability
public interface IExampleCapability {
// ...
}
持久化 LevelChunk 和 BlockEntity 能力
与 Level、Entity 和 ItemStack 不同,LevelChunk 和 BlockEntity 只有被标记为已更改(dirty)时才会写入磁盘。因此,它们的持久化能力实现应确保状态改变时将所属对象标记为已更改。
常用于方块实体物品栏的 ItemStackHandler 提供可重写的 void onContentsChanged(int slot),用于将方块实体标记为已更改。
public class MyBlockEntity extends BlockEntity {
private final IItemHandler inventory = new ItemStackHandler(...) {
@Override
protected void onContentsChanged(int slot) {
super.onContentsChanged(slot);
setChanged();
}
};
// ...
}
与客户端同步数据
默认情况下,能力数据不会发送给客户端。要改变这一点,模组必须使用数据包自行管理同步代码。以下三种情况下可以发送同步包,均为可选:
- 实体在维度中生成或方块放置时,将初始化赋予的值发送给客户端。
- 存储的数据改变时,通知部分或全部正在观察的客户端。
- 新客户端开始观察实体或方块时,将现有数据通知它。
实现网络数据包的详情见网络页面。
玩家死亡后保留数据
默认情况下,能力数据不会在死亡后保留。要改变这一点,必须在重生过程中克隆玩家实体时手动复制数据。
可通过 PlayerEvent$Clone 从原实体读取数据并赋给新实体。在该事件中,#isWasDeath 可用于区分死亡后重生和从末地返回。这一点很重要:从末地返回时数据已经存在,应避免重复添加值。