---
# System prepended metadata

title: 事件系統

---

事件系統
===

我想要在玩家丟雞蛋的時候生出雷電苦力怕！(∂ω∂)
那麼，你需要的就是事件
甚麼？你說你並不想要雷電苦力怕？(●▼●;)
欸都...我想你還是需要事件的，為了你的模組著想

在本章節你會了解
---
- 什麼是事件(Event)以及事件線(Event Bus)
- 如何知道有那些事件可以使用
- 該如何訂閱事件
- 訂閱了事件後我可以做什麼事
- 模組生命週期中重要的事件
- 事件範例

什麼是事件(Event)以及事件線(Event Bus)
---
事件系統是Forge建立的一個機制
模組作者可以透過"訂閱"(Subscribe)的方式，來達到**在某某事情發生時做某件事**的目的

並不是只有玩家在遊戲內動作才有事件(ㅇㅅㅇ)
模組的生命週期(Life Cycle)，網路封包的發佈、接受，還有像是註冊物品、方塊，一直到模型的烘培(Baking)都被Forge加進了事件的使用範圍內(๑´ㅁ\`)

流程大概是這樣的
1. 你在你的方法(Method)中用`@SubscribeEvent`來訂閱某一種事件
2. Forge在載入你的程式碼的時候，把你的這個方法加進這個事件的列表中
3. 執行時，若觸發事件，Forge會建立一個事件實例(Instance)並發佈到相對應的事件線上
4. 事件線收到事件實例後，把這實例傳給所有在列表中的方法，並執行

在這個Forge版本中，事件線有兩條(和之前版本的三個事件線不一樣囉>.0)
- Forge Bus
- Mod Bus

Forge Bus在整個遊戲執行只會有一個，它在MinecraftForge這個類別裡
要獲得這條Bus，只要這樣寫就可以了(*ﾟーﾟ)
```java=
IEventBus BUS = MinecraftForge.EVENT_BUS;
```
Mod Bus...簡單來說你裝了幾個模組就有幾個Mod Bus
它被載入模組時建立的FMLModContainer所持有著
要獲得這條Bus，你要這樣寫(*ﾟーﾟ)
```java=
IEventBus BUS = FMLJavaModLoadingContext.get().getModEventBus();
```

如何知道有那些事件可以使用
---

你說，好的M鼠我知道了，你說事件有這\~\~麼\~\~多種類，那有沒有事件列表呢？
有列表我才知道我有那些事件可以用對吧？(●⁰౪⁰●)

恩，對，想要一個事件列表，是所有模組作者夢寐以求，非常需要的東西
版上也有許多模組作者敲碗想要這份清單

但...事實上Forge並不打算以官方身分釋出一個事件清單
原因很簡單，要在版本更新的同時維護這份清單，太累了
~~你也不看看[Forge官方文件](https://mcforge.readthedocs.io/en/1.15.x/)在版本更新的過程中維護成什麼鬼樣子(´c_\`)~~

可是你說，欸，沒有事件清單很難搞耶，這樣我要怎麼知道有那些事件可以用？
其實...

**你自己都已經把Forge程式碼載到你環境裡了，直接從你環境裡看不是比較快又沒版本問題嗎？(╯‵□′)╯︵┴─┴**

首先，在Eclipse點開Navigate選單，選擇Open Type
![圖1-6-1 Navigate選單](https://i.imgur.com/XHGD4Zj.png)

輸入Event，找到`net.minecraftforge.eventbus.api.Event`然後打開
![圖1-6-2 Open Type搜尋畫面](https://i.imgur.com/xabMt36.jpg)

對著類別宣告名字右鍵，選擇Open Type Hiearchy
![圖1-6-3 右鍵選單](https://i.imgur.com/K1tWsEC.png)

左邊視窗出現的就是你要的列表囉~ ┬─┬ ノ( ' - 'ノ) 
![圖1-6-4 Event Type Hiearchy](https://i.imgur.com/Sa3D3hv.jpg)

要注意的是，有些事件是有**子事件**的
*(有些事件並不建議直接訂閱，也從來不會被發布，建議是訂閱子事件)*
比如說上圖中有
- AnvilUpdateEvent
- BabyEntitySpawnEvent
- BlockEvent
    - BreakEvent
    - CreateFluidSourceEvent
    - CropGrowEvent
        - Post
        - Pre
    - EntityPlaceEvent
    - ...
- ChunkWatchEvent
- ...

所以記得要把折疊選單點開來看喔~

該如何訂閱事件
---

要訂閱一個事件，首先你要知道你的這個事件會在哪一個事件線上發布(｡•ㅅ•｡)♡
剛剛說了，有兩個事件線，一個粗略的分別是
- Forge Bus
    大部分遊戲內的事件都是在這條線上發布
    Ex. PlayerEvent.Clone、HarvestDropsEvent、EntityMountEvent
- Mod Bus
    跟模組的載入有關的都在這條線上發布，特別是模組生命週期(Life Cycle)相關的
    Ex. FMLCommonSetupEvent、FMLClientSetupEvent、TextureStitchEvent
    
    你可以看看Forge在官方文件中的其中一段敘述

    > A good rule of thumb: events are fired on the mod event bus when they should be handled during initialization of a mod.

當然，這樣說還是很模糊(#\`皿´)
想更確切的知道這個事件到底是哪個事件線的，你可以試試看下面的方法

1. 看事件註解
    去你要訂閱的事件類別看看，很有可能Forge會在註解上寫上資訊
    大概會像這樣 030
    ```java=
    /**
     * ...
     * This event is fired on the {@link MinecraftForge#EVENT_BUS}.
     * ...
     */
    ```
2. 打開事件建構子的Call Hierarchy
    前面說過，事件被觸發時，Forge會建立一個事件實例，然後發佈到事件線上
    所以透過看事件的建構子被呼叫的地方，有機會看到他被發佈到哪裡去
    
    像是如果是Forge Bus，大概會像這樣
    ```java=
    MinecraftForge.EVENT_BUS.post(new RenderWorldLastEvent(context, mat, partialTicks));
    ```
    如果是Mod Bus，大概會像這樣
    ```java=
    ModLoader.get().postEvent(new TextureStitchEvent.Pre(map, resourceLocations));
    ```

你說如果這兩個方法，都沒能看出來你要的事件在哪條線上怎麼辦？இдஇ
恩...沒怎麼辦XD，如果訂閱後發現沒有成功觸發的話，就換一條
~~你可以先猜Forge Bus，因為Forge Bus上的事件占大多數~~

找到是哪一條事件線之後，就可以開始著手訂閱了
首先，你需要寫出一個傳入此事件類別實例的方法，並使用`@SubscribeEvent`註解，像這樣
```java=
@SubscribeEvent
public static void onPlayerClone(PlayerEvent.Clone event){
    /* Do whatever you want */
}
```

傳入的參數類型很重要，他決定了這個方法會被加進哪個事件的監聽列表中
而方法的名字是隨意的，不過建議以on為前綴，並充分說明是什麼事件( º﹃º )

之後，就把你這個方法註冊到事件線上
這邊以`IEventBus.register()`方法註冊為例
根據你的方法是靜態或是非靜態，你需要傳入不一樣的東西，請看以下範例
```java=
/* This is a example of calling Forge Bus static event register */
MinecraftForge.EVENT_BUS.register(ForgeEventHandlers.class);
/* This is a example of calling Forge Bus non-static event register */
MinecraftForge.EVENT_BUS.register(new ForgeEventHandlers());

/* This is a example of calling Mod Bus static event register */
FMLJavaModLoadingContext.get().getModEventBus().register(ModEventHandlers.class);
/* This is a example of calling Mod Bus non-static event register */
FMLJavaModLoadingContext.get().getModEventBus().register(new ModEventHandlers());
```

除了使用`IEventBus.register()`這方法以外，你還有其他選擇(ㆀ˘･з･˘) 
不過M鼠個人覺得其實上面`IEventBus.register()`加`@SubscribeEvent`就可以解決大部分的需求
- **在類別前面加上`@Mod.EventBusSubscriber`**，並傳入modid、Bus、執行端(可選)
    此功能限制只能使用在註冊的事件方法**是靜態的**
    事件方法前依舊要加上`@SubscribeEvent`註解
    
- **使用`IEventBus.addListener`**，並傳入一個`java.util.function.Consumer`
    這個Consumer的泛型型態就是事件的類別
    採用主動註冊，事件方法前就不需要`@SubscribeEvent`註解
    `IEventBus.addListener`有多載(Overload)，請自行查看有哪些可以使用(´･ω･\`)
    
- **使用`IEventBus.addGenericListener`**，並傳入一個類別(作為篩選器)，以及一個Consumer
    這個方法和`IEventBus.addListener`差別在於你可以傳入一個`Class<F>`作為篩選器使用
    `IEventBus.addGenericListener`也有多載，請自行查看有哪些可以使用(〃∀〃)
    
很明顯的，一個事件種類，**可能有很多人會註冊**，所以執行的先後順序就會是個問題
因此，Forge讓大家在訂閱的同時，你可以設定優先級
它被定義在`net.minecraftforge.eventbus.api.EventPriority`，是個列舉(Enum)，一共有五個
- `EventPriority.HIGHEST`
- `EventPriority.HIGH`
- `EventPriority.NORMAL`
- `EventPriority.LOW`
- `EventPriority.LOWEST`

越高的優先級會越先被執行，而同樣的優先級中，會依照事件監聽者被註冊的順序執行
以`@SubscribeEvent`而言，要指定優先級你需要這樣寫(๑•̀ω•́)ノ
```java=
@SubscribeEvent(priority = EventPriority.HIGHEST)
```
至於`IEventBus.addListener`和`IEventBus.addGenericListener`，請自行查看多載的方法宣告
如果沒有指定優先級，一律預設為`EventPriority.NORMAL`

當你需要設定優先級，一定要仔細思考過，否則會造成其他模組作者的困擾
舉個例子，EnderIO的作者在它新增的附魔書(Soulbound)中便公開抱怨(ﾒﾟДﾟ)ﾒ
> Note: Most gravestone mods are stupid and prevent this from working!

發生了什麼事呢？
對於一個墳墓模組(將玩家死後掉落的物品放進墳墓方塊中)，你覺得它在PlayerDropsEvent中的優先級應該要設定為多少？`EventPriority.HIGHEST`?

**錯，是`EventPriority.LOWEST`(\`へ´≠)!**

理由是因為可能會有其他模組會想要在玩家掉落物品時，修改掉落的物品列表內容

如果你寫墳墓模組，你的思考方式：
- ~~喔喔喔，玩家死了，我要把玩家的物品保存下來，這很重要，所以優先級是最高(X)~~
- **我就當最後一個吧，可能有人會改死亡後的物品(新增、刪除或修改)，等到一切要掉落東西都確定好之後，我再把最後的這個物品清單放進墳墓吧(O)**

所以回來看EnderIO的Soulbound附魔(玩家重生時，物品會留在身上)發生了什麼事

即使EnderIO把它監聽事件的優先級設為`EventPriority.HIGHEST`(事實上EnderIO模組作者真的這樣寫)，如果遇到另一個墳墓模組也把事件監聽設為`EventPriority.HIGHEST`，並且註冊順序又在EnderIO前的話，Soulbound這個附魔就會失效
因為在輪到EnderIO把掉落物清單中有Soulbound附魔物品加回玩家物品欄之前，墳墓模組就已經先把這物品放進墳墓了，EnderIO不會在清單裡看到這東西 (☍﹏⁰) 

所以如果你非要設定優先級，請先思考看看其他模組在這個事件上可能會做哪些事

**優先度 != 重要性
優先度 == 執行順序**

這點請千萬不要被語意所搞混了

最後，關於何時註冊事件
- Mod Bus上的事件，通常會在**模組主類別的建構子(Constructor)中註冊**
    因為Mod Bus上的事件大多和模組載入相關，因此要在很早的時期就註冊
    特別是`FMLCommonSetupEvent`，請務必要在建構子中註冊
- Forge Bus上的事件，通常會在**FMLCommonSetupEvent中註冊**
    是的，你必須要先註冊Mod Bus，並訂閱`FMLCommonSetupEvent`，然後在裡面註冊Forge Bus上的事件

當然，如果你不需要訂閱任何事件，你也不需要註冊這動作(ﾟ∀。)
詳細的註冊內容，本篇文後面的章節會有~~飯粒~~範例可供參考

訂閱了事件後我可以做什麼事
---

事件被發布的同時，會有一個事件實例被創建出來
這個事件實例通常包含了許多和事件有關的資訊

以`net.minecraftforge.event.entity.item.ItemTossEvent`為例
你可以透過`ItemTossEvent.getPlayer()`獲得~~亂丟垃圾~~的玩家 (σ′▽‵)′▽‵)σ
也可以透過`ItemTossEvent.getEntityItem()`獲得~~被當成垃圾的可憐~~物品 (σ′▽‵)′▽‵)σ

有了這兩個東西，你基本上就可以做到你大部分想要做的事了
像是讓~~亂丟垃圾的玩家遭到報應來個雷劈之類的~~ σ ﾟ∀ ﾟ) ﾟ∀ﾟ)σ 
或是讓~~可憐的物品直接複製5組放回玩家身上之類的~~ σ ﾟ∀ ﾟ) ﾟ∀ﾟ)σ 

咳咳，總之請自行去查看你要訂閱的事件有哪些方法可以用吧，還有事件的父類別也別忘記看囉

除此之外，所有事件還有兩個屬性
- `@HasResult`
    當事件類別有著這個註解的時候，代表它是一個擁有結果的事件(･8･)
    
    你可以透過`Event.hasResult()`確定這個事件是否擁有結果
    透過`Event.getResult()`獲得目前的結果
    透過`Event.setResult()`設定此事件的結果
    *對於一個`hasResult`為`false`的事件呼叫`setResult`會丟出`IllegalArgumentException`異常
    
    結果一共有三種，Result是一個定義在Event類別中的列舉(enum)
    - `Result.ALLOW`
    - `Result.DENY`
    - `Result.DEFAULT`

    根據事件的不一樣，這三個值會代表不一樣的意義┌(┌\^o\^)┐
    以`net.minecraftforge.event.entity.EntityMobGriefingEvent`為例
    - `Result.ALLOW` 代表此事件下的物品掉落行為已經被允許
    - `Result.DENY` 代表此事件下的物品掉落行為已經被拒絕
    - `Result.DEFAULT` 代表此事件下的物品掉落行為將會根據gamerule中的mobGriefing的布林值決定

    其他事件的詳細內容請自行查看該事件類別中Forge寫下的註解
    
- `@Cancelable`
    當事件類別有這著註解的時候，代表它是一個可以被取消的事件(ﾉ>ω<)ﾉ
    
    你可以透過`Event.isCancelable()`確定這個事件是否可以被取消
    透過`Event.isCanceled()`獲得目前事件是否已被取消
    透過`Event.setCanceled()`設定此事件的取消與否
    *對於一個`isCancelable`為`false`的事件呼叫`setCanceled`會丟出`UnsupportedOperationException`異常
    
    當一個事件被某個事件監聽器取消後，此事件就不會再傳給後面的事件監聽器處理
    也就是說，如果你取消了這個事件，順序排在你後面的人都不會收到這個事件Σ(*ﾟдﾟﾉ)ﾉ
    
    不過你可以透過在訂閱事件時給予額外的值，來接收到已經被取消的事件
    如果這樣做，你還可以把已經取消的事件撤銷取消( • ̀ω•́ )
    以`@SubscribeEvent`來說，要接收包含已被取消的事件你要這樣寫
    ```java=
    @SubscribeEvent(receiveCanceled = true)
    ```
    `IEventBus.addListener`和`IEventBus.addGenericListener`的部分請自行查看多載的方法宣告

模組生命週期中重要的事件
---

模組的載入階段，主要分以下四步，這個稱為模組的生命週期(Life Cycle)

- `FMLCommonSetupEvent`

    這是第一個被執行的階段 (//・ω・//)
    除了前面提到的註冊Forge Bus事件以外，你模組大部分的初始化都會在這裏面進行

    在這個階段之前，方塊和物品註冊就已經完成
    同時`@ObjectHolder`也已經完成工作，可以直接取值
    
- `FMLClientSetupEvent` & `FMLDedicatedServerSetupEvent`

    這是第二個被執行的階段
    是的，這裡有兩個事件，意味著這兩個事件是同時進行的( ﾟ∀ﾟ)o彡ﾟ

    這兩個事件會分別在客戶端和物理伺服器端觸發
    請注意，是**物理伺服器端**，也就是說你跑runClient是不會觸發`FMLDedicatedServerSetupEvent`的，就算是進入單人世界也一樣(´ΘωΘ\`)
    
    在這兩個階段請做各執行端該做的事，比如說你會想要在Client端設定按鍵綁定、顯示相關的初始化之類的ε(*´･∀･｀)зﾞ
    
- `InterModEnqueueEvent`
    
    這是第三個被執行的階段
    在這個階段你可以透過`net.minecraftforge.fml.InterModComms.sendTo()`向其他同樣被載入的模組發送訊息(｢･ω･)｢
    
- `InterModProcessEvent`
    
    這是第四個被執行的階段
    在這個階段你可以透過`net.minecraftforge.fml.InterModComms.getMessages()`來接受其他模組向你發出的訊息，並用來設定及初始化你跨模組相關功能」(･ω･」)

另外，還有`net.minecraftforge.fml.event.server.ServerLifecycleEvent`，它底下的子事件包含了所有**邏輯伺服器**的開始到結束的階段
這些事件全部都會在`InterModProcessEvent`之後才觸發

根據時間軸的先後順序排列，有以下事件
1. `FMLServerAboutToStartEvent`
    這個事件會在物理伺服器程式開始載入任何伺服器相關東西前觸發，單人世界則是當玩家按下Play Select World的那時觸發◝(　ﾟ∀ ﾟ )◟
2. `FMLServerStartingEvent`
    這個事件會在伺服器載入最基本的設定檔(Ex. server.properties...)以及一些其他基本初始化後觸發
    如果你模組有新增指令(Command)，通常會在這個階段註冊ヾ(´ε\`ヾ)
3. `FMLServerStartedEvent`
    這個事件會在伺服器完成所有載入，已經處於可以讓玩家登入並遊玩的時候觸發
4. `FMLServerStoppingEvent`
    這個事件會在伺服器收到停止指令的時候觸發
5. `FMLServerStoppedEvent`
    這個事件會在伺服器完全關閉前的一瞬間觸發( ´•̥×•̥\` )

事件範例
---

那麼，我們就動手做一個試試看吧~
目標是讓玩家丟雞蛋的時候生出個雷電苦力怕來！థ౪థ

首先我們要找到我們該要訂閱哪一個事件
我們的觸發條件是玩家丟雞蛋，雞蛋落地的時候
所以`net.minecraftforge.event.entity.ProjectileImpactEvent.Throwable`就是我們要的東西

那我們就開始來寫點程式碼吧

首先我們創建一個套件(Package)`com.github.immortalmice.modtutorialim.bus`
並在裡面建立一個ForgeEventHandlers的類別，未來所有Forge Bus上的Event我們都會放在這裡

先寫上一點簡單的程式碼( • ̀ω•́ )
```java=
public class ForgeEventHandlers{
    private static final IEventBus BUS = MinecraftForge.EVENT_BUS;
}
```

首先要解決的問題就是我們要把事件註冊上去
所以我們在ForgeEventHandlers裡建立registEvents方法
```java=
public static void registEvents(){
    ForgeEventHandlers.BUS.register(ForgeEventHandlers.class);
    ForgeEventHandlers.BUS.register(new ForgeEventHandlers());
}
```
事實上，你只要根據你的事件是靜態還是非靜態，選擇一個註冊即可
但考慮到未來模組的擴張，我習慣在這邊同時把靜態和非靜態的方式都註冊一遍ε≡ﾍ( ´∀\`)ﾉ

好的，再來我們就要在FMLCommonSetupEvent中呼叫剛剛寫好的`ForgeEventHandlers.registEvents()`把Forge Bus的事件註冊上去

同理，我們在`com.github.immortalmice.modtutorialim.bus`包裡建立一個ModEventHandlers，未來所有Mod Bus上的Event都會放在這

寫上簡單的程式碼( • ̀ω•́ )
```java=
public class ModEventHandlers{
    private static final IEventBus BUS = FMLJavaModLoadingContext.get().getModEventBus();
}
```

同樣的，我們要在模組主類別建構子把ModEventHandlers的事件註冊上去
所以在ModEventHandlers裡建立registEvents方法
```java=
public static void registEvents(){
    ModEventHandlers.BUS.register(ModEventHandlers.class);
    ModEventHandlers.BUS.register(new ModEventHandlers());
}
```

然在模組主類別的建構子中加上程式碼
```java=
/* com.github.immortalmice.modtutorialim.ModTutorialIM */
public ModTutorialIM(){
    ModEventHandlers.registEvents();
}
```

這時，我們要訂閱FMLCommonSetupEvent，並呼叫剛剛的`ForgeEventHandlers.registEvents()`
所以在ModEventHandlers新增onCommonSetup這個方法
```java=
@SubscribeEvent
public static void onCommonSetup(FMLCommonSetupEvent event){
    ForgeEventHandlers.registEvents();
}
```

這樣前置工作就都完成了
現在可以正式的來註冊我們的Throwable事件吧｡:.ﾟヽ(*´∀\`)ﾉﾟ.:｡

在ForgeEventHandlers中新增onThrowableImpact方法，並加上幾行程式碼作為測試
```java=
@SubscribeEvent
public static void onThrowableImpact(ProjectileImpactEvent.Throwable event){
    ThrowableEntity throwable = event.getThrowable();
    System.out.println("Someone throw something!");
}
```

重整專案，打開遊戲來測試看看吧
開新地圖，從創造背包中拿出雞蛋，往地上一丟
如果沒意外，你Eclipse中的console應該會輸出以下訊息

```
[22:45:08] [Server thread/INFO] [STDOUT/]: [com.github.immortalmice.modtutorialim.bus.ForgeEventHandlers:onThrowableImpact:18]: Someone throw something!
[22:45:08] [Render thread/INFO] [STDOUT/]: [com.github.immortalmice.modtutorialim.bus.ForgeEventHandlers:onThrowableImpact:18]: Someone throw something!
```


恩，我們有成功觸發到事件了，那接下來就可以來做我們要做的事了

首先，有注意到剛剛，你只丟一個雞蛋，卻有兩行輸出嗎？( ºΔº )
這兩行輸出一個是伺服器端，一個是客戶端

是的，如果你什麼都沒做，你的程式碼會同時在客戶端和伺服器端一起執行
但秉持著以伺服器端主導世界的原則，我們要限定接下來的程式碼只在伺服器端執行

首先我們先從事件實例得到被丟出的ThrowableEntity實例吧
透過這個ThrowableEntity，我們獲得這Entity所在的World，並用來判斷執行端
以及我們需要確定被丟出的這個ThrowableEntity是雞蛋，所以寫下一個if判斷
**如果你還不清楚如何判斷執行端和指定執行端，請先看[這個章節]()*

```java=
if(!throwable.world.isRemote() && throwable instanceof EggEntity){
    System.out.println("Inside the if");
}
```

這時候我們打開遊戲來測試看看吧
這邊我們要確定兩件事 ಠ_ಠ
1. 只有在丟雞蛋時才會有輸出顯示，雪球、終界珍珠等不該有反應
2. 只有伺服器端才會有輸出顯示，客戶端應保持安靜

確定都沒問題的話，我們就來生成雷電苦力怕吧！
在if裡加上以下程式碼
```java=
BlockPos impactPos = throwable.getPosition();
World world = throwable.world;

/* Create a creeper instance */
CreeperEntity creeper = new CreeperEntity(EntityType.CREEPER, world);
creeper.setPosition(impactPos.getX(), impactPos.getY(), impactPos.getZ());

/* Set creeper to charged */
CompoundNBT nbt = new CompoundNBT();
nbt.putBoolean("powered", true);
creeper.readAdditional(nbt);

/* Add creeper to world */
world.addEntity(creeper);
```

然後就打開遊戲，享受你的煙火吧~ (ㄏ￣▽￣)ㄏ   ㄟ(￣▽￣ㄟ)
![圖1-6-5 成果展示](https://i.imgur.com/BJuq6C7.gif)


---

*本頁面撰寫於2020/05/07，目前最後更新日期為2020/05/08*
*若上述時間與你閱讀的時間相距過遠，請自行斟酌是否採用本頁面的資訊*
*完整的程式碼可以到本教學文的[Github Repo](https://github.com/immortalmice/MinecraftForge1.15.2-ModdingTutorial)中查看*