← Java 基础教程

第 33 / 40 课 · 免费教程

方块事件侦探

读懂事件,建立边界清楚的开采规则

100–120 分钟环境:JDK 21、普通 Paper 1.21.11、同版本客户端。服务器插件由 Paper 加载;只有明确标注的规则测试使用 main。Paper 插件实践

本课作品

出生点保护与矿工执照

事件是一次动作的线索包。先认清世界、位置和模式,再决定是否取消;用同一份数字规则画清出生点保护范围,区分破坏许可与真正拾取。 前置背包:监听器、条件判断、方法与权限。新项目BlockAcademyL33继续使用Java21、Paper1.21.11和第31课构建脚本。

开始前:建议先完成第 31–32 课

01 / 15

开始前:先认清运行位置

前30课打好了普通 Java 程序的基础。这一阶段把代码接到 Paper 服务器:Minecraft 客户端负责进入世界,服务器加载插件,事件和命令把玩家动作交给你的方法。

本书固定使用 JDK 21 与 Paper 1.21.11,客户端版本也保持一致。下载页面上的最新版本可能已经变了,按版本号选择。本书采用普通 plugin.yml 与 Bukkit API;本阶段没有声称支持 Folia。

图框中的短例是局部片段,先读本节的位置说明,不要各自保存成插件。完整文件会明确给出文件名;同一文件分成多段时,按编号连接,包名、import 与外层类只出现一次。

  • 预测:先写成功条件和预期消息。
  • 运行:构建成功后,在专用本机测试服安装并重启。
  • 核对:普通玩家、管理身份与控制台分别走对应路径。

02 / 15

案发现场藏在 event 里

侦探拿到“方块被挖”的消息,差点给整个服务器都拉上警戒线。先问在哪个世界、什么模式、哪种方块,再决定是否拦住,这才是会办案的监听器。

event 是这一次动作的信息包。先过滤与规则无关的动作,余下的才判断权限和位置。取消事件改变本次动作是否发生;仅发送警告文字不会自动取消。

案发现场藏在 event 里
  1. 读取现场玩家、材料和坐标
  2. 限定范围世界、位置和模式
  3. 决定许可允许或取消本次动作

读取现场 · 位置见本节说明

var player = event.getPlayer();
var block = event.getBlock();
Material type = block.getType();
String worldName = block.getWorld().getName();

临时诊断日志 · 位置见本节说明

getLogger().info(player.getName() + " -> " + type
        + " @ " + block.getX() + "," + block.getY() + "," + block.getZ());

03 / 15

先筛选世界和游戏模式

练习规则只管名为 world 的世界。出生点保护对该世界所有模式生效;钻石执照只约束生存模式。把范围写清,管理员就知道为什么创造模式有不同表现。

先筛选世界和游戏模式
  1. 世界world其他世界直接返回
  2. 出生点圈所有模式都检查
  3. 生存执照圈外再检查钻石权限

沿箭头进入下一步前,必须满足前一步条件;被拒绝的分支提前返回。

只处理练习世界 · 位置见本节说明

if (!event.getBlock().getWorld().getName().equals("world")) return;

执照只检查生存模式 · 位置见本节说明

// 本句放在出生点保护之后
if (event.getPlayer().getGameMode() != GameMode.SURVIVAL) return;
核对表
场景出生点保护钻石执照
world 生存检查检查
world 创造检查跳过
其他世界跳过跳过

04 / 15

钻石也有深板岩外套

只判断 DIAMOND_ORE 会漏掉 DEEPSLATE_DIAMOND_ORE。把“是不是钻石矿”的规则写成方法,所有监听方法都调用同一份判断。

钻石也有深板岩外套
  1. DIAMOND_ORE普通钻石矿
  2. DEEPSLATE_DIAMOND_ORE深板岩钻石矿
  3. 其他材料执照规则不干预

统一判断矿石 · 位置见本节说明

private boolean isDiamond(Material type) {
    return type == Material.DIAMOND_ORE
            || type == Material.DEEPSLATE_DIAMOND_ORE;
}

