ゲーム好きな豚のしっぽ

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

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では送信されながら使用されないデータが多く、無駄が多かったという判断でしょうか。格納できるデータの量が増えていることも理由のひとつかもしれません。

*1:アクセサでのみアクセスできるオブジェクトの属性。実体がそのインスタンスのフィールドとは限らない。実装がどのように変化しても、利用者はアクセサのみを使用するため影響を受けにくい。今回のような同期をとる必要のあるフィールドや、単位や型の変換が必要なフィールドへのアクセスを容易にする。

*2:コレクションにプライマリ型は入れられないし、Streamにもできない