Skip to main content
Version: 1.21.4

能力系统(Capabilities)

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

一般来说,每个能力(capability)都以接口(interface)的形式提供某种功能。

NeoForge 为方块(block)、实体(entity)和物品堆叠(item stack)添加了能力支持。接下来的章节会更详细地介绍这些内容。

为什么要使用能力系统?

能力系统的设计初衷是将一个方块、实体或物品堆叠能够做什么(what)与如何实现(how)这件事分离开来。如果你在考虑是否该使用能力系统,可以自问以下几个问题:

  1. 我只关心一个方块、实体或物品堆叠能做什么,但不关心它是如何实现的吗?
  2. 这个“能做什么”——也就是行为——只对某些方块、实体或物品堆叠可用,而不是全部都有吗?
  3. 这个“如何实现”——也就是该行为的具体实现——是否依赖于具体的方块、实体或物品堆叠?

以下是一些合理使用能力系统的例子:

  • “我希望我的流体容器可以与其他模组(mod)的流体容器兼容,但我并不了解每种流体容器的具体实现。” —— 是的,应该使用 IFluidHandler 能力。
  • “我想统计某个实体中有多少物品,但我并不知道实体是如何存储这些物品的。” —— 是的,应该使用 IItemHandler 能力。
  • “我想给某个物品堆叠充能,但我并不知道该物品堆叠是如何存储能量的。” —— 是的,应该使用 IEnergyStorage 能力。
  • “我想给玩家当前瞄准的任意方块上色,但我并不知道该方块会如何被改变。” —— 是的。NeoForge 并没有内置给方块上色的能力,但你可以自己实现一个。

以下是不推荐使用能力系统的例子:

  • “我想检测某个实体是否在我的机器作用范围内。” —— 不要这样做,应该使用辅助方法(helper method)代替。

NeoForge 提供的能力类型(NeoForge-provided capabilities)

NeoForge 为以下三个接口(interface)提供了能力支持:IItemHandlerIFluidHandlerIEnergyStorage

IItemHandler 提供了一个用于管理物品栏槽位(inventory slot)的接口。IItemHandler 类型的能力有:

  • Capabilities.ItemHandler.BLOCK:可被自动化访问的方块物品栏(用于箱子、机器等)。
  • Capabilities.ItemHandler.ENTITY:实体的物品栏内容(如玩家额外槽位、怪物/生物的背包等)。
  • Capabilities.ItemHandler.ENTITY_AUTOMATION:可被自动化访问的实体物品栏(如船、矿车等)。
  • Capabilities.ItemHandler.ITEM:物品堆叠的内容(如便携式背包等)。

IFluidHandler 提供了一个用于管理流体库存(fluid inventory)的接口。IFluidHandler 类型的能力有:

  • Capabilities.FluidHandler.BLOCK:方块的可自动化访问的流体库存(fluid inventory)。
  • Capabilities.FluidHandler.ENTITY:实体的流体库存。
  • Capabilities.FluidHandler.ITEM:物品堆栈的流体库存。 由于桶(bucket)存储流体的方式不同,这个能力(capability)是特殊类型 IFluidHandlerItem

IEnergyStorage 提供用于处理能量容器的接口。它基于 TeamCoFH 的 RedstoneFlux API。类型为 IEnergyStorage 的能力有:

  • Capabilities.EnergyStorage.BLOCK:方块内部储存的能量。
  • Capabilities.EnergyStorage.ENTITY:实体内部储存的能量。
  • Capabilities.EnergyStorage.ITEM:物品堆栈内部储存的能量。

创建能力(Creating a capability)

NeoForge 支持为方块(blocks)、实体(entities)和物品堆栈(item stacks)创建能力(capabilities)。

能力(capabilities)允许通过一定的分发逻辑(dispatching logic)查找某些 API 的实现。NeoForge 中实现了以下几类能力:

  • BlockCapability:用于方块和方块实体(block entities)的能力,其行为取决于具体的 Block
  • EntityCapability:用于实体的能力,其行为取决于具体的 EntityType
  • ItemCapability:用于物品堆栈的能力,其行为取决于具体的 Item
tip

为了与其他模组(mod)兼容,推荐优先使用 NeoForge 在 Capabilities 类中提供的能力。如果无法满足需求,可以按照本节介绍的方法自定义能力。

创建一个能力只需调用一个函数,并将结果对象存储在 static final 字段中。必须提供以下参数:

  • 能力的名称。
    • 多次使用相同名称创建能力时,总是返回相同的对象。
    • 不同名称的能力完全独立,可用于不同的用途。
  • 被查询行为的类型。这是 T 类型参数。
  • 查询时提供的附加上下文类型。这是 C 类型参数。

例如,下面展示了如何声明一个针对方块、支持方向(side-aware)的 IItemHandler 能力:

public static final BlockCapability<IItemHandler, @Nullable Direction> ITEM_HANDLER_BLOCK =
BlockCapability.create(
// 提供一个名称以唯一标识该能力
ResourceLocation.fromNamespaceAndPath("mymod", "item_handler"),
// 提供被查询的类型。此处我们希望查找 `IItemHandler` 实例
IItemHandler.class,
// 提供上下文类型。我们允许查询时传入额外的 `Direction side` 参数
Direction.class);