快速退出无关方块 · 位置见本节说明

if (!isDiamond(event.getBlock().getType())) return;
// 下面只处理两种钻石矿
  • 把 STONE 输入这个规则,答案应该是 false。
  • 把两种钻石矿分别输入,答案都应该是 true。
  • 增加金矿时先列出对应材料名称,再修改方法和测试表。

05 / 15

取消动作时告诉玩家原因

权限节点像执照编号。没有钻石开采资格,就取消本次破坏并解释原因;它不会自动发一本纸质执照,也不会让玩家获得管理员的其他能力。

取消动作时告诉玩家原因
  1. 无执照取消并给出原因
  2. 有执照仍须通过出生点规则
  3. 已被取消不恢复别人的决定

拒绝无执照开采 · 位置见本节说明

if (!player.hasPermission("blockacademy.mine.diamond")) {
    event.setCancelled(true);
    player.sendMessage(Component.text("钻石矿需要矿工执照"));
    return;
}

本课权限声明 · 位置见本节说明

permissions:
  blockacademy.mine.diamond:
    default: op
  blockacademy.bypass:
    default: op

06 / 15

出生点保护是一根圆柱

把出生点的方块坐标当圆心,只比较 X 和 Z。半径10格、边界包含在内,因此上方和地下也受到保护。先确认同一世界,才有比较坐标的意义。

只看X/Z的保护圆:半径10,包含边界

dx² + dz² ≤ 10²

XZO (0,0)A (6,8)B (10,1)r=10
X / Z距离平方保护范围
O (0, 0)0是
A (6, 8)100是,含边界
B (10, 1)101否

只计算 X/Z 平面距离,不计算 Y 高度。

不用开平方也能比较 · 位置见本节说明

double dx = (double) block.getX() - spawn.getBlockX();
double dz = (double) block.getZ() - spawn.getBlockZ();
boolean protectedArea = dx * dx + dz * dz <= 100.0;
核对表
相对坐标 dx dz平方和受保护
0 00是
6 8100是,边界
10 1101否
11 0121否

07 / 15

规则处理和观察分开

优先级决定先后顺序,不代表谁更厉害。普通监听器可以修改事件;MONITOR 用来观察处理结果,按照约定不再改变事件。ignoreCancelled=true 会跳过已经取消的事件。

规则处理和观察分开
  1. 规则监听可以按合同取消
  2. MONITOR只观察,不修改事件
  3. ignoreCancelled跳过已经取消的动作

规则方法 · 位置见本节说明

@EventHandler(ignoreCancelled = true)
public void protect(BlockBreakEvent event) {
    // 检查条件,必要时 setCancelled(true)
}

观察方法 · 位置见本节说明

@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void observe(BlockBreakEvent event) {
    // 只观察,不取消,不恢复,不修改方块
}

08 / 15

侦探不能堵住服务器走廊

方块事件运行频繁。每挖一块都下载网页或保存整个文件,玩家就会在原地表演幻灯片。先做廉价过滤,必要时只更新内存,保存策略留到数据管理章节。

停下来想一想:代码只发出“这里不能挖”的提示,方块却仍被挖走。是聊天故障吗?还缺什么?

侦探不能堵住服务器走廊
  1. 先做廉价过滤无关世界尽早退出
  2. 只更新必要信息每次事件保持短
  3. 慢工作另安排不逐块下载或重写大文件

把无关事件尽早放行 · 位置见本节说明

if (!inLessonWorld(event)) return;
if (event.getPlayer().getGameMode() != GameMode.SURVIVAL) return;
if (!isDiamond(event.getBlock().getType())) return;

错误的事件做法 · 故意错误,先预测再修复

// 不要在每一次挖矿事件里调用耗时操作
// 下载网页、扫描整个世界、保存巨大文件
  • 测试时可暂时打印坐标;确认规则后去掉每块都打印的日志。
  • 频繁拒绝提示会刷屏,可在第34课学会按玩家记时后限制提示频率。
  • 不把“异步”当万能加速按钮;世界API的线程限制在第35课讨论。

