ゲーム好きな豚のしっぽ

筆者の下手の横好きなゲーム日記、情報メモです。主にプレイするのはtrophymanager、WoT、WoWS、洋モノSLGなどの予定。

The Capability System

注意事項

この記事は
Capabilities - Forge Documentation
の記事を筆者が記述した時点での拙い知識で解釈したもののメモです。
誤訳や理解不足の可能性を多分に含みます。ご覧になる際はご注意ください。

Capability システム

Capabilityは、手数をかけてインターフェイスを実装することなく、様々な機能を動的に、柔軟に公開するための手法です。
一般的には、Capabilityはデフォルトの実装に要求される機能を満たすインターフェイスと、少なくともこのデフォルトの実装に対するストレージハンドラを提供します。
このストレージハンドラはほかの実装をサポートすることもありますが、これはCapabilityの実装者次第ですので、デフォルトのストレージと(Capabilityの)デフォルトではない実装の使用に挑戦する前に、ドキュメントを参照するようにしましょう。
Foegeはあなたが実装した、Capabilityメソッドをオーバーライドしているか、イベントを通じて公開されているTileEntity,Entity,ItemStackにCapabilityのサポートを追加します。これについては続くセクションで説明します。

Foegeが提供するCapability

元の記事の執筆時点でFoegeは IItemHandler 1つだけのCapabilityを提供します。

このCapabilityはインベントリのスロットに対するハンドリングのインターフェースを公開します。これはTileEntity(チェスト、機械装置など)、Entity(プレイヤー、mobクリーチャーのインベントリやバッグ)、ItemStack(バックパックなど)に適用できます。これはかつてのIInventoryISidedInventoryを自動的なシステムに置き換えます。

既存のCapabilityを利用する

先に軽く触れたように、TileEntity,Entity,ItemStackは、ICapabilityProviderインターフェイスを実装してCapabilityの機能を提供しています。このインターフェイスhasCapabilitygetCapabilityの二つのメソッドを追加し、そのオブジェクトでCapabilityが使用できるかを照会できるようにします。
Capabilityを取得するには、そのインスタンスの参照が必要になります。ItemHandlerの場合、そのCapabilityは基本的にCapabilityItemHandler.ITEM_HANDLER_CAPABILITYに保存されていますが、@CapabilityInjectアノテーションを利用することで、他のインスタンスでも参照することができるようになります。

@CapabilityInject(IItemHandler.class)
static Capability<IItemHandler> ITEM_HANDLER_CAPABILITY = null;

このアノテーションはフィールドとメソッドに適用できます。フィールドに適用したときは、Capabilityが登録された際に、(アノテーションを適用されているすべてのフィールドに同一の)インスタンスが割り当てられます。もしもCapabilityが登録されていない場合はnullが割り当てられます。ローカルなstaticフィールドへのアクセスは高速ですので、Capabilityを使用するオブジェクトには上記のソースのようにローカルなコピーを保持しておくことをお勧めします。このアノテーションメソッドに適用された場合は、Capabilityが登録された際の通知を特定の条件下でのみ有効にするために使用することができます。

hasCapabilitygetCapabilityのどちらも、2つ目のパラメータにEnumFacingをとります。これによって特定の方向を向いたインスタンスだけを照会できます。もしもnullが渡された場合は、ブロックの中であるとか、異世界のように方向が意味をなさないどこかが指定されていると仮定されます。この場合は、リクエストは方向を気にしない一般的なCapabilityのインスタンスに入れ替えられます。getCapabilityは、メソッドに渡されたCapabilityに宣言されている型を返します。ItemHandler Capabilityが返す型はやはりIItemHandlerです。

Capabilityの公開

