この連載が書籍になりました! 『マインクラフトでマルチサーバーを立てよう!』好評発売中です。

プラグイン開発の応用

2016年10月27日(木)
ecolight

前回はプラグイン開発の基礎を解説しました。今回は前回に引き続き、プラグインの応用的な開発について解説します。応用とは言っても普段よく使う処理について実例を交えて紹介しますので、ぜひプラグイン開発の参考にしてください。

スケジューラ処理を実装する

プラグインはJava言語で作られていますが、Spigotがプラグインを管理しているため非同期処理は非推奨です。Spigotのスケジューラで管理する方法以外では、極力SpigotのAPIを使用しないようにしましょう。

プレイヤーがゲーム内でコマンドを入力してから10秒カウントダウンする機能を考えてみます。プラグインは前回で作成した「TestPlugin」を流用します。

まず、以下のようなクラスファイルを追加します。ここではTestTimerクラスとしています。

package jp.minecraftuser.test_plugin;

import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;

/**
 * 非同期タイマークラス
 * @author ecolight
 */
public class TestTimer extends BukkitRunnable{
    private final TestPlugin plg;
    private final Player pl;
    private int count;

    /**
     * コンストラクタ
     * @param plg_ プラグインメインクラスのインスタンス
     * @param pl_ 実行者
     * @param count_ 表示する値
     */
    public TestTimer(TestPlugin plg_, Player pl_, int count_)
    {
        plg = plg_;
        pl = pl_;
        count = count_;
    }

    /**
     * 非同期処理実行メソッド
     */
    public void run()
    {
        // 指定プレイヤーにtitleコマンドの結果を表示する。
        plg.getServer().dispatchCommand(plg.getServer().getConsoleSender(),
                "title "+pl.getName()+" title {\"text\":\""+count+"\",\"color\":\"red\",\"bold\":true}");
        
        // countを 1 減算し、0 以上であれば次のタイマーを起動する
        count--;
        if (count >= 0) {
            new TestTimer(plg, pl, count).runTaskLater(plg, 20);
        }
    }
}

続いて、testコマンドの実行メソッドを以下のようにします。

    @Override
    public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args){
        // test コマンドの処理
        if (cmd.getName().equalsIgnoreCase("test")){
            // プレイヤーからのコマンドであれば非同期処理を起動する
            if (sender instanceof Player) {
                new TestTimer(plg, (Player) sender, 10).runTaskLater(plg, 0);
                sender.sendMessage("カウントダウンコマンドを使用しました。");
                return true;
            }
        }
        // 該当するコマンド無し
        return false;
    }

Playerかどうかをチェックしているのは、コンソールやコマンドブロックから実行した場合にsenderがプレイヤーではない場合があるためです。またrunTaskLaterメソッドは指定したtick後にそのクラスのrunメソッドを実行するメソッドです。上記の例では、条件を満たす間は1秒(20tick)間隔で次々とタイマーを起動するようにしています。

では、testコマンドを使用してみましょう。画面に大きくカウントダウンの数字が表示されると思います(図1)。

図1:TITLEコマンドでカウントダウンを表示する

定期的にrunメソッドを実行してくれるrunTaskTimerメソッドを使用して、条件を満たした段階でcancelメソッドを呼び出して停止する作りにしても良いでしょう。以下に起動方法のサンプルを示します。runTaskTimerは初回起動までの時間と2回目以降の処理間隔をtickで指定します。

new TestTimer(plg, (Player) sender, 10).runTaskTimer(plg, 0, 20);

継承しているBukkitRunnableクラスはrunTaskLaterAsynchronouslyメソッドやrunTaskTimerAsynchronouslyメソッドなどの非同期処理を提供していますが、これらの非同期処理内からは直接SpigotのAPIを実行しないようにしましょう。

ワールドデータやプレイヤーの情報を処理する

ワールドデータの操作には、ワールド全体の設定やワールドを構成するブロックの操作などがあります。例えば、以下のようにすると名前が「world」のワールドインスタンスを取得できます。

World w = plg.getServer().getWorld("world");

取得した変数wにピリオドを付けると、World型で使用できるメソッドが候補として表示されます(図2)。

図2:Worldに使用できるメソッドの候補

多くのメソッドが使用できるため画面に収まっていない物もありますが、図2にあるメソッドだけでも爆発を生成するcreateExplosionや木を生成するgenerateTreeなど、さまざまな操作ができることを伺えます。

