石橋を叩いて壊すページ

エンティティのアンロードを防ぐ

今回の記事の内容はちょっとニッチ(隙間)。

ワールドにエンティティをスポーンさせたあと、そのエンティティをプラグインで
記憶しておくという場合があると思う。
例えば、村人をスポーンさせて、プレイヤーがその村人を探し出すといったプラグインを作る場合、
探す対象となる村人、具体的には村人を表すVillagerオブジェクトを覚えておく必要がある。

ところが、このVillagerオブジェクトが思ったとおりの動きをしないことがある。
getLocation()で村人の位置を調べて、その場所に向かっても、なぜか村人がいないのだ。
また、setCustomName(String)で村人に名前をつけても、なぜか名前がつかなかったりする。

思ったとおりに村人を操作できない上にエラーがでないので、この問題にはだいぶ悩まされたが、
タネがわかれば上記動作はとても簡単に理解できた。
記事のタイトルにも書いたが、村人をプレイヤーから遠い場所にスポーンさせてしまったため、
村人がメモリ上からアンロードされていた。

実験してみよう。
まず、適当なワールドの座標[0,100,0]に、小さなステージを作り、柵で囲って村人が逃げ出さないようにする。


空中に浮かぶお立ち台。

ここで、以下のコードのプラグインを用意する。

/**
 * 生成した村人
 */
public Villager vill = null;

/**
 * プレイヤーがクリックしたときに呼ばれる
 * 
 * @param e
 */
@EventHandler
public void onPlayerInteractEvent(PlayerInteractEvent e) {

    // 右クリックしたのか
    if ((e.getAction().equals(Action.RIGHT_CLICK_AIR) || e.getAction()
            .equals(Action.RIGHT_CLICK_BLOCK))) {

        // 村人は未生成か
        if (vill == null) {
            // 村人を生成する
            vill = (Villager) e
                    .getPlayer()
                    .getWorld()
                    .spawnEntity(
                            new Location(e.getPlayer().getWorld(), 0, 100,
                                    0), EntityType.VILLAGER);

            // 村人に名前をつける
            vill.setCustomName("村人");
            vill.setCustomNameVisible(true);
        } else {
            // 生成した村人の座標を表示する
            Location loc = vill.getLocation();
            Bukkit.broadcastMessage(String.format("%1.1f,%1.1f",
                    loc.getX(), loc.getZ()));
        }
    }
}

/**
 * プレイヤーがエンティティを右クリックすると呼び出される
 * 
 * @param e
 */
@EventHandler
public void onPlayerInteractEntityEvent(PlayerInteractEntityEvent e) {

    // 村人は生成済みか
    if (vill != null) {
        // 生成した村人と右クリックしたエンティティは同じか
        if (e.getRightClicked().equals(vill)) {
            Bukkit.broadcastMessage("同じエンティティ");
        } else {
            Bukkit.broadcastMessage("違うエンティティ");
        }
    }
}

紙を手に持って右クリックすると、1回目は村人をスポーンする。
2回目以降は、先にスポーンした村人の座標を表示する。
ほかに、エンティティを右クリックすると、先にスポーンした村人と同じエンティティかを調べてメッセージを表示する。

さっそく上記コードを動かしてみる。


紙を手に持って右クリックしたところ。村人がスポーンした。



さらに右クリックしたところ。
村人が動くに従って、Villager#getLocation()が返す座標も動いていることがわかる。


村人を右クリックしたところ。当然、同じエンティティである。

さて、問題はここからである。
/tpコマンドを使って、プレイヤーを1万ほど地平線の彼方へテレポートし、
一息ついてから同じ場所に戻ってくる。


地平線の彼方は海だった。


戻ってきた。海へ飛んで戻ってくるまで、約10秒。

さて、右クリックしてみよう。


村人を右クリックしたところ。
さっきスポーンした村人のはずなのに、違うエンティティになっている。



紙を手に持って右クリックしたところ。
村人は動いているが、Villager#getLocation()が返す座標は動いていない。

たった10秒ほど海にテレポートしただけで、村人は別人になっていた。
理由は簡単に予想がつく。
海に出かけた間に村人はディスクに保存されてメモリからアンロードされ、
戻ってきたときにディスクからエンティティタイプとその名前が呼び出されたが、
同一人物ではない、先ほどの彼と同じ情報を持ったただのエンティティとして復活を遂げたに違いない。

さて、そうと分かれば村人をアンロードさせなければいい話だが、
Villagerクラスとその親クラスにそれっぽいメソッドは見当たらない。
ならば無理やりやってくれようぞ。

先ほど書いたコードに、以下のコードを追加する。

/**
 * チャンクがアンロードされるとき呼び出される
 * 
 * @param e
 */
@EventHandler
public void onChunkUnloadEvent(ChunkUnloadEvent e) {

    // 村人は生成済みか
    if (vill != null) {
        // アンロードされるチャンクは生成した村人がいるチャンクか
        if (e.getChunk().equals(vill.getLocation().getBlock().getChunk())) {
            // アンロードをキャンセル
            e.setCancelled(true);
        }
    }
}