@Nullable Direction 作为方块能力的上下文非常常见,因此 NeoForge 提供了专用的辅助方法:

public static final BlockCapability<IItemHandler, @Nullable Direction> ITEM_HANDLER_BLOCK =
BlockCapability.createSided(
// 提供一个名称,用于唯一标识该能力(capability)。
ResourceLocation.fromNamespaceAndPath("mymod", "item_handler"),
// 提供要查询的类型。这里我们想要查找 `IItemHandler` 实例。
IItemHandler.class);

如果不需要上下文(context),应使用 Void。对于无上下文的能力(capability),还提供了专用的辅助方法:

public static final BlockCapability<IItemHandler, Void> ITEM_HANDLER_NO_CONTEXT =
BlockCapability.createVoid(
// 提供一个名称,用于唯一标识该能力(capability)。
ResourceLocation.fromNamespaceAndPath("mymod", "item_handler_no_context"),
// 提供要查询的类型。这里我们想要查找 `IItemHandler` 实例。
IItemHandler.class);

对于实体(entity)和物品堆(item stack),分别在 EntityCapabilityItemCapability 中也有类似的方法可用。

查询能力(Querying capabilities)

当我们已经将 BlockCapabilityEntityCapabilityItemCapability 对象存放在静态字段中后,就可以查询对应的能力(capability)了。

对于实体(entity)和物品堆(item stack),可以通过 getCapability 方法尝试查找某个能力的实现。如果结果为 null,说明没有可用的实现。

例如:

var object = entity.getCapability(CAP, context);
if (object != null) {
// 使用 object
}
var object = stack.getCapability(CAP, context);
if (object != null) {
// 使用 object
}

方块能力(block capability)的用法稍有不同,因为即使没有方块实体(block entity)的方块也可以拥有能力(capability)。此时查询需要在 level 上进行,并额外传入我们要查找的 pos(位置)参数:

var object = level.getCapability(CAP, pos, context);
if (object != null) {
// 使用 object
}

如果已知方块实体(block entity)和/或方块状态(block state),可以一并传入,以减少查询时间:

var object = level.getCapability(CAP, pos, blockState, blockEntity, context);
if (object != null) {
// 使用 object
}

举一个更具体的例子,下面展示了如何从 Direction.NORTH 方向查询某个方块的 IItemHandler 能力(capability):

IItemHandler handler = level.getCapability(Capabilities.ItemHandler.BLOCK, pos, Direction.NORTH);
if (handler != null) {
// 使用 handler 进行某些与物品相关的操作。
}

方块能力缓存(Block capability caching)

当查找某个能力(capability)时,系统会在底层进行如下步骤:

  1. 如果未提供,则获取方块实体(block entity)和方块状态(block state)。
  2. 获取已注册的能力提供者(capability provider)。(后文会详细介绍)
  3. 遍历这些提供者,询问它们是否能提供该能力(capability)。
  4. 某个提供者会返回一个能力实例(capability instance),并可能会分配一个新对象。 该实现方式相当高效,但对于那些被频繁查询的场景,例如每个游戏刻(game tick)都要执行的查询,这些步骤可能会占用服务器大量时间。BlockCapabilityCache 系统为在特定位置频繁查询的能力(capability)提供了显著的性能提升。
tip

通常,BlockCapabilityCache 只需创建一次,然后存储在执行频繁能力查询的对象的某个字段中。至于何时何地保存缓存,由你自行决定。

要创建缓存,请调用 BlockCapabilityCache.create,传入需要查询的能力(capability)、关卡(level)、位置(position)以及查询上下文(context)。

// 声明字段:
private BlockCapabilityCache<IItemHandler, @Nullable Direction> capCache;

// 之后,例如在方块实体(block entity)的 `onLoad` 方法中:
this.capCache = BlockCapabilityCache.create(
Capabilities.ItemHandler.BLOCK, // 需要缓存的能力
level, // 关卡
pos, // 目标位置
Direction.NORTH // 上下文
);

之后可以通过 getCapability() 方法查询缓存:

IItemHandler handler = this.capCache.getCapability();
if (handler != null) {
// 使用 handler 执行与物品相关的操作。
}

该缓存会被垃圾回收器自动清理,无需手动注销。

你还可以在能力对象发生变化时收到通知!这包括能力发生变化(oldHandler != newHandler)、变为不可用(null)或再次变为可用(不再是 null)。

此时,创建缓存时需要额外传入两个参数:

  • 有效性检查(validity check),用于判断缓存是否仍然有效。
    • 最简单的用法是在方块实体字段中传入 () -> !this.isRemoved()
  • 失效监听器(invalidation listener),当能力发生变化时会被调用。
    • 你可以在这里响应能力的变化、移除或出现。