09 / 15

把几何规则拿出来测试

新建与主类同包的 MiningRules.java。这个类只处理数字,不需要启动Minecraft。先验证边界,再把方法接入事件,定位问题会容易得多。

把几何规则拿出来测试
  1. 圆内距离平方小于100
  2. 边界6×6+8×8=100
  3. 圈外10×10+1×1=101

完整 MiningRules 类 · 完整文件

package de.teacherma.lesson33;

public final class MiningRules {
    private MiningRules() { }
    public static boolean isProtected(int x, int z, int sx, int sz) {
        double dx = (double) x - sx;
        double dz = (double) z - sz;
        return dx * dx + dz * dz <= 100.0;
    }
}

在普通 main 中验算 · 位置见本节说明

System.out.println(MiningRules.isProtected(6, 8, 0, 0));
System.out.println(MiningRules.isProtected(10, 1, 0, 0));
System.out.println(MiningRules.isProtected(106, 208, 100, 200));

运行结果

true
false
true

10 / 15

矿工执照程序上半部

建立 MinerLicensePlugin.java。本页与下一页按顺序合并,另外保留上一页的 MiningRules.java。主类登记监听器,并统一世界和矿石的判断。

矿工执照程序上半部
  1. 统一入口启用时登记
  2. 统一判断世界与两种钻石矿
  3. 保护优先圆内先检查绕过权限

主类第1部分 · 本段1/2,顺序连接

package de.teacherma.lesson33;

import net.kyori.adventure.text.Component;
import org.bukkit.GameMode;
import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.plugin.java.JavaPlugin;

