進階篇,第二章:MC與Forge的Event系統

<基於1.8 Forge的Minecraft mod製作經驗分享>

這一章其實才應該是第一章,礦物生成裏面用到了Event的一些內容。如果你對之前礦物生成那一章的將算法插入ORE_GEN_BUS那塊沒看懂,那麼相信這一章會給你解釋清楚。

下面開始逐一分析MC與Forge的Event系統。

一、EventHandler。

很熟悉不是?其實從製作mod的第一步開始,我們已經在於Event打交道了。我們用一個@EventHandler註解,標註了mod主類中的幾個帶有唯一事件參數方法,從而使這幾個方法取得了事件的控制權,從而插入一些操作到MC中去。確切的說,當MC運行開始,Forge就會把這幾個方法插進去,使得你運行的MC不再是原來的MC,而是被部分偷樑換柱了的。下面簡單回顧一下以前提到過的,幾個@EventHandler所接受的Forge的生命週期Event:

  1. FMLPreInitializationEvent,源碼註釋道:Run before anything else. Read your config, create blocks, items, etc, and register them with the {@link GameRegistry}.

    也就是說,這個事件是第一個運行的,建議你在這個事件裏完成讀取配置、創建方塊、物品等,以及在GameRegistry中註冊它們。

  2. FMLInitializationEvent,Do your mod setup. Build whatever data structures you care about. Register recipes, send {@link FMLInterModComms} messages to other mods.

    意思是建立你的mod,構造你的數據結構,註冊合成表,向其它mod發送FMLInterModComms消息。

  3. FMLPostInitializationEvent,Handle interaction with other mods, complete your setup based on this.

    與其它mod交互,並基於此完成最後的配置。

這三個事件很常用也很簡單,不做過多解釋。當然,你可以對着@EventHandler標籤Ctrl + clickL,看看Forge還爲你提供了哪些可用的事件。唯一需要注意的就是,這三個事件實際上都是在遊戲開始前執行的,當你運行打了Forge的MC,看到一個小錘子在那敲的時候,這三個事件就以此發生了。

二、EventBus,本文的重點。

EventBus,事件公交,一般我們稱它爲事件主線,其實就是觀察者模式。按照名稱來理解,EventBus是一個交通載體,事件是它的乘客,EventBus會載着它的乘客們貫穿於MC中。但我更喜歡這麼來理解:EventBus是一個電臺,每一個事件是一則廣播。我們用post();方法,向某個EventBus電臺發佈一則事件廣播,然後可以在其它地方用register();方法來註冊一個訂閱者(觀察者),來監聽這個事件廣播。一旦一個事件被post到一條事件主線上,那麼所有這條線上的訂閱者都可以監聽到它,並對它做出響應。

