• Forge论坛

导航页

  • 主页
  • 向文档做出贡献
  • 入门
    • 概述
    • 模组文件
    • 规划你的模组结构
    • 版本号
  • 核心概念
    • 注册表
    • 端位(Sides)
    • 事件
    • 模组生命周期
    • 资源
    • 国际化与本地化
  • 方块
    • 概述
    • 方块状态
  • 物品
    • 概述
    • BlockEntityWithoutLevelRenderer
  • 网络
    • 概述
    • SimpleImpl
    • 实体的同步
  • 方块实体
    • 概述
    • BlockEntityRenderer
  • 游戏特效
    • 粒子效果
    • 音效
  • 数据储存
    • Capabilities
      • Forge提供的Capability
      • 使用现存的Capability
      • 公开一个Capability
      • Capability的附加
      • 创建你自己的Capability
      • LevelChunk和BlockEntity的Capability的持久化
      • 向客户端同步数据
      • 在玩家死亡时的持久化
    • Saved Data
    • 编解码器(Codecs)
  • 图形用户界面
    • 菜单(Menus)
    • 屏幕(Screens)
  • 渲染
    • 模型扩展
      • 概述
      • 根变换
      • 渲染类型
      • 部分可见度
      • 面数据
    • 模型加载器
      • 概述
      • 烘焙模型
      • 变换
      • 物品重载
  • 资源
    • 客户端资源(Assets)
      • 概述
      • 模型
        • 概述
        • 纹理色调
        • 物品属性
    • 服务端数据(Data)
      • 概述
      • 配方
        • 概述
        • 自定义配方
        • 原料
        • 非数据包配方
      • 战利品表
      • 全局战利品修改器
      • 标签
      • 进度
      • 条件性加载数据
  • 数据生成
    • 概述
    • 客户端资源(Assets)
      • 模型提供者
      • 语言提供者
      • 音效提供者
    • 服务端数据(Data)
      • 配方提供者
      • 战利品表提供者
      • 标签提供者
      • 进度提供者
      • 全局战利品修改器提供者
      • 数据包注册表对象提供者
  • 杂项功能
    • 配置
    • 键盘布局
    • 游戏测试
    • Forge更新检查器
    • 调试分析器
  • 进阶主题
    • 访问转换器
  • 向Forge做出贡献
    • 概述
    • Pull Request准则
  • 旧版本
    • 概述
    • 移植到当前版本

Capability系统

Capability允许以动态和灵活的方式公开Capability,而不必直接实现许多接口。

一般来说,每个Capability都以接口的形式提供了一个Capability。

Forge为BlockEntity、Entity、ItemStack、Level和LevelChunk添加了Capability支持,这些Capability可以通过事件附加它们,也可以通过重写你自己的对象实现中的Capability方法来公开。这将在接下来的章节中进行更详细的解释。

Forge提供的Capability

Forge提供三种Capability:IItemHandler、IFluidHandler和IEnergyStorage。

IItemHandler公开了一个用于处理物品栏Slot的接口。它可以应用于BlockEntity(箱子、机器等)、Entity(额外的玩家Slot、生物/生物物品栏/袋子)或ItemStack(便携式背包等)。它用一个自动化友好的系统取代了旧的Container和WorldlyContainer。

IFluidHandler公开了一个用于处理流体物品栏的接口。它也可以应用于BlockEntitiy、Entity或ItemStack。

IEnergyStorage公开了一个用于处理能源容器的接口。它可以应用于BlockEntity、Entity或ItemStack。它基于TeamCoFH的RedstoneFlux API。

使用现存的Capability

如前所述,BlockEntity、Entity和ItemStack通过ICapabilityProvider接口实现了Capability提供者Capability。此接口添加了方法#getCapability,该方法可用于查询相关提供者对象中存在的Capability。

为了获得一个Capability,你需要通过它的唯一实例来引用它。在IItemHandler的情况下,此Capability主要存储在ForgeCapabilities#ITEM_HANDLER中,但也可以使用CapabilityManager#get获取其他实例引用。

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

当被调用时,CapabilityManager#get为你的相关类型提供一个非null的Capability。匿名的CapabilityToken允许Forge保持软依赖系统,同时仍然拥有获得正确Capability所需的泛型信息。

重要

即使你在任何时候都可以使用非null的Capability,但这并不意味着该Capability本身是可用的或已注册的。这可以通过Capability#isRegistered进行检查。

#getCapability方法有另一个参数,类型为Direction,可用于请求那一面的特定实例。如果传递null,则可以假设请求来自方块内,或者来自某个侧面没有意义的地方,例如不同的维度。在这种情况下,将请求一个不关侧面的一个通用的Capability实例。#getCapability的返回类型将对应于传递给方法的Capability中声明的类型的LazyOptional。对于物品处理器Capability,其为LazyOptional<IItemHandler>。如果该Capability不适用于特定的提供者,它将返回一个空的LazyOptional。

公开一个Capability

为了公开一个Capability,你首先需要一个底层Capability类型的实例。请注意,你应该为每个保有该Capability的对象分配一个单独的实例,因为该Capability很可能与所包含的对象绑定。