このように、Spigotで使用されている各クラスで「どういったメソッドがあるか」を知るだけでも色々な事ができるようになるでしょう。同様にプレイヤーに対する操作もさまざまなものがあるので、実際にコーディングしながら試したり、Spigotのjavadocsを読んだりしてみましょう。

WorldのJavaDocsページ
PlayerのJavaDocsページ

一例として、設置済みの羊毛ブロックを手に持った染料で色付けする処理を書いてみます。ついでにワールドデータの操作例として雷のエフェクトを発生させてみます。

まず、以下のようにイベント処理用のクラスでPlayerInteractEventを処理するメソッドを定義します。

    @EventHandler
    public void onPlayerInteractEvent(PlayerInteractEvent event) {
        // 手持ちのアイテムを取得
        ItemStack i = event.getPlayer().getItemInHand();
        // クリックしたブロックを取得
        Block b = event.getClickedBlock();
        // ブロックに対する右クリックか?
        if (event.getAction() == Action.RIGHT_CLICK_BLOCK) {
            // 対象は羊毛ブロックか?
            if (b.getType() == Material.WOOL) {
            // 手持ちは染料アイテムか?
                if (i.getType() == Material.INK_SACK) {
                    // 持ってる染料で羊毛に色を付ける
                    b.setData(((Dye)i.getData()).getColor().getData());
                    // クリックしたブロックに雷のエフェクトを発生
                    World w = b.getWorld();
                    w.strikeLightningEffect(b.getLocation());
                }
            }
        }
    }

これで、染料を持って羊毛を右クリックすると色が付けられるようになりました。

一部のメソッド(getItemInHandやget/setData)はSpigot 1.10.2で非推奨とされていますが、他の方法が上手くいかなかったため暫定で使用しています。これを拡張して特定の位置に設置された羊毛ブロックを特定の色にしたら得点に換算するようにして、旗取りゲームのようなものを作ることもできるでしょう。

この例はプレイヤーの手持ちアイテムと設置済みブロックを判定してブロックを操作する内容でしたが、プレイヤー同士のイベントでも利用できます。ここでもう一例紹介しましょう。先の例と同様にイベント処理用クラスに以下の処理を追加してみてください。

    @EventHandler
    public void PlayerInteractEntity(PlayerInteractEntityEvent e)
    {
        // 手渡し処理
        if (PlayerItemPut(e.getPlayer(), e.getRightClicked())) {
            e.setCancelled(true);
        }
    }

    public boolean PlayerItemPut(Player pl, Entity ent) {
        // 対象がプレイヤーでなければ何もしない
        if (ent.getType() != EntityType.PLAYER) {
            return false;
        }
        Player target = (Player)ent;
        // 自分が現在しゃがんでいるか?
        if (!pl.isSneaking()) {
            return false;
        }
        // 相手の手持ちが空でない場合
        if (target.getItemInHand().getType() != Material.AIR) {
            // 自分がOPであれば、相手の手持ちから自分の手持ちにアイテムを移動させる
            if (pl.isOp()) {
                // 自分の手持ちが空でない場合は何もしない
                if (pl.getItemInHand().getType() != Material.AIR) {
                    return false;
                }
                pl.setItemInHand(target.getItemInHand());
                target.setItemInHand(null);
                pl.sendMessage("プレイヤー[" + target.getDisplayName() + "]から[" +
                        pl.getItemInHand().getType().name() + "x" +
                        pl.getItemInHand().getAmount() + "]を奪い取りました");
                target.sendMessage("プレイヤー[" + pl.getDisplayName() + "]に[" +
                        pl.getItemInHand().getType().name() + "x" +
                        pl.getItemInHand().getAmount() + "]を奪われました");
            } else {
                return false;
            }
        // 相手の手持ちが空の場合
        } else {
            // 自分の手持ちが空であれば何もしない
            if (pl.getItemInHand().getType() == Material.AIR) {
                pl.sendMessage("手に何も持っていないため手渡せません");
                return false;
            }
            // 自分の手持ちから相手の手持ちにアイテムを移動させる
            target.setItemInHand(pl.getItemInHand());
            pl.setItemInHand(null);
            pl.sendMessage("プレイヤー[" + target.getDisplayName() + "]に[" +
                    target.getItemInHand().getType().name() + "x" +
                    target.getItemInHand().getAmount() + "]を手渡しました");
            target.sendMessage("プレイヤー[" + pl.getDisplayName() + "]から[" +
                    target.getItemInHand().getType().name() + "x" +
                    target.getItemInHand().getAmount() + "]を受け取りました");
        }
        return true;
    }

