能力系统

能力系统

能力系统可以动态、灵活地暴露功能,而不必直接实现大量接口。

通常,每种能力都以接口形式提供一种功能。

NeoForge 为方块、实体和物品堆提供能力支持,以下逐一说明。

为什么使用能力?

能力将方块、实体或物品堆能做什么与如何实现分开。判断是否适合使用能力时,可以问自己:

  1. 是否只关心它能做什么,而不关心具体如何实现?
  2. 这种行为是否只适用于部分方块、实体或物品堆?
  3. 行为的具体实现是否取决于对应的方块、实体或物品堆?

以下是适合使用能力的例子:

  • “希望自己的流体容器兼容其他 Mod 的容器,但不知道各个容器的实现细节。”适合使用 ResourceHandler<FluidResource> 能力。
  • “希望统计某实体中的物品数量,但不知道它如何存储物品。”适合使用 ResourceHandler<ItemResource> 能力。
  • “希望给某物品堆充能,但不知道它如何保存能量。”适合使用 EnergyHandler 能力。
  • “希望给玩家当前指向的任意方块上色,但不知道具体如何改变方块。”适合使用能力。NeoForge 没有内置方块染色能力,但可以自行实现。

以下情况不建议使用能力:

  • “希望检查实体是否在机器范围内。”应使用辅助方法。

NeoForge 提供的能力

NeoForge 为三种资源处理器提供能力:ResourceHandler<ItemResource>、ResourceHandler<FluidResource> 和 EnergyHandler。

ResourceHandler<ItemResource> 提供管理物品槽位的接口,对应能力有:

  • Capabilities.Item.BLOCK:可供自动化访问的方块物品栏,例如箱子、机器。
  • Capabilities.Item.ENTITY:实体物品栏,例如额外玩家槽位、生物背包。
  • Capabilities.Item.ENTITY_AUTOMATION:可供自动化访问的实体物品栏,例如船、矿车。
  • Capabilities.Item.ITEM:物品堆内的内容,例如便携背包。

ResourceHandler<FluidResource> 提供管理流体容器的接口,对应能力有:

  • Capabilities.Fluid.BLOCK:可供自动化访问的方块流体容器。
  • Capabilities.Fluid.ENTITY:实体流体容器。
  • Capabilities.Fluid.ITEM:物品堆流体容器。

EnergyHandler 提供操作能量容器的接口,基于 TeamCoFH 的 RedstoneFlux API。EnergyHandler 对应能力有:

  • Capabilities.Energy.BLOCK:方块内保存的能量。
  • Capabilities.Energy.ENTITY:实体内保存的能量。
  • Capabilities.Energy.ITEM:物品堆内保存的能量。

创建能力

NeoForge 支持方块、实体和物品堆能力。

能力通过分派逻辑查找 API 实现,NeoForge 提供以下类别:

  • BlockCapability:方块和方块实体能力,行为取决于具体 Block。
    • 通常使用 Direction 上下文,让不同方向提供不同资源。
  • EntityCapability:实体能力,行为取决于具体 EntityType。
    • 通常也使用 Direction 上下文区分方向。
  • ItemCapability:物品堆能力,行为取决于具体 Item。
    • 通常使用 ItemAccess 上下文,访问持有的物品资源。

建议 为兼容其他 Mod,尽可能使用 Capabilities 类中 NeoForge 提供的能力。确有需要时,再按本节方式创建自己的能力。

创建能力只需一次函数调用,结果应保存到 static final 字段。需要提供:

  • 能力名称。
    • 多次以相同名称创建能力,始终返回同一个对象。
    • 不同名称的能力完全独立,可用于不同用途。
  • 查询的行为类型,即泛型参数 T。
  • 查询所需的额外上下文类型,即泛型参数 C。

例如,支持方向上下文的方块物品处理器能力可以这样声明:

public static final BlockCapability<ResourceHandler<ItemResource>, @Nullable Direction> ITEM_HANDLER_BLOCK =
    BlockCapability.create(
        // Provide a name to uniquely identify the capability.
        Identifier.fromNamespaceAndPath("mymod", "item_handler"),
        // Provide the queried type. Here, we want to look up `ResourceHandler<ItemResource>` instances.
        ResourceHandler.asClass(),
        // Provide the context type. We will allow the query to receive an extra `Direction side` parameter.
        Direction.class
    );

