# 事件

# 了解事件

# 什么是事件

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

事件分为可控事件和不可控事件. 其最大区别在于能不能取消(也就是能不能setCancelled).
不难理解, 玩家如果退出服务器, 这不能被取消, 它是不可控事件. 玩家的移动可以被取消, 它是可控事件.

# 事件有什么用?

想象自己正在做一款登录插件, 登录插件是怎么制作出来的呢?
本章下方举例将延续这个题设展开.

利用BukkitAPI, 你可以监听事件, 事件触发时执行某些代码.
例如, 你可以监听玩家登录服务器, 玩家登录服务器后你可以执行某些代码.

那么, 如果你想写登录插件, 你需要监听玩家登录服务器的事件.
玩家进入服务器以后, 记录存储起来他的用户名. 等待玩家输入指令进行登录, 登录完毕以后去掉他的用户名.
然后再监听其他的各种事件(比如监听方块破坏事件), 如果这些事件被触发, 判断是哪个玩家触发的, 看看玩家用户名有没有存储起来, 如果有, 那么他没有登录, 那就把这个事件取消掉.

通过这样的例子可以发现, 事件是一个插件最重要的组成部分!

# 监听事件

上面我们提到可以实现事件触发时执行某些代码. 实现这个目的的方法就是写一个监听器.
监听器实质上是一个实现了Listener的类, 其中包含一些带有@EventHandler注解的方法.

我们继续以上面的登录插件作为展开, 写一个“玩家不登录就不允许移动”的插件出来.
因为截止到现在还没有说怎么注册命令, 这里我们设定玩家“只要右键空气就可以登录”.
这里我们为了偷懒, 下面把主类直接实现Listener当做监听器用.

public class HelloWorldPluginMain 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
	public void onPlayerJoin(PlayerJoinEvent e){ //玩家登录服务器就会调用这个方法
		if(!playerNameList.contains(e.getPlayer().getName())) //先判断这个玩家的名是不是记过了
			playerNameList.add(e.getPlayer().getName()); //玩家一登录就给他记上名, 代表他没登录
	}

	/*功能二:没登录的玩家不让移动*/
    @EventHandler //这个注解告诉Bukkit这个方法正在监听某个事件
    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作为名称的结尾.
稍后会详细讲述如何在JavaDoc找到需要的事件.

监听器类里由若干个带@EventHandler注解, 参数仅为一个XXXEvent的方法. 这些事件触发后会触发这些方法, 这就是事件监听的本质.
要特别注意, 监听器中带有@EventHandler的方法一个只能监听某一个事件, 而不能监听多个事件! 换而言之, 这也就意味着, 你不能填写两个参数, 实现一个方法同时监听两个事件的目的!

这里我们用到了玩家交互事件. 这个事件抽象不易理解.
确切的来说, PlayerInteractEvent指的是玩家与方块交互, 交互指的是左右键方块的几乎一切操作. 具体的解释完全可以在JavaDoc中了解到.
如果你曾经用过领地插件Residence, 你肯定对某个领地的权限use印象很深, 这个use权限与PlayerInteractEvent事件差不多, 可以近似认为Residence插件的use权限就是通过监听PlayerInteractEvent写出来的.

要注意, 监听器必须要注册才能算生效!
我们的监听器里的方法都能监听到对应的事件的原因是, 在onEnable方法中, 我们写了这样的代码:

Bukkit.getPluginManager().registerEvents(this,this); //这行代码注册了HelloWorld类为监听器, 如果没有这行代码, 下面所有带@EventHandler注解的方法都不会在事件触发时被调用!

registerEvents方法的第一个参数是监听器,第二个参数是插件主类的实例. 在这里主类就是监听器. 具体你可以在后面了解到.

# 客户端与服务端的关系

如果你实际去使用上面的那个代码, 你可能会发现一个问题: 玩家移动在游戏里还可以移动, 但是一会儿会被服务器"弹回来".
这样确实是达到了取消玩家移动的目的, 但是, 为什么最终的效果不是"玩家一点都动不了"呢?

事实上, 我们无法在服务端取消玩家一点也不能移动.
客户端移动玩家时, 会在客户端显示出移动后的样子, 然后才会传递给服务器玩家移动的信号, 服务端收到客户端的信号后, 服务器才会触发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接口, 因此它们无法被取消.
就像玩家退出服务器, 你总不能像刀剑神域一样, 不让玩家退出服务器吧.

值得注意的是, 如果玩家并没有改变他的X/Y/Z, 而只是利用鼠标转了一下身, 这也属于玩家移动, 仍会触发PlayerMoveEvent事件.

# 找到我们要找的事件

我们了解了如何监听事件, 那么我们想做到“不让玩家破坏方块”这个功能, 应该怎么做?
思考后可以发现, 我们需要监听“方块被破坏”这个事件!那破坏方块后触发什么事件? 你需要在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注解方法一个个监听的?
答案是, 的确如此. 你要想写登录插件, 你就应该去监听许许多多事件, 累也没办法, 就得这样写.