事件系統 === 我想要在玩家丟雞蛋的時候生出雷電苦力怕!(∂ω∂) 那麼,你需要的就是事件 甚麼?你說你並不想要雷電苦力怕?(●▼●;) 欸都...我想你還是需要事件的,為了你的模組著想 在本章節你會了解 --- - 什麼是事件(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  輸入Event,找到`net.minecraftforge.eventbus.api.Event`然後打開  對著類別宣告名字右鍵,選擇Open Type Hiearchy  左邊視窗出現的就是你要的列表囉~ ┬─┬ ノ( ' - 'ノ)  要注意的是,有些事件是有**子事件**的 *(有些事件並不建議直接訂閱,也從來不會被發布,建議是訂閱子事件)* 比如說上圖中有 - 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); ``` 然後就打開遊戲,享受你的煙火吧~ (ㄏ ̄▽ ̄)ㄏ ㄟ( ̄▽ ̄ㄟ)  --- *本頁面撰寫於2020/05/07,目前最後更新日期為2020/05/08* *若上述時間與你閱讀的時間相距過遠,請自行斟酌是否採用本頁面的資訊* *完整的程式碼可以到本教學文的[Github Repo](https://github.com/immortalmice/MinecraftForge1.15.2-ModdingTutorial)中查看*
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up