データパックでできるだけ簡単に作るチェストGUIの話

# はじめに

皆さんは、インベントリを用いたGUIというものをご存知でしょうか？
例えばこんな感じのもの。

![Demo_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779534/37d73514-f711-7145-f0cc-c76611876798.gif)

これは Hypixel という、様々なミニゲームが楽しめるサーバーで使われているGUIです。

アイテムをクリックすることで、表示されるインベントリが切り替わります。
ソーシャルメディアの一覧だったり、設定項目の一覧だったり。

ということでこの記事は、このようなインベントリを用いたGUIを **データパックでできるだけ簡単に作ってみよう！** という記事になります。

# 本当に作れるの？

「データパックでできるだけ簡単に作ってみよう！」と言ってみましたが、そもそも本当に作ることができるのでしょうか？

実は、先ほど紹介したHypixelサーバーのGUIは、データパックを基にして作られたものではありません。つまり、このようなGUIをデータパックで作ることは可能なのか？ということを最初に考える必要があります。

では、このようなGUIを作るために必要なものとは何でしょうか？

インベントリを用いたGUIということから、実際にはチェストや樽などのインベントリを持つブロックを開いたり、チェスト付きトロッコなどのインベントリを持つエンティティにインタラクトしたりして、GUIを操作します。

したがって、どのブロックやエンティティを用いてGUIとして構築するかを決める必要があります。

さらに、それらのブロックやエンティティに対応する、項目選択などのGUI操作を作る必要もあります。

したがって、GUIを作るために必要な要素は2つあります。

- GUIとして構築するためのブロックやエンティティを決める
- それらのブロックやエンティティに対応したGUI操作を作る

これらの要件を踏まえて、データパックで実現可能かどうかを考えてみましょう。

### どのブロックやエンティティを用いてGUIとして構築するかを決める

チェストや樽、エンダーチェスト、チェスト付きトロッコなど、インベントリを持つブロックやエンティティは多岐に渡ります。これらの中からどれを使えばよいでしょうか？

今回のテーマは「できるだけ簡単に」ということなので、GUIを容易に構築できそうなブロックやエンティティを使うことにしましょう。

まずはチェスト付きトロッコについて考えてみましょう。

少し先のことを考えると、GUIを構築する際に、そのGUIのインベントリにアイテムを配置する必要が出てきます。チェスト付きトロッコを使う場合、そのインベントリにアイテムを配置するためには、「プレイヤーが開いている特定のチェスト付きトロッコ」を特定する必要があります。これは少々手間がかかりそうなので、チェスト付きトロッコを使うのはやめておきましょう。

それでは、チェストや樽などのブロックについて考えてみましょう。

これらのブロックにアイテムを配置するためには、そのブロックの座標を指定する必要があります。初めに座標を決定しても、その後にゲーム内でチェストや樽の位置が変更されると、それに合わせてコマンド側の座標も変更する必要が出てきます。こうなってしまうと少し手間がかかりそうですね。他にもっと良い手段はないか考えてみましょう。

次に、エンダーチェストについて考えてみましょう。

エンダーチェストにアイテムを配置するには、そのエンダーチェストを所有するプレイヤーを指定する必要があります。GUIを操作するのはプレイヤー自身なので、プレイヤーを特定するのは簡単そうです。また、エンダーチェストでは座標を扱わないため、チェストや樽などのブロックで生じる座標の問題も発生しないと考えられます。これは良さそうですね！

したがって、今回はエンダーチェストを用いてGUIを構築することにします。

### それらのブロックやエンティティに対応したGUIの操作を作る

GUI操作に必須な要素として、項目選択が挙げられます。
アイテムをクリックしたことを項目選択とみなして、ページを切り替えたり、任意のコマンドを実行したりなど、項目選択によりGUIの幅は大きく広がります。

ここで、アイテムをクリックしたことを検知する必要が出てきました。
これを実現する方法について、少し考えてみます。

