"That,good sir,is but a phantasy of yours."
-Arthur Dimmesdale,The Scarlet Letter
在等東方心綺樓的試玩版發布前有些無聊,因此就寫一寫Forge的事件系統吧.
事件(Event)系統是Forge在4.0版本後引入的,用最通俗的話解釋,事件就是一件事發生後,就會觸發與它相關聯的事.比如一個玩家不幸掉進岩漿里受到傷害,就會引發一個事件(之後我們會了解到這個事件是LivingHurtEvent事件),Forge的系統會將這個事件派發給相關聯的方法(Method),那些方法來決定是否處理,如何處理.
很不幸這僅僅只是通俗的解釋,實際實現起來要比這個困難一些,首先Java不原生支持事件,它採用的替代方案(基於監聽者模式,使用監聽器和匿名類)相當令人噁心,因此Forge設計了一套自己的事件系統(基於發布/訂閱模式).Forge的事件總線(EventBus)是事件系統的處理中樞,任何人都可以發布事件,任何人也都可以訂閱事件並在事件被發布時接收到它.如果有複數個訂閱者訂閱了同一事件,那麼系統會將事件按優先級讓訂閱者們依次傳閱,事件在被傳閱完畢後會重新交還給系統並返還給發布者.遊戲中支持的事件類型是被預先編程好的,當然開發者可以自己定製事件.
訂閱一個事件
事件的訂閱是通過Annotation來實現,任何一個被添加了@SubscribeEvent的方法都會被視為事件訂閱者,但要想實際起作用必須滿足這三個條件:
①:方法有且只有一個參數,並且此參數的類型必須是Event類或其派生類.
②:方法的訪問級是public,不能是靜態(static)
③:方法必須屬於一個非靜態的類.
(注:這裡所說的Event是指net.minecraftforge.event包下的Event類,而不是cpw.mods.fml.common.event包下的FMLEvent)
首先先添加一個處理事件的方法(我這裡添加到了Mod主類當中),比如這裡我想在訂閱"生物下落"(一個生物從高處摔到地上時觸發),就讓它的參數是LivingFallEvent.然後為它加上@SubscribeEvent
@SubscribeEvent public void test1(LivingFallEvent event) { if(event.entityLiving instanceof EntityPlayer) { EntityPlayer entityPlayer = (EntityPlayer)event.entityLiving; entityPlayer.addChatMessage(new ChatComponentText("Falling Star! You fell "+event.distance+ " meters.That's cool, man!")); } }
然後我們要通過MinecraftForge.EVENT_BUS.register來註冊方法所在的那個類,通常來說我們是在Mod載入時進行註冊,如果你將處理事件的方法單獨寫在一個類中(假設叫做EventContainer),那麼就在Mod主類的Init方法中添加
MinecraftForge.EVENT_BUS.register(new EventContainer());
如果你直接把處理事件的方法寫在了Mod主類中,那就直接在Init方法中添加
MinecraftForge.EVENT_BUS.register(this);
(順便一提,EVENT_BUS也提供了一個相應的方法來進行反註冊,也就是將已註冊的實例解除掉)
注:這些圖是我在舊版時截的,因此sendChatToPlayer和新版有差別...
之後進入遊戲測試一下(注意要在生存/H模式下,創造模式玩家觸發不了LivingFallEvent事件喲...)
到了遊戲中確實有效...可是這坑爹玩意卻一口氣來了兩個啊!
這是因為新版Minecraft的單人模式採用了本地服務器模擬的方式,雖然你在運行着單人模式,但實質是你在本機運行着一個服務器,然後你的客戶端連接着你自己的本機服務器.然而客戶端和服務器都個會有一個玩家實體(EntityPlayer的派生類),客戶端的是net.minecraft.client.entity.EntityClientPlayerMP,服務器端的是net.minecraft.entity.player.EntityPlayerMP.兩者都繼承自EntityPlayer,因此我們在驗證event.entityLiving instanceof EntityPlayer會返回true.這時我們就需要討論如何取捨了.
執行端的選擇,事件的監聽位置,事件的過濾
我們先依次說起這三件事,首先是執行端的選擇,我們是在服務器端對事件進行響應,還是在客戶端?通常來說,我們認為凡是需要執行實際操作的東西都要放在服務器端,畢竟客戶端和服務器端存在延遲(對於更糟的情況,客戶端還會存在作弊插件),當玩家跳到地上時,他可能會認為自己完成了一次"下落",它的客戶端也是這樣認為,因為EntityClientPlayerMP這個實體確確實實落地了,並在客戶端引發了一個LivingFallEvent事件.然而由於延遲的存在(某些服務器能存在10秒的延遲),服務器端認為EntityPlayerMP依然停留在遠處的一個平地,並沒有進行下落,由此便產生了分歧,究竟玩家是否從高處落下來了?很不幸在這裡服務器是大佬,它認為既然EntityPlayerMP沒有下落,那玩家就是沒有下落,不管客戶端再怎麼鬧騰也沒有作用,如果客戶端是個圖形效果增強Mod之類的不影響實際數據的Mod,也許問題不大,如果是會對遊戲產生實際影響的Mod,只是神知道接下來會發生什麼可怕的事情,有可能是數據不同步造成的斷線.不過假如玩家足夠幸運,在更糟糕的事情發生之前讓自己的移動數據發送到了服務器那,讓服務器做出及時更新的話,或許結果會好一點,但我們禁不起這樣的風險,玩家會認為是我們的Mod有問題,而服務器管理者會考慮卸載掉我們的Mod,畢竟與其花高價租個更好的服務器,管理者更傾向於點點鼠標刪掉一個Mod,然後隨便敷衍玩家幾句了事.在這裡,我選擇在服務器端處理這個事件.對於如何控制執行端,一個是通過事件的過濾,一個是通過@SideOnly,但我不建議使用@SideOnly,除非是必要,否則最好不要考慮它.
對於事件的監聽位置,我建議在一個統一的類內執行,但也並非沒有特例,設想這樣的一個情景,我們做了一把很NB的劍,被它砍中的實體會立刻飛到高度255的地方然後信仰之墜,而且我們為了秀技術刻意不使用onLeftClickEntity方法,而使用一個訂閱LivingHurtEvent事件的方法,那麼我們該怎麼做?我們可以將事件放在Mod主類里,也可以專門建立一個類來監聽,但我們也可以把它直接放在那把劍的物品類里,並在"很NB"的劍的類的構造函數(希望你沒看暈)內註冊它自己.別忘了Minecraft的物品是採用類似於享元模式的方式啊...但注意不要讓事件監聽被註冊的太多,否則會對性能產生巨大的負面影響.
最後我們到了事件的過濾這個問題了,Forge的事件系統的發布方式需要稍微解釋一下,Forge的所有事件(再次強調,不是FML的事件)都繼承自Event類,如果你訂閱了一個事件(類),那麼任何該類及其派生類都會在發布時發送給你.比如LivingFallEvent就是繼承自EntityEvent,那麼如果你訂閱了EntityEvent的話,不光是EntityEvent事件發布時你會收到一份,LivingFallEvent事件發布時也會發給你一份,因為LivingFallEvent是EntityEvent的派生類."既然你連大事都管了,那麼小事你也得管",這或許和現實以及大多數人的認識相反,但這確實是Forge的事件系統的實際寫照.因此事件的過濾就分為兩部分,首先是事件類型的過濾,如果你真的訂閱EntityEvent事件的話我建議你一定要三思,因為LivingUpdateEvent這個事件是每一個實體在每一個Tick(幀)都要執行一次,你最好把它過濾掉,它的過濾判斷表達式是"傳來的事件 instanceof LivingUpdateEvent == false".第二部分的過濾是事件源的過濾,就拿我們實際遇到的情況舉例,EntityClientPlayerMP和EntityPlayerMP都會在同一時間引發相同的事件,既然我選擇在服務器端執行更新,那麼我就過濾掉只存在於客戶端的EntityClientPlayerMP,保留EntityPlayerMP.這個的過濾方法就有很多,對事件的參數進行判斷就行了.
另外再提出一個忠告:訂閱Event事件之前請三思.
小小修改一下代碼,將
event.entityLiving instanceof EntityPlayer
改為
event.entityLiving instanceof EntityPlayerMP
(至於下面的"(EntityPlayer)event.entityLiving"是否修改,在這裡是隨便的,畢竟我只是想過濾一下事件源而已...)
再次測試,這一次我們成功了.(教練我懶得截圖了...自行腦補成功的樣子)
(說的輕巧,事實上作者為了研究這個問題燒了一個晚上的時間...)
事件的優先權
有些時候,會有複數個訂閱者同時監聽着一個事件,有些情況下他們是互相競爭的,因此就有了優先權的設定,優先權分為五級:最高,高,普通(這個是默認的),低,最低.事實上只有前三個才會被開發者考慮,SB才會讓自己的事件排到最低級...對吧= = 所以必要時就有些公德心吧,別把無聊的東西排在高優先級處.
安排優先級的方法是修改@SubscribeEvent的priority屬性.比如:
@SubscribeEvent(priority = EventPriority.HIGH)
這個訂閱的優先級是High,它只會和同優先級的訂閱者競爭,對於比它低的訂閱者,它佔有絕對的優先權.我們這就來試試它的效果,將我們之前的方法改為(即刪掉整個test1然後輸入下面的代碼):
@SubscribeEvent(priority = EventPriority.HIGH) public void test2a(LivingFallEvent event) { if(event.entityLiving instanceof EntityPlayerMP) { EntityPlayer entityPlayer = (EntityPlayer)event.entityLiving; entityPlayer.addChatMessage(new ChatComponentText( "Falling Star! You fell "+event.distance+ " meters. That's cool, man! I will protect you from the falling damage!")); event.distance = 0.0f; } } @SubscribeEvent(priority = EventPriority.LOW) public void test2b(LivingFallEvent event) { if(event.entityLiving instanceof EntityPlayerMP) { EntityPlayer entityPlayer = (EntityPlayer)event.entityLiving; entityPlayer.addChatMessage(new ChatComponentText( "Falling fool! You had fallen... Damn it, WTF "+event.distance+ " meters? Are you fooling me?")); } }
再次進行測試,這次無論我們從多高的地方跳下去,都不會受傷(雖然會發出pia的聲音),因為test2a方法先截獲了跌落事件,並篡改了結果,因此它即糊弄了test2b方法,又欺騙了Minecraft的系統.使得玩家免遭傷筋斷骨之苦.
事件的結果
有時我們我們處理完一個事件後,需要告訴後來人我們已經處理掉它了,還有些情況下,這個事件最終會傳回Minecraft內核,供它就事件的結果進行決策,因此我們就有了Result這個東西.
Result是一個Enum(枚舉)類型.我們通過setResult和getResult來設置或讀取.Result有三個值:ALLOW(允許),DENY(否決)和DEFAULT(默認),然而,這玩意耍起來有風險.
首先,有些事件的Result是供你隨便耍的,有些則會影響到遊戲的運行,前者我稱之為無Result事件,後者我稱其為有Result事件,判斷它的辦法是調用hasResult方法,如果它返回true就代表它有Result,那麼就請仔細閱讀這個事件的JavaDoc.比如玩家拿着水桶對着物體(不一定是實體)按下鼠標右鍵時引發的FillBucketEvent事件就是有Result的事件,如果在最終傳回遊戲內核時它的Result的結果是ALLOW的話,那麼就會強制視為一次成功的操作.對於無Result的事件,它僅僅只是"Result這個參數對它沒有意義",而不代表我們用不了Result.LivingFallEvent就是無Result事件,但我們接下來就用它展示一下Result的使用.
(另外,由於Forge製作組的烏龍,不要將Result和某些事件里的result參數搞混,前者才是我們現在所說的Result,後者只是代表着傳來事件的某一參數.)
在test2a中加入:
event.setResult(Result.DENY);
在test2b中加入:
entityPlayer.addChatMessage(new ChatComponentText( "I understood... She made the event " + event.getResult().toString() + "."));
之後進入遊戲測試一下.
事件的取消
有時在我們執行完一個事件的操作後,希望能取消掉這次事件,讓後面的訂閱者無法處理它,同時也讓遊戲內核認為這個事件沒有發生.對於任何具有Cancelable(可取消)屬性的事件來說,我們都可以實現這一目標.
要判斷一個事件是否是可取消的,就調用它的isCancelable方法,如果返回的是true就代表它是可取消的,此外你也可以通過在IDE中直接觀察那個事件是否具有@Cancelable注釋來判斷它能否被取消.
圖:LivingFallEvent具有@Cancelable注釋,因此它是可取消的.
設置一個事件被取消的辦法是調用它的setCanceled方法,setCanceled的參數是一個bool,true代表此事件取消,被取消的事件在調用isCanceled時會返回true,並且尚未接收它的訂閱者們默認將不再會接收它,Forge的事件系統也將向發布者返回一個true(默認是返回false,具體的返回機制我會在後面解釋),對於已取消的事件,你也可以通過setCanceled(false)來複活它.
(注意,對於不可取消的事件,千萬不要setCanceled(true),遊戲會立刻拋錯的)
現在,將我們的test2a方法中的
event.distance = 0.0f;
替換為
event.setCanceled(true);
然後保存,測試.
這一次你依然是不會受到跌落傷害,甚至都不會收到第二個訂閱者的信息,因為你將這個事件取消掉了.
那麼被取消的事件該如何復活呢?如果要讓別的訂閱者復活它的話,首先我們得讓那個訂閱者能接收它,默認情況下訂閱者不會再接收被取消的事件,但我們可以通過修改@SubscribeEvent的receiveCanceled屬性來讓它能接收被取消的事件.對於任何receiveCanceled = true的訂閱者來說,它都可以接收被取消的事件.
將我們的test2b替換為
@SubscribeEvent(priority = EventPriority.LOW,receiveCanceled = true) public void test2b(LivingFallEvent event) { if(event.entityLiving instanceof EntityPlayerMP) { EntityPlayer entityPlayer = (EntityPlayer)event.entityLiving; entityPlayer.addChatMessage(new ChatComponentText("F@CK U Maribel!I know you are there!")); event.setCanceled(false); } }
事件的發布
事件的發布不比發一顆衛星容易多少,首先你要掌握軌道事件學,應用事件學,量子事件學,理論事件學,高維度事件學,隙間事件學,虛數維度事件學,亞空間事件學,節點空間事件學,遷躍斷層事件學,翹曲事件學,子維度事件學,相對事件學,形而上事件學,混沌事件學,核事件學,高等事件學,代數事件學,幾何事件學,事件工程學,事件哲學,事件心理學.此外你還得了解十事件問題,最小事件問題,事件樹等算法.最後你還得祈禱Yukari在和Cthulhu對轟時不會從你這裡神隱走幾個即將發送的事件,當做彈幕發射出去.
幸運的是,這些事情Forge全幫你包辦了!所以發布一個事件的方法是很容易的!
開發者可以通過調用事件總線(EVENT_BUS)的post方法來發布一個事件,post方法會返回一個bool值,true代表這個事件被取消了,false代表它順利執行到了最後.建議的格式是這樣的.
初始化一個事件的實例 MinecraftForge.EVENT_BUS.post(事件) 判斷事件的結果
看到的時候被雷了一下對吧...我這裡破天荒地使用了偽代碼,因為我自己實在懶得編一個拋出事件的例子,因此我就以Minecraft的ItemBow(弓)的拉弓部分為例:
ArrowNockEvent event = new ArrowNockEvent(par3EntityPlayer, par1ItemStack); MinecraftForge.EVENT_BUS.post(event); if (event.isCanceled()) { return event.result; }
ArrowNockEvent是拉弓事件,它的參數分別是觸發玩家的實體和玩家所拿的弓.
MinecraftForge.EVENT_BUS.post(event)則是將剛剛初始化完畢的事件向訂閱者發送出去,事件總線會完成事件的發布工作(為你省了很多事呢!笑).並最終返還給你返回值.
之後就到了對事件的處理工作了,由面向對象的特性可知,event這個事件在被發布出去後已經被各位訂閱者們糟蹋了一通了.因此我們不必寫一個專門接收處理結果的代碼,直接對event操作就能獲取處理結果.
(小吐槽:其實官方的那段代碼可以被簡化的,post返回的就是isCanceled的結果)
自定義事件
現在開始才是高潮吧,Satori?
自定義事件的發布和固有事件的發布沒有差別,唯一問題在於如何創建自定義事件.
既然所有事件都是繼承自Event類,那我們也創建一個繼承自Event的類好了.
並沒有太多需要注意的東西,一個是在構造函數中要調用基類的構造函數(super();),第二就是如果你不希望訂閱者修改你的事件中的參數的話,就將它們聲明為final.
此外我們可以設定它有無Result,能否被取消.
設定一個事件有Result,是在它的類上方加入@Event.HasResult
設定一個事件可以取消,是在它的類上方加入@Cancelable
之後我寫了一個很無聊的代碼,它的作用是在玩家輸入KABOOM時讓自己周圍的生物全都遭遇一次爆炸.
@SubscribeEvent public void letsrock(ServerChatEvent event) { if(event.message.equals("KABOOM")) { event.setCanceled(true);//截獲玩家的指令並不讓它顯示在屏幕上,用來模擬遊戲指令(Command) EntityPlayer player = event.player; EventHANDRU eventHANDRU = new EventHANDRU(player);//初始化一個事件 MinecraftForge.EVENT_BUS.post(eventHANDRU);//發布它 if(eventHANDRU.getResult() == Result.ALLOW) { //這個長的讓人髮指的東西是獲取玩家附近的生物 List list = player.worldObj.getEntitiesWithinAABB(EntityLiving.class, AxisAlignedBB.getBoundingBox(player.posX-30D, player.posY-20D, player.posZ-30D, player.posX+30D, player.posY+20D, player.posZ+30D)); //值得一提的是我這裡使用的是遍歷器,傳統的下標遍歷因為無法鎖定資源可能導致ConcurrentModificationException... for(Iterator iterator = list.iterator();iterator.hasNext();) { EntityLiving entity = (EntityLiving)iterator.next(); if(entity.equals(player)) //別把自己也給炸了... { continue; } player.worldObj.createExplosion(player, entity.posX, entity.posY, entity.posZ, 4f, true); } } } } @SubscribeEvent public void goodbyeRenko(EventHANDRU event) { event.entityPlayer.addChatMessage(new ChatComponentText("Have a nice day, Renko Usami."));//歡迎來到冥界,宇佐見蓮子. event.setResult(Result.ALLOW); }
進入遊戲後輸入KABOOM,然後看煙花吧...不過有些耐操的動物比如豬可能無法一發炸死.
結尾
"別這樣,紫,我們都是女孩子..."
-Reimu Hakurei,紫妹異聞錄
就這樣結束了嗎...也許是吧,Forge的事件系統就只有這麼點東西可說.然而如果你有興趣,你可以去研究更加高深的東西,例如事件的getListenerList是什麼東西?FML的事件系統怎麼使用?它和Forge的事件系統有什麼關係?學海無涯啊少年...願你在Forge之路上愈行愈遠吧,裝哉我大Forge!F@CK THE BUKKIT!
附錄1.OMG到底有幾條總線!?
事實上,Forge不只有Event_Bus這1個事件總線,Forge總共有3個事件總線,分別是TERRAIN_GEN_BUS(地形生成總線),ORE_GEN_BUS(礦物生成總線)和EVENT_BUS(我們敬愛的,事件總線).3個總線很好區分,分工也很明確,我就不詳細說明了...(群眾:我們去年買了個表)
你好,我現在用的是1.7.10。
要使得右擊指定方塊時執行內容。但是監聽PlayerInteractEvent後如何判定方塊種類是問題,參數只有方塊坐標,我也找不到可以取指定坐標上的方塊的方法。想問如何實現?
1.8是event.world.getBlockState(event.pos).getBlock(), 1.7是event.world.getBlock(event.x, event.y, event.z)
謝
ServerChatEvent 只能在服務器side 用吧?
我在client side 怎麼截取玩家輸入的String?
對了,KeyInputEvent 能不能監聽在輸入聊天信息的按鍵事件? 貌似不行-、-
請問自定義事件怎麼設置觸發條件?我做的自定義事件觸發不了,甚至將原事件改個名字,也觸發不了
按說發布事件時,new一個事件然後調用事件總線的post,發送給事件的監聽者就行了啊 難道Lex在新版Forge又整了什麼幺蛾子...
總覺得關於遍歷的那個注釋是錯的吧……是錯的吧……是錯的吧……
這麼嘛...其實有些教程中的代碼到現在我都忘了是為什麼這麼寫的了...不過看起來當時我說的有理有據,應該是在試過使用下標遍歷跪了的情況下寫的,等更新教程更新到那的時候我再試試用下標遍歷會發生什麼吧.現在看來我也覺得不可思議,理論上MC的事件系統是單線程的,為什麼會有並發異常呢
void net.minecraft.entity.Entity.addChatMessage(ITextComponent component)
教練您好這是1.10.2的,其中addChatMessage中的參數ITextComponent是一個接口。
小弟我不才,如果我要發送“hello”該如何實現這個接口?
麻煩了。
請問過了一周了,教練看到了么?
噫 這幾天忙着黑無人深空去了 (霧) 其實最簡單的辦法就是用new TextComponentString("hello")作為參數就行了,活用IDE的顯示類繼承結構(eclipse中是按F4)是必備技能啊...
萬分感謝。
@SubscribeEvent
public void cardBreakEvent(BreakEvent event){
if(event.getState().getBlock() == cardBlock ){
System.out.println("a");
ItemStack is1 = new ItemStack(card_white,1);
event.getPlayer().getInventoryEnderChest().addItem(is1);
教練再次麻煩您指點一下,其中cardBlock是自製block ,card_white是自製item 我想當玩家破壞cardBlock 的時候,在玩家背包裡面添加item - card_white 可是並不能成功添加,System.out.println("a");可以成功輸出。是在哪出現了問題?
還有如何像鑽石塊或者紅石青金石那樣,打破方塊掉落物品?
麻煩了。感謝。
getInventoryEnderChest...難道不是末影箱??
我按照您的F4大法,已經搞清楚怎麼讓Block掉落東西了,但是我的那段代碼還是不行。求弄明白why?
啊,原來是這樣啊。那沒有直接在玩家背包當中添加物品的方法嗎?