Skip to content

Suspending Delayed, Repeating Tasks

This page explains how you can delay and repeat tasks using Kotlin Coroutines.

Delaying tasks

If you are already in a suspend function, you can simply use delay to delay an execution.

Using delay we can delay the current context (e.g. Main Thread) by some milliseconds, to easily delay actions without blocking the server. delay essentially suspends the current context and continuous after the given time.

Difference between delay() and Thread.sleep()

There is a big difference with delay() and Thread.sleep(). Consult the official Kotlin Coroutines documentation for details, however essentially Thread.sleep() blocks the thread for a given time and delay() suspends the thread for a given time. When a thread is suspended, it can do other work (e.g. server handles other operations like players joining or commands) compared to when a thread is blocked, it cannot do other work (e.g. server appears frozen).

suspend fun sayHello() {
    println("Please say hello in 2 seconds")
    delay(2000) // Delay for 2000 milliseconds
    println("hello")
}

If you are not in a suspend function, use plugin.launch together with delay.

fun sayHello() {
    plugin.launch {
        println("Please say hello in 2 seconds")
        delay(2000) // Delay for 2000 milliseconds
        println("hello")
    }
}

Delay Ticks

MCCoroutine offers an extension method to use delay together with Bukkit and Sponge ticks.

delay(1.ticks)

Prefer using delay(1.ticks) when delaying on the minecraft main thread instead of delay(50). The tick extension function is more accurate than using milliseconds directly. The technical details are explained in this github issue.

Repeating tasks

If you are already in a suspend function, you can simply use traditional loops with delay to repeat tasks.

suspend fun sayHello() {
    println("Please say hello 10 times every 2 seconds")

    for (i in 0 until 10) {
        delay(2000) // Delay for 2000 milliseconds
        println("hello")
    }
}

If you are not in a suspend function, use plugin.launch together with delay.

fun sayHello() {
    plugin.launch {
        println("Please say hello 10 times every 2 seconds")

        for (i in 0 until 10) {
            delay(2000) // Delay for 2000 milliseconds
            println("hello")
        }
    }
}

Creating a Minigame using delay (Bukkit)

One example where delay is really useful is when creating minigames. It makes the contract of minigame classes very easy to understand. Let's start by implementing a basic minigame class.

The first example shows a countdown in the start function of the minigame.

import kotlinx.coroutines.delay
import org.bukkit.entity.Player

class MiniGame {
    private var isStarted = false;
    private var players = HashSet<Player>()

    fun join(player: Player) {
        if (isStarted) {
            return
        }

        players.add(player)
    }

    suspend fun start() {
        if (isStarted) {
            return
        }

        isStarted = true

        // This loop represents a traditional repeating task which ticks every 1 second and is called 20 times.
        for (i in 0 until 20) {
            sendMessageToPlayers("Game is starting in ${20 - i} seconds.")
            delay(1000)
        }

        // ... Teleport players to game.
    }

    private fun sendMessageToPlayers(message: String) {
        players.forEach { p -> p.sendMessage(message) }
    }
}

Add a run function to the MiniGame class

We can extend the start method to call run which contains a loop to tick the miniGame every 1 second.

import kotlinx.coroutines.delay
import org.bukkit.entity.Player

class MiniGame {
    private var isStarted = false;
    private var players = HashSet<Player>()
    private var remainingTime = 0

    //...

    suspend fun start() {
        if (isStarted) {
            return
        }

        isStarted = true

        // This loop represents a traditional repeating task which ticks every 1 second and is called 20 times.
        for (i in 0 until 20) {
            sendMessageToPlayers("Game is starting in ${20 - i} seconds.")
            delay(1000)
        }

        // ... Teleport players to game.
        run()
    }

    private suspend fun run() {
        remainingTime = 300 // 300 seconds

        while (isStarted && remainingTime > 0) {
            sendMessageToPlayers("Game is over in ${remainingTime} seconds.")
            delay(1000)
            remainingTime--
        }
    }

    //...
}

Add a function to stop the game.

An admin should be able to cancel the minigame, which we can implement by a stop function.

import kotlinx.coroutines.delay
import org.bukkit.entity.Player

class MiniGame {
    private var isStarted = false;
    private var players = HashSet<Player>()
    private var remainingTime = 0

    //...

    private suspend fun run() {
        remainingTime = 300 // 300 seconds

        while (isStarted && remainingTime > 0) {
            sendMessageToPlayers("Game is over in ${remainingTime} seconds.")
            delay(1000)
            remainingTime--
        }

        if (!isStarted) {
            sendMessageToPlayers("Game was cancelled by external stop.")
        }

        isStarted = false
        // ... Teleport players back to lobby.
    }

    fun stop() {
        if (!isStarted) {
            return
        }

        isStarted = false
    }

    //...
}

The full MiniGame class:

import kotlinx.coroutines.delay
import org.bukkit.entity.Player

class MiniGame {
    private var isStarted = false;
    private var players = HashSet<Player>()
    private var remainingTime = 0

    fun join(player: Player) {
        if (isStarted) {
            return
        }

        players.add(player)
    }

    suspend fun start() {
        if (isStarted) {
            return
        }

        isStarted = true

        // This loop represents a traditional repeating task which ticks every 1 second and is called 20 times.
        for (i in 0 until 20) {
            sendMessageToPlayers("Game is starting in ${20 - i} seconds.")
            delay(1000)
        }

        // ... Teleport players to game.
        run()
    }

    private suspend fun run() {
        remainingTime = 300 // 300 seconds

        while (isStarted && remainingTime > 0) {
            sendMessageToPlayers("Game is over in ${remainingTime} seconds.")
            delay(1000)
            remainingTime--
        }

        if (!isStarted) {
            sendMessageToPlayers("Game was cancelled by external stop.")
        }

        isStarted = false
        // ... Teleport players back to lobby.
    }

    fun stop() {
        if (!isStarted) {
            return
        }

        isStarted = false
    }

    private fun sendMessageToPlayers(message: String) {
        players.forEach { p -> p.sendMessage(message) }
    }
}

Connect JavaPlugin, Listener and MiniGame

import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.player.PlayerJoinEvent

class MiniGameListener(private val miniGame: MiniGame) : Listener {
    @EventHandler
    suspend fun onPlayerJoinEvent(playerJoinEvent: PlayerJoinEvent) {
        miniGame.join(playerJoinEvent.player)

        // Just for testing purposes
        miniGame.start()
    }
}
import com.github.shynixn.mccoroutine.bukkit.SuspendingJavaPlugin
import com.github.shynixn.mccoroutine.bukkit.registerSuspendingEvents
import com.github.shynixn.mccoroutine.bukkit.setSuspendingExecutor

class MCCoroutineSamplePlugin : SuspendingJavaPlugin() {
    private val database = Database()
    private val miniGame = MiniGame()

    override suspend fun onEnableAsync() {
        // Minecraft Main Thread
        database.createDbIfNotExist()
        server.pluginManager.registerSuspendingEvents(PlayerDataListener(database), this)
        getCommand("playerdata")!!.setSuspendingExecutor(PlayerDataCommandExecutor(database))
        server.pluginManager.registerSuspendingEvents(MiniGameListener(miniGame), this)
    }

    override suspend fun onDisableAsync() {
        // Minecraft Main Thread
    }
}

Test the MiniGame

Join your server to observe Minigame messages getting printed to your server log.