今回はイベントハンドリングの方法を書いていきます。だんだん、準備段階から開発段階に進んできた感じがしますね。イベントハンドリングができればプレイヤーが何かをしたときに爆発させたり建築ができたり色々と夢広がりんぐです。ちなみにこの記事の執筆時点でmunecraftもまだ勉強中なので今後大いに修正する可能性あり。。
環境
- OS: Windows10
- Jdk: openjdk version "15.0.2" 2021-01-19
※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
何はともあれイベントハンドリングをしてみる
仕組みの話とか色々ありますがまずは動かしてみたいよね~ということでサンプルコード。これを中心に解説していきます。前回の記事の空のmodをベースに少しコードを書き加えてみます。
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のイベントバス
これは前述のサンプルコードにあるMinecraftForge.EVENT_BUS
が該当します。こちらはメインのイベントバスで、基本的にゲームが開始(ワールドが読み込まれ始めるタイミング)以降のイベントを拾うためのイベントバスです。 - FML(Forge Mod Loader)イベントバス
こちらはゲームを開始する前、modを読み込んでいる段階で発火されるイベント群のイベントバスの模様。今のところ、アイテムやブロックを追加する用途で使用する以外の用途はわかっておりません。。アイテム追加についての記事はこちら。
イベントハンドラの登録方法
明示的に各イベントバスのregister()
を呼び出して登録する方法と、アノテーションを使う方法があります。正確にはほかにもaddListener()
にラムダ式で渡す方法がありますがそちらは今度追記します。
明示的にregister()
を呼び出す方法
public EmptyMod() {
LOGGER.info("Welcome to munecraft mod!!!");
// こっちがForgeのイベントバスにイベントハンドラとしてthisを登録する方法
MinecraftForge.EVENT_BUS.register(this);
}
public EmptyMod() {
LOGGER.info("Welcome to munecraft mod!!!");
// こっちはFMLのイベントバスにイベントハンドラとしてthisを登録する方法
FMLJavaModLoadingContext.get().getModEventBus().register(this);
}
同じクラスをForgeとFMLの両方のイベントバスに登録しないようにしましょう。じゃないと後述する@SubscribeEent
がどちらのイベントハンドラなのか混乱してランタイムエラーを引き起こすことになります。
ところでイベントハンドラとして登録しているthis
は何のクラスもインターフェイスも継承/実装していません。上述したregister()
メソッドはObject
型を受け取るのでなんでも登録できちゃうんですね。じゃあどうやってイベントハンドラのメソッドを呼ぶんだ。。。と謎に思ってしまったけど、その種明かしが@SubscribeEvent
アノテーションにありました。これもまた後述。
クラスをアノテーションでイベントハンドラとして登録する方法
2つクラスを追加しました。
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.");
}
}
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
というクラスを継承しています。例えば先ほどのコードを次のように書き換えてみる。
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
ですね。コマンド追加関連はネット上に転がっているドキュメントが軒並み古くて苦労したので次回記事にしようと思います。