# 事件的监听

事件是服务器里发生的事.
例如, 天气的变化, 玩家的移动. 玩家把树打掉, 又捡起了掉落地上的原木. 这些都是事件.

事件分为可控事件和不可控事件. 其最大区别在于能不能取消(也就是能不能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事件都有明显的特征.

  1. 功能决定名称, 看了名称你就能大致明白它的功能.
  2. 都以Event作为结尾. 这也就说BukkitAPI中所有名字最后是Event的类都是事件类.
  3. 开头的第一个词决定作用范围. 例如上面两个类开头都是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时你可能发现PlayerEventBlockEvent这种事件.这些都是不可以被监听的事件.
你不可以通过监听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没有被触发.
如果有其他监听已经取消了该事件, 设置ignoreCancelledtrue将可以忽略掉这个事件, 所以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分钟

监听器(Listener)

理解客户端与服务端的关系

查询我们想了解的事件

事件是怎么取名的

可取消事件与不可取消事件怎么判断

找到我们要找的事件

并不是所有的事件都能监听.

EventHandler注解的参数

ignoreCancelled

监听器的注册

常用事件简介

登录、进入服务器

玩家移动

玩家打开背包