public final class MinerLicensePlugin extends JavaPlugin implements Listener {
    @Override
    public void onEnable() {
        getServer().getPluginManager().registerEvents(this, this);
    }
    private boolean inLessonWorld(BlockBreakEvent event) {
        return event.getBlock().getWorld().getName().equals("world");
    }
    private boolean isDiamond(Material type) {
        return type == Material.DIAMOND_ORE
                || type == Material.DEEPSLATE_DIAMOND_ORE;
    }
    @EventHandler(ignoreCancelled = true)
    public void protect(BlockBreakEvent event) {

主类第1部分 · 本段2/2,顺序连接

        if (!inLessonWorld(event)) return;
        var player = event.getPlayer();
        var block = event.getBlock();
        var spawn = block.getWorld().getSpawnLocation();
        boolean nearSpawn = MiningRules.isProtected(block.getX(), block.getZ(),
                spawn.getBlockX(), spawn.getBlockZ());
        if (nearSpawn && !player.hasPermission("blockacademy.bypass")) {

11 / 15

矿工执照程序下半部

继续上一页,完成取消规则和观察消息。src/main/resources/plugin.yml 的基础六行参照第31课,将name改为BlockAcademyL33、main改为de.teacherma.lesson33.MinerLicensePlugin;再加入本课权限声明。

矿工执照程序下半部
  1. 取消规则拒绝时说明原因
  2. 模式分流执照仅管生存
  3. 观察结果未取消时报告获准开采

主类第2部分 · 顺序接在同一源文件

            event.setCancelled(true);
            player.sendMessage(Component.text("出生点十格以内请保留原貌"));
            return;
        }
        if (player.getGameMode() != GameMode.SURVIVAL) return;
        if (isDiamond(block.getType())
                && !player.hasPermission("blockacademy.mine.diamond")) {
            event.setCancelled(true);
            player.sendMessage(Component.text("钻石矿需要矿工执照"));
        }
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void observe(BlockBreakEvent event) {
        if (!inLessonWorld(event)) return;
        if (event.getPlayer().getGameMode() != GameMode.SURVIVAL) return;
        if (!isDiamond(event.getBlock().getType())) return;
        getServer().broadcast(Component.text(
                event.getPlayer().getName() + " 获准开采钻石矿!"));
    }
}

plugin.yml:把入口与权限放在同一个完整文件

src/main/resources/plugin.yml · 完整文件

name: BlockAcademyL33
version: 1.0.0
main: de.teacherma.lesson33.MinerLicensePlugin
api-version: '1.21.11'
author: TeacherMa
website: https://www.teacher-ma.de
permissions:
  blockacademy.mine.diamond:
    default: op
  blockacademy.bypass:
    default: op

12 / 15

验收规则要分清谁先拦住

出生点和执照可能同时阻止一次开采。测试执照时先走出保护圈,否则你看到的始终是出生点提示。测试表每一行只改变一个条件。

验收规则要分清谁先拦住
  1. 保护测试圆内、边界与圈外
  2. 执照测试走出圈再改权限
  3. 观察测试已取消时不得发成功消息
核对表
条件预期
world圈内,无绕过权限所有模式的破坏被取消
world边界6 8,无绕过取消;边界也保护
圈外生存,无执照,钻石矿取消并提示执照
圈外生存,无执照,普通石头允许
圈外生存,有执照,两种钻石矿允许并广播获准开采
其他世界本插件不干预
事件先被其他插件取消不广播成功消息

13 / 15

拓展挑战与自己的验收记录

留下源代码、实际运行结果和一条问题记录。先用自己的话解释,再运行验证;结果与预测不同的地方,往往最值得讨论。

拓展任务与通关复盘
  1. 木镐解释圆柱形状
  2. 铁镐增加矿石并验收
  3. 钻石镐限制拒绝提示频率
  • 木镐:挖两种金矿时发提示,普通石头保持安静。
  • 铁镐:把保护半径改为8格,增加半径内、边界和半径外测试。
  • 钻石镐:结合第34课Map,为每位玩家记录获准开采次数;说明取消事件是否计数。
  • 验收要求:分别测试普通钻石矿和深板岩钻石矿;在保护半径内、边界上、边界外记录是否允许及原因。
核对表
我能独立完成已掌握再练一次
能按世界和材料过滤□□
能解释圆柱保护边界□□
能区分取消与观察□□

14 / 15

可独立运行的规则验证

在项目内新建 checks/L33RuleCheck.java,完整保存下面代码。新建 checks/sources.txt,写入下面两条路径,然后从项目根目录执行命令。这是普通 Java 测试,不是插件主类。

L33RuleCheck.java · 完整文件

import de.teacherma.lesson33.MiningRules;

public class L33RuleCheck {
    public static void main(String[] args) {
        System.out.println(MiningRules.isProtected(0, 0, 0, 0));
        System.out.println(MiningRules.isProtected(6, 8, 0, 0));
        System.out.println(MiningRules.isProtected(10, 1, 0, 0));
        System.out.println(MiningRules.isProtected(106, 108, 100, 100));
    }
}

运行结果

true
true
false
true

checks/sources.txt · 完整内容

src/main/java/de/teacherma/lesson33/MiningRules.java
checks/L33RuleCheck.java

项目根目录终端命令 · 分别执行两行

javac -encoding UTF-8 -d checks/out @checks/sources.txt
java -cp checks/out L33RuleCheck

15 / 15

把证据留给下一次冒险

把“能编译”“数字规则通过”和“游戏实际表现正确”分成三栏。没有运行的场景写待测;发现 Bug 时记录触发条件、预期、实际、最小修复与复测结果。

你的验证记录
类别你选择的操作留下的证据
正常本课的一次成功路径版本与实际输出
边界结束前一刻、最大值或满背包与合同逐项比较
拒绝错误参数、无权限或不相关事件反馈清楚,状态未误改
  • 解释本课每条箭头为什么从这里指向那里;并列卡片没有先后含义。
  • 先改一个规则,再同时修改说明和预期,用同一输入复测。

TAKE IT WITH YOU

下载本课 PDF

完整课文、代码、图解与练习,选择你需要的语言。