やっていることは簡単である。村人がいるチャンクがアンロードされそうになったらキャンセルしている。
これで、そのチャンクに立っている村人もアンロードされなくなるはずである。

実験してみよう。先と同じ手順で実験してみる。


海へ行って…


戻ってきた。


村人を右クリック。海へ行く前と同一人物のようである。



村人の移動に従い、Villager#getLocation()で得られる座標も変化している。

これで、村人がアンロードされて思い通りに動かない問題が解消できた。
かなり昔のアップデートで、プレイヤーから32ブロック以上離れたエンティティはその場を移動しない仕様になっているため、
村人が自主的に移動してアンロードされた隣のチャンクへ移動してしまう可能性はないと考えている。
なお言うまでもないが、チャンクをアンロードしなければその分メモリを食ってしまうのでやりすぎには要注意。

ちなみに、上記コード実行中に海ではなくネザーに行き、その後戻ってきた場合も、
意図通り同一人物として認識されたので、別世界へ出かける場合も有効のようである。

もうひとつ別のアプローチもある。EntityクラスのgetUniqueId()メソッドを使う。
このメソッドは、エンティティの固有のIDを取得することができる。
つまり、村人が一度アンロードされても、次にロードされるときに、同じIDを持っている村人を探せば、
アンロード前の村人を識別することができる。

/**
 * 村人
 */
private Villager vill = null;

/**
 * 村人のID
 */
private UUID villid = null;

/**
 * プラグインが有効化されるとき呼び出される
 */
public void onEnable() {

    log = this.getLogger();

    // デフォルトの設定ファイルを作る
    saveDefaultConfig();

    // 村人のIDは設定ファイルに記録されているか
    if (getConfig().isString("VillagerID")) {
        // 村人のIDを読み込む
        villid = UUID.fromString(getConfig().getString("VillagerID"));
    }

    // イベントリスナーの登録
    getServer().getPluginManager().registerEvents(this, this);
}

/**
 * プレイヤーがクリックしたときに呼ばれる
 * 
 * @param e
 */
@EventHandler
public void onPlayerInteractEvent(PlayerInteractEvent e) {

    // 右クリックしたのか
    if ((e.getAction().equals(Action.RIGHT_CLICK_AIR) || e.getAction()
            .equals(Action.RIGHT_CLICK_BLOCK))) {

        // 村人は未生成か
        if (vill == null) {
            // 村人を生成する
            vill = (Villager) e
                    .getPlayer()
                    .getWorld()
                    .spawnEntity(
                            new Location(e.getPlayer().getWorld(), 0, 100,
                                    0), EntityType.VILLAGER);

            // 村人に名前をつける
            vill.setCustomName("村人");
            vill.setCustomNameVisible(true);

            // 村人のIDを記憶する
            villid = vill.getUniqueId();

            Bukkit.broadcastMessage("村人のIDは"
                    + vill.getUniqueId().hashCode() + "です。");

            // 村人のIDを設定ファイルに保存する
            getConfig().set("VillagerID", vill.getUniqueId().toString());
            saveConfig();
        } else {
            // 生成した村人の座標を表示する
            Location loc = vill.getLocation();
            Bukkit.broadcastMessage(String.format("%1.1f,%1.1f",
                    loc.getX(), loc.getZ()));
        }
    }
}

/**
 * プレイヤーがエンティティを右クリックすると呼び出される
 * 
 * @param e
 */
@EventHandler
public void onPlayerInteractEntityEvent(PlayerInteractEntityEvent e) {
    Bukkit.broadcastMessage("そのエンティティのIDは"
            + e.getRightClicked().getUniqueId().hashCode() + "です");
}

/**
 * チャンクがロードされるとき呼び出される
 * 
 * @param e
 */
@EventHandler
public void onChunkLoadEvent(ChunkLoadEvent e) {

    // 村人のIDは覚えているか
    if (villid != null) {
        // ロードされるチャンクにいるエンティティを取得
        Entity[] entities = e.getChunk().getEntities();

        // ロードされるチャンクのなかにあの時の村人は含まれているか
        for (Entity entity : entities) {

            // そのエンティティはあの時の村人か
            if (entity.getUniqueId().equals(villid)) {
                // その村人のことを覚える
                vill = (Villager) entity;
                Bukkit.broadcastMessage("村人をリロードしました");
                break;
            }
        }
    }
}

上のコードでは、チャンクロードイベントを捕まえて、読み込まれるチャンク内にスポーンさせた村人と
同じIDを持つ村人を探している。

さらに、このUUIDはマインクラフトのセーブデータに記録されるため、
Bukkitを再起動しても、同じ村人には同じIDが付与される。
上のコードでは、getConfig().set(…)のところで設定ファイルに村人のIDを記録しているので、
Bukkitが再起動しても、プラグインを再有効化しても、以前にスポーンした村人を探すことができる。

この記事を評価

この記事にコメント

  1. ...

【この記事にコメント】
お名前:
コメント:

Menu