方块常用 @Nullable Direction,因此提供了专用辅助方法:

public static final BlockCapability<ResourceHandler<ItemResource>, @Nullable Direction> ITEM_HANDLER_BLOCK =
    BlockCapability.createSided(
        // Provide a name to uniquely identify the capability.
        Identifier.fromNamespaceAndPath("mymod", "item_handler"),
        // Provide the queried type. Here, we want to look up `ResourceHandler<ItemResource>` instances.
        ResourceHandler.asClass()
    );

不需要上下文时应使用 Void,也有专用辅助方法:

public static final BlockCapability<ResourceHandler<ItemResource>, Void> ITEM_HANDLER_NO_CONTEXT =
    BlockCapability.createVoid(
        // Provide a name to uniquely identify the capability.
        Identifier.fromNamespaceAndPath("mymod", "item_handler_no_context"),
        // Provide the queried type. Here, we want to look up `ResourceHandler<ItemResource>` instances.
        ResourceHandler.asClass()
    );

实体和物品堆分别在 EntityCapability、ItemCapability 中提供类似方法。

查询能力

将 BlockCapability、EntityCapability 或 ItemCapability 保存到静态字段后,即可查询。

实体和物品堆通过 getCapability 查找实现,返回 null 表示没有可用实现。

例如:

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

方块的用法略有不同,因为没有方块实体的方块也可以提供能力。需要在 level 上查询,并额外提供目标位置 pos:

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

如果已知方块实体或方块状态,可以一起传入以减少查询开销:

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

具体来说,从方块的 Direction.NORTH 一侧查询物品处理器能力可以这样写:

ResourceHandler<ItemResource> handler = level.getCapability(Capabilities.Item.BLOCK, pos, Direction.NORTH);
if (handler != null) {
    // Use the handler for some item-related operation.
}

方块能力缓存

查询能力时,系统内部执行以下步骤:

  1. 如果未提供,获取方块实体和方块状态。
  2. 获取已注册的能力提供者,详见后文。
  3. 遍历提供者,询问其是否能提供该能力。
  4. 某个提供者返回能力实例,期间可能创建新对象。

实现本身较高效,但如果每个游戏刻等频繁查询,仍可能占用明显的服务端时间。BlockCapabilityCache 可显著加快固定位置的频繁能力查询。

建议 通常只创建一次缓存,并将其保存在频繁查询能力的对象字段中。具体何时创建、存在哪里由你决定。

调用 BlockCapabilityCache.create,传入能力、世界、位置和查询上下文,创建缓存。

// Declare the field:
private BlockCapabilityCache<ResourceHandler<ItemResource>, @Nullable Direction> capCache;

// Later, for example in `onLoad` for a block entity:
this.capCache = BlockCapabilityCache.create(
    Capabilities.Item.BLOCK, // capability to cache
    level, // level
    pos, // target position
    Direction.NORTH // context
);

然后通过 getCapability() 查询:

ResourceHandler<ItemResource> handler = this.capCache.getCapability();
if (handler != null) {
    // Use the handler for some item-related operation.
}

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

还可以在能力对象变化时收到通知,包括对象替换(oldHandler != newHandler)、变得不可用(null),以及重新可用(不再为 null)。

为此,创建缓存时额外提供两个参数:

  • 有效性检查,判断缓存本身是否仍有效。
    • 缓存保存在方块实体字段中时,最简单可使用 () -> !this.isRemoved()。
  • 失效监听器,能力变化时调用。
    • 可在这里响应能力变化、移除或出现。
// In `onLoad` for a block entity:
// With optional invalidation listener:
this.capCache = BlockCapabilityCache.create(
    Capabilities.Item.BLOCK, // capability to cache
    level, // level
    pos, // target position
    Direction.NORTH, // context
    () -> !this.isRemoved(), // validity check (because the cache might outlive the object it belongs to)
    () -> onCapInvalidate() // invalidation listener
);

