我们即将进入 Bukkit 中最迷人的部分:事件系统。
Minecraft 中的事件有许多:
- 玩家打开物品栏
- 玩家加入服务器
- TNT 爆炸
- 服务器崩溃
- ……
Bukkit 为这些事件都进行了分类、封装,我们可以很轻易地使用它们。
在 Minecraft 的规范中,有些事件是可以被取消掉的,比如:
- 玩家的破坏、交互、移动等
- 作物的生长,动物的繁殖,怪物的生成
而有些事件是没法取消的,这些也显而易见:
- 玩家加入、退出服务器
- 服务端崩溃
- 客户端发送来的数据包(可以不处理,但没法不接收)
可以取消指的是可以阻止事件的进一步处理,或者在处理完成后能够恢复到之前的状态。比如玩家的移动,虽然原版 Minecraft 客户端无法阻止玩家移动,但可以通过在服务端将玩家强行送回原来的位置,再通过数据包同步到客户端。
相比之下,玩家要是强行拔掉网线,你怎么也没办法将环境恢复到之前的状态。
这里比较特殊的是玩家进入服务器,按照常规来说这应该可以取消,但是在 Bukkit 的思维模式里,玩家尝试建立连接时就已经接触了服务器,这一步没办法阻止。
Minecraft 的运作模式是一堆事件触发器挂着一堆事件处理器,当有事件发生时,对应的处理器就开始执行,Minecraft 由此开始运转。
有鉴于图床失效、原图丢失,上图系定稿后再行修补,图文未必贴合。
当外部状态发生改变,例如有方块被破坏了,玩家移动了,对应的触发器会被激活,Bukkit 把这个事件分派到注册好的事件处理器中去处理。这就叫事件驱动。
我们的插件要想实现我们的功能,就需要改变游戏对事件的处理方式。因此,我们需要创建一个独特的事件处理器,并把它「安插」到 Bukkit 中「搞破坏」(实现我们的功能)。
事件处理器是一个对象(面向对象!面向对象!),我们创建一个类来描述它:
public class MyEventProcessor {}
你会问我了,这里是不是要 extends
什么东西啊?
不是,但也差不多。这里我们要使用 implements
。
这是个啥?
implements
和 extends
差不多,但它表示实现一个接口。
那啥系接口捏?
类描述对象的属性和方法。接口则包含类要实现的方法。
接口是一种协议。
举个例子,想象这么一个场景……
你要订酒店,在火车上你问酒店前台:「你们都有什么服务啊?」
酒店工作人员拿出了一个接口(服务单):
- 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」。
这里列出了直接子类,单击其中一个,比如 PlayerEvent
:
如果这个类被 abstract
修饰了(图中所示),那对不起,这个事件无法被监听。请继续通过「Direct Known Subclass」查找下一级吧!这次我们选择 PlayerLoginEvent
。
很好,这个类可以被监听。那么我们就可以将它填在参数中,然后进行处理啦!
下面我们以阻止玩家移动但允许玩家转向为例,演示事件处理的方法:
先编写好类并 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
有两个方法,getFrom
和 getTo
。
分别调用这两个方法就可以获得两个 Location
对象,再利用 Location
自身的 distance
方法即可计算出距离。
如果距离不是 0,表示玩家走路了,因此调用 Cancellable
的 setCancelled
方法。
这里的 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 处理。这样,我们的事件就进入了服务端,开始发挥作用了。
上面这里列出的只是最简单的事件处理,在后面的章节中我们会再一次见到它,届时我们将讲解自定义事件的方法。