强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 标签和模型数据。