Capabilityを公開するためには、Capabilityの基礎となる型のインスタンスが必要になります。Capabilityのインスタンスはおそらくほかのオブジェクトの内部に結び付けられるので、各オブジェクトに別々のCapabilityのインスタンスを割り当てる必要があることに注意してください。
こうしたインスタンスを取得するには、二つの方法があります。Capability自身を通じて取得するか、それを実装するオブジェクトを明示的にインスタンス化するかです。最初の方法は、デフォルトの実装を使用するように設計されています。これは、デフォルトの値が有用であれば便利です。ItemHandlerのCapabilityの場合、デフォルトの実装は一つのスロットを持つインベントリを公開します。これはおそらくあなたの希望するものではないでしょう。
二つ目の方法は、独自の実装を提供することです。IItemHandlerの場合、デフォルトの実装はItemStackHandlerクラスを使用し、このコンストラクタはオプションの引数にスロットの数をとります。しかしながら、Capabilityのシステムの目的の一つがロード時のエラーを防止ことにありますから、こうしたデフォルトの実装に依存することなく、インスタンス化はCapabilityが登録されているかのチェックを行った後に保護されるべきです。(前節の@CapabilityInjectアノテーションについて参照してください。)
Capabilityインターフェイスインスタンスを取得したら、ユーザーにCapabilityを公開するためにシステムの通知を送ることになります。これにはhasCapabilityメソッドをオーバーライドして、インスタンスがあなたの公開したCapabilityと一致するか比較します。もしもあなたの実装したクラスが方向によって別々なスロットを持つようなものであるなら、facingパラメータを利用することができます。EntityとItemStackではこのパラメータは無視されますが、プレイヤーの防具スロット(top=>頭スロット?)や、インベントリのブロック(西=>左のスロット?)などのようにコンテキストから方向を再現することは可能です。
忘れてはならないのは、親クラスのhasCapabilityへのフォールバックです。さもないと、Capabilityは機能を停止してしまいます。

@Override
public boolean hasCapability(Capability<?> capability, EnumFacing facing) {
  if (capability == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY) {
    return true;
  }
  return super.hasCapability(capability, facing);
}

同様に、リクエストによってCapabilityのインスタンスへの参照を渡す時も、親クラスへのフォールバックを忘れてはいけません。

@Override
public <T> T getCapability(Capability<T> capability, EnumFacing facing) {
  if (capability == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY) {
    return (T) inventory;
  }
  return super.getCapability(capability, facing);
}

Capabilityが毎tickごとに多くのオブジェクトを捜査し、ゲームの遅延を起こすことを可能な限り避けるために、Mapなどのデータ構造に依存せず、直接Capabilityをチェックすることが強く提示されています。

Capabilityの付加

軽く触れたように、Entity,ItemstackはAttachCapabilityEventを通じてCapabilityが付与されています。このイベントは粒度の異なる3つのイベントからなります。

  • AttachCapabilityEvent.Entity:Entityでのみ発火する
  • AttachCapabilityEvent.TileEntity:TileEntityでのみ発火する
  • AttachCapabilityEvent.Item:ItemStackでのみ発火する

いずれの場合でも、対象のオブジェクトにCapabilityを付与するためのaddCapabilityメソッドを持っています。これらをリストに加える代わりに、サーバかクライアントかの必要なサイドでのみCapabilityを返すCapability プロバイダを加えることが可能です。プロバイダはICapabilityProviderのみを実装する必要がありますが、もしも持続的にデータを保存しておきたいのであれば、ICapabilitySerializable<T extends NBTBase>を上乗せすれば、NBTの保存、読み込みの機能を提供することができます。

IcapabilityProviderの実装方法についての情報はCapabilityの公開を参照してください。

独自Capabilityの制作

一般的には、Capabilityの宣言と登録はCapabilityManager.INSTANCE.Register()の呼び出しを通じて行われます。Cpabilityクラスに専用のstaticメソッドregister()を作る方法も考えられますが、これではCapabilityシステムに組み込むことができません。匿名クラスを作成するというのも選択肢ではありますが、このドキュメントの目的でもありますのでクラスを記述しましょう。

CapabilityManager.INSTANCE.register(capability interface class, storage, default implementation factory);
このメソッドの最初の引数はCapabilityの型となります。ここではIExampleCapability.classとしましょう。

二つ目の引数はCapability.IStorage<T>を実装したクラスのインスタンスとなります。ここでのTは第一引数で指定したものと同じになります。このストレージクラスはデフォルト実装の読み書きを補助し、他の実装をサポートすることも可能です。

private static class Storage
    implements Capability.IStorage<IExampleCapability> {

  @Override
  public NBTBase writeNBT(Capability<IExampleCapability> capability, IExampleCapability instance, EnumFacing side) {
    // return an NBT tag
  }

  @Override
  public void readNBT(Capability<IExampleCapability> capability, IExampleCapability instance, EnumFacing side, NBTBase nbt) {
    // load from the NBT tag
  }
}

