🍣

MinecraftのGUIフレームワークを作ってみた

2024/12/08に公開

はじめに

こんにちは (*'▽')
普段はアジ鯖というマインクラフトサーバーで開発をしているつきしめじといいます。

今回はマインクラフトのプラグイン開発者なら誰もが通るインベントリやスコアボードを利用したGUIの実装に便利なフレームワークを作ってみました。

GitHub リポジトリはこちらになります:https://github.com/tksimeji/visualkit
(ほしよろこびます☆彡)

なぜ作ったのかというと、マイクラフトプラグインでのUXにおけるGUIの重要度に対して、生のPaper APIでは、高機能なGUIを構築することに少し敷居が感じられるからです。

ちなみにお名前は「Visualkit」といいます。
由来は「Visual」+「Bukkit」 とっても安直 (^^ゞ

(注) Paper API を前提に作られたフレームワークです。Spigot サーバーでは動作しません。 (kyori.net の Adventure API を使用すれば Spigot API の環境でも使用できます)

Visualkitで作るGUIって?

VisualkitのGUIは一つのクラスとして定義します。(ここでは定義クラスと呼ぶことにします。)
また、プレイヤーに表示させるにはインスタンスを作成します。
とってもシンプル。
作りたいGUIの種類によってGUIを定義するクラスで継承すべきクラスが変わってきますが、元をたどっていくとどれもcom.tksimeji.visualkit.VisualkitUIというインターフェースにたどり着くようになっています。全体像はこんな感じです。

緑色がインターフェース、青色が抽象クラスとなっています。(※ IAnvilUI、AnvilUIは未実装)
「IChestUI」と「IAnvilUI」は「InventoryUI」にインベントリのタイプをジェネリクスとして渡しています。

プレースホルダと文字装飾と

GUI上のテキストには、Paper APIでお馴染みのnet.kyori.adventure.text.Componentを使用します。

少しだけComponentの使い方を紹介すると、こんな感じです。

Component.text("Hello,")
    .appendSpace()
    .append(Component.text("tksimeji").color(NamedTextColor.LIGHT_PURPLE))
    .append(Component.text("!"));

Visualkitではさらにここでプレースホルダを使用できます。

Component.text("Hello,")
    .appendSpace()
    .append(Component.text("${name}").color(NamedTextColor.LIGHT_PURPLE))
    .append(Component.text("!"));

${name} の部分がプレースホルダですね。
先ほどGUIはクラスとして定義するというお話をしたかと思うのですが、そのクラス内のフィールドがレンダリング時にこの部分に置換されるというわけです。
値が変更された場合には、表示が更新されます。

例では、${name}というプレースホルダを使用していますから、定義クラス内のnameというフィールドに置き換えて表示されることになります。

private String name;

基本的には、フィールドをtoString()した結果が値として注入されますが、
例外として、java.lang.Stringでは「&」記号を使用した文字装飾が利用できるほか、
データ型がnet.kyori.adventure.text.Componentの場合にはコンポーネントがそのままプレースホルダ部分に挿入されます。

置換を直接注入することも可能。

void bind(String key, Object value);
void unbind(String key);

このプレースホルダはVisualkitのあらゆる場所で使用することができます。

チェストUI

プラグインでのUIの定番ですね。
継承するクラスはcom.tksimeji.visualkit.ChestUIです。

public class MyChestUI extends ChestUI {
    @Element(13)
    private final VisualkitElemente cookie = VisualkitElement.create(Material.COOKIE)
        .title(Component.text("Click me!").color(NamedTextColor.GREEN))
        .lore(Component.text("Count: ${count}").color(NamedTextColor.GRAY));

    private int count = 0;
    
    public MyChestUI(@NotNull Player player) {
        super(player);
    }

    @Override 
    public @NotNull Component title() {
        return Component.text("Cookie Clicker");
    }

    @Override
    public @NotNull Size size() {
        return Size.SIZE_36;
    }

    @Handler(slot = 13)
    public void onClick() {
        count ++;
    }
}

順番に見ていきましょう。

要素の宣言

定義クラス内に@Elementを付与したVisualkitElement型またはItemStack型のフィールドを定義することで要素を宣言することができます。

@Element(13)
private final VisualkitElemente cookie = VisualkitElement.create(Material.COOKIE)
    .title(Component.text("Click me!").color(NamedTextColor.GREEN))
    .lore(Component.text("Count: ${count}").color(NamedTextColor.GRAY));

loreでは前の章で紹介したプレースホルダを使用しています。

動的に要素を追加することも可能です。

// VisualkitElement を要素に設定する
setElement(13, VisualkitElement.create(Material.COOKIE)
    .title(Component.text("Click me!").color(NamedTextColor.GREEN))
    .lore(Component.text("Count: ${count}").color(NamedTextColor.GRAY)));

// ItemStack を要素に設定する
setElement(13, new ItemStack(Material.COOKIE, 1));

// スロットを空にする
setElement(13, null);

ハンドラ

ハンドラは、GUIでイベントが発生したときに呼び出される特別なメソッドです。
メソッドには@Handlerを付与する必要があります。

この例では、スロット13( = @Elementで定義したクッキーボタン)がクリックされたときに、フィールドのcountをインクリメントしています。

@Handler(slot = 13)
public void onClick() {
    count ++;
}

// ^0.3.6ではラムダ式で要素に直接ハンドラを設定できます

@Element(13)
private final VisualkitElemente cookie = VisualkitElement.create(Material.COOKIE)
    .handler((slot, click, mouse) -> count ++)
    .sound(Sound.UI_BUTTON_CLICK, 1.0f, 1.2f); // クリック時のサウンドも設定可能

また、アノテーションでスロットのほかに、クリックやマウスの条件を指定することも可能です。

@Handler(slot = 13, click = {Click.DOUBLE, Click.SHIFT}, mouse = Mouse.LEFT)

その他の機能としては、特定の型でプロパティを定義することで、イベント情報を引数にとることができます。

@Handler(slot = 13)
public void onClick(int slot, Click click, Mouse mouse) {
    count ++;
}

例ではすべてのイベント情報を取得していますが、これらすべてを指定する必要はなく、順序も気にする必要はありません。

スロットの指定

ここまで紹介していた@Element@Handlerでは、スロットのインデックス番号を指定していましたが、少しだけ高度な方法も紹介してみます。

// 複数個のスロットを指定できる
@Element({2, 3, 5, 7, 11, 13, 17, 19})

// ASM (Advanced Slot Mapping) を利用して範囲で指定できる ;アセンブリではないです
@Element(asm = {@Asm(from = 0, to = 8), @Asm(from = 18, to = 26)})

これらの方法は@Element@Handlerのどちらでも同じように使用できます。

プレイヤーに表示させる

最後に定義したGUIをプレイヤーに表示させてみましょう。
とは言っても、インスタンスを作成するだけで何も難しいことはありません。

MyChestUI ui = new MyChestUI(player);

引数に渡したplayerにGUIが表示されるはずです。

GUIを閉じさせたい場合には、

ui.close();

としてあげましょう。

パネルUI

「パネルUI」は、スコアボードのサイドバーを利用したGUIです。

チェストの時のように一人のプレイヤーに対して表示する場合はcom.tksimeji.visualkit.PanelUIを、複数人のプレイヤーに同時に表示させたい場合にはcom.tksimeji.visualkit.SharedPanelUIを継承します。

今回は例としてPanelUIを使用した実装例を紹介します。

public class MyPanelUI extends PanelUI {
    private String name;
    private int health;
    private int ping;

    public MyPanelUI(@NotNull Player player) {
        super(player);

        setTitle(Component.text("INFO").color(NamedTextColor.YELLOW).decorate(TextDecoration.BOLD));

        addLine(Component.text("Hello, ${name}"));
        addLine();
        addLine(Component.text("Health: ").append(Component.text("${health}♥").color(NamedTextColor.RED);
        addLine(Component.text("Ping: ").append(Component.text("${ping} ms").color(NamedTextColor.GREEN));
    }

    @Override
    public void onTick() {
        name = player.getName();
        health = (int) player.getHealth();
        ping = player.getPing();
    }
}

表示内容の取得

getを使用して現在表示されている内容を取得できます。

// 行を取得する
@Nullable Component get(int index);

// 行数を取得する
int size();

// パネルが空かを取得する
boolean empty();

表示内容の編集

パネルの内容の編集には次のメソッドが使用できます。

// 行を設定する
void setLine(int index, @NotNull Component line);

// 行を追加する
void addLine(@NotNull Component line);

// 空白行を追加する
void addLine();

// 引数のamountの分だけ空白行を追加する
void addLine(int amount);

// 行を削除する
void removeLine(int index);

// すべての行を削除してパネルを初期化する
void clear();

例ではコンストラクタ内で表示内容を設定しています。

プレイヤーに表示させる

チェストのときと同様、インスタンスを作成するだけです。

new MyPanelUI(player);

今回はPanelUIを使用しましたが、SharedPanelUIの場合は、次のようになります。

// 可変長引数としてプレイヤーを指定する
new MySharedPanelUI(player1, player2);

// リストを渡す
new MySharedPanelUI(List.of(player1, player2));

// インスタンス生成後に追加する
new MySharedPanelUI().addAudience(player);

今回の例だと、このように表示されるはずです。

おしまい

初めての投稿で至らない点多々あるかと思いますが、
ここまでお付き合いいただきありがとうございました m(_ _)m

Discussion