PaperMC 插件开发完全指南 / 第 13 章:任务调度
第 13 章:任务调度
掌握 Bukkit 任务调度器,正确使用同步/异步任务、定时器和延迟任务。
13.1 任务调度概述
Minecraft 服务器以**游戏刻(Game Tick / Tick)**为单位运行,每秒 20 个 tick。所有与游戏世界的交互(方块、实体、玩家操作)必须在主线程执行。
核心概念
| 概念 | 说明 |
|---|---|
| 同步任务 | 在主线程执行,可以操作游戏世界 |
| 异步任务 | 在独立线程执行,不能直接操作游戏世界 |
| 延迟任务 | 指定延迟后执行一次 |
| 定时任务 | 按固定间隔重复执行 |
| BukkitTask | 任务对象,可用于取消任务 |
时间单位换算
| Tick | 秒 | 用途 |
|---|---|---|
| 1 tick | 0.05 秒 | 最小时间单位 |
| 20 ticks | 1 秒 | 一般定时任务 |
| 60 ticks | 3 秒 | 中频更新 |
| 1200 ticks | 1 分钟 | 低频保存 |
| 24000 ticks | 20 分钟 | 游戏一天 |
13.2 同步任务
同步任务在主线程执行,可以直接操作游戏世界。
基本用法
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;
// 方式一:Lambda 表达式(推荐)
BukkitTask task = Bukkit.getScheduler().runTask(plugin, () -> {
// 主线程执行
player.sendMessage("§a同步消息!");
block.setType(Material.DIAMOND_BLOCK);
});
// 方式二:BukkitRunnable
BukkitTask task = new BukkitRunnable() {
@Override
public void run() {
// 主线程执行
player.sendMessage("§a同步消息!");
}
}.runTask(plugin);
13.3 异步任务
异步任务在独立线程执行,适合 I/O 操作(数据库、HTTP 请求等)。
基本用法
// 方式一:Lambda 表达式
BukkitTask task = Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
// 异步线程执行
// 可以安全地进行数据库操作
PlayerData data = database.loadPlayer(uuid);
// 需要回到主线程操作游戏世界
Bukkit.getScheduler().runTask(plugin, () -> {
// 回到主线程
player.sendMessage("§a余额: " + data.getBalance());
});
});
// 方式二:BukkitRunnable
new BukkitRunnable() {
@Override
public void run() {
// 异步执行
String response = httpClient.get("https://api.example.com/data");
// 回到主线程
this.cancel();
new BukkitRunnable() {
@Override
public void run() {
player.sendMessage("§a获取到数据: " + response);
}
}.runTask(plugin);
}
}.runTaskAsynchronously(plugin);
警告: 异步任务中不能操作游戏世界(方块、实体、背包等),否则可能导致数据损坏或崩溃。
13.4 延迟任务
延迟任务在指定延迟后执行一次。
基本用法
// 延迟 3 秒后执行(60 ticks)
BukkitTask task = Bukkit.getScheduler().runTaskLater(plugin, () -> {
player.sendMessage("§a3 秒后执行!");
}, 60L); // 60 ticks = 3 秒
// 异步延迟任务
BukkitTask task = Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, () -> {
// 异步延迟执行
database.savePlayer(player);
}, 20L); // 1 秒后
业务场景:传送倒计时
public class TeleportCountdown {
private final JavaPlugin plugin;
private final Player player;
private final Location destination;
private BukkitTask task;
public TeleportCountdown(JavaPlugin plugin, Player player, Location destination) {
this.plugin = plugin;
this.player = player;
this.destination = destination;
}
public void start(int seconds) {
final int[] remaining = {seconds};
task = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
if (remaining[0] <= 0) {
// 传送
player.teleport(destination);
player.sendMessage("§a传送成功!");
return;
}
// 显示倒计时
player.sendActionBar(Component.text(
"§e传送中... §c" + remaining[0] + " 秒"
));
player.playSound(player.getLocation(),
Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 1.0f);
remaining[0]--;
}, 0L, 20L); // 每秒执行
}
public void cancel() {
if (task != null) {
task.cancel();
player.sendMessage("§c传送已取消!");
}
}
}
13.5 定时任务(重复执行)
基本定时任务
// 每 5 秒(100 ticks)执行一次
BukkitTask task = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
// 执行逻辑
broadcastMessage("§e服务器提醒:欢迎游玩!");
}, 0L, 100L); // 初始延迟 0 ticks,周期 100 ticks
// 异步定时任务
BukkitTask task = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, () -> {
// 定期保存数据
saveAllData();
}, 6000L, 6000L); // 每 5 分钟执行
BukkitRunnable 方式
public class AutoSaveTask extends BukkitRunnable {
private final JavaPlugin plugin;
public AutoSaveTask(JavaPlugin plugin) {
this.plugin = plugin;
}
@Override
public void run() {
// 定期保存逻辑
for (Player player : Bukkit.getOnlinePlayers()) {
savePlayerData(player);
}
plugin.getLogger().info("自动保存完成!");
}
}
// 启动任务
new AutoSaveTask(plugin).runTaskTimer(plugin, 0L, 6000L); // 每 5 分钟
13.6 任务管理
存储和取消任务
public class TaskManager {
private final Map<String, BukkitTask> tasks = new HashMap<>();
private final List<BukkitTask> playerTasks = new ArrayList<>();
/**
* 注册任务
*/
public void registerTask(String name, BukkitTask task) {
BukkitTask existing = tasks.get(name);
if (existing != null) {
existing.cancel();
}
tasks.put(name, task);
}
/**
* 取消指定任务
*/
public void cancelTask(String name) {
BukkitTask task = tasks.remove(name);
if (task != null) {
task.cancel();
}
}
/**
* 取消所有任务
*/
public void cancelAll() {
tasks.values().forEach(BukkitTask::cancel);
tasks.clear();
playerTasks.forEach(BukkitTask::cancel);
playerTasks.clear();
}
/**
* 取消玩家相关任务
*/
public void cancelPlayerTasks(Player player) {
// 取消该玩家的所有任务
Bukkit.getScheduler().cancelTasks(plugin);
// 或者用 BukkitTask.isCancelled() 检查
}
}
在插件禁用时取消所有任务
@Override
public void onDisable() {
// 取消所有本插件的任务
Bukkit.getScheduler().cancelTasks(this);
}
13.7 Folia 调度器(区域化调度)
Folia 是 Paper 的分区多线程分支,提供了新的调度器 API。
Paper 的区域调度器
// Paper 1.20+ 的 Player Scheduler(兼容 Folia)
player.getScheduler().run(plugin, task -> {
// 在玩家所在的区域执行
player.sendMessage("§a区域调度执行!");
}, () -> {
// 取消回调(玩家下线时)
});
兼容 Folia 的任务调度
public class CompatibleScheduler {
/**
* 判断是否运行在 Folia
*/
public static boolean isFolia() {
try {
Class.forName("io.papermc.paper.threadedregions.RegionizedServer");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
/**
* 兼容的同步任务
*/
public static BukkitTask runTask(JavaPlugin plugin, Runnable task) {
if (isFolia()) {
return Bukkit.getGlobalRegionScheduler()
.run(plugin, t -> task.run());
} else {
return Bukkit.getScheduler().runTask(plugin, task);
}
}
/**
* 兼容的延迟任务
*/
public static BukkitTask runTaskLater(JavaPlugin plugin, Runnable task,
long delayTicks) {
if (isFolia()) {
return Bukkit.getGlobalRegionScheduler()
.runDelayed(plugin, t -> task.run(), delayTicks);
} else {
return Bukkit.getScheduler().runTaskLater(plugin, task, delayTicks);
}
}
}
13.8 常见任务模式
粒子效果定时显示
public class ParticleTask extends BukkitRunnable {
private final Location center;
private double angle = 0;
public ParticleTask(Location center) {
this.center = center;
}
@Override
public void run() {
double radius = 3.0;
double x = center.getX() + radius * Math.cos(angle);
double z = center.getZ() + radius * Math.sin(angle);
Location particleLoc = new Location(center.getWorld(), x, center.getY() + 1, z);
center.getWorld().spawnParticle(
Particle.FLAME, particleLoc, 5, 0.1, 0.1, 0.1, 0.01
);
angle += Math.PI / 20; // 旋转速度
if (angle >= 2 * Math.PI) angle = 0;
}
}
资源监控
public class ServerMonitor extends BukkitRunnable {
private final JavaPlugin plugin;
public ServerMonitor(JavaPlugin plugin) {
this.plugin = plugin;
}
@Override
public void run() {
Runtime runtime = Runtime.getRuntime();
long maxMemory = runtime.maxMemory() / (1024 * 1024);
long totalMemory = runtime.totalMemory() / (1024 * 1024);
long freeMemory = runtime.freeMemory() / (1024 * 1024);
long usedMemory = totalMemory - freeMemory;
// 计算 TPS
double tps = getServer().getTPS()[0];
plugin.getLogger().info(String.format(
"内存: %d/%d MB | TPS: %.1f | 玩家: %d",
usedMemory, maxMemory, tps, Bukkit.getOnlinePlayers().size()
));
}
}
13.9 异步 HTTP 请求
public class HttpUtil {
/**
* 异步 GET 请求
*/
public static void asyncGet(JavaPlugin plugin, String url,
Consumer<String> callback) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("User-Agent", "MyPlugin/1.0")
.build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
Bukkit.getScheduler().runTask(plugin, () ->
callback.accept(response.body()));
}
} catch (Exception e) {
plugin.getLogger().warning("HTTP 请求失败: " + e.getMessage());
}
});
}
}
13.10 性能注意事项
| 问题 | 影响 | 解决方案 |
|---|---|---|
| 主线程执行 I/O | TPS 下降 | 使用异步任务 |
| 过于频繁的定时任务 | CPU 占用高 | 增加间隔或按需执行 |
| 任务未取消 | 内存泄漏 | 插件禁用时取消所有任务 |
| 异步任务中操作世界 | 数据损坏 | 用 runTask() 回到主线程 |
| 大量 BukkitRunnable 对象 | GC 压力 | 复用任务对象 |
13.11 常见问题排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 任务不执行 | 插件已禁用 | 检查 isCancelled() |
| 异步操作报错 | 访问了同步 API | 使用 runTask() 回到主线程 |
| 延迟任务重复执行 | 未取消旧任务 | 存储 BukkitTask 引用 |
| Folia 上任务报错 | 使用了旧调度器 API | 使用兼容层 |
13.12 扩展阅读
13.13 本章小结
| 要点 | 内容 |
|---|---|
| 同步任务 | 主线程执行,可操作游戏世界 |
| 异步任务 | 独立线程,适合 I/O 操作 |
| 延迟任务 | 指定 tick 后执行一次 |
| 定时任务 | 固定间隔重复执行 |
| 任务管理 | 存储引用,插件禁用时全部取消 |
| Folia 兼容 | 使用区域化调度器 API |
下一章: 第 14 章:占位符扩展 — 学习 PlaceholderAPI 集成和自定义占位符。