在IItemHandler的情况下,默认实现使用ItemStackHandler类来指定多个Slot,该类在构造函数中有一个可选参数。然而,应避免依赖这些默认实现的存在,因为Capability系统的目的是防止在不存在Capability的情况下出现加载错误,因此如果Capability已注册,则应在检查测试之后对实例化进行保护(请参阅上一节中关于CapabilityManager#get的备注)。

一旦你拥有了自己的Capability接口实例,你将希望通知Capability系统的用户你公开了此Capability,并提供接口引用的LazyOptional。这是通过重写#getCapability方法来完成的,并将Capability实例与你要公开的Capability进行比较。如果你的机器根据被查询的一侧有不同的Slot,你可以使用side参数进行测试。对于实体和物品栈,此参数可以忽略,但仍然可以将侧面作为上下文,例如玩家上的不同护甲Slot(Direction#UP暴露玩家的头盔Slot),或物品栏中的周围方块(Direction#WEST暴露熔炉的输入Slot)。不要忘记回到super,否则现有的附加Capability将停止工作。

在提供者生命周期结束时,必须通过LazyOptional#invalidate使Capability失效。对于拥有的BlockEntitiy和Entity,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();
}

提示

如果给定对象上只公开了一个Capability,则可以使用Capability#orEmpty作为if/else语句的替代语句。

@Override
public <T> LazyOptional<T> getCapability(Capability<T> cap, Direction side) {
  return ForgeCapabilities.ITEM_HANDLER.orEmpty(cap, inventoryHandlerLazyOptional);
}

Item是一种特殊情况,因为它们的Capability提供者存储在ItemStack上。相反的是,应该通过Item#initCapabilities附加提供者。其应该在物品栈的生命周期中保持你的Capability。

强烈建议在代码中使用直接检查来测试Capability,而不是试图依赖Map或其他数据结构,因为每个游戏刻都可以由许多对象进行Capability测试,并且它们需要尽可能快,以避免减慢游戏速度。

Capability的附加

如前所述,可以使用AttachCapabilitiesEvent将Capability附加到现有提供者、Level和LevelChunk。同一事件用于所有可以提供Capability的对象。AttachCapabilitiesEvent有5个有效的泛型类型,提供以下事件:

  • AttachCapabilitiesEvent<Entity>: 仅为实体触发。
  • AttachCapabilitiesEvent<BlockEntity>: 仅为方块实体触发。
  • AttachCapabilitiesEvent<ItemStack>: 仅为物品栈触发。
  • AttachCapabilitiesEvent<Level>: 仅为存档触发。
  • AttachCapabilitiesEvent<LevelChunk>: 仅为存档区块触发。

泛型类型不能比上述类型更具体。例如:如果要将Capability附加到Player,则必须订阅AttachCapabilitiesEvent<Entity>,然后在附加Capability之前确定所提供的对象是Player。

在所有情况下,该事件都有一个方法#addCapability,可用于将Capability附加到目标对象。不是将Capability本身添加到列表中,而是添加Capability提供者,这些提供者有机会仅从某些面返回Capability。虽然提供者只需要实现ICapabilityProvider,但如果该Capability需要持久存储数据,则可以实现ICapabilitySerializable<T extends Tag>,该Capability除了返回Capability外,还将提供标签保存/加载Capability。

有关如何实现ICapabilityProvider的信息,请参阅公开一个Capability部分。

创建你自己的Capability

Capability可通过以下两种方式之一被注册:RegisterCapabilitiesEvent或@AutoRegisterCapability。

RegisterCapabilitiesEvent

通过向#register方法提供Capability类型的类,可以使用RegisterCapabilitiesEvent注册Capability。该事件在模组事件总线上被处理。

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

@AutoRegisterCapability

Capability也可通过使用@AutoRegisterCapability注释以被注册。

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

LevelChunk和BlockEntity的Capability的持久化

与Level、Entity和ItemStack不同,LevelChunk和BlockEntity只有在标记为脏时才会写入磁盘。因此,LevelChunk或BlockEntity具有持久状态的Capability实现应确保无论何时其状态发生变化,其所有者都被标记为脏。

ItemStackHandler通常用于BlockEntity中的物品栏,它有一个可重写的方法void onContentsChanged(int slot),用于将BlockEntity标记为脏。

public class MyBlockEntity extends BlockEntity {

  private final IItemHandler inventory = new ItemStackHandler(...) {
    @Override
    protected void onContentsChanged(int slot) {
      super.onContentsChanged(slot);
      setChanged();
    }
  }

  // ...
}

向客户端同步数据

默认情况下,Capability数据不会发送到客户端。为了改变这一点,模组必须使用数据包管理自己的同步代码。

在三种不同的情况下,你可能希望发送同步数据包,所有这些情况都是可选的:

  1. 当实体在存档中生成或放置方块时,你可能希望与客户端共享初始化指定的值。
  2. 当存储的数据发生更改时,你可能需要通知部分或全部正在监视的客户端。
  3. 当新客户端开始查看实体或方块时,你可能希望将现有数据通知它。

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

在玩家死亡时的持久化

默认情况下,Capability数据不会在死亡时持续存在。为了改变这一点,在重生过程中克隆玩家实体时,必须手动复制数据。

这可以通过PlayerEvent$Clone完成,方法是从原始实体读取数据并将其分配给新实体。在这种情况下,#isWasDeath方法可以用于区分死后重生和从末地返回。这一点很重要,因为从末地返回时数据已经存在,因此在这种情况下必须注意不要重复值。

Built with MkDocs using a custom theme. Hosted by Read the Docs.
Enable Dark Theme