net.minecraftforge.common.capabilities.Capability
注意事項
このリファレンスは筆者が記述した時点での拙い知識で翻訳、解釈したもののメモです。
誤訳や理解不足の可能性を多分に含みます。ご覧になる際はご注意ください。
ソースは1.8.9 - 11.15.1.1722のものを使用しています。
net.minecraftforge.common.capabilities.Capability
Forgeが追加する、NBTでの永続化が可能なオブジェクトのホルダ。おそらくは、ある機能を実現するオブジェクトをクラスの拡張や書き換えを行わずに動的に追加するための機構。有効な使い道がぱっとは浮かんでこない。
クラス宣言
public class Capability<T>
内部インターフェイス
public static interface IStorage<T>
T型を保存するためのインターフェイス。Capabilityクラスしか使用しないとはいえ拡張しないと使えないものを内部で宣言する理由はない。
IStorageのメソッド
NBTBase writeNBT(Capability<T> capability, T instance, EnumFacing side)
void readNBT(Capability<T> capability, T instance, EnumFacing side, NBTBase nbt)
CapabilityインスタンスをNBTTagにシリアライズ、デシリアライズします。
これをデータセーブのセントラル実装にすることができます。
重要な注意点として、Capabilityにどのようなインスタンス値が必須であるかはそのAPI定義次第です。
インターフェイスは利用できるメソッドを定義するだけなのでそのインスタンスのフィールドについては保証できない。
Capability自体はあくまでも情報伝搬の手段で、機能そのものはT型のオブジェクトの実装による。
T型オブジェクトの実装が変更されてもその情報を読み書きするためのインターフェイスは保障されるため、Capabilityは安全に機能する(はず)という思想かと思われる。
内部のデータを操作する可能性を考えると、「とある実装」は「デフォルト実装」のインスタンスを含んでいる必要があるかもしれません。
よくわかりません
メソッド
public String getName()
Capabilityの一意な名前を返します。一般的にはこれはターゲットになるインターフェイスの完全修飾名です。
public IStorage<T> getStorage()
デフォルトのストレージハンドラのインスタンスを返します。あなたはこれをあなたのNBTのデフォルト実装として安全に使用できます。
public T getDefaultInstance()
デフォルト実装のインスタンスを返します。デフォルトのストレージを使用したい場合、この正確に実装されたデフォルトを使用する必要があることが注意点です。
問題となるCapabilityの所有するAPIを参照してください。
どのみちT型の中身を把握していないと「デフォルトではない実装」を使用できないはずなので、正確に実装しろよという警告
net.minecraft.client.model.ModelBiped
注意事項
この記事はforge-1.8.9-11.15.1.1722のソースを筆者が記述した時点での拙い知識で解釈したもののメモです。
誤訳や理解不足の可能性を多分に含みます。ご覧になる際はご注意ください。
ModelBiped
二足歩行のLivingEntityのモデルの親クラス。
人型のモデルを作るならこれを基本にするので調べておく。
クラス宣言
@SideOnly(Side.CLIENT) public class ModelBiped extends ModelBase
ModelBaseを拡張している。
フィールド
public ModelRenderer bipedHead; public ModelRenderer bipedHeadwear; public ModelRenderer bipedBody; public ModelRenderer bipedRightArm; public ModelRenderer bipedLeftArm; public ModelRenderer bipedRightLeg; public ModelRenderer bipedLeftLeg;
モデルの各パーツとなるレンダラ。ModelBipedでは非常にシンプルで一つのパーツに一つのレンダラ、一つのボックスしかない。
public int heldItemLeft; public int heldItemRight;
左右の手にブロックを持っている時にそれがレンダリングするべきかどうかを記録する。
呼び出し階層で追いかけるとアイテムを手に持っている時に可能な行動であるEnumAction
で判別しており、それをswitch文でintに置き換えて保持している。大変古いコードの名残と思われるが、よくないコーディングスタイル。
メソッド
public void render(Entity entityIn, float p_78088_2_, float p_78088_3_, float p_78088_4_, float p_78088_5_, float p_78088_6_, float scale)
実際にモデルをレンダリングするメソッド。ここでは子どもではないかとスニークしているかで処理を分けている。継承したクラスでは(スティーブ氏と同じ体系、動作でよい場合を除いて)完全に上書きする必要がある。
public void setRotationAngles(float p_78087_1_, float p_78087_2_, float p_78087_3_, float p_78087_4_, float p_78087_5_, float p_78087_6_, Entity entityIn)
各パーツの角度を決める。モデルが取る可能性のあるポーズをすべてこの中で処理するため、メソッドが長くなる傾向がある。
継承したクラスではこのメソッドを参考にしながら完全に上書きする必要がある。理想でいえばポーズや状態ごとにそれ用の角度設定メソッドを用意して、このメソッド内では分岐条件を判断してそれを呼ぶだけにしたい。
ModelBipedで行っている処理は
1.初期化
2.騎乗している時
3.両手のアイテムによる調整
4.歩行時の腕振り角度
5.スニーク時とそれ以外のときの体と頭の角度
6.弓で狙う動作
の流れ。
継承したモデルではこれらについてすべて調整したうえで、独自の動作があればその処理を追加しなくてはならない。
public void setModelAttributes(ModelBase model)
別のモデルから状態(動作のフラグなど)をコピーする。メソッドではこれのみがsuperを呼び出している。
public void setInvisible(boolean invisible)
不可視状態を設定する
public void postRenderArm(float scale)
コメントとソースの追跡によると右腕をレンダリング実行後に角度を変化させている。効果は不明。
net.minecraft.client.model.ModelRenderer
注意事項
この記事はforge-1.8.9-11.15.1.1722のソースを筆者が記述した時点での拙い知識で解釈したもののメモです。
誤訳や理解不足の可能性を多分に含みます。ご覧になる際はご注意ください。
ModelRenderer
モデルのレンダリングを実際に引き受けているクラス。
基本的には1つのMedelRendererに一つの四角とテクスチャ。
3Dグラフィックに触れたことがないので不明点が多い。
フィールド
public float textureWidth; public float textureHeight;
テクスチャ画像の幅と高さ。単位はピクセル。
private int textureOffsetX; private int textureOffsetY;
テクスチャ画像のうち実際に使用する範囲の始点。単位はピクセル
public float rotationPointX; public float rotationPointY; public float rotationPointZ;
回転処理の始点座標
public float rotateAngleX; public float rotateAngleY; public float rotateAngleZ;
モデルの方向(角度)
private boolean compiled; private int displayList;
OpenGLの機能であるdisplayListとそれ用の描画情報がコンパイル済かのフラグ
public boolean showModel; public boolean isHidden;
名前からして表示非表示の切り替えだと思われるが使いわけ方が不明。
public List<ModelBox> cubeList;
レンダラが持つModelBoxのリスト。基本的に同じテクスチャ画像をオフセット指定で使いまわす。
public List<ModelRenderer> childModels;
子のレンダラ。こちらはModelRendererなのでそれぞれにテクスチャを指定できる。
public final String boxName;
レンダラの名前。
private ModelBase baseModel;
レンダラの親となるモデル。テクスチャのオフセットを得るために使用している模様
public float offsetX; public float offsetY; public float offsetZ;
座標のオフセット。どこを原点にしてのオフセットなのか不明
コンストラクタ
public ModelRenderer(ModelBase model, String boxNameIn) { this.textureWidth = 64.0F; this.textureHeight = 32.0F; this.showModel = true; this.cubeList = Lists.<ModelBox>newArrayList(); this.baseModel = model; model.boxList.add(this); this.boxName = boxNameIn; this.setTextureSize(model.textureWidth, model.textureHeight); }
一番詳細なコンストラクタ。別のコンストラクタはboxNameをnullで転送する。また、このコンストラクタはtexturOffsetを受け取らず、受け取るコンストラクタは別途setTextureOffsetを自分で呼び出す。
メソッド
public void addChild(ModelRenderer renderer)
現在のボックスの回転位置と回転角度を他のボックスへセットする
別のレンダラをこのレンダラと連動するようにする。
public ModelRenderer setTextureOffset(int x, int y)
テクスチャのオフセットのセッター
public void addBox(float p_78790_1_, float p_78790_2_, float p_78790_3_, int width, int height, int depth, float scaleFactor)
テクスチャ付きのボックスを追加する。引数はX,Y,Z,幅、高さ、奥行き、スケール倍率。
唯一コメントのついているaddBox。
他のaddBoxと見比べると、名前指定をするタイプのaddBoxの時は親Modelの持つMapからテクスチャのオフセットを取得してテクスチャを部分使用する。
それ以外はまったく同じテクスチャを使いまわすと思われる。
public void setRotationPoint(float rotationPointXIn, float rotationPointYIn, float rotationPointZIn)
回転処理の中心座標をセットする。
net.minecraft.client.model.ModelBase
注意事項
この記事はforge1.9-12.16.0.1797-1.9-snapshotのソースを筆者が記述した時点での拙い知識で解釈したもののメモです。
誤訳や理解不足の可能性を多分に含みます。ご覧になる際はご注意ください。
ModelBase
すべてのエンティティモデルの基底クラス。意外なほどフィールドやメソッドは少ない。
mod制作の中でも比較的大きな比重を占めるモデルづくりの根底なのできちんと調べておく。
クラス宣言
public abstract class ModelBase
基底クラスだけにいたってシンプル。
フィールド
public float swingProgress;
通常アニメーションのサイクルカウント用
public boolean isRiding;
騎乗状態か
public boolean isChild = true;
子供か(なんでridingは初期化してないのにこっちはしてるのか)
public List<ModelRenderer> boxList = Lists.<ModelRenderer>newArrayList();
実際にモデルをレンダリングするModelRendererのリスト。1パーツに1つ?
private Map<String, TextureOffset> modelTextureMap = Maps.<String, TextureOffset>newHashMap();
パーツごとのテクスチャ画像の始点位置のMap
public int textureWidth = 64;
テクスチャ画像の幅
public int textureHeight = 32;
テクスチャ画像の高さ
メソッド
public void render(Entity entityIn, float p_78088_2_, float limbSwing, float ageInTicks, float netHeadYaw, float headPitch, float scale)
モデルの様々な角度を設定してからレンダリングを行う
引数はレンダリングされるエンティティ、(bipedの実装によると)手足の前後の振りのパーセンテージ、手足の振りの最大がどれだけ遠いか、ティック数のカウント?、頭の水平方向、頭の仰俯角、スケール倍率
public void setRotationAngles(float limbSwing, float limbSwingAmount, float ageInTicks, float netHeadYaw, float headPitch, float scaleFactor, Entity entityIn)
モデルの様々な角度を設定する。
renderの角度設定部分の実装
public void setLivingAnimations(EntityLivingBase entitylivingbaseIn, float p_78086_2_, float p_78086_3_, float partialTickTime)
エンティティに固有なアニメーションの追加を容易にする。第二、第三の引数はSetRotationAnglesと同じ。
実はそこが一番簡単じゃない。
public ModelRenderer getRandomModelBox(Random rand)
モデルに追加されているパーツ(ボックス)から一つをランダムで取得する。ダメージなどのアニメーションに利用できる。
protected void setTextureOffset(String partName, int x, int y)
指定したパーツにテクスチャ画像の始点を指定する。
public TextureOffset getTextureOffset(String partName)
指定したパーツのテクスチャの始点を取得する。
public static void copyModelAngles(ModelRenderer source, ModelRenderer dest)
あるオブジェクトの角度を別のオブジェクトへコピーする。頭と髪の毛のように、互いが同じ方向を向くオブジェクトに利用できる。
public void setModelAttributes(ModelBase model)
与えられたモデルが持っている属性を受け取ったモデルへコピーする。
これだけでモデルが動いてくれれば苦労しないな…
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(バックパックなど)に適用できます。これはかつてのIInventory
やISidedInventory
を自動的なシステムに置き換えます。
既存のCapabilityを利用する
先に軽く触れたように、TileEntity,Entity,ItemStackは、ICapabilityProvider
インターフェイスを実装してCapabilityの機能を提供しています。このインターフェイスはhasCapability
とgetCapability
の二つのメソッドを追加し、そのオブジェクトで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が登録された際の通知を特定の条件下でのみ有効にするために使用することができます。
hasCapability
とgetCapability
のどちらも、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つでしょう。
- エンティティがワールドにスポーンした、あるいは、ブロックが設置された。これらの初期化情報をクライアントに共有させたくなる。
- 保存されているデータが変更された。あなたはそれを監視しているクライアントに通知を送りたくなる。
- 新しいクライアントがエンティティやブロックを表示し始めた。あなたは存在しているデータを通知したくなる。
プレイヤーの死をまたぐ永続性
デフォルトでは、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の登録メソッドを呼び出します。
EntityDataManager
注意事項
このリファレンスは筆者が記述した時点での拙い知識で翻訳、解釈したもののメモです。
誤訳や理解不足の可能性を多分に含みます。ご覧になる際はご注意ください。
ソースは1.9-12.16.0.1797-1.9-snapshotのものを使用しています。
EntityDataManager
Minecraft1.9からDataWatcherに変わって導入されたデータ同期のためのオブジェクトです。
DataWatcherの問題点であった、クラスごとにIDの指すオブジェクトの型や意味が違う、アクセス時にタイプを特定しないため、キャストが必要となり最悪の場合エラーの原因になる、IDでのみアクセスするため、そのアクセスが何を意図したものかわかりづらい、などを解決しています。以下、DataManagerと略して表記します。
DataManagerの利用法
DataManagerは、エンティティのプロパティ*1のうち、同期の必要のあるものを一括で管理するオブジェクトです。エンティティのインスタンス1つにつき1つが用意され、DataWatcherと同様に、初期化の際に監視するプロパティを登録します。
プロパティ生成と登録
プロパティはDataParamater<T>で実装されており、利用するときはDataManagerのcreateKeyメソッドから生成します。生成されたDataParamaterをフィールドとして保持しておき、これに対するアクセスをアクセサで管理することでプロパティとしての機能を実現しています。
生成の例:Entityクラスから
private static final DataParameter<Byte> FLAGS = EntityDataManager.<Byte>createKey(Entity.class, DataSerializers.BYTE); private static final DataParameter<Integer> AIR = EntityDataManager.<Integer>createKey(Entity.class, DataSerializers.VARINT); private static final DataParameter<String> CUSTOM_NAME = EntityDataManager.<String>createKey(Entity.class, DataSerializers.STRING); private static final DataParameter<Boolean> CUSTOM_NAME_VISIBLE = EntityDataManager.<Boolean>createKey(Entity.class, DataSerializers.BOOLEAN); private static final DataParameter<Boolean> SILENT = EntityDataManager.<Boolean>createKey(Entity.class, DataSerializers.BOOLEAN);
ジェネリクスでデータの型が明らかになっていること、フィールド名で用途を明示できることが大きな利点です。
生成時にcreateKeyメソッドに渡しているDataSirializarsの定数は、格納されるデータの型を表します。
DataSirializerの種類
名前 | 型 | 予想される用途 |
BLOCK_POS | BlockPos | ブロック基準の座標 |
BOOLEAN | boolan | 各種フラグ |
BYTE | Byte | 8bitのデータやフラグの組み合わせ |
FACING | EnumFacing | ブロックが向いている方向 |
FLOAT | Float | 単精度浮動小数 |
OPTIONAL_BLOCK_POS | Optional<BlockPos> | OptionalでラップしたBlockPos |
OPTIONAL_BLOCK_STATE | Optional<IBlockState> | OptionalでラップしたBlockState |
OPTIONAL_ITEM_STACK | Optional<ItemStack> | OptionalでラップしたItemStack |
OPTIONAL_UNIQUE_ID | Optional<UUID> | OptionalでラップしたUUID。プレイヤーやエンティティの識別子 |
ROTATIONS | Rotations | 現在のところArmorStandの部位の回転方向のみ |
STRING | String | 文字列 |
TEXT_COMPONENT | ITextComponent | チャットメッセージ用の装飾指定つき文字列 |
VARINT | Integer | 整数 |
登録には、DataManagerのregister(DataParamater<T>, T)を使用します。
DataWatcherのaddObject(int,Object)に相当しますが、数値ではなく生成しておいたDataParamater自体をキーに使用しますので、可読性が格段に高くなります。また、内部的にはIDに数値を使用していますが、利用者はそれを知っている必要はありません。DataManagerが自動的に採番して重複を避けるようになっています。
登録の例:Entityクラスから
this.dataWatcher = new EntityDataManager(this);//エンティティ自身を渡してDataManagerを生成 this.dataWatcher.register(FLAGS, Byte.valueOf((byte)0));//悪名高いflags this.dataWatcher.register(AIR, Integer.valueOf(300)); this.dataWatcher.register(CUSTOM_NAME_VISIBLE, Boolean.valueOf(false)); this.dataWatcher.register(CUSTOM_NAME, ""); this.dataWatcher.register(SILENT, Boolean.valueOf(false));
登録されたDataParameterへのアクセスも、DataManagerを通して行います。
アクセスに使用するメソッドはget(DataParameter)とset(DataParameter,T value)でシンプルです。
と言いたいところですが、古いバージョンのソースでプライマリ型を指定されているプロパティについては、キャストやvalueOfで型変換行う必要があります。自作modで新しくプロパティを追加するときには、プライマリ型ではなくラッパークラスを使用するように心がけた方がよさそうです*2。
DataParamaterを定数として宣言していますので、実際のソースは以下のようになります。
アクセサの例:Entityクラスから
public int getAir() { return ((Integer)this.dataWatcher.get(AIR)).intValue(); } public void setAir(int air) { this.dataWatcher.set(AIR, Integer.valueOf(air)); }
DataWatcherの時とは違い、どのデータにアクセスしようとしているかが明確になっています。また、IDEを使用しているのであればデータの型も確かめやすいので、コーディングは格段に楽になるといえるでしょう。先ほども述べましたが、型を宣言しているのに前方互換のためにプライマリ型に戻さなくてはならなかったりするのは、仕様上仕方がありません。
セッターから新しい値が与えられると、エンティティのnotifyDataManagerChange(DataParamater key)が呼び出されます。
エンティティはこのkeyがどのプロパティかを確認して、必要があるならばアニメーションを開始したり、SEを鳴らしたりといった処理を行います。このメソッドの中では親クラスの挙動を変更したい場合を除いて、親クラスの同メソッドを呼び出す必要があります。忘れるとどのような不具合が発生するかわかりませんので、忘れないようにしましょう。
notifyDataManagerChangeの実装の例:EntityLivingクラスから
public void notifyDataManagerChange(DataParameter<?> key) { super.notifyDataManagerChange(key); if (HAND_STATES.equals(key) && this.worldObj.isRemote) { if (this.isHandActive() && this.activeItemStack == null) { this.activeItemStack = this.getHeldItem(this.getActiveHand()); if (this.activeItemStack != null) { this.activeItemStackUseCount = this.activeItemStack.getMaxItemUseDuration(); } } else if (!this.isHandActive() && this.activeItemStack != null) { this.activeItemStack = null; this.activeItemStackUseCount = 0; } } }
この例では(おそらく)1.9で追加された両手持ちのアイテムを入れ替える処理を行っています。
DataWatcherから変更点
利用法とその目的はDataWatcherから大きく変わってはいませんが、その仕組みは大きく変化しており、いくつかの制限が撤廃されているようです。
登録できるプロパティが255個(ID0~254)になった
DataManagerのregisterメソッドを見ると、自動採番のIDが255未満で新しいオブジェクトを追加できるようになっています。
登録できる型が増えた
今後も増えるかもしれません。
変更部分のみが送信されるようになった
ゲームサイクルのアップデートの際に、実際に数値の変更があったプロパティのみを送信するように設計されています。DataWatcherでは送信されながら使用されないデータが多く、無駄が多かったという判断でしょうか。格納できるデータの量が増えていることも理由のひとつかもしれません。
DataWatcherに登録されているフィールド
注意事項
このリファレンスは筆者が記述した時点での拙い知識で翻訳、解釈したもののメモです。
誤訳や理解不足の可能性を多分に含みます。ご覧になる際はご注意ください。
ソースは1.8.9 - 11.15.1.1722のものを使用しています。
1.9からDataWatcherに代わってEntityDataManagerが使用されています。
この表でもわかるようにクラスごとにIDが指しているオブジェクトの意味も型も違っているため、その混乱を避ける意味と、キャストが減り、実行速度の改善とメモリの適正な使用が期待できるという理由から変更されたのだと思われます。おそらくこの効果は追加するエンティティが多い大型のMODほど高いです。
DataWatcherに登録されているフィールド
主にベースクラスのものをソースコードから拾っています。
DataWatcher-id | 宣言クラス#プロパティ名 | 型 | 用途 |
0 | Entity#flag | boolean(byte) | 一つのオブジェクトに5つのflagを詰め込んでいる。0:isBurning,1:isSneaking,2:isRiding,3:isSprinting,4:isEating |
1 | Entity#air | Short | 呼吸量? |
2 | Entity#customNameTag | String | エンティティの固有名 |
3 | Entity#allwaysRenderNameTag | boolean(byte) | 名前を常時表示するかどうか |
4 | Entity#silent | boolean(byte) | 音を出すかどうか |
6 | EntityLivingBase#helth | float | 体力値 |
7 | EntityLivingBase#potionMetadata | Integer | ポーション効果の色 |
8 | EntityLivingBase#portionMetadata | boolean(byte) | ポーション効果があるかどうか |
9 | EntityLivingBase#arrowCountInEntity | byte | エンティティに刺さっている矢の数 |
10 | EntityPlayer | byte | 不明 |
12 | EntityAgeable#growingAge | byte | 成長までの年齢(時間) |
15 | EntityLivig#isAiDisabled | boolean(byte) | AIを使用するかどうか |
16 | EntityTameable#isSitting | boolean(bit1) | お座り状態か |
16 | EntityTameable#isTamed | boolean(bit4) | テイムされているかどうか |
16 | EntityVilager#profession | Integer | 村人の職業 |
17 | EntytyPlayer#absorptionAmount | float | 呼吸量 |
17 | EntityTameable#ownerId | String | 飼い主のID |
18 | EntityPlayer#score | Integer | スコア(経験値) |