使方块能力缓存失效

说明 失效机制只用于方块能力。实体和物品堆能力不能缓存,也不需要通知失效。

为让缓存正确更新,能力发生变化、出现或消失时,必须调用 level.invalidateCapabilities(pos)。

// whenever a capability changes, appears, or disappears:
level.invalidateCapabilities(pos);

NeoForge 已处理区块加载与卸载、方块实体创建与移除等常见情况,其他情况需由 Mod 显式处理,例如:

  • 先前返回的能力不再有效。
  • 提供能力但没有方块实体的方块被放置或改变状态,在 onPlace 中处理。
  • 提供能力但没有方块实体的方块被移除,在 onRemove 中处理。

普通方块的例子可参考 ComposterBlock.java。

更多信息请参阅 IBlockCapabilityProvider 的 Javadoc。

注册能力

能力提供者最终负责返回能力实例。它是一个函数,能够返回能力实例,不能提供时返回 null。提供者针对:

  • 它所提供的特定能力。
  • 它对应的方块实例、方块实体类型、实体类型或物品实例。

提供者必须在 RegisterCapabilitiesEvent 中注册。

方块提供者使用 registerBlock:

@SubscribeEvent // on the mod event bus
public static void registerCapabilities(RegisterCapabilitiesEvent event) {
    event.registerBlock(
        Capabilities.Item.BLOCK, // capability to register for
        (level, pos, state, be, side) -> <return the ResourceHandler<ItemResource>>,
        // blocks to register for
        MY_ITEM_HANDLER_BLOCK,
        MY_OTHER_ITEM_HANDLER_BLOCK
    );
}

通常只为某些方块实体类型注册,因此还提供 registerBlockEntity 辅助方法:

event.registerBlockEntity(
    Capabilities.Item.BLOCK, // capability to register for
    MY_BLOCK_ENTITY_TYPE, // block entity type to register for
    (myBlockEntity, side) -> myBlockEntity.myResourceHandlerForTheGivenSide
);

重要警告 方块或方块实体提供者先前返回的能力不再有效时,必须调用 level.invalidateCapabilities(pos) 使缓存失效,详见上面的失效处理。

实体注册方式类似,使用 registerEntity:

event.registerEntity(
    Capabilities.Item.ENTITY, // capability to register for
    MY_ENTITY_TYPE, // entity type to register for
    (myEntity, v) -> myEntity.myResourceHandlerForTheGivenContext
);

物品注册方式也类似,注意提供者会收到物品堆:

event.registerItem(
    Capabilities.Item.ITEM, // capability to register for
    (stack, itemAccess) -> <return the ResourceHandler<ItemResource> for the itemStack>,
    // items to register for
    MY_ITEM,
    MY_OTHER_ITEM
);

为所有对象注册能力

如果需要为所有方块、实体或物品注册提供者,需要遍历相应注册表,为每个对象分别注册。

例如,NeoForge 使用此机制,为所有 BucketItem,不包括其子类,注册流体资源处理器能力:

// For reference, you can find this code in the `CapabilityHooks` class.
for (Item item : BuiltInRegistries.ITEM) {
    if (item.getClass() == BucketItem.class) {
        event.registerItem(Capabilities.Fluid.ITEM, (stack, itemAccess) -> new BucketResourceHandler(itemAccess), item);
    }
}

提供者按注册顺序接受查询。如果希望先于 NeoForge 为某个对象注册的提供者执行,应将 RegisterCapabilitiesEvent 处理器设为更高优先级。

例如:

// use HIGH priority to register before NeoForge!
@SubscribeEvent(priority = EventPriority.HIGH) // on the mod event bus
public static void registerCapabilities(RegisterCapabilitiesEvent event) {
    event.registerItem(
        Capabilities.Fluid.ITEM,
        (stack, itemAccess) -> new BucketResourceHandler(itemAccess),
        // Items to register for
        MY_CUSTOM_BUCKET
    );
}

NeoForge 自行注册的提供者列表请参阅 CapabilityHooks。


来源: NeoForged/Documentation(26.1)。中文翻译依据提交 4a680cdd4d15。