跳转至

原文:MinecraftForge/Documentation · 目标版本:Forge 1.20.1

能力系统(The Capability System)

能力(Capability)允许以动态且灵活的方式暴露功能,而无需直接实现大量接口。

一般来说,每种能力都以接口的形式提供一项功能。

Forge 为方块实体(BlockEntity)、实体(Entity)、物品堆(ItemStack)、关卡(Level)和区块(LevelChunk)添加了能力支持。这些能力既可以通过事件附加到对象上,也可以在对象自身的实现中覆写(Override)能力方法。下文将对这些内容进行更详细的说明。

Forge 提供的能力(Forge-provided Capabilities)

Forge 提供了三种能力:IItemHandlerIFluidHandlerIEnergyStorage

IItemHandler 暴露了用于处理物品栏槽位的接口。它可以应用于方块实体(箱子、机器等)、实体(额外的玩家槽位、生物/生物的物品栏/背包)或物品堆(便携背包等)。它用一套面向自动化的系统取代了旧的 ContainerWorldlyContainer

IFluidHandler 暴露了用于处理流体库存的接口。它同样可以应用于方块实体、实体或物品堆。

IEnergyStorage 暴露了用于处理能量容器的接口。它可以应用于方块实体、实体或物品堆。它基于 TeamCoFH 的 RedstoneFlux API。

使用已有能力(Using an Existing Capability)

如前所述,方块实体、实体和物品堆通过 ICapabilityProvider 接口实现了能力提供器(capability provider)功能。该接口添加了 #getCapability 方法,可用于查询关联提供器对象中存在的各种能力。

为了获得能力,你需要通过其唯一实例来引用它。就 IItemHandler 而言,该能力主要存储在 ForgeCapabilities#ITEM_HANDLER 中,但也可以通过 CapabilityManager#get 获取其他实例引用。

public static final Capability<IItemHandler> ITEM_HANDLER = CapabilityManager.get(new CapabilityToken<>(){});

调用时,CapabilityManager#get 会为你的关联类型提供一个非空的能力。匿名的 CapabilityToken 让 Forge 在保留获取正确能力所需的泛型信息的同时,维持一套软依赖系统。

Important

即使你始终能拿到一个非空的能力,也不意味着该能力本身已经可用或已注册。这可以通过 Capability#isRegistered 来检查。

#getCapability 方法有第二个参数,类型为 Direction,可用于请求特定朝向的实例。如果传入 null,则可以认为该请求来自方块内部,或来自朝向没有意义的地方(例如不同的维度)。在这种情况下,将请求一个不关心朝向的通用能力实例。#getCapability 的返回类型对应于传入该方法的能力所声明的类型的 LazyOptional。对于物品处理能力,即为 LazyOptional<IItemHandler>。如果特定提供器无法提供该能力,则返回一个空的 LazyOptional

暴露能力(Exposing a Capability)

要暴露能力,首先需要底层能力类型的实例。注意,你应该为每个持有该能力的对象分配一个单独的实例,因为该能力很可能会与包含它的对象绑定。

IItemHandler 而言,默认实现使用 ItemStackHandler 类,其构造函数有一个可选参数,用于指定槽位数量。然而,应避免依赖这些默认实现的存在——能力系统的目的是防止在能力不存在的上下文中出现加载错误,因此实例化应该先检查该能力是否已注册(参见上一节中关于 CapabilityManager#get 的说明),并放在该检查的保护之后。

一旦你拥有了能力接口的实例,就需要告知能力系统的使用者你暴露了这种能力,并提供接口引用的 LazyOptional。这通过覆写 #getCapability 方法,并将传入的能力实例与你要暴露的能力进行比较来完成。如果你的机器(machine)根据查询的朝向具有不同的槽位,可以用 side 参数进行区分。对于实体和物品堆,此参数可以忽略,但仍有将朝向作为上下文的可能,例如玩家身上的不同盔甲槽位(Direction#UP 暴露玩家的头盔槽位),或物品栏周围的方块(Direction#WEST 暴露熔炉的输入槽)。不要忘记回退到 super,否则已附加的能力将停止工作。

能力必须在提供器生命周期结束时通过 LazyOptional#invalidate 失效。对于自有的方块实体和实体,LazyOptional 可以在 #invalidateCaps 中失效。对于非自有的提供器,应将执行失效逻辑的 runnable 传入 AttachCapabilitiesEvent#addListener

// 在你的 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();
}

Tip

如果某个对象上只暴露了一种能力,可以使用 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 附加,并应在物品堆的生命周期内持有你的能力。

