Skip to content

Visuals

Arcade provides a wide array of visual components, read the Virtual Visuals Section to see more information about those. This section is dedicated to how those gui components can be integrated within minigames.

All the gui elements are handled by a MinigameVisualsManager, you can add all of your visual components to this manager, and it will ensure that players will be displayed the components, and they will be updated for those players. If a player joins the minigame, it will correctly update all the player's visuals, and if a player leaves, all the visuals will be removed.

Adding & Removing Visual Components

We simply construct our visual component then add it to the manager using one of the respective methods, once registered, the minigame will handle any ticking or updating that is needed for that component:

kotlin
val minigame: Minigame = // ...

val bossbar: VirtualBossbar = // ...
minigame.visuals.addBossbar(bossbar)

val nametag: Nametag = // ...
minigame.visuals.addNametag(nametag)

val sidebar: VirtualSidebar = // ...
minigame.visuals.setSidebar(sidebar)

val list: VirtualPlayerList = // ...
minigame.visuals.setPlayerListDisplay(list)

The manager will display the visual to every player in the minigame and tick it for us, so we don't need to do any of this ourselves.

NOTE

You can add as many bossbars and nametags as you wish, however, there can only every be one sidebar and one player list display.

We can also remove any elements with their respective methods:

kotlin
val minigame: Minigame = // ...

val bossbar: VirtualBossbar = // ...
minigame.visuals.removeBossbar(bossbar)
minigame.visuals.removeAllBossbars()

val nametag: Nametag = // ...
minigame.visuals.removeNametag(nametag)
minigame.visuals.removeAllNametags()

minigame.visuals.removeSidebar()

minigame.visuals.removePlayerListDisplay()

There are also two additional things that the visual manager controls, that is the Countdown and the ready broadcasters, these are used in minigames by default when unpausing to count down and to check that all players are ready. You may also want to use these for other applications. These will have default implementations, but you can overwrite them.

kotlin
val minigame: Minigame = // ...
    
minigame.visuals.countdown = TitledCountdown.titled(Component.literal("My Titled Countdown!"))

// Launch a co-routine on the minigame scheduler
minigame.launch {
    // This transition is suspending
    minigame.visuals.countdown.transition(10.Seconds, 1.Seconds, minigame.players::all)
    // Executes *after* our transition is complete!
    println("Minigame Countdown Finished!")
}

Readiness is handled by the playerReadyBroadcaster and teamReadyBroadcaster, which determine how players are prompted to ready up (by default they use chat). You can overwrite these, and then check readiness using the suspending checkReadyPlayers and checkReadyTeams extensions, or track it yourself with trackReadyPlayers and trackReadyTeams:

kotlin
val minigame: Minigame = // ...

// Optionally customize how players are asked to ready up
minigame.visuals.playerReadyBroadcaster = // ...

minigame.launch {
    // Suspends until all playing players are ready
    minigame.checkReadyPlayers()
    println("Playing players are ready!")
}

// Or track readiness manually
val tracker = minigame.trackReadyPlayers()
if (tracker.isReady()) {
    println("Everyone is ready!")
}

Phased Visual Components

Often we want a gui component to be shown only for a certain phase of our minigame, and removed appropriately even if we backtrack to a previous phase.

Let's take a look at a more concrete example. Let's say we've got a timer bossbar that lasts 10 minutes denoting a minigame phase change. There are a couple issues with this; what if we want to step into the next phase before the 10 minutes is up, the bossbar would not automatically disappear. Okay well we could solve this by simply storing a bossbar as a field and always removing it when the phase ends:

kotlin
class ExampleMinigame(
    server: MinecraftServer,
    uuid: UUID
): Minigame(server, uuid, ID, ExamplePhase.entries) {
    // ...

    var bossbar: VirtualBossbar = // ...

    @Listener
    private fun onSetPhase(event: MinigameSetPhaseEvent) {
        if (event.phase == ExamplePhase.Grace) {
            this.visuals.addBossbar(this.bossbar)
        } else {
            this.visuals.removeBossbar(this.bossbar)
        }
    }
}

But this feels very clunky, especially if you have multiple different gui components that you want to manage. Not to mention, this will also make serialization more difficult if that's something you're also aiming for.

Instead, what we can do is launch a coroutine in a scope which closes when the phase ends, and remove the bossbar from a finally block:

kotlin
class ExampleMinigame(
    server: MinecraftServer,
    uuid: UUID
): Minigame(server, uuid, ID, ExamplePhase.entries) {
    // ...

    private suspend fun runGraceLogic() {
        val duration = 10.Minutes
        val timer = TimerElement(duration)
        val bossbar = DynamicVirtualBossbar(this.server)
        bossbar.addTickable(timer)
        bossbar.setProgress(timer.progress())

        this.visuals.addBossbar(bossbar)
        try {
            delay(duration + 1.Ticks)
        } finally {
            this.visuals.removeBossbar(bossbar)
        }
    }
}

The bossbar is added when the coroutine starts, and removed when it finishes, because the cleanup is in a finally. If the full duration elapses, delay returns and the bossbar is removed. If the phase changes first, the minigame cancels everything in the phase's scope, which unwinds the coroutine through the same finally, and the bossbar is removed then instead.

This works for anything with a lifetime, not just a phase; create a scope with the lifetime you want and launch the coroutine there instead, see the Scheduling Section. And if your minigame is serializable, the same try/finally pattern works inside a Routine, see the Serialization Section.