首先我們需要掌握幾個與EventBus交互的幾個重要方法:


  1. public boolean post(Event event);
    發佈一個事件廣播

    參數是一個事件的實例,可以是預設的,也可以是你自己的。關鍵是返回值。這個返回值並不是你的事件發佈成功或失敗,事實上當你post事件後,事件第一時間就會發給各個訂閱者。而這裏的返回值,實際上是由這個事件是否被取消決定的(決定!=是,如果取消,返回的是true!如果沒有取消,返回的反而是false!!!)。也就是說,如果訂閱者對你發佈的event,setCanceled(true);,那麼post();執行完畢後就會返回true。

    但要注意,setCanceled();僅僅決定post();的返回值,而不會改變post();的動作,post();一定會在逐一爲每個訂閱者發佈完事件後返回,每個訂閱者自然也必定會監聽到事件廣播,你無法組織!由此,又引發了另一個問題:最終cancel與否,是由最後一個執行setCanceled();方法的訂閱者的選擇來決定的!所以,請不要閒的沒事隨便setCanceled();,如果post();的返回值與你無關,你就不應該參與到這個競爭裏面去,無論setCanceled(true);還是setCanceled(false)。

    還要注意,根據源碼來看,EventBus的訂閱者目前是被一個ArrayList所記錄的,所以它是有序的,但我仍然不建議你做任何依賴於訂閱者訂閱先後順序的操作,因爲這個ArrayList是完全的內部實現,不是對外約定的一部分,Forge可以隨時輕易的換掉它,這會爲你的mod造成很大的遺留問題隱患。


  2. public void register(Object target);

    註冊一個訂閱者

    訂閱者可以是一個任何形式的類,它必須具備一個監聽器。監聽器是一個帶有@SubscribeEvent註解的、含有唯一參數爲一個Event的方法。你可以把這個監聽方法寫在你的主類或Proxy裏,然後任性的evenBus.register(this);,稍好些的童鞋可能會傳入一個匿名內部類。但我建議你新建幾個類作爲訂閱者,最起碼也要用幾個內部類的實例域或靜態內部類,理由是,你可能會需要訂閱多個分屬於不同EventBus主線上的事件,那麼把主類作爲訂閱者,把所有監聽器放在裏面,註冊this,就會一股腦的在每個主線上註冊同一個訂閱者,這個訂閱者包含了你所有的監聽器方法,而不管這個監聽器要訂閱的事件是否在這個主線上,以後每次有事件廣播時,還可能會平白多出很多次沒必要的響應。另一方面,也爲接下來的註銷訂閱者帶來了困難,因爲這個訂閱者會帶走所有的監聽器。與前面幾次不同的是,訂閱者註冊時一次性的,而不是像生成算法那樣頻繁的調用,所以分別創建幾個不同的訂閱者,管理不同的事件監聽並不是問題。


  3. public void register(Object target);

    註銷一個訂閱者。

    一般情況下,我們如果hold住之前註冊的訂閱者,那麼現在把它原樣傳回去就ok。如果不能呢?當我們之前的實例無法獲取或者已經不在了,該怎麼辦?如果你聽從了我之前的建議(什麼建議?呵呵,往上翻,認真看),那麼還有救。嚴謹的Java程序猿的應該知道:1、要實現從集合中刪除對象這類操作,一定要先爲這個對象的類重寫

    public boolean equals(Object obj);
    方法。2、如果要重新equals();,那麼最好也同時重寫掉
    public int hashCode();
    第一條還好,第二條卻很容易被忽略,而很不幸的,據源碼觀察,Forge在這個方法內使用了ConcurrentHashMap的remove方法。當註銷一個訂閱者時,ConcurrentHashMap會使用hashCode來辨識你傳入的object,所以如果你沒有重寫hashCode,很可能掉坑。如果你掉了,這次真不是Forge的錯,是你自己zuo死。那麼是不是意味着,咱就覆寫hashCode();就行了呢?當然不是。因爲ConcurrentHashMap同樣是屬於Forge的內部實現,不是對外約定的一部分,人家隨時可能選擇更好的方案。至於你,老老實實的按照規範,equals();、hashCode();一起復寫吧,然後你就可以new一個新的對象,把它作爲參數傳入。

再接下來,我們還需掌握幾個重要的Event的通用方法:

  1. 觀察

    @Cancelable
    ......
    @hasResult
    ......
    註解。

    是的,我把它作爲一個方法,因爲前者其實等價於

    public boolean isCancelable();
    ,後者等價於
    public boolean hasResult();
    。如果一個事件是可取消的,那麼isCancelable();將返回true,並且按照約定,它會得到一個@Cancelable註解。@hasResult同理。當你拿不準一個event是否可以被取消,或者是否可以有一個返回值,其實更多的情況下二者只取其一,我們只是想要讓某個event不在執行,弄不清是應該用setCanceled(true);還是setResult(Event.Result.Deny);來取消它,那麼就可以Ctrl + clickL,到源碼裏去尋找上面兩個註解,以及對應的方法,來確定該怎麼做。

    鑑於你從上面的方法裏得到的方便,也請你也遵守這個約定來給別人方便:如果你自定義的事件複寫了isCanelable();或hasResult();,並返回了true,請打上相應的個註解。繼承自Event且未手動重寫的這兩個方法默認返回值爲false,但如果你繼承了它的子類,並且不打算重寫這兩個方法,那麼請一定要看看它的子類是否打了註解、返回true,如果是,這個方法的返回值對你是否重要,是,則請打上註解,否,建議你把它們重寫回false。


  2. public void setCanceled(boolean cancel);
    public void public void setResult(Result value);

    前文已經討論了setCanceled();的注意事項和建議,這裏再次總結一下:setCanceled();可以設定event的isCanceled屬性,但最終的值總是由最後一次執行的setCanceled();操作決定。如果是否取消對你不重要,那麼就不要set,無論set個true還是false。如果當前event並沒有@Cancelable註解、isCancelable();的返回值爲false,請不要setCanceled();,無論true還是false。

    這些注意事項和建議也同樣適用於setResult();,不過特殊的是,Event提供了一個公開的Result枚舉,它只有三個元素:DENY, DEFAULT, ALLOW,而不是像isCanceled那樣的倒黴boolean。你可以通過setResult(Event.Result.DEFAULT);,來一定程度降低一些其它訂閱者搞出的亂子,甚至你可以更精確的,通過getResult();方法,來看看是否由其他訂閱者做了羞羞事。最後,除非你確定其它訂閱者的確造成了麻煩,否則還是別參與到set的競爭裏面去。


  3. public Result getResult();
    public ListenerList getListenerList();

    getResult();方法可以獲取到event的result域,這是個Result枚舉的實例,通過它,你可以與事件的發佈者,甚至其它訂閱者交流。與事件的發佈者交流,這很好理解,與其它訂閱者交流是怎麼回事呢?前文說過,Result是個public的枚舉類,你可以爲一個event對象替換掉它的Result,但你需要保證的新Result枚舉中仍然有DENY, DEFAULT, ALLOW這三個元素,最好也不要添加新的元素名,以免引起不必要的麻煩。但你可以爲元素增加新的屬性,這樣不就可以做到與其它訂閱者的交流了。什麼?我怎麼想到的?仔細看看Event類吧,它有一個getListenerList();方法,這個方法顯然是拿來獲取監聽者列表的,也就是我前面一直說的其它訂閱者。爲什麼這裏不叫getSubscriberList();,應該是因爲,我們實際得到且需要的並不是其它訂閱者,而是它們的監聽方法吧(吐槽一下,Forge的各種命名系統真心太坑,還記得前面的Json那張不。。。)。那麼,既然能獲取到其他訂閱者的監聽器,自然也就可以與其它訂閱者交流咯,怎麼交流呢?聯繫Result那個奇怪的非靜態非不可變枚舉,一切水到渠成。

