解説が考えつかなかったので、ForgeDocに沿って解説します。※編集時点220f7216094def0d75802ef7d23c94d449d3a591
Capabilityシステムの動的で柔軟な方法によって、たくさんのインターフェースを実装しなくても様々な機能を公開できるようになります。
大雑把に言うとそれぞれのCapabilityは、Interfaceの形式、並びに要求可能なデフォルト実装、そして少なくともデフォルト実装に対して機能する保存用ハンドラを提供します。
保存用ハンドラは他の実装もサポート可能ですが、これはCapability実装クラスによりますので、デフォルト実装以外でデフォルト保存ハンドラを使おうと試みる前にそれらのドキュメントを読むようにしてください。
ForgeによってTileEntityとEntityとItemStackに対してCapabilityのサポートが追加されています。これによってイベントを通じてもしくは自身のオブジェクトであればCapability関係のメソッドをオーバーライドすることで付加し機能を公開することができます。
このことは下の節でより詳細に説明します。
目次
Forgeによって提供されるCapability
ForgeはIItemHandler、IFluidHandlerそしてIEnergyStorageの3つのCapabilityを提供します。
IItemHandlerはインベントリのスロットを制御するためのインターフェースを公開します。TileEntity(チェスト、機械など)やEntity(プレーヤスロットの拡張、モブや生物のインベントリやバッグ)、ItemStack(持ち運び可能なバックパックなど)に適用できます。
IInventory
やISidedInventory
は、この自動化に適応したシステムによって置き換えられます。
IFluidHandlerは流体インベントリを制御するためのインターフェースを公開します。これもTileEntityやEntity、ItemStackに適用できます。
IFluidHandler
は、この統一性があって自動化に適応したシステムによって置き換えられます。
IEnergyStorageはエネルギコンテナを制御するためのインターフェースを公開します。これもTileEntityやEntity、ItemStackに適用できます。
これはTeamCoFHのRedstoneFlux APIにもとづいています。
既存のCapabilityの利用
簡単に言うと、TileEntityとEntityとItemStackのCapabilityの提供の実装はICapabilityProvider
を通じています。
このインターフェースはhasCapability
とgetCapability
というCapabilityを参照するための2つのメソッドを提供します。
Capabilityを取得するためには、一意なインスタンスを参照する必要があります。
アイテムのハンドラであれば、このCapabilityはCapabilityItemHandler.ITEM_HANDLER_CAPABILITY
に格納されていますが、@CapabilityInject
アノテーションを用いて他のインスタンス参照を得ることも可能です。
@CapabilityInject(IItemHandler.class) static Capability<IItemHandler> ITEM_HANDLER_CAPABILITY = null;
このアノテーションはフィールドとメソッドに付与できます。
フィールドに適用した場合、そのフィールドはCapabilityのインスタンス(すべてのフィールドで共通)がCapabilityの登録時に代入されます。もしCapabilityが登録されなければ、既存の値 (null
)となります。
ローカルの静的フィールドアクセスは速いので、Capabilityとともに動作するオブジェクトの参照のローカルコピーを保持するのはいいアイデアでしょう。
メソッドにも適用でき、Capabilityが登録されたことの通知を受け取ることで、状況に応じた機能のオンオフが可能になります。
hasCapability
とgetCapability
のどちらもがEnumFacing型の第二引数を持ちます。これは一つの面に対してのインスタンスを要求するために使用できます。
null
を渡した場合、ブロック内または面が意味をなさない場所での要求が想定できます。この場合は面に依存しない一般的なCapabilityが代わりに返されます。
getCapability
の返す型はメソッドに渡されたCapabilityで宣言された型によって決まります。Item HandlerのCapabilityであれば、IItemHandler
となります。
Capabilityの公開
Capabilityを公開するためには、まず根本的なCapability型のインスタンスを必要とします。
Capabilityは殆どの場合それを含むオブジェクトに結び付けられていますので、Capabilityを保持するそれぞれのオブジェクトに分けてインスタンスを代入すべきことを心がけましょう。
そのインスタンスを取得する方法は、Capability自体を通して、もしくは明示的にCapabilityの実装をインスタンス化する、という2つがあります。
一つ目の方法は、デフォルトの実装を使用するためにデザインされています。
Item HandlerのCapabilityの場合、デフォルトの実装では一スロットインベントリを公開します。おそらくあなたの望むものではありませんが。
二つ目の方法は、カスタムした実装を提供するために使用できます。
IItemHandler
の場合、デフォルト実装はItemStackHandler
を使用しますが、このクラスはスロット数を指定できるコンストラクタを持ちます。
しかしながら、デフォルト実装の存在を確信するのは避けるべきでしょう。Capabilityが存在しない状況でのロード時エラーを防ぐためにもCapabilityが登録されているかを先に確かめておくべきです。
もしあなたがCapabilityインタフェースのインスタンスを持っているならば、あなたはこのCapabilityを公開していることをCapabilityシステムのユーザーに通知したいと考えるでしょう。
これはhasCapability
をオーバーライドし、公開しているCapabilityとインスタンス比較することによって達成できます。
もし面によって対応するスロットが異なるのであれば、facing
引数で検査できます。
EntityやItemStackではこの引数は無視できますが、アーマースロット(上面なら頭のスロットとか?)やインベントリ内の周辺ブロック(西面なら左のスロットとか?)などのように活用もできます。
オーバーライドするときにはsuper
の呼び出しを忘れないよう気をつけてください。(もちろん機能停止する場合は別ですが)
@Override public boolean hasCapability(Capability<?> capability, EnumFacing facing) { if (capability == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY) { return true; } return super.hasCapability(capability, facing); }
同様に、リクエストされた際にインターフェースの参照を提供できます。繰り返しますが、super
の呼び出しをお忘れなく。
@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に様々なブロックから為されますので、インスタンスチェックのみに留めることを強く推奨します。
ゲームの実行を遅らせたくはありませんからね。
Capabilityの付加
先に伝えましたが、Capabilityの付加はAttachCapabilityEvent
を使ってできます。
すべてのCapability提供可能オブジェクトに対して同じイベントが利用できます。
AttachCapabilityEvent
には以下に示す4つの有効なジェネリックタイプを持ちます。
AttachCapabilityEvent<Entity>
: Entityに対してのみ。AttachCapabilityEvent<TileEntity>
: TileEntityに対してのみ。AttachCapabilityEvent<Item>
: ItemStackに対してのみ。AttachCapabilityEvent<World>
: Worldに対してのみ。
上に示した以上に絞って指定することはできません。
例えばEntityPlayer
にCapabilityを付加したい場合、AttachCapabilityEvent<Entity>
からオブジェクトを検査した後にCapabilityを付加する必要があります。
すべての場合において、addCapability
というメソッドから対象のオブジェクトへCapabilityを付加できます。
Capability自体を追加するだけでなく、Capabilityプロバイダも追加できます。
プロバイダはICapabilityProvider
を実装するだけでできます。
もしデータを永続的に保持する必要があるならICapabilitySerializable<T extends NBTBase>
を実装することでCapabilityを返すこととNBTのセーブ・ロードの両方が可能になります。
ICapabilityProvider
の実装方法についてはCapabilityの公開を参照してください。
自作Capabilityの作成
一般的に言えば、CapabilityはCapabilityManager.INSTANCE.register()
を通して宣言し登録できます。
一つの可能性として、静的なregister()
メソッドをCapabilityのためのクラスに定義するということが考えられますが、これは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のテストに使える程度には基本的な骨組みを提供すべきです。
Tips
クライアントとの同期
デフォルトでは、Capabilityのデータはクライアントと同期されません。
これを変えるには、パケットを使うことで同期用コードを管理する必要があります。
同期用パケットを送りたくなるであろう3つの異なる状況を以下に示します。
- Entityのスポーン時、Blockの設置時など初期化時の値をクライアントと共有したいとき。
- 保持しているデータが変わった際にクライアントに通知したいとき。
- 新たなクライアントがEntityまたはBlockを観測し始めた際にすでに存在するデータを通知したいとき。
パケットの実装については、ForgeのNetworkingシステム(1.7のパケットについて,ForgeDoc Networking)を参照してください。
死亡時を越えての持続
デフォルトでは、Capabilityのデータは死亡時に消失します。
これを変えるには、プレイヤーがリスポーンの過程でクローンされるときに手動でコピーされる必要があります。
これはPlayerEvent.Clone
イベントを利用することで可能です。もとのEntityからデータを読み込み、新たなEntityに代入することでできます。
このイベント内で、wasDead
フィールドによってEndからの帰還時か死亡からのリスポーン時か判定できます。
End帰還時はCapabilityのデータは破棄されませんので、重複した値を与えないためにもこの判定は重要です。
IExtendedEntityPropertiesからの移行
CapabilityシステムはIExtendedEntityProperties(以下IEEP)がしてきた、またそれ以上の機能を実現できます。
しかしながら、この2つのコンセプトは完全に対応しているわけではありません。IEEPをCapabilityに移行する方法を説明しましょう。
下はIEEPとCapabilityの対応早見リストです
- 固有の 名前/ID (
String
): Capabilityのキー (ResourceLocation
) - 登録 (
EntityConstructing
): (AttachCapabilityEvent<Entity>
)での付加。Capability自体の登録はpre-initです。 - NBTのread/write : 自動で実行はされません。 イベントで
ICapabilitySerializable
を付加し、serializeNBT
/deserializeNBT
からread/writeを呼び出してください。
必要とされないであろうCapabilityの機能(IEEPを内部的にしか使っていないのであれば)
- デフォルト実装を提供するCapability。内部的ならば、ファクトリはnullで良いでしょう。
IStorage
を提供するCapability。デフォルト実装を提供しないのであれば、これは要求されません。空のままで良いです。
以下の手順は、このドキュメントをしっかり読みCapabilityについて理解したという前提で書いています。
移行手順
- IEEP key/id の文字列を
ResourceLocation
に変更する。ドメインにはModIDを指定します。 - ハンドラクラス内(Capabilityインターフェースを実装したクラスではなく)で、Capabilityを保持するフィールドを作成。
EntityConstructing
イベントをAttachCapabilityEvent
イベントに変え、IEEPの代わりにICapabilityProvider
を付加する。- 登録メソッドを作成し、その中でCapability登録メソッドを実行する。