これで、プレイヤーがしゃがんだ状態で他のプレイヤーを右クリックするとアイテムを手渡しできるようになります。

このように、各イベントは関連するオブジェクトをパラメータにして返してくるので、これを利用して様々な処理を行うことができるでしょう。

モンスターや動物の情報を処理する

モンスターや動物も同様に処理できます。ここでは対象がウィザー(Wither)かどうかを確認し、ウィザーの場合には召喚をキャンセルする処理を書いてみます。

イベント処理用のクラスに、以下のコードを追加してみましょう。

    @EventHandler
    public void CreatureSpawn(CreatureSpawnEvent event)
    {
        // 召喚されたMOBのエンティティを取得する
        LivingEntity ent = event.getEntity();
        // ウィザーの召喚操作によるものか?
        if ((ent.getType() == EntityType.WITHER) &&
            (event.getSpawnReason() == SpawnReason.BUILD_WITHER)){
            // メッセージをサーバー全体に通知する
            StringBuilder b = new StringBuilder("ウィザーの召喚が抑止されました。LOC:");
            b.append(ent.getLocation().toString());
            plg.getServer().broadcastMessage(b.toString());
            // 召喚をキャンセルして終了する
            event.setCancelled(true);
        }
    }

これでウィザーがいたずらに召喚された場合は抑止し、サーバー内に告知されるようになりました(図3)。

図3:ウィザー(Wither)の召喚が抑止される

先に紹介したPlayerInteractEntityイベントはモンスターや動物などのクリック操作でも発生するので、例えばゾンビに装備品を渡して装備させたり、逆に奪い取ったりといった事も可能でしょう。

動物にも様々な操作ができます。ペットにできる動物は誰がオーナーなのかを参照したり、逆にオーナーを他の人に移し替えたりすることもできます。

参考として、猫のステータス表示をプレイヤーに送信する処理を書いてみます。

        pl.sendMessage("===== 猫ステータス表示 =====");
        if (cat.getCustomName() != null) pl.sendMessage("Name:" + cat.getCustomName());
        pl.sendMessage("MaxHealth:" + cat.getMaxHealth());
        pl.sendMessage("Health:" + cat.getHealth());
        if (cat.getOwner() != null) pl.sendMessage("Owner:" + cat.getOwner().getName());
        pl.sendMessage("Age:" + cat.getAge());
        pl.sendMessage("CatType:" + cat.getCatType().name());
        pl.sendMessage("===== 猫ステータスここまで =====");

少し雑ですが、名前が付いている場合やオーナーがいる場合にはそれらも表示するようにしています。

ここで参照している値は、逆に設定も可能であるという点を考慮すると様々な事に応用できると思いますので、ぜひ色々と試してみてください。

アイテムの情報を処理する

プラグインからはアイテムも自由に操作できます。一例として、ログイン時にサーバーのルールを説明する本をプレイヤーに渡すような機能を作ってみましょう。

前回で作成したPlayerJoinイベントの処理を、次のように変更します。

    @EventHandler
    public void onPlayerJoin(PlayerJoinEvent event) {
        // ログインしたプレイヤーにようこそメッセージを表示する
        Player p = event.getPlayer();
        p.sendMessage("ようこそテストサーバーへ!");
        
        // プレイヤーに本を作成して渡す
        ItemStack item = new ItemStack(Material.WRITTEN_BOOK);
        BookMeta meta = (BookMeta)item.getItemMeta();
        meta.setAuthor("さばかん");
        meta.setDisplayName("ようこそテストサーバーへ! (サーバールールブック)");
        meta.addPage(new String[]{
            // 1ページ目
            "ようこそテストサーバーへ!\n" +
            "\n" +
            "この本はこのサーバーのルールを説明する本です。",
            // 2ページ目
            "★ルール★\n" +
            "\n" +
            "§c§l荒らし厳禁!§r\n" +
            "みんな仲良く!\n" +
            "何かあったら管理人まで"});
        item.setItemMeta(meta);
        p.getInventory().addItem(item);
    }