// 带可选失效监听器的创建方式:
this.capCache = BlockCapabilityCache.create(
Capabilities.ItemHandler.BLOCK, // 需要缓存的能力
level, // 关卡
pos, // 目标位置
Direction.NORTH, // 上下文
() -> !this.isRemoved(), // 有效性检查(因为缓存的生命周期可能超过所属对象)
() -> onCapInvalidate() // 失效监听器
);

方块能力失效(Block capability invalidation)

info

失效机制仅适用于方块能力(block capabilities)。实体(entity)和物品堆叠(item stack)能力无法被缓存,也不需要失效处理。

为了确保缓存能够正确更新其存储的能力,模组开发者必须在能力发生变化、出现或消失时调用 level.invalidateCapabilities(pos)

// 每当能力发生变化、出现或消失时调用:
level.invalidateCapabilities(pos);

NeoForge 已经自动处理了常见的情况,比如区块加载/卸载(chunk load/unloads)和方块实体(block entity)的创建/移除,但其它情况则需要模组开发者(modder)显式处理。例如,模组开发者必须在以下情况下使能力(capability)失效:

  • 如果之前返回的能力(capability)已经不再有效。
  • 如果一个提供能力(capability)的方块(没有方块实体)被放置或状态发生变化时,需要通过重写 onPlace 方法来处理。
  • 如果一个提供能力(capability)的方块(没有方块实体)被移除时,需要通过重写 onRemove 方法来处理。

关于普通方块的示例,可以参考 ComposterBlock.java 文件。

更多信息请参考 IBlockCapabilityProvider 的 javadoc。

注册能力(Registering capabilities)

能力提供者(capability provider)最终负责提供能力(capability)。能力提供者是一个函数,可以返回一个能力实例(capability instance),或者在无法提供时返回 null。能力提供者针对以下对象是特定的:

  • 它所提供的具体能力(capability),以及
  • 它所针对的方块实例、方块实体类型、实体类型或物品实例。

这些能力提供者需要在 RegisterCapabilitiesEvent 事件中进行注册。

方块的能力提供者可以通过 registerBlock 方法进行注册。例如:

private static void registerCapabilities(RegisterCapabilitiesEvent event) {
event.registerBlock(
Capabilities.ItemHandler.BLOCK, // 要注册的能力
(level, pos, state, be, side) -> <返回 IItemHandler>,
// 要注册的方块
MY_ITEM_HANDLER_BLOCK,
MY_OTHER_ITEM_HANDLER_BLOCK
);
}

通常,注册会针对某些方块实体类型(block entity types),因此也提供了 registerBlockEntity 辅助方法:

event.registerBlockEntity(
Capabilities.ItemHandler.BLOCK, // 要注册的能力
MY_BLOCK_ENTITY_TYPE, // 要注册的方块实体类型
(myBlockEntity, side) -> myBlockEntity.myIItemHandlerForTheGivenSide
);
danger

如果某个方块或方块实体提供者之前返回的能力(capability)已经不再有效,你必须调用 level.invalidateCapabilities(pos) 来使缓存失效。更多信息请参考上文的 失效处理章节

实体(entity)的能力注册方式类似,使用 registerEntity 方法:

event.registerEntity(
Capabilities.ItemHandler.ENTITY, // 要注册的能力
MY_ENTITY_TYPE, // 要注册的实体类型
(myEntity, context) -> myEntity.myIItemHandlerForTheGivenContext
);

物品(item)的注册方式也类似。注意,提供者会接收到物品堆(stack):

event.registerItem(
Capabilities.ItemHandler.ITEM, // 要注册的能力
(itemStack, context) -> <返回该 itemStack 的 IItemHandler>,
// 要注册的物品
MY_ITEM,
MY_OTHER_ITEM
);

为所有对象注册能力(Registering capabilities for all objects)

如果你出于某些原因需要为所有方块(blocks)、实体(entities)或物品(items)注册一个提供器(provider),你需要遍历对应的注册表(Registry),并为每个对象注册提供器。

例如,NeoForge 就利用这种方式为所有 BucketItem(不包括其子类)注册了流体处理器(fluid handler)能力(capability):

// 供参考,你可以在 `CapabilityHooks` 类中找到这段代码。
for (Item item : BuiltInRegistries.ITEM) {
if (item.getClass() == BucketItem.class) {
event.registerItem(Capabilities.FluidHandler.ITEM, (stack, ctx) -> new FluidBucketWrapper(stack), item);
}
}

提供器会按照它们被注册的顺序被调用。如果你希望在 NeoForge 已经为你的某个对象注册的提供器之前运行自己的逻辑,可以将你的 RegisterCapabilitiesEvent 事件处理器注册为更高的优先级(priority)。

例如:

modBus.addListener(RegisterCapabilitiesEvent.class, event -> {
event.registerItem(
Capabilities.FluidHandler.ITEM,
(stack, ctx) -> new MyCustomFluidBucketWrapper(stack),
// 要注册的物品
MY_CUSTOM_BUCKET);
}, EventPriority.HIGH); // 使用 HIGH 优先级,以便在 NeoForge 之前注册!

参见 CapabilityHooks,了解 NeoForge 自身注册的所有提供器列表。