PaperMC 插件开发完全指南 / 第 5 章:事件系统
第 5 章:事件系统
事件系统是 Bukkit 插件开发的核心,理解事件监听、优先级和自定义事件是必备技能。
5.1 事件系统概述
Bukkit 采用**观察者模式(Observer Pattern)**实现事件系统。插件通过注册监听器(Listener)来监听游戏中的各种事件。
核心概念
| 概念 | 说明 |
|---|---|
| Event | 事件对象,封装了事件的所有信息 |
| Listener | 监听器,包含一个或多个事件处理方法 |
| @EventHandler | 注解,标记一个方法为事件处理器 |
| EventPriority | 事件优先级,决定处理顺序 |
| Cancellable | 可取消接口,允许阻止事件的默认行为 |
基本监听器示例
package com.example.myplugin.listeners;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
public class PlayerJoinListener implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
player.sendMessage("§a欢迎来到服务器!");
// 修改加入消息
event.joinMessage(Component.text(
"§e" + player.getName() + " 加入了游戏!",
NamedTextColor.YELLOW
));
}
}
注册监听器
// 在 onEnable() 中
getServer().getPluginManager().registerEvents(
new PlayerJoinListener(), this
);
5.2 事件优先级(EventPriority)
事件优先级决定了多个监听器处理同一事件的顺序。
优先级从高到低
| 优先级 | 含义 | 执行时机 | 典型用途 |
|---|---|---|---|
LOWEST | 最早处理 | 默认行为之前 | 记录原始状态 |
LOW | 较早处理 | 默认行为之前 | 修改事件参数 |
NORMAL | 正常处理 | 默认行为之前 | 普通事件处理 |
HIGH | 较晚处理 | 默认行为之前 | 覆盖其他插件的行为 |
HIGHEST | 最晚处理 | 默认行为之前 | 最终检查 |
MONITOR | 仅监控 | 默认行为之后 | 日志记录、统计 |
设置优先级
@EventHandler(priority = EventPriority.HIGH)
public void onBlockBreak(BlockBreakEvent event) {
// 高优先级处理,早于 NORMAL
if (isProtected(event.getBlock().getLocation())) {
event.setCancelled(true);
}
}
优先级执行流程
事件触发
│
├── LOWEST 处理器 → LOW 处理器 → NORMAL 处理器
│ → HIGH 处理器 → HIGHEST 处理器
│
├── [服务端执行默认行为(如果未被取消)]
│
└── MONITOR 处理器(仅观察结果,不应修改事件)
注意:
MONITOR优先级的处理器不应修改事件,只能用于日志记录或统计。因为默认行为已经执行了。
5.3 取消事件(Cancellable)
实现了 Cancellable 接口的事件可以被取消,阻止默认行为的发生。
取消事件示例
@EventHandler
public void onBlockBreak(BlockBreakEvent event) {
Player player = event.getPlayer();
// 检查是否在保护区
if (isInProtectedRegion(player.getLocation())) {
event.setCancelled(true); // 取消事件,方块不会被破坏
player.sendMessage("§c你不能在这里破坏方块!");
}
}
常见的可取消事件
| 事件 | 说明 | 取消效果 |
|---|---|---|
BlockBreakEvent | 破坏方块 | 方块保留 |
BlockPlaceEvent | 放置方块 | 方块不放置 |
PlayerInteractEvent | 玩家交互 | 交互不生效 |
EntityDamageEvent | 实体受伤 | 不扣血 |
PlayerMoveEvent | 玩家移动 | 回弹到原位 |
InventoryClickEvent | 点击物品栏 | 操作被阻止 |
PlayerChatEvent | 玩家聊天 | 消息不发送 |
注意事项
// 错误:在 MONITOR 中取消事件!
@EventHandler(priority = EventPriority.MONITOR)
public void onMonitor(BlockBreakEvent event) {
event.setCancelled(true); // 这是不好的实践!
}
// 正确:在 MONITOR 中只读取状态
@EventHandler(priority = EventPriority.MONITOR)
public void onMonitor(BlockBreakEvent event) {
if (event.isCancelled()) {
// 记录被取消的事件(日志用途)
getLogger().info("破坏事件被取消");
}
}
5.4 常用事件分类
玩家事件
public class PlayerEvents implements Listener {
@EventHandler
public void onJoin(PlayerJoinEvent event) {
// 玩家加入服务器
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
// 玩家离开服务器
}
@EventHandler
public void onDeath(PlayerDeathEvent event) {
// 玩家死亡
Player player = event.getEntity();
event.deathMessage(Component.text(
player.getName() + " 去世了!"
));
// 设置掉落经验
event.setDroppedExp(100);
}
@EventHandler
public void onRespawn(PlayerRespawnEvent event) {
// 玩家重生
}
@EventHandler
public void onChat(AsyncPlayerChatEvent event) {
// 玩家聊天(注意:这是异步事件)
Player player = event.getPlayer();
event.setFormat("§7[%s§7] %s");
}
@EventHandler
public void onCommand(PlayerCommandPreprocessEvent event) {
// 命令预处理(在命令执行前触发)
String cmd = event.getMessage();
if (cmd.startsWith("/plugins") || cmd.startsWith("/pl")) {
if (!event.getPlayer().hasPermission("bukkit.command.plugins")) {
event.setCancelled(true);
event.getPlayer().sendMessage("§c未知命令!");
}
}
}
}
方块事件
public class BlockEvents implements Listener {
@EventHandler
public void onBlockBreak(BlockBreakEvent event) {
Player player = event.getPlayer();
Block block = event.getBlock();
// 获取方块类型
Material type = block.getType();
// 取消时掉落物品
if (event.isCancelled()) return;
// 自定义掉落逻辑
if (type == Material.DIAMOND_ORE) {
event.setDropItems(false); // 取消默认掉落
block.getWorld().dropItemNaturally(
block.getLocation(),
new ItemStack(Material.DIAMOND, 2) // 双倍掉落
);
}
}
@EventHandler
public void onBlockPlace(BlockPlaceEvent event) {
// 检查放置的方块
Block block = event.getBlock();
if (block.getType() == Material.TNT) {
event.getPlayer().sendMessage("§c不允许放置 TNT!");
event.setCancelled(true);
}
}
@EventHandler
public void onPistonExtend(BlockPistonExtendEvent event) {
// 活塞推出事件
}
}
实体事件
public class EntityEvents implements Listener {
@EventHandler
public void onEntityDamage(EntityDamageEvent event) {
Entity entity = event.getEntity();
// 检查伤害原因
DamageCause cause = event.getCause();
if (cause == DamageCause.FALL) {
// 减少摔落伤害
event.setDamage(event.getDamage() * 0.5);
}
}
@EventHandler
public void onEntityDamageByEntity(EntityDamageByEntityEvent event) {
Entity damager = event.getDamager();
Entity victim = event.getEntity();
// PVP 保护
if (damager instanceof Player attacker
&& victim instanceof Player target) {
if (isInSafeZone(target.getLocation())) {
event.setCancelled(true);
attacker.sendMessage("§c安全区内禁止 PVP!");
}
}
}
@EventHandler
public void onCreatureSpawn(CreatureSpawnEvent event) {
// 生物生成事件
if (event.getSpawnReason() == CreatureSpawnEvent.SpawnReason.SPAWNER) {
// 限制刷怪笼生成的数量
// ...
}
}
}
物品/背包事件
public class InventoryEvents implements Listener {
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
// 检查是否是自定义 GUI
if (event.getView().getTitle().equals("§6我的菜单")) {
event.setCancelled(true); // 阻止移动物品
int slot = event.getRawSlot();
if (slot == 11) {
// 点击了第 11 格
Player player = (Player) event.getWhoClicked();
player.sendMessage("§a你点击了按钮!");
player.closeInventory();
}
}
}
@EventHandler
public void onItemPickup(EntityPickupItemEvent event) {
if (event.getEntity() instanceof Player player) {
Item item = event.getItem();
// 自定义拾取逻辑
}
}
@EventHandler
public void onCraftItem(CraftItemEvent event) {
// 合成物品事件
}
}
5.5 Paper 原生异步事件
Paper 提供了一些原生异步事件,不会阻塞主线程。
异步聊天事件
import io.papermc.paper.event.player.AsyncChatEvent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
public class AsyncChatListener implements Listener {
@EventHandler
public void onAsyncChat(AsyncChatEvent event) {
Player player = event.getPlayer();
Component message = event.message();
// 获取纯文本内容
String plainText = PlainTextComponentSerializer.plainText()
.serialize(message);
// 敏感词过滤(可以在异步线程安全执行)
if (containsBannedWord(plainText)) {
event.setCancelled(true);
player.sendMessage(Component.text("§c消息包含违禁词!"));
return;
}
// 修改消息格式
event.renderer((source, sourceDisplayName, msg, viewer) ->
Component.text()
.append(Component.text("[VIP] ", NamedTextColor.GOLD))
.append(sourceDisplayName)
.append(Component.text(": ", NamedTextColor.WHITE))
.append(msg)
.build()
);
}
private boolean containsBannedWord(String text) {
// 敏感词检查逻辑
return false;
}
}
异步登录事件
import com.destroystokyo.paper.event.profile.ProfileWhitelistVerifyEvent;
public class LoginEvents implements Listener {
@EventHandler
public void onPreLogin(AsyncPlayerPreLoginEvent event) {
// 异步预登录检查(白名单、Ban 等)
UUID uuid = event.getUniqueId();
if (isBanned(uuid)) {
event.disallow(
AsyncPlayerPreLoginEvent.Result.KICK_BANNED,
Component.text("§c你已被封禁!")
);
}
}
}
5.6 自定义事件
当内置事件无法满足需求时,可以创建自定义事件。
步骤一:定义事件类
package com.example.myplugin.events;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
/**
* 自定义金币交易事件
*/
public class CoinTransactionEvent extends Event implements Cancellable {
private static final HandlerList HANDLERS = new HandlerList();
private final Player player;
private final double amount;
private final TransactionType type;
private String reason;
private boolean cancelled;
public enum TransactionType {
EARN, // 赚取
SPEND, // 消费
TRANSFER // 转账
}
/**
* @param player 玩家
* @param amount 金额(正数)
* @param type 交易类型
* @param reason 交易原因
*/
public CoinTransactionEvent(Player player, double amount,
TransactionType type, String reason) {
this.player = player;
this.amount = amount;
this.type = type;
this.reason = reason;
}
public Player getPlayer() { return player; }
public double getAmount() { return amount; }
public TransactionType getType() { return type; }
public String getReason() { return reason; }
public void setReason(String reason) { this.reason = reason; }
@Override
public boolean isCancelled() { return cancelled; }
@Override
public void setCancelled(boolean cancel) { this.cancelled = cancel; }
@Override
public @NotNull HandlerList getHandlers() { return HANDLERS; }
public static @NotNull HandlerList getHandlerList() { return HANDLERS; }
}
步骤二:触发自定义事件
public class EconomyManager {
/**
* 尝试扣除玩家金币
* @return 是否成功
*/
public boolean deductCoins(Player player, double amount, String reason) {
// 创建并触发事件
CoinTransactionEvent event = new CoinTransactionEvent(
player, amount,
CoinTransactionEvent.TransactionType.SPEND,
reason
);
Bukkit.getPluginManager().callEvent(event);
// 检查事件是否被取消
if (event.isCancelled()) {
return false;
}
// 执行实际扣款
double balance = getBalance(player);
if (balance < amount) {
return false;
}
setBalance(player, balance - amount);
return true;
}
}
步骤三:监听自定义事件
public class CoinEventListener implements Listener {
@EventHandler(priority = EventPriority.MONITOR)
public void onCoinTransaction(CoinTransactionEvent event) {
if (event.isCancelled()) return;
// 记录交易日志
logTransaction(
event.getPlayer().getName(),
event.getType(),
event.getAmount(),
event.getReason()
);
}
@EventHandler(priority = EventPriority.HIGH)
public void onCoinEarn(CoinTransactionEvent event) {
if (event.getType() != CoinTransactionEvent.TransactionType.EARN) return;
// VIP 双倍收益
if (event.getPlayer().hasPermission("myplugin.vip")) {
// 修改金额(通过新事件)
event.setCancelled(true);
// 用新的金额重新触发
CoinTransactionEvent newEvent = new CoinTransactionEvent(
event.getPlayer(),
event.getAmount() * 2,
event.getType(),
event.getReason() + " (VIP 双倍)"
);
Bukkit.getPluginManager().callEvent(newEvent);
}
}
}
异步自定义事件
如果事件可能在异步线程触发,需要继承 Event 并传入 true:
public class AsyncDataLoadEvent extends Event {
private final UUID playerId;
private final Map<String, Object> data;
public AsyncDataLoadEvent(UUID playerId, Map<String, Object> data) {
super(true); // 关键:标记为异步事件
this.playerId = playerId;
this.data = data;
}
// ... getter 方法
}
注意: 异步事件的处理器中不能调用同步的 Bukkit API(如操作方块、实体等)。
5.7 事件工具类
批量注册监听器
public final class EventUtils {
/**
* 批量注册监听器
*/
public static void registerAll(JavaPlugin plugin, Listener... listeners) {
PluginManager pm = plugin.getServer().getPluginManager();
for (Listener listener : listeners) {
pm.registerEvents(listener, plugin);
}
}
/**
* 通过包扫描自动注册
*/
public static void registerFromPackage(JavaPlugin plugin, String packageName) {
// 使用 Reflections 库扫描
Reflections reflections = new Reflections(packageName);
Set<Class<? extends Listener>> classes = reflections.getSubTypesOf(Listener.class);
for (Class<? extends Listener> clazz : classes) {
try {
Listener listener = clazz.getDeclaredConstructor().newInstance();
plugin.getServer().getPluginManager().registerEvents(listener, plugin);
} catch (Exception e) {
plugin.getLogger().warning("无法注册监听器: " + clazz.getName());
}
}
}
}
在 onEnable() 中使用
@Override
public void onEnable() {
EventUtils.registerAll(this,
new PlayerJoinListener(),
new BlockListener(),
new InventoryListener(),
new EntityListener()
);
}
5.8 业务场景:战斗系统事件设计
/**
* 战斗系统监听器
*/
public class CombatListener implements Listener {
private final Map<UUID, Long> combatTag = new HashMap<>(); // 战斗标记
private final Set<UUID> pvpDisabled = new HashSet<>(); // PVP 关闭
@EventHandler(priority = EventPriority.HIGH)
public void onPVP(EntityDamageByEntityEvent event) {
if (!(event.getDamager() instanceof Player attacker)) return;
if (!(event.getEntity() instanceof Player victim)) return;
// PVP 开关检查
if (pvpDisabled.contains(victim.getUniqueId())) {
event.setCancelled(true);
attacker.sendMessage("§c该玩家已关闭 PVP!");
return;
}
// 标记双方进入战斗状态
long now = System.currentTimeMillis();
combatTag.put(attacker.getUniqueId(), now);
combatTag.put(victim.getUniqueId(), now);
// 通知
attacker.sendMessage("§c你已进入战斗状态!");
victim.sendMessage("§c你已进入战斗状态!");
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
UUID uuid = event.getPlayer().getUniqueId();
if (isInCombat(uuid)) {
// 战斗中退出,给予惩罚
Player player = event.getPlayer();
player.setHealth(0); // 击杀
Bukkit.broadcastMessage("§c" + player.getName()
+ " 在战斗中退出游戏!");
}
}
@EventHandler
public void onCommand(PlayerCommandPreprocessEvent event) {
if (isInCombat(event.getPlayer().getUniqueId())) {
String cmd = event.getMessage().split(" ")[0].toLowerCase();
List<String> blockedCmds = List.of("/spawn", "/tp", "/home", "/warp");
if (blockedCmds.contains(cmd)) {
event.setCancelled(true);
event.getPlayer().sendMessage("§c战斗中不能使用此命令!");
}
}
}
public boolean isInCombat(UUID uuid) {
Long lastCombat = combatTag.get(uuid);
if (lastCombat == null) return false;
return System.currentTimeMillis() - lastCombat < 15_000; // 15 秒
}
}
5.9 性能注意事项
| 问题 | 说明 | 解决方案 |
|---|---|---|
PlayerMoveEvent 频率极高 | 每次移动都触发 | 只监听 FROM/TO 不同区块时 |
BlockPhysicsEvent 量大 | 物理更新频繁 | 快速判断并 return |
| 监听器中有同步 I/O | 阻塞主线程 | 移到异步线程 |
| 不必要的监听器 | 空方法也会有开销 | 条件性注册监听器 |
优化示例
// 不好:每次移动都处理
@EventHandler
public void onMove(PlayerMoveEvent event) {
// 处理逻辑...
}
// 好:只在跨越区块时处理
@EventHandler(ignoreCancelled = true)
public void onMove(PlayerMoveEvent event) {
if (event.getFrom().getBlockX() == event.getTo().getBlockX()
&& event.getFrom().getBlockZ() == event.getTo().getBlockZ()) {
return; // 同一区块,跳过
}
// 跨区块时才处理...
}
5.10 常见问题排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 监听器不触发 | 未注册 | 检查 registerEvents 调用 |
| 事件无法取消 | 未实现 Cancellable | 检查事件是否支持取消 |
| 异步事件中报错 | 调用了同步 API | 使用 runTask() 回到主线程 |
| 优先级不生效 | 多个插件冲突 | 确认优先级设置正确 |
HandlerList 缺失 | 自定义事件忘记声明 | 必须声明 getHandlers() 和 getHandlerList() |
5.11 扩展阅读
5.12 本章小结
| 要点 | 内容 |
|---|---|
| 事件模型 | 观察者模式,Listener + @EventHandler |
| 优先级 | LOWEST → LOW → NORMAL → HIGH → HIGHEST → MONITOR |
| 取消事件 | 实现 Cancellable 的事件可以被取消 |
| 自定义事件 | 继承 Event,实现 HandlerList |
| 异步事件 | Paper 提供了 AsyncChatEvent 等原生异步事件 |
| 性能 | 避免在高频事件中做耗时操作 |
下一章: 第 6 章:物品 API — 掌握物品创建、自定义物品、NBT 标签和模型数据。