# 事件的监听
事件是服务器里发生的事.
例如, 天气的变化, 玩家的移动. 玩家把树打掉, 又捡起了掉落地上的原木. 这些都是事件.
事件分为可控事件和不可控事件. 其最大区别在于能不能取消(也就是能不能setCancelled).
不难理解, 玩家如果退出服务器, 这不能被取消, 它是不可控事件. 玩家的移动可以被取消, 它是可控事件.
BukkitAPI给了一些基本的服务器事件. 大多数情况下可以满足我们的需求.
本章以监听这些事件为例, 讲述事件的监听如何实现.
# 监听器(Listener)
监听器实质上是一个实现了Listener的类, 其中包含一些带有@EventHandler注解的方法.
当服务器某个事件触发后, 例如玩家移动事件, 服务器就会创建一个对应的PlayerMoveEvent对象, 如果你的插件有注册并正在监听该事件的监听器, 那么服务端会按照@EventHandler注解找到对应的方法并调用, 你的插件因而便可监听到玩家移动事件了.
我们以一个登录插件作为展开, 写一个“玩家不登录就不允许移动”的插件出来.
因为截止到现在还没有说怎么注册命令, 这里我们设定玩家“只要右键空气就可以登录”.
这里我们为了偷懒, 下面把主类直接实现Listener当做监听器用. 其实可以分开
public class HelloWorld extends JavaPlugin implements Listener {
private List<String> playerNameList = new ArrayList<String>(); //这是没登录玩家列表
public void onEnable() {
this.getLogger().info("Hello World!");
Bukkit.getPluginManager().registerEvents(this,this); //这里HelloWorld类是监听器, 将当前HelloWorld对象注册监听器
}
public void onDisable() {}
/* 功能一:刚进入服务器的玩家都记录到“小本本”playerNameList上,他们是没登录的玩家 */
@EventHandler // 这个注解告诉Bukkit这个方法正在监听某个事件
public void onPlayerJoin(PlayerJoinEvent e) { // 玩家登录服务器就会调用这个方法
if(!playerNameList.contains(e.getPlayer().getName())) { // 先判断这个玩家的名是不是记过了
playerNameList.add(e.getPlayer().getName()); // 玩家一登录就给他记上名, 代表他没登录
}
}
/* 功能二:没登录的玩家不让移动 */
@EventHandler
public void onPlayerMove(PlayerMoveEvent e) { //玩家移动时Bukkit就会调用这个方法
if(playerNameList.contains(e.getPlayer().getName())) {
e.setCancelled(true); //判断玩家是不是没登录, 是则取消事件
}
}
/* 功能三:右击空气登录(本质就是从playerNameList把他删了) */
@EventHandler
public void onPlayerInteract(PlayerInteractEvent e) { // 玩家交互时会调用这个方法(这个下面会解释)
if(e.getAction()==Action.RIGHT_CLICK_AIR) { // 判断是不是右键空气
playerNameList.remove(e.getPlayerName());
}
}
}
从上面的代码我们可以看出每一个事件都对应着一个XXXEvent对象. 事件类都以Event作为名称的结尾.
监听器类里由若干个带@EventHandler注解, 参数仅为一个XXXEvent的方法. 这些事件触发后会触发这些方法, 这就是事件监听的本质.
要特别注意, 监听器中带有@EventHandler的方法一个只能监听某一个事件, 而不能监听多个事件! 换而言之, 这也就意味着, 你不能填写两个参数, 实现一个方法同时监听两个事件的目的!
这里我们用到了玩家交互事件. 这个事件抽象不易理解.
确切的来说, PlayerInteractEvent指的是玩家与方块交互, 交互指的是左右键方块的几乎一切操作. 具体的解释完全可以在JavaDoc中了解到.
如果你曾经用过领地插件Residence, 你肯定对某个领地的权限use印象很深, 这个use权限与PlayerInteractEvent事件差不多, 可以近似认为Residence插件的use权限就是通过监听PlayerInteractEvent写出来的.
要注意, 监听器必须要注册才能算生效!
我们的监听器里的方法都能监听到对应的事件的原因是, 在onEnable方法中, 我们写了这样的代码:
Bukkit.getPluginManager().registerEvents(this,this); //这行代码注册了HelloWorld类为监听器, 如果没有这行代码, 下面所有带@EventHandler注解的方法都不会在事件触发时被调用!
registerEvents方法的第一个参数是监听器,第二个参数是插件主类的实例. 在这里主类就是监听器. 具体你可以在后面了解到.
# 理解客户端与服务端的关系
如果你实际去使用上面的那个代码, 你可能会发现一个问题: 玩家移动在游戏里还可以移动, 但是一会儿会被服务器"弹回来".
这样确实是达到了取消玩家移动的目的, 但是, 为什么最终的效果不是"玩家一点都动不了"呢?
事实上, 我们无法在服务端取消玩家一点也不能移动.
客户端移动玩家时, 会在客户端显示出移动后的样子, 然后才会传递给服务器玩家移动的信号, 服务端收到客户端的信号后, 服务器才会触发PlayerMoveEvent事件, 做出响应.
也就是说, 客户端与服务端之间, 客户端往往都是"先斩后奏"的. 客户端不管你服务端取不取消, 先那么显示出来再说.
值得注意的是, 如果玩家并没有改变他的X/Y/Z, 而只是利用鼠标转了一下身, 这也属于玩家移动, 仍会触发PlayerMoveEvent事件.
如果要是真的想实现让玩家在服务器的某个坐标一点也动不了, 也许需要发挥你的聪明才智了. 让玩家卡在一个透明方块里? 也许有更好的方案? 现在有人已经实现了!
目前我们通常利用设置玩家移动速度的方法来让玩家无法移动!
# 查询我们想了解的事件
# 事件是怎么取名的
你可以发现, 玩家移动PlayerMoveEvent、玩家进入服务器PlayerJoinEvent事件都有明显的特征.
- 功能决定名称, 看了名称你就能大致明白它的功能.
- 都以
Event作为结尾. 这也就说BukkitAPI中所有名字最后是Event的类都是事件类. - 开头的第一个词决定作用范围. 例如上面两个类开头都是
Player, 这两个类都是与玩家有关的事件类.
所有的事件类都在org.bukkit.event包或其子包里.
# 可取消事件与不可取消事件怎么判断
例如PlayerMoveEvent在JavaDoc中, 我们可以注意到这些内容:
public class PlayerMoveEvent
extends PlayerEvent
implements Cancellable
PlayerMoveEvent事件实现了Cancellable接口.
Cancellable中定义了setCancelled方法和isCancelled方法.
通过setCancelled方法, 你可以在事件触发时设置是否取消该事件. 例如, 如果监听玩家移动, 事件触发时使用setCancelled方法, 可以取消玩家移动.
isCancelled方法可以判断该事件是否被取消.
对于不可取消事件, 它们没有实现Cancellable接口, 因此它们无法被取消.
就像玩家退出服务器, 你总不能像刀剑神域一样, 不让玩家退出服务器吧.
如果你真的想这么做,你或许可以考虑用MOD去阻止玩家关闭进程. 因为链接到服务器是客户端主动发起的.
# 找到我们要找的事件
我们了解了如何监听事件, 那么我们想做到“不让玩家破坏方块”这个功能, 应该怎么做?
思考后可以发现, 我们需要监听“方块被破坏”这个事件!那破坏方块后触发什么事件? 你需要在JavaDoc中找才能找到!
分析: 破坏方块这个事件是一个与方块有关的事件. 打开JavaDoc你可以发现BlockXXXXEvent这类的类有许多.
你也许会说, 玩家破坏方块为什么不是一个与玩家有关的事件呢?很有道理!你也可以在玩家事件中找找看有没有这样的事件.
JavaDoc左侧上方是所有的包, 点击org.bukkit.event.block就能在左侧下方看所有与方块有关的事件了.
你可以轻松地发现, 在前几个的位置迅速就能看到BlockBreakEvent, 根据名字就能判断出, 这就是你想找的方块破坏事件, 打开后看到描述为Called when a block is broken by a player., 很明显, 监听它就对了.
@EventHandler
public void onBlockBreak(BlockBreakEvent e) {
e.setCancelled(true);
}
这样我们就写出了想要的功能.
# 并不是所有的事件都能监听.
在查阅JavaDoc时你可能发现PlayerEvent、BlockEvent这种事件.这些都是不可以被监听的事件.
你不可以通过监听PlayerEvent事件来达到一次性监听所有与玩家有关的事件的目的.
它们不能被监听的原因是没有做HandlerList. 在这里不多说明, 后面讲述如何自己做一个自定义事件时你会明白.
一般来说,如果事件名由两个词构成(例如PlayerEvent)都不能监听, 大多数事件都可以监听.
你可能好奇, 常见的登录插件都是把所有需要的玩家事件都写了@EventHandler注解方法一个个监听的?
答案是, 的确如此. 你要想写登录插件, 你就应该去监听许许多多事件, 累也没办法, 就得这样写.
# EventHandler注解的参数
##监听优先级
想象一下, 如果有两个插件, 他们同时监听玩家移动. 其中一个插件判断后发现玩家没有充够450块钱, 于是它取消了这名玩家的移动. 但是另外一个插件判断后发现玩家非常帅, 于是它允许了这名玩家的移动.
那么就会存在问题: 有一个插件setCancelled(true), 而又有插件setCancelled(false). 应该以谁为准?
那就要看监听优先级了!
下面是两个插件处理PlayerMoveEvent的部分:
A插件:
// A插件
@EventHandler(priority=EventPriority.LOWEST)
public void onPlayerMove(PlayerMoveEvent e) {
System.out.println("testA");
e.setCancelled(true);
}
B插件:
// B插件
@EventHandler(priority=EventPriority.HIGHEST)
public void onPlayerMove(PlayerMoveEvent e){
System.out.println("testB");
e.setCancelled(false);
}
在实际的运行中, 当玩家移动时你会发现, 控制台中先输出了testA后输出了testB, 玩家都在服务器内可以自如移动.
这意味着A插件第一个响应了玩家移动, 然后B插件才相应的玩家移动.
@EventHandler注解有一个成员叫做priority, 给他设置对应的EventPriority, 即可设置监听优先级. 在上面的例子中, Bukkit会在所有的LOWEST级监听被调用完毕后, 再去调用HIGHEST级监听.
EventPriority提供了五种优先级, 按照被调用顺序,为:
LOWEST < LOW < NORMAL(如果你不设置, 默认就是它) < HIGH < HIGHEST < MONITOR .
其中, LOWEST最先被调用, 但对事件的影响最小. MONITOR最后被调用, 对事件的影响最大.
# ignoreCancelled
@EventHandler注解除了priority之外, 还有ignoreCancelled. 如果不设置, 它默认为false.
让我们回到上面的A插件与B插件的例子中. 我们把B插件的onPlayerMove改成这样:
// B插件
@EventHandler(priority=EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerMove(PlayerMoveEvent e) {
System.out.println("testB");
e.setCancelled(false);
}
可以发现, 后台只输出了testA, 玩家无法在服务器中移动. 这说明B插件的onPlayerMove没有被触发.
如果有其他监听已经取消了该事件, 设置ignoreCancelled为true将可以忽略掉这个事件, 所以B插件的onPlayerMove方法没有被触发.
# 监听器的注册
可能你已经发现了, 在之前的代码中, 我们都会在onEnable方法中插入这样的语句:
Bukkit.getPluginManager().registerEvents(this,this);
当时解释的是, registerEvents方法注册了该监听器.
如果没有这样的注册语句, 那么Bukkit就不会在事件触发时调用监听器类的对应方法.
该方法的第一个参数是监听器, 第二个参数是插件主类的实例. 当时由于我们为了偷懒, 直接把主类实现了Listener作为监听器, 因此我们可以这样写.
可我们不能写插件的时候把代码都堆在主类中. 这也就意味着, 我们可以把其他类实现Listener, 用同样的方式注册它, 这样我们就可以把监听事件部分的代码放在别的地方, 使插件代码更有条理性.
我们新创建一个类, 让它实现Listener, 再写对应的方法监听玩家移动, 就像这样:
public class DemoListener implements Listener {
@EventHandler
public void onPlayerMove(PlayerMoveEvent e) {
System.out.println("PLAYER MOVE!");
}
}
现在我们在主类的onEnable方法里, 就可以注册它了!
Bukkit.getPluginManager().registerEvents(new DemoListener(), this);
# 常用事件简介
这里可能罗列不会全面, 在我想到哪些“坑事件”后会列在这里.
# 登录、进入服务器
BukkitAPI中与登录有关的常见的有: PlayerLoginEvent PlayerJoinEvent.
值得注意的是, 所有玩家进入服务器的事件都是不可取消事件.
在玩家尝试连接服务器时, 会触发PlayerLoginEvent, 玩家完全地进入服务器后, 会触发PlayerJoinEvent.
在PlayerLoginEvent触发的时候, 你不可以操控玩家Player对象获取其背包等信息, 而仅可以获取UUID、玩家名和网络信息(IP等)等.
*顺便一提, 玩家如果不在线, 你不可以通过BukkitAPI操控其背包. *
PlayerJoinEvent触发时, 服务器内将会出现玩家实体. 此时你可以当做玩家完全进入服务器, 对其自由操作.
打个比方, 你家有一扇防盗门, 有人想进入你家.
首先他需要敲门, 在门外喊出自己的基本信息(名字等), 这是PlayerLoginEvent触发的时候. 如果你想从他背包里拿出东西, 不可以, 因为他在门外面.
当你给他打开门, 他进了你家中站稳了以后, 这是PlayerJoinEvent触发的时候, 这时候不管你是想打他还是想拿走他的东西, 都可以.
# 玩家移动
在上面我们已经提及过, 玩家移动是“先斩后奏”被触发的. 具体请见上文.
# 玩家打开背包
也许你会看到InventoryOpenEvent. 根据描述你大概明白, 类似右击箱子后出现的那种带格子的界面被打开可以被监听.
但是有一件事很重要: 玩家按E打开背包是没办法被监听的.
一般如果要实现禁止玩家打开背包, 其实最常规的做法就是开一个BukkitRunnable, 定时调用p.closeInventory()关闭玩家正在打开的背包实现的.
这里不详细讲述具体如何操作, 感兴趣可以在Github翻阅优秀插件的源码进行学习.
后面会讲述Runnable, 也许看后你会明白如何操作.
入门
30分钟