これで、プレイヤーのサーバー参加時にサーバーのルールブックが渡せるようになりました。実際にログインしてみると、図4のような形でアイテムが配布されると思います(図4)。もちろん、本の中身もちゃんと書き込まれています(図5)。

図4:アイテム欄では本のタイトルが表示される

図5:複数ページ構成やセクション記号で文字装飾などができる

ItemStackのコンストラクタ引数としてMaterial列挙体の定義値を渡すと任意のアイテムを作成できるので、プラグインの用途に応じて様々なアイテムを自動的に配布したり、プレイヤーが持っているアイテムに様々な処理を行えたりできます。

本連載では紹介しきれませんでしたが、上記の例でもMetaやInventoryといった言葉が出てきたように、これらにまつわるさらに応用的な技術も多くあるので、ぜひ自身で勉強しながら身に着けてください。

プラグイン開発の学習方法

プラグインの開発にはMinecraftの知識はもとより、Java言語やオブジェクト指向プログラミングなどの知識も必要になります。逆に言うとプラグイン開発ができれば言語やプログラミングの知識を身に着けることにも繋がるので、ぜひ意欲的に取り組んでみてください。

特にマルチサーバーの管理とセットで行うことができれば、直接参加者から意見やアドバイスをもらう機会が持てるため大変お勧めです。

プラグイン開発を学習するには、Java言語はIDEを駆使してサクサクとコーディングできるため、ひたすらデバッグ文(Loggerによるコンソール出力)を入れつつ色々なAPIを実験してみるのが一番早いと考えています。あとはGoogleで検索してプラグインのサンプルコードを自分でも動かしてみると良いでしょう。

Spigot(Bukkit)のプラグインは比較的単純な命令が連続しているため、ゆっくりと読み解いていけば必ず理解できると思います。もちろん上手くいかないこともあると思いますが、試行錯誤を繰り返すことで反復学習にもなり、より理解が深まるでしょう。

最後に1つツールを紹介したいと思います。こちらで配布されている「Java Decompiler」を使用すると、jarファイルから構成する元のソースコードを参照できます。IDEのNetBeansはデフォルトで逆アセンブラが搭載されており、JVMのバイトコードを参照できますが、それを更に可読性込みで復元可能にするツールです。

梱包されているYAMLファイルなどは参照できませんが、jarファイルは特殊なディレクトリ構成のZIPファイルなので、どうしても参照したい場合はZIPファイルを展開できるアーカイバで展開しましょう。試しに、今回作成したプラグインのjarファイルをJD-GUIで開いてみました(図6)。

図6:Javaのソースコードが参照できる

なお、このツールを学習用途で用いるのも一手ですが、配布されているプラグインの使用ライセンスで逆コンパイルが禁止されている場合もありますので、必ずライセンスを遵守した上で利用するようにしましょう。本ツールはソースコードを紛失してしまった場合や誤って変な更新をしてしまった場合でもプラグインが復旧できるという点で非常に有用なので、JD-GUIあたりをjarファイルに関連付けしておくと便利かと思います。EclipseやIntelliJのプラグインも公開されているので、IDEに組み込んでしまうのも良いでしょう。

最近はMinecraftのバニラ環境でもコマンドブロックや各コマンドを使用して様々な処理が行えるようになってきました。プラグインの利用はプラグイン間で連携する場合やデータベースやネットワーク通信などの高度な処理を行う場合を考えると、まだまだ多くのアドバンテージがあると思います。サーバー管理に必要な機能をさっとIDE上で作れてしまうのも非常に魅力的です。より発展的なサーバー管理を行うためにも、ぜひプラグイン開発にも興味を持っていただければと思います。

今回は、プラグインのより実践的な開発方法について解説しました。次回はいよいよ最終回です。クラウド環境や仮想環境でのサーバー構築について解説したいと思います。

本連載について

Minecraftの公式記事ではありません。本連載の内容はMojangから承認されているわけではなく、Mojangとは関係ありません。

Minecraftの非公式日本ユーザーフォーラム管理人

同フォーラムに付帯する生活系マルチサーバーもMinecraft製品版発売以降個人運営を続けている。ご相談などありましたらフォーラム(http://forum.minecraftuser.jp/)やTwitter@ecolightまで。

連載バックナンバー

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています