能力
能力(Capability)允许以动态、灵活的方式暴露功能,无需直接实现大量接口。概括来说,每种能力都以接口形式提供一项功能。
NeoForge 为方块、实体和物品堆提供能力支持,下文会分别介绍。
为什么使用能力?
能力将方块、实体或物品堆“能做什么”与“如何实现”分离。如果不确定某项工作是否适合使用能力,可以考虑以下问题:
- 我是否只关心它能做什么,而不关心如何实现?
- 这种行为是否只存在于部分方块、实体或物品堆,而非全部对象?
- 行为的实现方式是否取决于具体方块、实体或物品堆?
以下是适合使用能力的例子:
- “我希望自己的流体容器兼容其他 Mod 的容器,但不知道它们的具体实现。”——使用
IFluidHandler能力。 - “我想统计某个实体包含多少物品,但不知道它如何存储物品。”——使用
IItemHandler能力。 - “我想给某个物品堆充能,但不知道它如何存储能量。”——使用
IEnergyStorage能力。 - “我想为玩家指向的任意方块着色,但不知道方块应如何变换。”——适合使用能力。NeoForge 没有内置方块着色能力,但可以自行实现。
以下情况不推荐使用能力:
- “我想检查实体是否处于机器范围内。”——应使用辅助方法。
NeoForge 提供的能力
NeoForge 为 IItemHandler、IFluidHandler 和 IEnergyStorage 三种接口提供能力。
IItemHandler 暴露物品栏槽位处理接口,对应能力包括:
Capabilities.ItemHandler.BLOCK:可供自动化访问的方块物品栏,例如箱子、机器。Capabilities.ItemHandler.ENTITY:实体物品栏内容,例如玩家额外槽位、生物物品栏或背包。Capabilities.ItemHandler.ENTITY_AUTOMATION:可供自动化访问的实体物品栏,例如船和矿车。Capabilities.ItemHandler.ITEM:物品堆内容,例如便携背包。
IFluidHandler 暴露流体存储处理接口,对应能力包括:
Capabilities.FluidHandler.BLOCK:可供自动化访问的方块流体存储。Capabilities.FluidHandler.ENTITY:实体流体存储。Capabilities.FluidHandler.ITEM:物品堆流体存储。由于桶存储流体的方式特殊,此能力使用专门的IFluidHandlerItem类型。
IEnergyStorage 暴露能量容器处理接口,基于 TeamCoFH 的 RedstoneFlux API。对应能力包括:
Capabilities.EnergyStorage.BLOCK:方块中的能量。Capabilities.EnergyStorage.ENTITY:实体中的能量。Capabilities.EnergyStorage.ITEM:物品堆中的能量。
创建能力
NeoForge 支持方块、实体和物品堆的能力。能力通过某种分派逻辑查找 API 的实现,具体有三类:
BlockCapability:方块和方块实体的能力,行为取决于具体Block。EntityCapability:实体能力,行为取决于具体EntityType。ItemCapability:物品堆能力,行为取决于具体Item。
建议 为了兼容其他 Mod,应尽可能使用
Capabilities中由 NeoForge 提供的能力。没有合适能力时,再按本节方式自行创建。
调用一个函数即可创建能力,结果应保存在 static final 字段中。必须提供以下参数:
- 能力名称。
- 多次用同一名称创建能力,始终返回同一对象。
- 不同名称的能力完全独立,可用于不同用途。
- 查询的行为类型,即类型参数
T。 - 查询附加上下文的类型,即类型参数
C。
例如,可按如下方式声明区分方向的方块 IItemHandler 能力:
public static final BlockCapability<IItemHandler, @Nullable Direction> ITEM_HANDLER_BLOCK =
BlockCapability.create(
// Provide a name to uniquely identify the capability.
ResourceLocation.fromNamespaceAndPath("mymod", "item_handler"),
// Provide the queried type. Here, we want to look up `IItemHandler` instances.
IItemHandler.class,
// Provide the context type. We will allow the query to receive an extra `Direction side` parameter.
Direction.class);
方块能力经常使用 @Nullable Direction,因此提供了专用辅助方法:
public static final BlockCapability<IItemHandler, @Nullable Direction> ITEM_HANDLER_BLOCK =
BlockCapability.createSided(
// Provide a name to uniquely identify the capability.
ResourceLocation.fromNamespaceAndPath("mymod", "item_handler"),
// Provide the queried type. Here, we want to look up `IItemHandler` instances.
IItemHandler.class);
无需上下文时应使用 Void,也有专用辅助方法:
public static final BlockCapability<IItemHandler, Void> ITEM_HANDLER_NO_CONTEXT =
BlockCapability.createVoid(
// Provide a name to uniquely identify the capability.
ResourceLocation.fromNamespaceAndPath("mymod", "item_handler_no_context"),
// Provide the queried type. Here, we want to look up `IItemHandler` instances.
IItemHandler.class);
实体与物品堆分别在 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 方向查询方块 IItemHandler 能力的具体示例:
IItemHandler handler = level.getCapability(Capabilities.ItemHandler.BLOCK, pos, Direction.NORTH);
if (handler != null) {
// Use the handler for some item-related operation.
}
方块能力缓存
查询能力时,系统内部会执行以下步骤:
- 如果未提供方块实体和方块状态,先获取它们。
- 获取已注册的能力提供器,下文会介绍。
- 遍历提供器,询问能否提供能力。
- 某个提供器返回能力实例,过程中可能分配新对象。
虽然实现比较高效,但如果每刻等高频查询,这些步骤仍可能占用大量服务器时间。BlockCapabilityCache 可以显著加速对固定位置的频繁查询。
建议 通常只创建一次
BlockCapabilityCache,并将其存储在高频查询能力的对象字段中。具体存储时机和位置由你决定。
调用 BlockCapabilityCache.create 创建缓存,传入能力、世界、位置和查询上下文。
// Declare the field:
private BlockCapabilityCache<IItemHandler, @Nullable Direction> capCache;
// Later, for example in `onLoad` for a block entity:
this.capCache = BlockCapabilityCache.create(
Capabilities.ItemHandler.BLOCK, // capability to cache
level, // level
pos, // target position
Direction.NORTH // context
);
然后调用 getCapability() 查询缓存:
IItemHandler handler = this.capCache.getCapability();
if (handler != null) {
// Use the handler for some item-related operation.
}
缓存会由垃圾回收器自动清理,无需注销。
能力对象变化时,也可以接收通知,包括对象变化(oldHandler != newHandler)、变为不可用(null)或重新可用(不再为 null)。此时创建缓存还需两个参数:
- 有效性检查,判断缓存本身是否仍有效。
- 最简单的方块实体字段用法中,
() -> !this.isRemoved()即可。
- 最简单的方块实体字段用法中,
- 失效监听器,在能力变化时调用。
- 可以在此响应能力变更、移除或出现。
// With optional invalidation listener:
this.capCache = BlockCapabilityCache.create(
Capabilities.ItemHandler.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
);
方块能力失效
说明 失效机制仅适用于方块能力。实体和物品堆能力不能缓存,也无需标记失效。
为了让缓存正确更新所保存的能力,每当能力变化、出现或消失时,Mod 开发者必须调用 level.invalidateCapabilities(pos)。
// whenever a capability changes, appears, or disappears:
level.invalidateCapabilities(pos);
NeoForge 已处理区块加载/卸载、方块实体创建/移除等常见情况,其他情况需要显式处理。例如:
- 先前返回的能力不再有效。
- 提供能力但没有方块实体的方块被放置或改变状态,应在重写的
onPlace中处理。 - 提供能力但没有方块实体的方块被移除,应在重写的
onRemove中处理。
普通方块的示例可查看 ComposterBlock.java。更多信息请参阅 IBlockCapabilityProvider 的 Javadoc。
注册能力
能力提供器最终负责提供能力。它是一个函数,能够返回能力实例;若无法提供,则返回 null。提供器针对:
- 它所提供的具体能力;
- 它所服务的方块实例、方块实体类型、实体类型或物品实例。
提供器必须在 RegisterCapabilitiesEvent 中注册。
使用 registerBlock 注册方块提供器,例如:
@SubscribeEvent // on the mod event bus
public static void registerCapabilities(RegisterCapabilitiesEvent event) {
event.registerBlock(
Capabilities.ItemHandler.BLOCK, // capability to register for
(level, pos, state, be, side) -> <return the IItemHandler>,
// blocks to register for
MY_ITEM_HANDLER_BLOCK,
MY_OTHER_ITEM_HANDLER_BLOCK
);
}
通常会针对特定方块实体类型注册,因此还提供 registerBlockEntity 辅助方法:
event.registerBlockEntity(
Capabilities.ItemHandler.BLOCK, // capability to register for
MY_BLOCK_ENTITY_TYPE, // block entity type to register for
(myBlockEntity, side) -> myBlockEntity.myIItemHandlerForTheGivenSide
);
重要警告 方块或方块实体提供器先前返回的能力不再有效时,必须调用
level.invalidateCapabilities(pos)使缓存失效。更多信息见上文失效机制。
实体使用类似的 registerEntity 注册:
event.registerEntity(
Capabilities.ItemHandler.ENTITY, // capability to register for
MY_ENTITY_TYPE, // entity type to register for
(myEntity, context) -> myEntity.myIItemHandlerForTheGivenContext
);
物品注册也类似。注意,提供器会收到物品堆:
event.registerItem(
Capabilities.ItemHandler.ITEM, // capability to register for
(itemStack, context) -> <return the IItemHandler 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.FluidHandler.ITEM, (stack, ctx) -> new FluidBucketWrapper(stack), item);
}
}
提供器按注册顺序接受查询。如果想在 NeoForge 已为某个对象注册的提供器之前运行,就为自己的 RegisterCapabilitiesEvent 处理器设置更高优先级。例如:
modBus.addListener(RegisterCapabilitiesEvent.class, event -> {
event.registerItem(
Capabilities.FluidHandler.ITEM,
(stack, ctx) -> new MyCustomFluidBucketWrapper(stack),
// blocks to register for
MY_CUSTOM_BUCKET);
}, EventPriority.HIGH); // use HIGH priority to register before NeoForge!
NeoForge 自身注册的提供器列表,可查看 CapabilityHooks。
来源: NeoForged/Documentation(1.21.1)。中文翻译依据提交
4a680cdd4d15。