UPD(2023-5-3):調整了部分文字的格式。
關鍵詞:事件(event)、事件偵聽器(Event Listener)、示例(給玩家發送登錄歡迎)、示例(彩色文字)、示例(使玩家無法移動)。
在本篇教程中我將引入事件(event)這一概念,並實現一些簡單的功能。
很多程序是依賴事件機制工作的,比如說某些 GUI 庫,當用戶按下鍵盤上的某個鍵時,就會產生一個事件,當用戶松開那個鍵時,又會產生一個事件,在這個過程中,程序可以捕獲事件,根據事件的內容進行相應的處理。在 Minecraft 中,玩家移動會產生事件,破壞方塊會產生事件,退出服務器會產生事件……程序員只需要給對應的事件編寫相應的處理代碼,就能實現豐富的交互效果。
事件機制有什么優點呢?試想,如果不使用事件機制,為了實現同樣的效果,程序就要不停地檢測許多變量的變化情況,一般來說,這是非常浪費資源的,而且也不方便處理。使用事件機制,我們不僅可以更方便地管理這些“變化”,還能根據實際情況解決 “什么時候去處理?怎樣處理?先處理誰?” 這些問題。
有了以上知識作為鋪墊,現在我們來看代碼。如果你正確地按照教程的上一篇配置好了環境,新建項目后應該會看到如下預置的模板代碼:
package group.testplugin; // 這一行因你的項目配置變化而變化,並不一定要和我一樣 import org.bukkit.plugin.java.JavaPlugin; public final class Testplugin extends JavaPlugin { @Override public void onEnable() { // Plugin startup logic } @Override public void onDisable() { // Plugin shutdown logic } }
現在讓我們來引入事件機制。(注:下面的一些代碼需要你自行添加相應的 import)
為了處理事件,我們需要一個事件偵聽器類 (EventListener),那么如何讓一個類成為所謂的事件偵聽器類呢?很簡單,只需在定義后面加上 implements Listener 即可,對於上面的代碼,就是寫成
public final class Testplugin extends JavaPlugin implements Listener
但是,插件此時並不知道我們定義的事件偵聽器的存在,這也很好理解,因為接口是一個“被動”的存在,而你只是通過 implements 關鍵字表明自己要去實現里面的方法,為了能在觸發各種事件時調用我們實現的接口,我們必須注冊這個事件偵聽器類,很簡單,一行代碼就可以搞定:
getServer().getPluginManager().registerEvents(this, this);
其實,事件偵聽器類不一定是我們的 Testplugin 類(插件主類,即上面代碼中繼承了 JavaPlugin 的類),你可以另外定義一個類來實現事件偵聽器然后與 Testplugin 類來交互(但是 Testplugin 類是必需的,因為它繼承了 JavaPlugin 類,是整個插件的核心,負責插件啟動和關閉時的行為)。
參照 registerEvents 的定義:
我們就可以把插件主類和事件偵聽器類分開,然后進行注冊。這里我把兩個類合二為一,一方面方便講解,一方面要實現的功能不是很多,所以放在一起更緊湊。
一般來說,上面這行注冊代碼必須在一開始就要執行掉,如果你的事件偵聽器類和插件主類是同一個類,那么把它放在插件主類的 onEnable() 方法里是不錯的選擇。如果兩個類不同,你可以考慮將注冊代碼寫在事件偵聽器的構造函數內,或者在插件主類中定義一個事件偵聽器類的對象,依然在插件主類的 onEnable() 方法中調用注冊代碼。
下面我們來實現一些基本的功能。(以下的方法均定義在你的事件偵聽器類里面)
比如說,當玩家加入游戲的時候給他們發送歡迎消息:
@EventHandler public void onPlayerJoin(PlayerJoinEvent event) { event.getPlayer().sendMessage("Welcome, " + ChatColor.AQUA + event.getPlayer().getName() + ChatColor.WHITE + " !"); if (!player_list.containsKey(event.getPlayer().getUniqueId())) { event.getPlayer().sendMessage("Please register before playing :)"); event.getPlayer().sendMessage("Use " + ChatColor.AQUA + "\"/register <password>\"" + ChatColor.WHITE + " to register."); event.getPlayer().sendMessage("And use " + ChatColor.AQUA + "\"/login <password>" + ChatColor.WHITE + " to login.\""); } else event.getPlayer().sendMessage("Use " + ChatColor.AQUA + "\"/login <password>" + ChatColor.WHITE + " to login.\""); }
上面的代碼來自是我初學的時候寫的簡易登錄插件。注意:每一個事件處理方法都要在最前面加上 @EventHandler 的注釋。如果你想知道為什么這個方法是這樣定義的,請查詢插件的官方文檔,對於這段代碼,我查到的是PlayerJoinEvent (Paper-API 1.16.5-R0.1-SNAPSHOT API)。只要知道對應的事件名稱,我們就可以編寫相應的方法來處理它。
細心的你也許會發現,我們編寫的方法名 onPlayerJoin 似乎和事件名 PlayerJoinEvent 有關,其實這樣寫是方便理解,當然你想寫 onPlayerFxxkJoin 也是可以的,不過可讀性就降低了,只要你保證唯一的參數是 PlayerJoinEvent 類型的即可。
我們具體到方法內部看,很容易可以知道給玩家發送信息的方法是 sendMessage,但是發送信息前我們需要先知道給誰發送,這里使用了 event.getPlayer() 來獲得這個事件對應的玩家——絕大多數事件都是和玩家有關的,而你就可以通過這個方法來獲得對應的玩家,並且,不僅僅可以做到對其發送信息,他的生殺大權已經掌握在你手里了!比如 event.getPlayer().setHealth(0.0)……
可以看到輸了個 set 就出來這么多方法,大家可以結合官方文檔自行嘗試效果。注意,上面這個方法當然要定義在你的事件偵聽器類里面。
還有一個很重要的功能是彩色文字,這個從上面的代碼里也可以知道,只需要在字符串之間用 ChatColor.XXX 來連接即可實現,每一種 ChatColor 的效果會被應用於其之后的文字:
並且,在左邊還會貼心地顯示出你當前使用的顏色。
再來看一個功能,比如,當玩家未注冊時無法移動,這個在很多服務器里非常常見:
@EventHandler public void onPlayerMove(PlayerMoveEvent event) { if (!player_list.containsKey(event.getPlayer().getUniqueId())) event.setCancelled(true); else if (player_list.get(event.getPlayer().getUniqueId()).if_logined == Boolean.FALSE) event.setCancelled(true); }
使用 event.setCancelled(true) 即可讓玩家移動的事件被“取消”,直觀上來看就是無法移動,或者移動了一點點距離就馬上被拉回原位,注意這里的移動包括視角的變化。
注意,上面這段代碼直接復制粘貼是無法正確運行的,因為里面的 player_list 和 if_logined 都是我自己定義的,如果你想實現類似的效果需要自己編寫額外的代碼。