Scheduling
The BukkitScheduler can be used to schedule your code to be run later or repeatedly.
What is a tick?
Section titled “What is a tick?”Every game runs something called a game loop, which essentially executes all the logic of the game over and over. A single execution of that loop in Minecraft is called a ‘tick’.
In Minecraft, there are 20 ticks per second or in other words, one tick every 50 milliseconds. This means that the game loop is executed 20 times per second. A tick taking more than 50ms to execute is the moment when your server starts to fall behind on its work and lag.
A task that should run after 100 ticks will run after 5 seconds (100 ticks / 20 ticks per second = 5 seconds). However, if the server is only running at 10 ticks per second, a task that is scheduled to run after 100 ticks will take 10 seconds.
Converting between human units and Minecraft ticks
Section titled “Converting between human units and Minecraft ticks”Every method of the scheduler that takes a delay or period uses ticks as a unit of time.
Converting from human units to ticks and back is as simple as:
ticks = seconds * 20seconds = ticks / 20
You can make your code more readable by using the
TimeUnit
enum, e.g. to convert 5 minutes to ticks and back:
TimeUnit.MINUTES.toSeconds(5) * 20TimeUnit.SECONDS.toMinutes(ticks / 20)
You can also use the Tick class from Paper to convert between human units and ticks, e.g. to convert 5 minutes to ticks:
Tick.tick().fromDuration(Duration.ofMinutes(5)) will yield 6000 ticks.
Obtaining the scheduler
Section titled “Obtaining the scheduler”To obtain a scheduler, you can use the getScheduler method
on the Server class, e.g. in your onEnable method:
@Overridepublic void onEnable() { BukkitScheduler scheduler = this.getServer().getScheduler();}Scheduling tasks
Section titled “Scheduling tasks”Scheduling a task requires you to pass:
- Your plugin’s instance
- The code to run, either with a
RunnableorConsumer<BukkitTask> - The delay in ticks before the task should run for the first time
- The period in ticks between each execution of the task, if you’re scheduling a repeating task
Difference between synchronous and asynchronous tasks
Section titled “Difference between synchronous and asynchronous tasks”Synchronous tasks (on the main thread)
Section titled “Synchronous tasks (on the main thread)”Synchronous tasks are tasks that are executed on the main server thread. This is the same thread that handles all game logic.
All tasks scheduled on the main thread will affect the server’s performance. If your task is making web requests, accessing files, databases or otherwise time-consuming operations, you should consider using an asynchronous task.
Asynchronous tasks (off the main thread)
Section titled “Asynchronous tasks (off the main thread)”Asynchronous tasks are tasks that are executed on separate threads, therefore will not directly affect your server’s performance.
Different ways to schedule tasks
Section titled “Different ways to schedule tasks”Using Runnable
Section titled “Using Runnable”The Runnable interface is used for the simplest tasks
that don’t require a BukkitTask instance.
You can either implement it in a separate class, e.g.:
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("Hello, World!")); }}scheduler.runTaskLater(plugin, new MyRunnableTask(plugin), 20);Or use a lambda expression, which is great for simple and short tasks:
scheduler.runTaskLater(plugin, /* Lambda: */ () -> { plugin.getServer().broadcast(Component.text("Hello, World!"));}, /* End of the lambda */ 20);Using Consumer<BukkitTask>
Section titled “Using Consumer<BukkitTask>”The Consumer interface is used for tasks
that require a BukkitTask instance (usually in repeated tasks),
e.g. when you want to cancel the task from inside it.
You can either implement it in a separate class similarly to the Runnable, e.g.:
public class MyConsumerTask implements Consumer<BukkitTask> {
private final UUID entityId;
public MyConsumerTask(UUID entityId) { this.entityId = entityId; }
@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(); // The entity is no longer valid, there's no point in continuing to run this task }}scheduler.runTaskTimer(plugin, new MyConsumerTask(someEntityId), 0, 20);Or use a lambda expression, which again is much cleaner for short and simple tasks:
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(); // The entity is no longer valid, there's no point in continuing to run this task} /* End of the lambda */, 0, 20);Using BukkitRunnable
Section titled “Using BukkitRunnable”BukkitRunnable is a class that implements Runnable
and holds a BukkitTask instance. This means that you do not need to access the task from inside the run() method,
you can simply use the this.cancel() method:
public class CustomRunnable extends BukkitRunnable {
private final UUID entityId;
public CustomRunnable(UUID entityId) { this.entityId = entityId; }
@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(); // The entity is no longer valid, there's no point in continuing to run this task }}This simply adds a potion effect until the entity dies.
Using a delay of 0 ticks
Section titled “Using a delay of 0 ticks”A delay of 0 ticks is treated as you wanting to run the task on the next tick. If you schedule a task with a delay of 0 ticks while the server is starting, or before it is enabled, it will be executed before the server is enabled.
Using ScheduledTask
Section titled “Using ScheduledTask”Paper also includes the global region, region, async and entity schedulers, which are required on Folia and work on Paper as well. See Supporting Paper and Folia to learn when to use each of them.
Instead of a BukkitTask, these schedulers use a ScheduledTask.
It is returned by methods such as run, runDelayed and runAtFixedRate, and it is
passed to the Consumer<ScheduledTask> that you give to them.
Cancelling a task from inside itself
Section titled “Cancelling a task from inside itself”Just like with Consumer<BukkitTask>, you can cancel a repeating task from inside it, e.g. a countdown that stops once it reaches zero:
AtomicInteger secondsLeft = new AtomicInteger(10);
server.getGlobalRegionScheduler().runAtFixedRate(plugin, task -> { int seconds = secondsLeft.getAndDecrement();
if (seconds > 0) { server.broadcast(Component.text(seconds + "...")); return; }
server.broadcast(Component.text("Go!")); task.cancel(); // The countdown is over, there's no point in continuing to run this task}, 1, 20);Checking the state of a task
Section titled “Checking the state of a task”cancel() does not interrupt a task
that is currently running; it only prevents it from running again. It returns a
ScheduledTask.CancelledState describing what happened:
CANCELLED_BY_CALLER: The task was cancelled by this call and will not run.CANCELLED_ALREADY: The task was already cancelled before this call.RUNNING: The task is not repeating and could not be cancelled, because it is currently running.ALREADY_EXECUTED: The task is not repeating and could not be cancelled, because it has already finished.NEXT_RUNS_CANCELLED: The repeating task is currently running, but this call stopped its future runs.NEXT_RUNS_CANCELLED_ALREADY: The repeating task is currently running, and its future runs were already cancelled.
You can also check a task at any time with these methods:
getExecutionState()returns aScheduledTask.ExecutionState:IDLE,RUNNING,FINISHED,CANCELLEDorCANCELLED_RUNNING(cancelled, but still running).isCancelled()returnstrueif the state isCANCELLEDorCANCELLED_RUNNING.isRepeatingTask()returnstrueif the task was scheduled to run repeatedly.getOwningPlugin()returns the plugin that scheduled the task.