呼~終於掌握了足夠的與EventBus、Event交互的方法,來到了最後一步:看看四條Forge爲我們提供的EventBus。(童鞋挺住啊,鬥羅大坑在前方等待着你呢)認識這四條主線很重要,如果事件在一條線上發佈、廣播了,你卻跑到另一條線上去訂閱、收聽,怎麼可能收到呢。什麼?爲什麼會有這麼多條“主線”?爲了分工啊童鞋,不然所有的發佈事件、註冊訂閱者都對同一條總線操作,會堵車的啊,則也再次印證了我上文的建議——建立多個訂閱者,分管不同的事件:你看,Forge都這麼做了。你雖然沒辦法得到一條真正的“主線”,但你可以直接在ListenerList層面上進行操作,它有大量公開的諸如
public void register(int id, EventPriority priority, IEventListener listener);
之類的定製程度很高的方法,第一個參數id就是主線的id,一個事件的所有存在監聽器的主線都被放在了一個ListenerListInst[]中,所以這個id其實應該是主線在[]裏的index。ListenerList裏面還有一個靜態的allLists域,雖然是私有的,但也能看出它擁有很高的自定義度了。不過目前我還沒有實際運用過,所以暫不討論,有興趣的童鞋自行探索。

  1. FMLCommonHandler.eventBus:

    這條主線是由FML提供的,主要負責最基本的事件的彙總。

    FML,ForgeModLoader,顧名思義,是用來加載Mod的,比Forge更加底層。所以發佈在這裏面的事件,都是些最基本層面的東西,它們基本位於net.minecraftforge.fml.common及其子包中,比如InputEvent、PlayerEvent、TickEvent。現在不對這幾個事件做過多解釋,以後用到了再說。通常我們不應該往這麼基礎的主線裏發佈事件,事實上真正需要發佈到這裏的事件也就前面那三個。但我們可能經常需要從這裏面訂閱事件,來實現鍵盤按鍵、鼠標點擊之類的事件的監聽。

    要取得這條主線,你需要先取得一個FMLCommonHandler的實例,然後調用它的bus();方法獲取它的eventBus域:

    FMLCommonHandler.instance().bus();
    。接着你就可以在這裏訂閱你要監聽的事件了。



  2. MinecraftForge.ORE_GEN_BUS:

    這條主線由Forge提供,上一章教程沒看懂的童鞋要仔細了。

    顧名思義(我怎麼總愛用這個詞?嗯,一定是Java的錯),這條事件主線負責礦物(ore)的生成(generate),所以準確的說,它是一條礦物生成事件專線。礦物生成事件是位於net.minecraftforge.event.terraingen包下面的OreGenEvent類,它還有幾個子類:Pre、Post、GenerateMinable,發佈的時機分別是在礦物準備生成前、礦物已經生成後、可開採的礦物生成前,其中最常用的無疑是GenerateMinable,這裏講細一點,來彌補上一章的部分內容。OreGenEvent有三個公開不可變域(但不是常量域,非靜態,你不能在類上調用它們),world、rand、pos,分別持有當前世界、當前的隨機數發生器、區塊位置三個對象。Pre、Post只是簡單繼承了OreGenEvent,同上。而GenerateMinable在OreGenEvent的基礎上,擴展了兩個自己的公開不可變域:type、generator,持有要生成的礦物類型的枚舉實例與一個礦物生成器,以及一個可變的枚舉靜態域(但注意,枚舉本身不可變,你只能選擇暴力的替換它)。結合上一章,我們現在可以明白,“把礦物生成算法插入到遊戲中”,實際做的是註冊了一個礦物生成事件的監聽器(MinecraftForge.ORE_GEN_BUS.register(new MinableOreGen()),訂閱了礦物生成的事件(在監聽器MinableOreGen類中用@SubscribeEvent註解一個以OreGenEventGenerateMinable evnt事件爲唯一參數的方法,來訂閱一個OreGenEventGenerateMinable event事件),並在礦物生成事件發佈時響應它(在這個方法裏,生成自己的礦石),最後把你的迴應綁定在這個事件上(event.setResult();)。

    看命名就知道這是一個常量域,你無需取得MinecraftForge的實例就可直接調用這個public的域。如何?現在明白上一章的內容了沒?豁然開朗是吧?什麼?暈了?抱歉,這是進階篇,挺住。。。


  3. MinecraftForge.TERRAIN_GEN_BUS:

    除去礦石,MC裏還有各種地形地貌。這條總線就是用來彙總地形生成事件的。

    TERRAIN_GEN_BUS主線所管理的事件應該都在net.minecraftforge.event.terraingen這個包下面,可以看到挺多的。爲什麼你看到了BiomeEvent(生物羣落事件)?別忘了MC中的生物羣落是伴隨地貌的。你居然還看到了OreGenEvent?是的,礦石生成算是地形的一部分,放在這裏面也無可厚非。所以,我把ORE_GEN_BUS放在了前面講,因爲它的轄域較小,記住OreGenEvent顯然是由ORE_GEN_BUS專線管理,那麼net.minecraftforge.event.terraingen包裏其它的事件就交給TERRAIN_GEN_BUS了。什麼?SaplingGrowTreeEvent?這個我也是醉了,估計樹木生成也是在創建世界就開始了,而且樹木生長本質也就是放置新的方塊,所以就算入terraingen了吧。如何?這些事件夠用不?不夠?沒關係,隨便點開一個看看,就比如BiomeEvent,那麼你會看到它裏面又有幾個靜態內部類,其它的也一樣如此,所以Forge提供的可用事件其實是非常豐富的。至於具體的用法,那麼多事件怎麼說的完,如果用的到我再說吧。其實認真看前面的內容,掌握了方法,那麼你就應該有些思路了吧。然後就自己摸索唄,我不也是自己摸索的。


  4. MinecraftForge.EVENT_BUS:

    除去地形地貌,MC裏還有很多其它的事件,這些就交給它了。

    有了之前ORE_GEN_BUS到TERRAIN_GEN_BUS的經驗,應該就不難理解這種從小範圍逐一排除的分類方式了。除了net.minecraftforge.event.terraingen這個包以外的其它Forge定義的事件,基本散落在net.minecraftforge.event、net.minecraftforge.client.event這兩個包及其子包,它們都歸EVENT_BUS這條主線負責。一些諸如UI渲染之類的事件,顯然只能發生在客戶端,所以在net.minecraftforge.client.event包裏找,其它去net.minecraftforge.event,這不難理解吧。

終於結束了,後面我們要涉及的問題越來越深入,文章也會越來越長。別怪我不分成小章節,這畢竟不是系統的書本教程,我需要讓大家遇到什麼問題後能很清楚的找到對它的分析與解決它的所有相關知識點,以免出紕漏,自然不能把關聯的問題分的太散,否則可能需要你讀好幾章才能徹底搞明白一個問題,萬一漏讀了一章會出大麻煩。我只能說,我會盡量把章與章之間的界限劃分的更加明顯。

GitHub鏈接:https://github.com/zhengxiaoyao0716/DouroMod

鬥羅大坑真不是我一個人能完成的,我甚至都不敢肯定自己會不會棄坑,所以我公開源碼,分析源碼,還專門寫這些文章來分享我的經驗,就是爲了拉你一起入坑。都說咱國人盜版強,MC如此開放,Notch如此寬容,咱倒是做一個震撼點的mod出來啊。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章