强烈建议在代码中使用直接检查来测试能力,而不是依赖映射表或其他数据结构——因为每 tick 都可能有大量对象进行能力测试,它们必须尽可能快,以避免拖慢游戏。

附加能力(Attaching Capabilities)

如前所述,向已有提供器、LevelLevelChunk 附加能力可以使用 AttachCapabilitiesEvent。所有能提供能力的对象都使用同一个事件。AttachCapabilitiesEvent 有 5 个合法的泛型类型,分别提供以下事件:

  • AttachCapabilitiesEvent<Entity>:仅针对实体触发。
  • AttachCapabilitiesEvent<BlockEntity>:仅针对方块实体触发。
  • AttachCapabilitiesEvent<ItemStack>:仅针对物品堆触发。
  • AttachCapabilitiesEvent<Level>:仅针对关卡触发。
  • AttachCapabilitiesEvent<LevelChunk>:仅针对区块触发。

泛型类型不能比上述类型更具体。例如:如果你想为 Player 附加能力,你必须订阅 AttachCapabilitiesEvent<Entity>,然后在附加能力之前确定提供的对象是一个 Player

在所有情况下,事件都有一个 #addCapability 方法,可用于向目标对象附加能力。你添加的不是能力本身,而是能力提供器——提供器有机会仅从某些朝向返回能力。虽然提供器只需要实现 ICapabilityProvider,但如果能力需要持久化存储数据,可以实现 ICapabilitySerializable<T extends Tag>,它除了返回能力之外,还会提供标签(Tag)的保存/加载函数。

关于如何实现 ICapabilityProvider,请参阅暴露能力一节。

创建你自己的能力(Creating Your Own Capability)

可以通过两种方式之一注册能力:RegisterCapabilitiesEvent@AutoRegisterCapability

RegisterCapabilitiesEvent

可以通过 RegisterCapabilitiesEvent 注册能力,方法是将能力类型的类提供给 #register 方法。该事件在模组事件总线(EventBus)上处理

@SubscribeEvent
public void registerCaps(RegisterCapabilitiesEvent event) {
  event.register(IExampleCapability.class);
}

@AutoRegisterCapability

通过在能力类型上添加 @AutoRegisterCapability 注解来注册能力。

@AutoRegisterCapability
public interface IExampleCapability {
  // ...
}

持久化 LevelChunk 和方块实体的能力(Persisting LevelChunk and BlockEntity Capabilities)

与关卡、实体和物品堆不同,LevelChunk 和方块实体只有在被标记为脏(dirty)时才会写入磁盘。因此,为 LevelChunk 或方块实体实现具有持久状态的能力时,应确保每当其状态改变时,其所有者都被标记为脏。

常用于方块实体物品栏的 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();
    }
  }

  // ...
}

与客户端同步数据(Synchronizing Data with Clients)

默认情况下,能力数据不会发送给客户端。要改变这一点,模组必须使用数据包(Packet)自行管理同步代码。

你可能需要发送同步数据包的场景有三种,它们都是可选的:

  1. 当实体在关卡中生成或方块被放置时,你可能希望与客户端共享初始化时分配的值。
  2. 当存储的数据发生变化时,你可能希望通知部分或全部正在观察的客户端。
  3. 当新客户端开始观察该实体或方块时,你可能希望将现有数据通知给它。

有关实现网络数据包的更多信息,请参阅网络(Networking)页面。

在玩家死亡后持久化(Persisting across Player Deaths)

默认情况下,能力数据在死亡时不会持久化。要改变这一点,数据必须在重生过程中克隆玩家实体时手动复制。

这可以通过 PlayerEvent$Clone 完成,即从原始实体读取数据并分配给新实体。在该事件中,#isWasDeath 方法可用于区分死亡后重生与从末地返回。这一点很重要,因为从末地返回时数据已经存在,因此在这种情况下必须小心,避免重复赋值。

待复核清单

  • 按术语表校准:1.20.1 能力体系为 ICapabilityProvider + CapabilityManager#get + AttachCapabilitiesEvent + RegisterCapabilitiesEvent/@AutoRegisterCapability,未使用 1.21 的 DataAttachment。
  • 原文中 ICapabilitySerializable<T extends Tag>Tag 在 1.20.1 中对应 net.minecraft.nbt.Tag,按原文直译。
  • 「关卡(Level)」沿用 05-menus.md 的既有译法;若主会话更倾向「世界/维度」,需全局统一。
  • 原文代码块与注释均已核对,未改动代码本身。