そして最後の引数はデフォルト実装の新しいインスタンスを返すファクトリとなります。

private static class Factory implements Callable<IExampleCapability> {

  @Override
  public IExampleCapability call() throws Exception {
    return new Implementation();
  }
}

最後に、このファクトリがインスタンス化できるようにデフォルト動作の実装が必要です。このクラスの設計はあなた次第ですが、少なくとも、このクラスを使用する人々が自身ですべての実装を行わなくても、機能のテストを行うことができるような骨格を提供する必要があります。

クライアントとのデータ同期

デフォルトでは、Capabilityのデータはクライアントへは送信されません。これを変更するにはmodがパケット通信で同期を管理する必要があります。

あなたが同期を行いたくなるようなシチュエーションは、下記の3つでしょう。

  1. エンティティがワールドにスポーンした、あるいは、ブロックが設置された。これらの初期化情報をクライアントに共有させたくなる。
  2. 保存されているデータが変更された。あなたはそれを監視しているクライアントに通知を送りたくなる。
  3. 新しいクライアントがエンティティやブロックを表示し始めた。あなたは存在しているデータを通知したくなる。
パケット通信の詳細な情報についてはNetworking後日訳しますを参照してください。

プレイヤーの死をまたぐ永続性

デフォルトでは、Capabilityのデータはプレイヤーの死後に永続しません。これを変更するためには、プレイヤーが死亡してリスポーンする際に手動でデータをコピーする必要があります。

これについては、PlayerEvent.Cloneイベントをハンドリングして、オリジナルのエンティティからデータを読み取り、新しいエンティティに割り当てを行うことで実現できます。このイベントの中で、wasDeadフィールドはエンド世界から帰還するとき実のところエンドに限らずディメンションの移動時は全部と死んでリスポーンするときを区別するために使用できます。エンドから帰還するときにはすでにデータが存在しているので、それを重複させてしまわないようにするために、これは重要なことです。

記事全体的に、使用法についてともうちょっと具体的な例がないとピンとこないですね…

4/12追記:元記事にForge1.9向けの記事追加されていました(IEEPがDuprecatedになったため)

IExtendedEntityPropertiesからの移行

CapabilityはIExtendedEntityProperties(IEEP)が行っていた以上のすべてを行うことができますが、二つの概念のすべてが1:1でマッチするわけではありません。

IEEPの機能:同等なCapabilityの機能早見

  • Property name/id(String):Capability key(ResourceLocation)
  • Registration(EntityConstracting):Attaching(AttachCapalityEvent.Entity)、実際のCapabilityの登録はPre-initの間に発生する
  • NBTの読み書きメソッド:自動的には発生しない。ICapabilitySerializableをイベントに接続して、serializeNBT/deserializeNBTを呼び出す。

(IEEPを内部的にのみ使用していたのなら)おそらく必要のない機能

  • Capabilityシステムは第三者の利用者が容易に使用できるデフォルト実装を提供しますが、これがIPEEを置き換えるために内部的に使用されるCapabilityの場合、あまり意味を持ちません。もしもCapabilityを内部的にのみ使用するのであれば、あなたはファクトリから安全にnullを返すこともできます。
    逆説的にはCapabilityを第三者も使用するAPIとして公開する場合にはファクトリがnullを返すのは望ましくない、ということ
  • Capabilityシステムはデフォルト実装のデータの読み書きを可能にするISorageを提供します。もしもデフォルト実装を提供しないことを選択したのであれば、IStorageは呼び出されることがないので、空白のまま残すこともできます。


以下のステップは、あなたがここまでの文章を読んで、Capabilityのコンセプトを理解していると仮定しています。

変換手順:
1.IEEPのkey/idの文字列をResourceLoacationに置き換える。(これはあなたのMODIDであり、ドメインです)
2.あなたのハンドラクラス(Capabilityインターフェイスの実装ではなく)にCapabilityのインスタンスを保持するフィールドを作成する。
3.EntityConstructingイベントをAttachCapabilityEventに変更する。IEEPに問い合わせを行う代わりに、ICapabilityProvider(おそらくNBTからの読み書きをできるICapabilitySerializableを持ったもの)に接続する。
4.(IEEPのイベントハンドラで使用していたはずですが)もしも作っていないのなら登録用メソッドを作成してCapabilityの登録メソッドを呼び出します。