能力系统
能力系统可以动态、灵活地暴露功能,而不必直接实现大量接口。
通常,每种能力都以接口形式提供一种功能。
NeoForge 为方块、实体和物品堆提供能力支持,以下逐一说明。
为什么使用能力?
能力将方块、实体或物品堆能做什么与如何实现分开。判断是否适合使用能力时,可以问自己:
- 是否只关心它能做什么,而不关心具体如何实现?
- 这种行为是否只适用于部分方块、实体或物品堆?
- 行为的具体实现是否取决于对应的方块、实体或物品堆?
以下是适合使用能力的例子:
- “希望自己的流体容器兼容其他 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.
}
方块能力缓存
查询能力时,系统内部执行以下步骤:
- 如果未提供,获取方块实体和方块状态。
- 获取已注册的能力提供者,详见后文。
- 遍历提供者,询问其是否能提供该能力。
- 某个提供者返回能力实例,期间可能创建新对象。
实现本身较高效,但如果每个游戏刻等频繁查询,仍可能占用明显的服务端时间。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。