Skip to content

Latest commit

 

History

History
269 lines (158 loc) · 9.85 KB

2-2.md

File metadata and controls

269 lines (158 loc) · 9.85 KB

2-2 事件系统概述

我们即将进入 Bukkit 中最迷人的部分:事件系统

什么是事件

Minecraft 中的事件有许多:

  • 玩家打开物品栏
  • 玩家加入服务器
  • TNT 爆炸
  • 服务器崩溃
  • ……

Bukkit 为这些事件都进行了分类、封装,我们可以很轻易地使用它们。

可取消事件和不可取消事件

在 Minecraft 的规范中,有些事件是可以被取消掉的,比如:

  • 玩家的破坏、交互、移动等
  • 作物的生长,动物的繁殖,怪物的生成

而有些事件是没法取消的,这些也显而易见:

  • 玩家加入、退出服务器
  • 服务端崩溃
  • 客户端发送来的数据包(可以不处理,但没法不接收)

可以取消指的是可以阻止事件的进一步处理,或者在处理完成后能够恢复到之前的状态。比如玩家的移动,虽然原版 Minecraft 客户端无法阻止玩家移动,但可以通过在服务端将玩家强行送回原来的位置,再通过数据包同步到客户端。

相比之下,玩家要是强行拔掉网线,你怎么也没办法将环境恢复到之前的状态。

这里比较特殊的是玩家进入服务器,按照常规来说这应该可以取消,但是在 Bukkit 的思维模式里,玩家尝试建立连接时就已经接触了服务器,这一步没办法阻止。

事件处理器

事件驱动理论

Minecraft 的运作模式是一堆事件触发器挂着一堆事件处理器,当有事件发生时,对应的处理器就开始执行,Minecraft 由此开始运转。

EVENT.png

有鉴于图床失效、原图丢失,上图系定稿后再行修补,图文未必贴合。

当外部状态发生改变,例如有方块被破坏了,玩家移动了,对应的触发器会被激活,Bukkit 把这个事件分派到注册好的事件处理器中去处理。这就叫事件驱动

我们的插件要想实现我们的功能,就需要改变游戏对事件的处理方式。因此,我们需要创建一个独特的事件处理器,并把它「安插」到 Bukkit 中「搞破坏」(实现我们的功能)。

创建事件处理器

事件处理器是一个对象(面向对象!面向对象!),我们创建一个类来描述它:

public class MyEventProcessor {}

你会问我了,这里是不是要 extends 什么东西啊?

不是,但也差不多。这里我们要使用 implements

这是个啥?

implementsextends 差不多,但它表示实现一个接口

那啥系接口捏?

类描述对象的属性和方法。接口则包含类要实现的方法。

接口是一种协议。

举个例子,想象这么一个场景……

你要订酒店,在火车上你问酒店前台:「你们都有什么服务啊?」

酒店工作人员拿出了一个接口(服务单):

  • 24 小时热水供应
  • 免费早餐
  • 免费 WiFi
  • 免费租借 My Little Pony: Equestria Girls 四部电影的光盘

你看了很开心,说道:「这些都能正常提供吧?」

前台回答:「是的呢亲,不会有问题的~」

交易就这么愉快地完成了。


在 Java 中,接口也扮演着类似的角色。

  • 接口的调用方(这里是 Bukkit)只管调用,不管其内部如何工作
  • 接口的实现方(这里是我们的插件)只管保证准确地完成任务,不管会被拿去做什么

接口就是一纸保证书,在 Java 中协调着各个类之间的数据交换。


回到 implements 中来。implements 表示「我接受任务,交给我吧,我来搞定」,由于我们要创建的是事件处理器,因此自然要接受「处理事件」的任务。

虽然我们要实现一个接口,但我们的事件处理器本身是一个类。

「处理事件」这个接口是 org.bukkit.event.Listener,接口的导入和类一样,都用 import

因此我们的类是这样的:

import org.bukkit.event.Listener;

public class EventProcessor implements Listener {
    
}

Bukkit 在 Listener 这张「协议」上对我们的要求非常宽松:只需要实现我们需要实现的方法就可以了。

也就是说,即使我们什么也不写,以上也是一个合法的事件处理器。

创建事件处理函数

事件处理器中包含许多事件处理函数。每个事件处理函数处理一个事件

一个事件处理函数的签名是这样的:

@EventHandler
public void 任意的函数名(事件类型 e)

这里的 @EventHandler 是 Bukkit 识别事件处理函数的标志(还记得吗,我们说注解相当于关键字),该注解位于 org.bukkit.event.EventHandler,也需要导入。

