使用调度器
BukkitScheduler可以用来调度你的代码在稍后或重复运行。
本指南是为非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 * 20seconds = ticks / 20
你可以使用TimeUnit
枚举使你的代码更易读,例如将5分钟转换为tick并返回:
TimeUnit.MINUTES.toSeconds(5) * 20TimeUnit.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();
}
调度任务
调度任务需要你传递:
- 你的插件实例
- 要运行的代码,可以是
Runnable或Consumer<BukkitTask> - 任务首次运行前的延迟(以tick为单位)
- 如果你在调度重复任务,则需要任务每次执行之间的周期(以tick为单位)
同步和异步任务的区别
同步任务(在主线程上)
同步任务是在服务器主线程上执行的任务。这是处理所有游戏逻辑的 同一个线程。
所有在主线程上调度的任务都会影响服务器的性能。如果你的任务 正在进行网络请求、访问文件、数据库或其他耗时操作,你应该考虑使用 异步任务。
异步任务(在主线程之外)
异步任务是在单独的线程上执行的任务,因此不会直接影响 你的服务器性能。
Bukkit API的大部分内容在异步任务中使用都是不安全的。如果一个方法改变或 访问世界状态,它在异步任务中使用是不安全的。
虽然任务是在单独的线程上执行的,但它们仍然是从主线程启动的, 并且如果服务器出现延迟,它们会受到影响,例如20个tick可能不正好是1秒。
如果你需要一个独立于服务器运行的调度器,考虑使用你自己的
ScheduledExecutorService。
你可以按照本指南学习如何使用它。
调度任务的不同方式
使用Runnable
Runnable接口用于最简单的任务,
这些任务不需要BukkitTask实例。
你可以在单独的类中实现它,例如:
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在单独的类中实现它,例如:
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()方法:
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的任务, 它将在服务器启用之前执行。