まず初めに、「アイテムをクリックしたかどうか」を言い換えて「アイテムを持ち上げているかどうか」として考えてみましょう。

実は、 `/clear` コマンドは、インベントリ内のアイテムを消去するだけではなく、インベントリ内で持ち上げているアイテムも消去することができます。つまり、 `/execute store success` を使用することで、「指定したアイテムの消去に成功した時、アイテムをクリックした」 とみなすことができるのです。

ということで、アイテムのクリック検知（項目選択）は実現することが出来そうです。

### データパックでも作れそう

これらの要件から、データパックで作れることが分かりました！
それでは、これらを踏まえて実際にデータパックを作っていきましょう。

# 実際に作っていく

はじめに見せておくと、これから作るGUIの完成品はこのようになります。

![Demo_4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779534/da225e6e-e83a-de71-b171-04c95718f17f.gif)

リンゴをクリックする度にダイヤモンドが渡されるGUIです。

選択できる項目も1つだけで簡単そうに見えますが、実はさまざまな目的の処理が集まって構成されています。ひとつひとつ分解して作っていきましょう。

### 前提知識
- 基本的なデータパックの知識がある
- 基本的なコマンドの知識がある

また、一部のコマンドではstorageを使用しています。
storageについて分からない方はこちらの動画をご視聴ください。
（メイリオさんによるstorageの解説動画です）

https://youtu.be/tRSalxNAvqM?feature=shared

### 項目選択を作る

まず、項目選択の処理を作りましょう。

`minecraft/tags/functions/tick.json` で、以下のコードを書きます。

valuesに指定された関数はtick時に呼び出されるようになります。今回では `simple_gui:tick` 関数がtick時に呼び出されることになります。

```json: minecraft/tags/functions/tick.json
{
    "values": [
        "simple_gui:tick"
    ]
}
```

`simple_gui/functions/tick.mcfunction` で、以下のコードを書きます。

マルチプレイヤーに対応させるため、プレイヤーごとに `simple_gui:tick_player` 関数を呼び出すことで処理を分岐させています。

```: simple_gui/functions/tick.mcfunction
#> simple_gui:tick

# プレイヤーごとのtick処理
execute as @a run function simple_gui:tick_player
```

`simple_gui/functions/tick_player.mcfunction` で、以下のコードを書きます。

`/execute store success` を用いた `/clear` コマンドを使うことで、アイテムを持ち上げたことを検知しています。

```: simple_gui/functions/tick_player.mcfunction
#> simple_gui:tick_player

# 持ち上げたアイテムを削除できたなら、項目を選択したとみなして処理
execute store success storage simple_gui: isClicked byte 1.0 run clear @s minecraft:apple{SimpleGuiItem:true}
execute if data storage simple_gui: {isClicked:true} run tellraw @s "今、項目を選択しましたね！"

# リセット
data remove storage simple_gui: isClicked
```

ここまでで、プレイヤーが `{SimpleGuiItem:true}` というNBTの付いたリンゴを持ち上げたときにチャットが表示されるコマンドを書きました。

エンダーチェストでGUIを構築したいので、このリンゴをエンダーチェスト内に配置しましょう。
次のコマンドをチャット欄で実行します。

```
/item replace entity @s enderchest.13 with apple{SimpleGuiItem:true}
```

エンダーチェストを開いて、正しく動作しているか確認してみましょう。

![Demo_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779534/be949896-2317-ab63-6acb-636b9b3a85b4.gif)

リンゴをクリックしたらチャットが表示されています。正しく動作していますね！

ですが、リンゴをクリックするとリンゴが消えてしまいます。もう一度選択したい場合は、またリンゴを配置しなおさないといけません。これは少し手間ですね…。

### GUIの内容を設定する

リンゴが消えてしまう問題を解決するため、GUIの内容をあらかじめデータパック側で設定しておきましょう。

`simple_gui/functions/set_menu.mcfunction` で、以下のコードを書きます。

この関数内でGUIの内容を設定しておくことで、この関数を呼び出せばいつでもGUIが使える状態になるようにしておきます。

