Eclipse+Forgeでマイクラのmod開発～イベントハンドリングしてみようず

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191672/9d65929a-a885-d977-4261-ff18f621ef35.png)
今回はイベントハンドリングの方法を書いていきます。だんだん、準備段階から開発段階に進んできた感じがしますね。イベントハンドリングができればプレイヤーが何かをしたときに爆発させたり建築ができたり色々と夢広がりんぐです。ちなみにこの記事の執筆時点でmunecraftもまだ勉強中なので今後大いに修正する可能性あり。。

## 環境

* OS: Windows10
* Jdk: openjdk version "15.0.2" 2021-01-19<br>
※16.xでもいいかもだけどgradleやらなにやら色々変わってしまって心配だったので今回は15で。
* IDE: Eclipse IDE for Java Developers (Version 4.19.0)
* Buildship(Eclipseのgradleプラグイン): 3.0
* Minecraft: Java版 1.16.5
* Forge: 1.16.5

## 何はともあれイベントハンドリングをしてみる

仕組みの話とか色々ありますがまずは動かしてみたいよね～ということでサンプルコード。これを中心に解説していきます。[前回の記事](https://qiita.com/kawamune/items/cc31848c070bcbb3e8f4)の空のmodをベースに少しコードを書き加えてみます。

```java:EmptyMod.java
package jp.munecraft.mod.test;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.entity.player.PlayerEvent.ItemPickupEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;

@Mod("emptymod")
public class EmptyMod {

	private static final Logger LOGGER = LogManager.getLogger();

	public EmptyMod() {
		LOGGER.info("Welcome to munecraft mod!!!");
		MinecraftForge.EVENT_BUS.register(this);
	}
	
	@SubscribeEvent
	public void onItemPickUp(ItemPickupEvent event) {
		LOGGER.info("Item Pickup!");
	}	
}

```

実行してみましょう。いかにもアイテムをとった時にログを出すイベント処理のように見えますが、実際にアイテムを拾ってみると。。。

```
[22:29:02] [Render thread/INFO] [minecraft/AdvancementList]: Loaded 7 advancements
[22:29:06] [Server thread/WARN] [minecraft/MinecraftServer]: Can't keep up! Is the server overloaded? Running 11568ms or 231 ticks behind
[22:29:16] [Server thread/INFO] [jp.mu.mo.te.EmptyMod/]: Item Pickup!
[22:29:16] [Render thread/INFO] [minecraft/AdvancementList]: Loaded 9 advancements
[22:29:19] [Server thread/INFO] [minecraft/IntegratedServer]: Saving and pausing game...
```
ログが出ましたね。ほら、簡単。


なんて話で終わりだとなんの説明にもならないのでここで少し解説。さらに詳細を後述していきます。
まずはコンストラクタの中の`MinecraftForge.EVENT_BUS.register(this);`。これは要するにForgeのイベントハンドラをイベントバス、という仕組みに登録する処理です。これをすることによって、今後EmptyModがイベントハンドラとして認識されます。
次に`@SubscribeEvent`アノテーションがついた`public void onItemPickUp(ItemPickupEvent event)`。これは、イベントバスに対して、イベント発生時に呼び出してほしいメソッドにマークする処理になります。この合わせ技で、マイクラの中でプレーヤがアイテムを拾った時に`public void onItemPickUp(ItemPickupEvent event)`を呼び出してもらえるようになるという寸法です。

## イベントバス

Forgeではイベントバスという仕組みでイベントハンドリングの仕組みを提供しています。これはある意味名前の通りですが、イベントが発生するとイベントバスを通じてイベントバスに登録されているイベントハンドラを発火させていく、というものです。
Forgeには次の二つのイベントバスがあります。

* Forgeのイベントバス<br>これは前述のサンプルコードにある`MinecraftForge.EVENT_BUS`が該当します。こちらはメインのイベントバスで、基本的にゲームが開始（ワールドが読み込まれ始めるタイミング）以降のイベントを拾うためのイベントバスです。
* FML(Forge Mod Loader)イベントバス<br>こちらはゲームを開始する前、modを読み込んでいる段階で発火されるイベント群のイベントバスの模様。今のところ、アイテムやブロックを追加する用途で使用する以外の用途はわかっておりません。。アイテム追加についての記事は[こちら](https://qiita.com/kawamune/items/9a58fa6a3e0752ac55f5)。

## イベントハンドラの登録方法

明示的に各イベントバスの`register()`を呼び出して登録する方法と、アノテーションを使う方法があります。<font color="lightgray">正確にはほかにも`addListener()`にラムダ式で渡す方法がありますがそちらは今度追記します。</font>

#### 明示的に`register()`を呼び出す方法

```java
	public EmptyMod() {
		LOGGER.info("Welcome to munecraft mod!!!");
		// こっちがForgeのイベントバスにイベントハンドラとしてthisを登録する方法
		MinecraftForge.EVENT_BUS.register(this);
	}
```

```java
	public EmptyMod() {
		LOGGER.info("Welcome to munecraft mod!!!");
		// こっちはFMLのイベントバスにイベントハンドラとしてthisを登録する方法
		FMLJavaModLoadingContext.get().getModEventBus().register(this);
	}
```

同じクラスをForgeとFMLの両方のイベントバスに登録しないようにしましょう。じゃないと後述する`@SubscribeEent`がどちらのイベントハンドラなのか混乱してランタイムエラーを引き起こすことになります。
ところでイベントハンドラとして登録している`this`は何のクラスもインターフェイスも継承/実装していません。上述した`register()`メソッドは`Object`型を受け取るのでなんでも登録できちゃうんですね。じゃあどうやってイベントハンドラのメソッドを呼ぶんだ。。。と謎に思ってしまったけど、その種明かしが`@SubscribeEvent`アノテーションにありました。これもまた後述。

#### クラスをアノテーションでイベントハンドラとして登録する方法

2つクラスを追加しました。

```java:ForgeEventHandlerTest.java
package jp.munecraft.mod.test;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import net.minecraftforge.event.entity.player.PlayerEvent.ItemPickupEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;

@Mod.EventBusSubscriber(bus=Mod.EventBusSubscriber.Bus.FORGE)
public class ForgeEventHandlerTest {

	private static final Logger LOGGER = LogManager.getLogger();

	@SubscribeEvent
	public static void onItemPickupEvent(ItemPickupEvent event) {
		LOGGER.info("ForgeEventHandlerTest::onItemPickupEvent called.");
	}
}
```

```java:ModEventHandlerTest.java
package jp.munecraft.mod.test;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;

@Mod.EventBusSubscriber(bus=Mod.EventBusSubscriber.Bus.MOD)
public class ModEventHandlerTest {

	private static final Logger LOGGER = LogManager.getLogger();

	@SubscribeEvent
	public static void onFMLCommonSetupEvent(FMLCommonSetupEvent event) {
		LOGGER.info("ModEventHandlerTest::onFMLCommonSetupEvent called");
	}	
}
```

起動してみるとまずゲームを開始する前に`ModEventHandlerTest::onFMLCommonSetupEvent called`がログに出力され、次にゲームを開始してアイテムを拾うと`orgeEventHandlerTest::onItemPickupEvent called.`がログに出力されます。うん、期待通り。
コードを見ると大体想像がつくと思いますが、`@Mod.EventBusSubscriber(bus=Mod.EventBusSubscriber.Bus.FORGE)`アノテーションと`@Mod.EventBusSubscriber(bus=Mod.EventBusSubscriber.Bus.MOD)`アノテーションがミソですな。前者は、このクラスがForgeのイベントバスに登録したいイベントハンドラであることを示し、後者はFMLのイベントバスに登録したいイベントハンドラであることを示しています。
注意してほしいのは、今回はメソッドが`static`な点。インスタンス化してイベントバスに渡していないので、このアノテーションで登録する場合はメソッドは`static`である必要があります。

## イベントハンドラのメソッド

ここが正直悩みました。。。なんのイベントが発生したときにどのメソッドが呼ばれるのか。。。メソッド名から識別している！？なら指定できるメソッドがわかるようにスーパークラスがあればいいけどイベントハンドラは`Object`型で受け取っていて、任意のクラスを渡せるし。。。
答えは引数でした。

先ほどのサンプルコードで`onItemPickupEvent(ItemPickupEvent event)`は引数に`ItemPickupEvent`を指定してます。Forgeは`@SubscriveEvent`のメソッドの引数**だけ**を確認して、該当するイベントが発生したときに発火してくれます。なので、メソッド名は`hoge`でも`piyo`でも`@SubscriveEvent`アノテーションを付けて`ItemPickupEvent`を引数につければアイテムを拾ったときに発火されます。

#### イベントハンドリングのポリモーフィズム

実はイベントハンドラのメソッドにはポリモーフィズムな仕組みが備わっています。例えば先ほどの`ItemPickupEvent`は`PlayerEvent`というクラスを継承しています。例えば先ほどのコードを次のように書き換えてみる。


```java:ForgeEventHandlerTest.java
package jp.munecraft.mod.test;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import net.minecraftforge.event.entity.player.PlayerEvent.ItemPickupEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;

@Mod.EventBusSubscriber(bus=Mod.EventBusSubscriber.Bus.FORGE)
public class ForgeEventHandlerTest {

	private static final Logger LOGGER = LogManager.getLogger();

	@SubscribeEvent
	public static void onItemPickupEvent(PlayerEvent event) {
		LOGGER.info("ForgeEventHandlerTest::onItemPickupEvent called.");
	}
}
```

すると、`ItemPickupEvent`も含めた、`PlayerEvent`を継承したすべてのイベントをハンドリングするようにコードが変わります（ログが膨大に出るので注意）。極端な話、最上位クラスの`Event`クラスを引数に指定するとゲーム中の（おそらく）すべてのイベントでハンドラが発火することになります（こちらもログが大量に出るので注意）。

### FMLのイベントバスへの登録時の注意点

FMLのイベントバスに登録するイベントハンドラのメソッドには注意が要ります。Forgeのイベントハンドラは`Event`クラスを継承していればなんでも受け取れますが、FMLのイベントバスでは`IModBusEvent`を継承したイベントしか`@SubscribeEvent`できません。

## 主なイベント

を書こうと思ったけど。。。膨大にあるし全部はわからないので挫折。。いずれ使いでのある重要なイベントだけでもリファレンス化しようと思いますが。。とりあえずゲーム中のイベントとしては`net.minecraftforge.event`パッケージは以下をあさってみてくださいな。クラス名から直感的にいつ発火するかわかりやすいイベントも多く存在しているので。特にプレイヤーが何かをしたときに発火させたくなることが多いと思うので`net.minecraftforge.event.entity.player`の下を掘ると色々出てくると思います。
あと、今度記事にしようと思いますがコマンド追加関連で重要なのは`net.minecraftforge.event.RegisterCommandsEvent`ですね。コマンド追加関連はネット上に転がっているドキュメントが軒並み古くて苦労したので次回記事にしようと思います。