石橋を叩いて壊すページ

Bukkitで地面を検出する


前回の記事のとおり、エンダードラゴンの討伐を完了。
これにてV1.7の冒険はとりあえずひと段落。


ところで、討伐中に宙に浮かぶエンダークリスタルがあったんだけど、なにこれ?

エンダードラゴンを倒したので、またプラグイン作りを再開したい。
ところで、プラグインを作っていて「地上に村人を配置したい」という欲求が生まれたのだが、
「地上」を検出するには、どうすればよいのだろうか。

幸いにして、BukkitのWorldクラスにはgetHighestBlockAt(…)という、
XとZの座標を指定すると「その位置の最も高い位置にある空気ではないブロックの位置」を返してくれるメソッドがある。
これを使って、村人ジョセフィーヌを地上にスポーンさせてみた。


ジョセフィーヌ、困惑。

地上がただの平原なら、getHighestBlockAtでもまったく問題はないのだが、
木などが植わっていると、葉ブロックが「最も高い位置にある空気ではないブロック」として選択されてしまうので、
ジャングルバイオームなどではgetHighestBlockAtは使い物にならない。

では、「地面とは、土の上である」という直感をもとに、
「その位置にある、最も高い位置にある土ブロックの位置」にジョセフィーヌをスポーンさせてみた。


ジョセフィーヌ、悶絶。

読める…読めるぞ…これ以上ないくらいオチが読める。
海バイオームには土は海底にしかないし、砂漠バイオームには土は地下にしかないから、地下に埋まるか奈落に落ちるか、どのみち無事では済まない。
じゃあ一番高いところにある土か水か砂ブロックの位置を地上に認定…と行きたいところだが、
V1.7でバイオームが大量に追加されたし、今後も追加されるであろうことを考えると、
地上にあるブロックのタイプをしらみつぶしに設定していくのはあまりにも無駄が多すぎる

…で、仕方がないので、「太陽光が8以上届く、最も低い位置にある連続する高さ2の空気ブロック」を
地上と認定することにしてみた。基準の太陽光を8としたのにはあまり意味はないが、
葉ブロックなどで光が弱まる場合があるので、あまり大きい値は指定できないし、
かといって小さい値を設定すると、洞窟の入り口から洞窟内にわずかに差し込む光を地上と誤認してしまうので、
適度な値を指定しないといけない。

ソースは以下のとおり。

/**
 * 指定した地点の地面の高さを返す
 * 
 * @param loc
 *            地面を探したい場所の座標
 * @return 地面の高さ(Y座標)
 */
private int getGroundPos(Location loc) {

    // 最も高い位置にある非空気ブロックを取得
    loc = loc.getWorld().getHighestBlockAt(loc).getLocation();

    // 最後に見つかった地上の高さ
    int ground = loc.getBlockY();

    // 下に向かって探索
    for (int y = loc.getBlockY(); y != 0; y--) {
        // 座標をセット
        loc.setY(y);

        // そこは太陽光が一定以上届く場所で、非固体ブロックで、ひとつ上も非固体ブロックか
        if (loc.getBlock().getLightFromSky() >= 8
                && !loc.getBlock().getType().isSolid()
                && !loc.clone().add(0, 1, 0).getBlock().getType().isSolid()) {
            // 地上の高さとして記憶しておく
            ground = y;
        }
    }

    // 地上の高さを返す
    return ground;
}

動かしてみた。


ジョセフィーヌ、安堵。

これで、完璧ではないが、だいたい妥当な地上を検出できるようになった。
完璧ではない、というのは、地上からの光がこぼれるダンジョン内も、「太陽光8以上」を満たしてしまうため、
たまにはダンジョンの中に着地してしまうこともあるという意味。
なにかうまい解決法はあるだろうか。

ちなみにgetLightFromSky()は、その場所に差し込む太陽光の強さを0~15の整数で返す。
夜でも朝でも時間帯に関係なく、getLightFromSky()は常に昼の場合の太陽光の強さを返す。
このため夜間でも問題なく地上をトレースすることができる。

ついでに、エンダーパールを持って右クリックすると、100mほどランダムにテレポートするプログラムを書いてみた。
こちらでは、水上や溶岩上・樹の上が候補に挙がった場合は、もっと別の安全な着地地点を再探索するようにした。

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

    // エンダーパールを手に持って右クリックしたのか
    if ((e.getAction().equals(Action.RIGHT_CLICK_AIR) || e.getAction()
            .equals(Action.RIGHT_CLICK_BLOCK))
            && e.getPlayer().getItemInHand().getType()
                    .equals(Material.ENDER_PEARL)) {

        // 100メートル程度ランダムテレポートさせる
        randomTeleport(e.getPlayer(), 100);
    }
}

/**
 * プレイヤーを周辺の安全な場所にランダムテレポートさせる
 * 
 * @param p
 *            テレポートさせるプレイヤー
 * @param width
 *            テレポートする距離の上限
 */
private void randomTeleport(Player p, int width) {

    // 乱数ジェネレーター
    Random rand = new Random();

    // 無限ループにならないよう5回上限で繰り返し安全な場所を探す
    for (int n = 0; n != 5; n++) {

        // 現在のプレイヤーの座標を取得
        Location loc = p.getLocation();

        // X,Z方向に-width~widthメートルの範囲でランダムに移動
        loc.add(rand.nextInt(width * 2) - width, 0, rand.nextInt(width * 2)
                - width);

        // その場所の地上の座標を取得
        int y = getGroundPos(loc);
        loc.setY(y);

        // その場所は液体か、または下が葉ブロックか
        Location under = loc.clone().add(0, -1, 0);
        if (loc.getBlock().isLiquid()
                || under.getBlock().getType().equals(Material.LEAVES)) {

            // 水上・マグマ上・樹の上へのテレポートは安全ではないので、別の場所を探す
            continue;
        }

        // テレポートして処理終了
        p.teleport(loc);
        return;
    }

    // テレポートできずにループが終了した場合メッセージを送る
    p.sendMessage("§e[Teleport] 安全な場所が見つからなかった。");
}

/**
 * 指定した地点の地面の高さを返す
 * 
 * @param loc
 *            地面を探したい場所の座標
 * @return 地面の高さ(Y座標)
 */
private int getGroundPos(Location loc) {

    // 最も高い位置にある非空気ブロックを取得
    loc = loc.getWorld().getHighestBlockAt(loc).getLocation();

    // 最後に見つかった地上の高さ
    int ground = loc.getBlockY();

    // 下に向かって探索
    for (int y = loc.getBlockY(); y != 0; y--) {
        // 座標をセット
        loc.setY(y);

        // そこは太陽光が一定以上届く場所で、非固体ブロックで、ひとつ上も非固体ブロックか
        if (loc.getBlock().getLightFromSky() >= 8
                && !loc.getBlock().getType().isSolid()
                && !loc.clone().add(0, 1, 0).getBlock().getType().isSolid()) {
            // 地上の高さとして記憶しておく
            ground = y;
        }
    }

    // 地上の高さを返す
    return ground;
}

この記事を評価

この記事にコメント

  1. ...

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

Menu