```: simple_gui/functions/set_menu.mcfunction
#> simple_gui:set_menu

# チャットを表示するリンゴを配置
item replace entity @s enderchest.13 with minecraft:apple{SimpleGuiItem:true}
```

`simple_gui/functions/tick_player.mcfunction` を、以下の内容に更新します。

アイテムを持ち上げたときに先ほどの関数を呼び出すことで、アイテムクリック時にGUIの内容が再設定され、リンゴが再配置されるようにしています。

```diff: simple_gui/functions/tick_player.mcfunction
  #> simple_gui:tick_player

  # 持ち上げたアイテムを削除できたなら、項目を選択したとみなして処理
  execute store success storage simple_gui: isClicked byte 1.0 run clear @s minecraft:apple{SimpleGuiItem:true}
  execute if data storage simple_gui: {isClicked:true} run tellraw @s "今、項目を選択しましたね！"
+ execute if data storage simple_gui: {isClicked:true} run function simple_gui:set_menu

  # リセット
  data remove storage simple_gui: isClicked
```

それでは、もう一度正しく動作しているか確認してみましょう。
次のコマンドをチャット欄で実行し、エンダーチェストを開きます。

```
/function simple_gui:set_menu
```

![Demo_3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779534/53a5e8b3-874f-52ac-cc1c-5a1ad5302f20.gif)

リンゴをクリックしてもリンゴが消えなくなっています。正しく動作していますね！

### アイテムドロップの対策をする

今のままでは、GUI内のアイテムにカーソルを合わせてアイテムドロップのキーを押すことで、GUIからアイテムをドロップさせることが出来てしまいます。GUI外にGUIのアイテムを持ち出すと少し不都合が生じてしまうので、これは対策したいです。

`simple_gui/functions/tick.mcfunction` を、以下の内容に更新します。

`{SimpleGuiItem:true}` というNBTの付いたアイテムがドロップしているとき、そのアイテムから `simple_gui:tick_item` 関数を呼び出すようにしています。

```diff: simple_gui/functions/tick.mcfunction
  #> simple_gui:tick

  # プレイヤーごとのtick処理
  execute as @a run function simple_gui:tick_player
+
+ # ドロップされたアイテムごとのtick処理
+ execute as @e[type=minecraft:item] if data entity @s Item.tag{SimpleGuiItem:true} run function simple_gui:tick_item
```

`simple_gui/functions/tick_item.mcfunction` で、以下のコードを書きます。

アイテムをドロップしたプレイヤーのGUIを再設定し、ドロップされたアイテムを消しています。こうすることで、GUIにもGUI外にも変化が無かったように見せています。

```: simple_gui/functions/tick_item.mcfunction
#> simple_gui:tick_item

# 自分をドロップしたプレイヤーのGUIを再設定
execute on origin run function simple_gui:set_menu

# 自分を消す
kill @s
```

これで、アイテムをドロップ出来てしまう問題が解決できました！

# ゴーストアイテムについて

先ほど作ったGUIのアイテムにカーソルを合わせ、オフハンドのアイテムと交換するキーを押すと、オフハンドにGUI内のアイテムが移動します。
GUIのアイテムは常に `/clear` され続けているので、オフハンドに移動したこのアイテムも消えるはずですが、実際には消えることはありません。
これは、オフハンドに移動したアイテムが **ゴーストアイテム** であるためです。

ゴーストアイテムとは、見た目上は存在しているように見えるのに、実際のデータ上では存在していない、という幽霊のようなアイテムのことを呼びます。

なぜアイテムをオフハンドに移動させると、そのアイテムがゴーストアイテムになってしまうのかは不明です。現在（記事公開時点）、アイテムをGUIからオフハンドに移動させたときにゴーストアイテムが発生してしまう問題への対策は見つかっていません…。

### ゴーストアイテムの対策について

ゴーストアイテムの対策として、「ゴーストアイテムを発生させない」という対策と「発生したゴーストアイテムを消す」という対策の2つが挙げられます。

