跳到主要内容

使用调度器

BukkitScheduler可以用来调度你的代码在稍后或重复运行。

Folia

本指南是为非Folia的Bukkit服务器设计的。如果你使用Folia,你应该使用它各自的调度器。

什么是tick?

每个游戏都运行一个叫做游戏循环的东西,它本质上是一遍又一遍地执行游戏的所有逻辑。 在Minecraft中,游戏循环的一次执行被称为一个"tick"。

在Minecraft中,每秒有20个tick,换句话说,每50毫秒一个tick。这意味着游戏循环每秒执行 20次。当一个tick执行时间超过50毫秒时,就是你的服务器开始落后于其工作并出现延迟的时候。

一个应该在100个tick后运行的任务将在5秒后运行(100个tick / 每秒20个tick = 5秒)。然而, 如果服务器只以每秒10个tick的速度运行,那么一个计划在100个tick后运行的任务将需要10 秒。

在人类单位和Minecraft tick之间转换

调度器的每个方法在接受延迟或周期时都使用tick作为时间单位。

在人类单位和tick之间转换很简单:

  • ticks = seconds * 20
  • seconds = ticks / 20

你可以使用TimeUnit 枚举使你的代码更易读,例如将5分钟转换为tick并返回:

  • TimeUnit.MINUTES.toSeconds(5) * 20
  • TimeUnit.SECONDS.toMinutes(ticks / 20)

你也可以使用Paper的Tick类在人类单位和tick之间转换,例如将5分钟转换为tick: Tick.tick().fromDuration(Duration.ofMinutes(5))将得到6000个tick。

获取调度器

要获取调度器,你可以使用Server类上的 getScheduler方法, 例如在你的onEnable方法中:

@Override
public void onEnable() {
BukkitScheduler scheduler = this.getServer().getScheduler();
}

调度任务

调度任务需要你传递:

  • 你的插件实例
  • 要运行的代码,可以是RunnableConsumer<BukkitTask>
  • 任务首次运行前的延迟(以tick为单位)
  • 如果你在调度重复任务,则需要任务每次执行之间的周期(以tick为单位)

同步和异步任务的区别

同步任务(在主线程上)

同步任务是在服务器主线程上执行的任务。这是处理所有游戏逻辑的 同一个线程。

所有在主线程上调度的任务都会影响服务器的性能。如果你的任务 正在进行网络请求、访问文件、数据库或其他耗时操作,你应该考虑使用 异步任务。

异步任务(在主线程之外)

异步任务是在单独的线程上执行的任务,因此不会直接影响 你的服务器性能。

注意

Bukkit API的大部分内容在异步任务中使用都是不安全的。如果一个方法改变或 访问世界状态,它在异步任务中使用是不安全的。

信息

虽然任务是在单独的线程上执行的,但它们仍然是从主线程启动的, 并且如果服务器出现延迟,它们会受到影响,例如20个tick可能不正好是1秒。

如果你需要一个独立于服务器运行的调度器,考虑使用你自己的 ScheduledExecutorService。 你可以按照本指南学习如何使用它。

调度任务的不同方式

使用Runnable

Runnable接口用于最简单的任务, 这些任务不需要BukkitTask实例。

你可以在单独的类中实现它,例如:

MyRunnableTask.java
public class MyRunnableTask implements Runnable {

private final MyPlugin plugin;

public MyRunnableTask(MyPlugin plugin) {
this.plugin = plugin;
}

@Override
public void run() {
this.plugin.getServer().broadcast(Component.text("你好,世界!"));
}
}
scheduler.runTaskLater(plugin, new MyRunnableTask(plugin), 20);

或者使用lambda表达式,这对于简单和短小的任务来说很好:

scheduler.runTaskLater(plugin, /* Lambda: */ () -> {
this.plugin.getServer().broadcast(Component.text("你好,世界!"));
}, /* Lambda结束 */ 20);

使用Consumer<BukkitTask>

Consumer接口用于需要 BukkitTask实例的任务(通常在重复任务中), 例如当你想从任务内部取消任务时。

你可以类似于Runnable在单独的类中实现它,例如:

MyConsumerTask.java
public class MyConsumerTask implements Consumer<BukkitTask> {

private final UUID entityId;

public MyConsumerTask(UUID uuid) {
this.entityId = uuid;
}

@Override
public void accept(BukkitTask task) {
Entity entity = Bukkit.getServer().getEntity(this.entityId);

if (entity instanceof LivingEntity livingEntity) {
livingEntity.addPotionEffect(new PotionEffect(PotionEffectType.SPEED, 20, 1));
return;
}

task.cancel(); // 实体不再有效,继续运行这个任务没有意义
}
}
scheduler.runTaskTimer(plugin, new MyConsumerTask(someEntityId), 0, 20);

或者使用lambda表达式,这对于简单和短小的任务来说同样更清晰:

scheduler.runTaskTimer(plugin, /* Lambda: */ task -> {
Entity entity = Bukkit.getServer().getEntity(entityId);

if (entity instanceof LivingEntity livingEntity) {
livingEntity.addPotionEffect(new PotionEffect(PotionEffectType.SPEED, 20, 1));
return;
}

task.cancel(); // 实体不再有效,继续运行这个任务没有意义
} /* Lambda结束 */, 0, 20);
使用BukkitRunnable

BukkitRunnable是一个实现了Runnable 并持有BukkitTask实例的类。这意味着你不需要从run()方法内部访问任务, 你可以简单地使用this.cancel()方法:

CustomRunnable.java
public class CustomRunnable extends BukkitRunnable {

private final UUID entityId;

public CustomRunnable(UUID uuid) {
this.entityId = uuid;
}

@Override
public void run() {
Entity entity = Bukkit.getServer().getEntity(this.entityId);

if (entity instanceof LivingEntity livingEntity) {
livingEntity.addPotionEffect(new PotionEffect(PotionEffectType.SPEED, 20, 1));
return;
}

this.cancel(); // 实体不再有效,继续运行这个任务没有意义
}
}

这只是在实体死亡之前一直添加药水效果。

使用0 tick延迟

0 tick的延迟被视为你想在下一个tick运行任务。如果你在服务器启动时或在启用之前调度一个延迟为0 tick的任务, 它将在服务器启用之前执行。