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

PaperMC 插件开发完全指南 / 第 13 章:任务调度

第 13 章:任务调度

掌握 Bukkit 任务调度器,正确使用同步/异步任务、定时器和延迟任务。


13.1 任务调度概述

Minecraft 服务器以**游戏刻(Game Tick / Tick)**为单位运行,每秒 20 个 tick。所有与游戏世界的交互(方块、实体、玩家操作)必须在主线程执行。

核心概念

概念说明
同步任务在主线程执行,可以操作游戏世界
异步任务在独立线程执行,不能直接操作游戏世界
延迟任务指定延迟后执行一次
定时任务按固定间隔重复执行
BukkitTask任务对象,可用于取消任务

时间单位换算

Tick用途
1 tick0.05 秒最小时间单位
20 ticks1 秒一般定时任务
60 ticks3 秒中频更新
1200 ticks1 分钟低频保存
24000 ticks20 分钟游戏一天

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/OTPS 下降使用异步任务
过于频繁的定时任务CPU 占用高增加间隔或按需执行
任务未取消内存泄漏插件禁用时取消所有任务
异步任务中操作世界数据损坏runTask() 回到主线程
大量 BukkitRunnable 对象GC 压力复用任务对象

13.11 常见问题排查

问题原因解决方案
任务不执行插件已禁用检查 isCancelled()
异步操作报错访问了同步 API使用 runTask() 回到主线程
延迟任务重复执行未取消旧任务存储 BukkitTask 引用
Folia 上任务报错使用了旧调度器 API使用兼容层

13.12 扩展阅读


13.13 本章小结

要点内容
同步任务主线程执行,可操作游戏世界
异步任务独立线程,适合 I/O 操作
延迟任务指定 tick 后执行一次
定时任务固定间隔重复执行
任务管理存储引用,插件禁用时全部取消
Folia 兼容使用区域化调度器 API

下一章: 第 14 章:占位符扩展 — 学习 PlaceholderAPI 集成和自定义占位符。