前者に関しては、常時 `/clear` するという仕組みを根本から変えなければならず、達成するのは難しそうです。では、後者のように「ゴーストアイテムを消す」のであれば、どのような対策を取るのがいいのでしょうか？

ここで、私が試したうえで達成できなかった対策を少し紹介します。これからゴーストアイテムの対策を考えようとしている方は、よければ参考にしてみてください。

#### 常時 `/item` でオフハンドのアイテムを空気に変える
 - 通常のアイテムはオフハンドに持った瞬間に消える
 - ゴーストアイテムはオフハンドに持っても消えない

#### 常時 `/item` でオフハンドのアイテムを石に変えてから空気に変える
 - 通常のアイテムはオフハンドに持った瞬間に消える
 - チェストを閉じるとゴーストアイテムも消える
  （ただしチェストを開いている間はゴーストアイテムが発生している）

#### チェストを開いたときにオフハンドに石を配置し、オフハンドが石でない時はオフハンドを空気に変える
 - していることはほとんど1個目の対策と同じ
 - 石以外のアイテムはオフハンドに持った瞬間に消える
 - ゴーストアイテムはオフハンドに持っても消えない

# GUIを完成させる
最後に、このGUIに少し手を入れて完成にしましょう！
項目を選択したときにチャットが表示されるだけでは寂しいので、アイテムが渡されるようにしてみます。

`simple_gui/functions/tick_player.mcfunction` を、以下の内容に更新します。

アイテムを持ち上げたときに `/give` を実行することで、アイテムが渡されるようにします。

```diff: simple_gui/functions/tick_player.mcfunction
  #> simple_gui:tick_player

  # 持ち上げたアイテムを削除できたなら、項目を選択したとみなして処理
  execute store success storage simple_gui: isClicked byte 1.0 run clear @s minecraft:apple{SimpleGuiItem:true}
- execute if data storage simple_gui: {isClicked:true} run tellraw @s "今、項目を選択しましたね！"
+ execute if data storage simple_gui: {isClicked:true} run give @s minecraft:diamond
  execute if data storage simple_gui: {isClicked:true} run function simple_gui:set_menu

  # リセット
  data remove storage simple_gui: isClicked
```

エンダーチェストを開いて、正しく動作しているか確認してみましょう。

![Demo_4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779534/da225e6e-e83a-de71-b171-04c95718f17f.gif)

リンゴをクリックするとダイヤモンドが渡されていますね。正しく動作しています。

さて、項目は一つだけですが、GUIを作ることができました！

ここから、リンゴ以外のアイテムを持ち上げたことを検知してGUIの項目の数を増やしたり、
実行するコマンドの内容を変えたりして、自分だけのGUIに拡張することができます。

# まとめ

以上、できるだけ簡単にデータパックで作るチェストGUIの話でした。

今回の記事のまとめです。
- 最初にどのブロックやエンティティを用いてGUIを構築するかを考える
- `/clear` コマンドを使ってアイテムのクリックを検知できる
- GUIからのアイテムドロップも対策が必要
- ゴーストアイテムの対策はわからない

工夫すれば、今回使わなかったブロックやエンティティでも容易にGUIを構築することが出来ます。エンダーチェストだけの技術だと思わないで、皆さんの好きな方法で、好きなGUIを構築してみて欲しいです。

# 宣伝

私が公開しているデータパックの「Sketch」について少し宣伝させてもらいます。

Sketchは、今回のようなチェストGUIをより簡単に構築するためのフレームワークです。

今回の記事で紹介したような処理のほとんどをSketchに任せることができ、アイテムとその配置スロットを指定するだけでGUIが構築できるというデータパックになっています。

GUIの内容をコマンドで視覚的に表現することが出来たり、「条件によって配置するアイテムを変える」などの処理を簡潔に書くことができる機能を搭載しています。

以下のリンクからSketchのリポジトリに飛ぶことができます。

https://github.com/rarula/Sketch