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

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