函数名无所谓,因为 Bukkit 不依靠函数名识别事件处理函数。

所有的事件处理函数都接受一个事件对象作为参数。括号内的 e 只是告诉 Java:「把调用者给我的那个参数,在我这里命名为 e 哦~」,改成别的名字也是一样的。

这里的事件类型,即代表我们要监听的事件。

查找可监听的事件

Bukkit 中有许多许多的事件,那是不是哪个我们都可以监听呢?

不是的,事件处理器只能监听abstract 的类。

这一定义并不严谨,后面我们会明白这些处理器无法被监听的本质。

Bukkit 对可监听事件采取了详细的命名规范:<主语><谓语>Event

例如:

  • PlayerMoveEvent 玩家移动事件
  • InventoryOpenEvent 物品栏被打开事件
  • EntityBreedEvent 实体繁殖事件

等等。

所有的事件都是 org.bukkit.event.Event 类的子类。这么多的事件,要怎么查找它们呢?

这里有个小技巧。

首先前往 org.bukkit.event.Event,你能看到 「Direct Known Subclasses」。

EVENTCLAZZ.png

这里列出了直接子类,单击其中一个,比如 PlayerEvent

PLAYEREVENT.png

如果这个类被 abstract 修饰了(图中所示),那对不起,这个事件无法被监听。请继续通过「Direct Known Subclass」查找下一级吧!这次我们选择 PlayerLoginEvent

LOGINEVENT.png

很好,这个类可以被监听。那么我们就可以将它填在参数中,然后进行处理啦!

事件处理示例

下面我们以阻止玩家移动但允许玩家转向为例,演示事件处理的方法:

先编写好类并 implements Listener

import org.bukkit.event.Listener;

public class EventProcessor implements Listener {
}

一个方法监听一个事件,因此我们创建一个方法,参数中填上 PlayerMoveEvent

import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;

public class EventProcessor implements Listener {
    @EventHandler
    public void dontMove(PlayerMoveEvent e) {
        // 方法名随意
    }
}

然后就该开始我们自己的表演了。

查阅 JavaDocs 可知,PlayerMoveEvent 有两个方法,getFromgetTo

分别调用这两个方法就可以获得两个 Location 对象,再利用 Location 自身的 distance 方法即可计算出距离。

如果距离不是 0,表示玩家走路了,因此调用 CancellablesetCancelled 方法。

这里的 Cancellable 是什么呢?上面我们说到,有些事件是可取消的,有些不行。

可取消的事件实现了 Cancellable,具有 setCancelled 方法(能够完成「取消」这一任务),而没有实现的则没有这个方法(没法取消)。

综上,代码如下:

import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;

public class EventProcessor implements Listener {
    @EventHandler
    public void dontMove(PlayerMoveEvent e) {
        double distance = e.getFrom().distance(e.getTo());
        if (distance != 0) {
            e.setCancelled(true);
        }
    }
}

Minecraft 中用 double(双精度小数)计算距离。

这里出现了新的 if 语句,if 后面有对括号,其中的值如果是 true,就执行 {} 内的语句,否则跳过。

!= 是不等于运算符:Java 比较 != 两边的东西(操作对象),如果操作对象不相等,这个表达式就为 true,否则就为 false

setCancelled 接受一个参数,表示是否取消。

那你又要问了,Bukkit 是怎么实现取消的呢?

每个实现了 Cancellable 的对象中,都有一个标志标识这个事件是否被取消了。

处理函数结束时,Bukkit 看看这个标志,如果是 false,就将事件发往下一个事件处理器,否则就结束处理。

setCancelled 正是用来改变这个标志的方法。

由于插件的事件处理器排在 Mojang 的事件处理器前面,因此我们具有是否取消的优先裁定权。

setCancelled 可被多次调用,但以处理函数结束时的设定为准

注册事件处理器

创建好事件处理器后,我们还需要把它「安插」到 Bukkit 中去,这就是注册(Register)。

Bukkit 已经为我们提供了这样的方法,要注册事件,我们只需要在插件主类(继承了 JavaPlugin 的那个类)的 onEnable 方法中写上:

Bukkit.getPluginManager().registerEvents(new EventProcessor(), this);

this 指代的是当前实例,由于这条语句在插件主类中,因此它代表的就是「插件」。

new EventProcessor() 创建一个事件监听器的实例并交给 Bukkit 处理。这样,我们的事件就进入了服务端,开始发挥作用了。


上面这里列出的只是最简单的事件处理,在后面的章节中我们会再一次见到它,届时我们将讲解自定义事件的方法。