Paper Plugin Guide
File mode0

Paper essentials

Titles, action bars, boss bars and sounds

Show big titles, quick status lines and progress bars, and play sounds to players.

Beginner39 min read

Chat messages are easy to miss. On this page you will show big titles in the middle of the screen, quick status lines above the hotbar, progress bars at the top, and play sounds, and then combine all four into a working /countdown command.

Four ways to get a player's attention

Everything on this page is something Minecraft already knows how to draw or play. Your plugin only has to ask for it. Each one has a different job:

Boss bar Title Subtitle Chat stays in history Action bar Hotbar Boss bar progress, color, stays until hidden Title big, fades away, you set the timing Action bar one short line, fades after 3 s Sounds: heard
Where each thing appears on the player's screen. Sounds have no place on the screen: the player hears them.
WhatBest forHow long it stays
Title and subtitleBig moments: "Welcome!", "Round 2", "You died"A few seconds, then fades. You choose the timing.
Action barShort, changing status: cooldowns, health, money, damage numbersAbout three seconds. Send it again to keep it.
Boss barProgress or time left: events, countdowns, loadingUntil you hide it.
SoundFeedback: success, error, tick, warningAs long as the sound itself.

All four work on any audience (a player, or the whole server), just like sendMessage did on the previous page. The text parts take components, so you can use colors, bold and MiniMessage in all of them.

To try everything, this page uses a small plugin with a /tour command. Type /tour title, /tour actionbar, /tour bossbar or /tour sound and your screen shows the example. Here is the command class; the next sections explain one method at a time.

TourCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livestitles-actionbar-bossbar-sounds-toursrcmainjavacomexampletitlesactionbarbossbarsoundstourTourCommand.java

The package com.example.titlesactionbarbossbarsoundstour is the folder path com/example/titlesactionbarbossbarsoundstour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • titles-actionbar-bossbar-sounds-tour/
    • src/main/
      • java/com/example/titlesactionbarbossbarsoundstour/Package com.example.titlesactionbarbossbarsoundstour
        • TourCommand.javayou are hereCommand (BasicCommand)
        • TourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

28@Override29public void execute(CommandSourceStack source, String[] args) {30    if (!(source.getExecutor() instanceof Player player)) {31        source.getSender().sendRichMessage("<red>Only players have a screen.");32        return;33    }34    if (args.length == 0) {35        player.sendRichMessage("<gray>Usage: /tour <white>title, actionbar, bossbar, sound</white> or <white>clear</white>");36        return;37    }38 39    switch (args[0].toLowerCase()) {40        case "title" -> showTitle(player);41        case "actionbar" -> showActionBar(player);42        case "bossbar" -> showBossBar(player);43        case "sound" -> playSounds(player);44        case "orb" -> playOrbSound(player);45        case "clear" -> player.clearTitle();46        default -> player.sendRichMessage("<red>Unknown option. Try title, actionbar, bossbar, sound or clear.");47    }48}
  1. Titles and boss bars are drawn on one player's screen. The console has no screen, so we stop and say so. This check also gives us the variable player.
  2. The player typed just /tour. Show how to use it.
  3. Picks one branch by the first word the player typed. toLowerCase() makes TITLE and title mean the same. The arrow form case "x" -> ... is the modern switch; Making decisions explains it.
  4. Runs when the word matches none of the cases.

Titles

A title is the big text in the middle of the screen, with a smaller subtitle underneath. It appears, stays for a while and fades away.

TourCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livestitles-actionbar-bossbar-sounds-toursrcmainjavacomexampletitlesactionbarbossbarsoundstourTourCommand.java

The package com.example.titlesactionbarbossbarsoundstour is the folder path com/example/titlesactionbarbossbarsoundstour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • titles-actionbar-bossbar-sounds-tour/
    • src/main/
      • java/com/example/titlesactionbarbossbarsoundstour/Package com.example.titlesactionbarbossbarsoundstour
        • TourCommand.javayou are hereCommand (BasicCommand)
        • TourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

59private void showTitle(Player player) {60    Title.Times times = Title.Times.times(Duration.ofMillis(500), Duration.ofSeconds(3), Duration.ofMillis(500));61    Title title = Title.title(62        Component.text("Welcome!", NamedTextColor.GOLD, TextDecoration.BOLD),63        Component.text("Enjoy your stay, " + player.getName(), NamedTextColor.GRAY),64        times);65    player.showTitle(title);66}
  1. The timing, built from three Duration values. A Duration is Java's way to say "this long": Duration.ofSeconds(3), Duration.ofMillis(500).
  2. The order is always fade in, stay, fade out.
  3. Bundles the big text, the subtitle and the timing into one Title object. Both texts are components, so they can have any color or style.
  4. Shows it on this player's screen.
Welcome! Enjoy your stay, Steve
How visible the title is over time (default times) seen gone Fade in 0.5 s Stay 3.5 s Fade out 1 s Title.Times.times(fadeIn, stay, fadeOut)
A title fades in, stays and fades out. Without a Times object Minecraft uses 10, 70 and 20 ticks, which is 0.5, 3.5 and 1 second.

Timing

The game counts time in ticks: 20 ticks make one second (see Timing and tasks). Adventure's Title.Times uses Duration instead, so you can write seconds directly. If you have a tick number, Ticks.duration(10) from net.kyori.adventure.util.Ticks converts it.

If you leave the times out, the default is used: Title.title(title, subtitle) fades in for 10 ticks, stays 70 ticks and fades out for 20 ticks.

Title only, subtitle only

Pass Component.empty() for the part you do not want. A title with only a subtitle is a nice, quiet way to say something:

Only a subtitle
1Title subtitleOnly = Title.title(Component.empty(), Component.text("Round 2", NamedTextColor.AQUA));2player.showTitle(subtitleOnly);

Clearing a title

  • player.clearTitle() removes the title that is on screen right now.
  • player.resetTitle() also puts the timing back to the default values.

Try /tour title and then quickly /tour clear to see the title vanish.

The action bar

The action bar is the single line just above the hotbar. Minecraft uses it for "Now playing: a song". It is perfect for small, changing numbers.

TourCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livestitles-actionbar-bossbar-sounds-toursrcmainjavacomexampletitlesactionbarbossbarsoundstourTourCommand.java

The package com.example.titlesactionbarbossbarsoundstour is the folder path com/example/titlesactionbarbossbarsoundstour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • titles-actionbar-bossbar-sounds-tour/
    • src/main/
      • java/com/example/titlesactionbarbossbarsoundstour/Package com.example.titlesactionbarbossbarsoundstour
        • TourCommand.javayou are hereCommand (BasicCommand)
        • TourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

68private void showActionBar(Player player) {69    player.sendActionBar(Component.text()70        .append(Component.text("Cooldown: ", NamedTextColor.GRAY))71        .append(Component.text("4.5 s", NamedTextColor.RED))72        .build());73}
  1. Takes one component. There is no timing to give: Minecraft shows the line for about three seconds.
  2. The builder from the previous page: it collects the gray label and the red number into one component.
Cooldown: 4.5 s

Because the line fades after about three seconds, a changing value, such as a cooldown, is shown by sending the action bar again every few ticks. A new action bar replaces the old one at once. There is no "clear" method; to hide it, send an empty component with player.sendActionBar(Component.empty()).

With MiniMessage the same line is shorter. This is what you would write for a health display:

Action bar with MiniMessage
1player.sendActionBar(MiniMessage.miniMessage().deserialize(2    "<red>Health: <white><health></white>",3    Placeholder.unparsed("health", String.valueOf(Math.round(player.getHealth())))));

Boss bars

A boss bar is the bar at the top of the screen that the Ender Dragon and the Wither show. It has a name, a colored bar that is filled to some amount, and optionally notches. Plugins use it for anything with progress: event timers, capture points, loading.

Unlike a title, a boss bar stays until you hide it. It is also an object you keep and change: you create the BossBar, show it to players, change its progress later, and finally hide it.

TourCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livestitles-actionbar-bossbar-sounds-toursrcmainjavacomexampletitlesactionbarbossbarsoundstourTourCommand.java

The package com.example.titlesactionbarbossbarsoundstour is the folder path com/example/titlesactionbarbossbarsoundstour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • titles-actionbar-bossbar-sounds-tour/
    • src/main/
      • java/com/example/titlesactionbarbossbarsoundstour/Package com.example.titlesactionbarbossbarsoundstour
        • TourCommand.javayou are hereCommand (BasicCommand)
        • TourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

75private void showBossBar(Player player) {76    BossBar bar = BossBar.bossBar(77        Component.text("Loading the arena...", NamedTextColor.GREEN),78        0.0f,79        BossBar.Color.GREEN,80        BossBar.Overlay.PROGRESS);81    player.showBossBar(bar);82 83    plugin.getServer().getScheduler().runTaskTimer(plugin, task -> {84        float next = Math.min(1.0f, bar.progress() + 0.1f);85        bar.progress(next);86        if (next >= 1.0f) {87            player.hideBossBar(bar);88            task.cancel();89        }90    }, 10L, 10L);91}
  1. Creates the bar with four things: a name, the progress, a color and an overlay (the shape).
  2. Progress is a number from 0.0 (empty) to 1.0 (full). The f makes it a float, Java's small decimal number. Any number outside 0 to 1 throws an IllegalArgumentException.
  3. Pink, blue, red, green, yellow, purple or white.
  4. A plain bar. Others are notched into 6, 10, 12 or 20 segments.
  5. Puts the bar on this player's screen. The bar is empty at first.
  6. Starts a repeating task: after 10 ticks, and then every 10 ticks, the code in the braces runs. The part task -> { ... } is a lambda: a small piece of code handed over to run later. task is a handle you can use to stop the repeating task.
  7. Takes a little step forward but never goes past 1.0, so a rounding error cannot throw an exception.
  8. Changes the bar. Every player who sees this bar sees it move at once, because they all share the same BossBar object.
  9. Removes the bar from the screen when it is full.
  10. Stops the repeating task. Forget this and the task keeps running forever.
Loading the arena...

Colors and overlays

The seven colors are pink, blue, red, green, yellow, purple and white. The overlay decides whether the bar is one smooth strip (PROGRESS) or cut into 6, 10, 12 or 20 segments (NOTCHED_6 and so on). The previews below show four of the colors; the notches are only drawn by the real game.

Pink bar
Blue bar
Red bar
Purple bar
SetterWhat it changes
bar.name(component)The text above the bar.
bar.progress(0.5f)How full the bar is.
bar.color(BossBar.Color.RED)The bar color.
bar.overlay(BossBar.Overlay.NOTCHED_10)Plain or notched.
bar.addFlag(BossBar.Flag.DARKEN_SCREEN)Optional effects: DARKEN_SCREEN, PLAY_BOSS_MUSIC and CREATE_WORLD_FOG.

These setters change the bar in place, and everyone who is watching sees the change immediately.

Sounds

Minecraft has hundreds of sounds: a chest opening, a creeper hissing, a note block pling. Playing one needs three decisions: which sound, how (volume and pitch) and to whom.

Which sound

Paper lists every sound as a constant on org.bukkit.Sound, for example Sound.ENTITY_EXPERIENCE_ORB_PICKUP (the ding when you collect XP) or Sound.BLOCK_NOTE_BLOCK_PLING. In IntelliJ type Sound. and use autocomplete, or look names up in the Names Lookup tool. The name usually says what it is: first the source (ENTITY, BLOCK, UI, MUSIC) and then the sound.

Volume and pitch

  • Volume is 1.0 for normal. Lower is quieter. Higher than 1.0 does not make the sound louder; it makes the sound reach farther, which matters when you play a sound at a place instead of straight to a player.
  • Pitch is 1.0 for normal. Lower is deeper and slower, higher is squeakier and faster. The useful range is 0.5 to 2.0. This is how note blocks make different notes from one sound.

The Adventure way

Adventure describes a sound as an object with a name, a source, a volume and a pitch. The source says which of the player's volume sliders (Master, Music, Blocks, Hostile Creatures, Players and so on) controls it:

TourCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livestitles-actionbar-bossbar-sounds-toursrcmainjavacomexampletitlesactionbarbossbarsoundstourTourCommand.java

The package com.example.titlesactionbarbossbarsoundstour is the folder path com/example/titlesactionbarbossbarsoundstour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • titles-actionbar-bossbar-sounds-tour/
    • src/main/
      • java/com/example/titlesactionbarbossbarsoundstour/Package com.example.titlesactionbarbossbarsoundstour
        • TourCommand.javayou are hereCommand (BasicCommand)
        • TourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

93private void playSounds(Player player) {94    float[] pitches = {0.5f, 1.0f, 2.0f};95    for (int i = 0; i < pitches.length; i++) {96        Sound sound = Sound.sound(org.bukkit.Sound.BLOCK_NOTE_BLOCK_PLING, Sound.Source.MASTER, 1.0f, pitches[i]);97        plugin.getServer().getScheduler().runTaskLater(plugin, () -> player.playSound(sound), i * 10L);98    }99}
  1. Three pitches: low, normal and high. An array holds several values of one type. 0.5f is a decimal float.
  2. Builds an Adventure sound. Two classes are both called Sound: Adventure's net.kyori.adventure.sound.Sound (imported in this file) and Paper's org.bukkit.Sound with the constants. The code writes org.bukkit.Sound in full for the constant so Java knows which one is meant.
  3. Which volume slider applies. MASTER is the general one.
  4. Volume 1.0 and one of the three pitches.
  5. Runs the code once, later. The last number is the delay in ticks, so the three notes play at 0, 10 and 20 ticks.
  6. Plays the sound for this player only, at the player's own location.

The same playSound(Sound) method exists on every audience. To play a sound for everyone online, call it on the server: getServer().playSound(sound). Each player hears it at their own location.

The Bukkit way

Paper also keeps the classic method, which you will see in a lot of code. It plays a sound at a place in the world. Try it with /tour orb:

TourCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livestitles-actionbar-bossbar-sounds-toursrcmainjavacomexampletitlesactionbarbossbarsoundstourTourCommand.java

The package com.example.titlesactionbarbossbarsoundstour is the folder path com/example/titlesactionbarbossbarsoundstour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • titles-actionbar-bossbar-sounds-tour/
    • src/main/
      • java/com/example/titlesactionbarbossbarsoundstour/Package com.example.titlesactionbarbossbarsoundstour
        • TourCommand.javayou are hereCommand (BasicCommand)
        • TourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

101private void playOrbSound(Player player) {102    player.playSound(player.getLocation(), org.bukkit.Sound.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.PLAYERS, 1.0f, 1.0f);103}
  1. The place where the sound comes from. Here, the player's own position.
  2. The same idea as the Adventure source: which volume slider controls it.
AdventureBukkit
Callplayer.playSound(Sound.sound(...))player.playSound(location, sound, category, volume, pitch)
Who hears itThe audience you call it on.Only the player you call it on, but it is played from a location. world.playSound(location, ...) plays for everyone near.
Source or categorySound.Source.MASTER, PLAYER, UI, ...SoundCategory.MASTER, PLAYERS, UI, ...
NamesThe same constants, or a custom Key.The same constants, or a text name.

Both end up playing the same thing. Prefer the Adventure style in new plugins because it is the same API that titles and boss bars use, and because you can build a Sound once and reuse it. To stop a sound early, call player.stopSound(sound).

Project: a /countdown command

Now we combine all four. /countdown 10 starts a ten second countdown for everyone. Every second shows a big number as a title, moves a boss bar down, and plays a pling. The last three seconds turn red and the pling gets higher. When it reaches zero, "GO!" appears with a level-up sound. /countdown stop cancels it.

The plan, as small pieces:

  1. Create the boss bar once

    One BossBar object is kept in a field of a class called Countdown, so everyone shares it.

  2. Start a repeating task

    A task that runs once a second (every 20 ticks) calls a method named tick().

  3. In each tick, update everything

    Count down one second, then update the bar, show a title, and play a sound.

  4. At zero, finish

    Show "GO!", play a sound, cancel the task and hide the bar.

The Countdown class

Countdown.javaCompiles on Paper 26.3Compile Lab
Where this file livestitles-actionbar-bossbar-sounds-countdownsrcmainjavacomexampletitlesactionbarbossbarsoundscountdownCountdown.java

The package com.example.titlesactionbarbossbarsoundscountdown is the folder path com/example/titlesactionbarbossbarsoundscountdown inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • titles-actionbar-bossbar-sounds-countdown/
    • src/main/
      • java/com/example/titlesactionbarbossbarsoundscountdown/Package com.example.titlesactionbarbossbarsoundscountdown
        • Countdown.javayou are hereHelper class
        • CountdownCommand.javaCommand (BasicCommand)
        • CountdownJoinListener.javaListener: reacts to events
        • CountdownPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.titlesactionbarbossbarsoundscountdown;2 3import java.time.Duration;4import net.kyori.adventure.bossbar.BossBar;5import net.kyori.adventure.sound.Sound;6import net.kyori.adventure.text.Component;7import net.kyori.adventure.text.format.NamedTextColor;8import net.kyori.adventure.text.format.TextDecoration;9import net.kyori.adventure.text.minimessage.MiniMessage;10import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;11import net.kyori.adventure.title.Title;12import org.bukkit.entity.Player;13import org.bukkit.plugin.java.JavaPlugin;14import org.bukkit.scheduler.BukkitTask;15 16public final class Countdown {17 18    private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage();19    private static final Title.Times ONE_SECOND_TIMES =20        Title.Times.times(Duration.ZERO, Duration.ofMillis(900), Duration.ZERO);21 22    private final JavaPlugin plugin;23    private final BossBar bossBar = BossBar.bossBar(24        Component.empty(), 1.0f, BossBar.Color.YELLOW, BossBar.Overlay.NOTCHED_10);25 26    private BukkitTask task;27    private int totalSeconds;28    private int secondsLeft;29 30    public Countdown(JavaPlugin plugin) {31        this.plugin = plugin;32    }33 34    public boolean isRunning() {35        return task != null;36    }37 38    public void start(int seconds) {39        totalSeconds = seconds;40        secondsLeft = seconds;41        bossBar.progress(1.0f);42        bossBar.color(BossBar.Color.YELLOW);43 44        for (Player player : plugin.getServer().getOnlinePlayers()) {45            player.showBossBar(bossBar);46        }47        task = plugin.getServer().getScheduler().runTaskTimer(plugin, this::tick, 0L, 20L);48    }49 50    public void stop() {51        if (task == null) {52            return;53        }54        task.cancel();55        task = null;56        plugin.getServer().hideBossBar(bossBar);57    }58 59    public void showTo(Player player) {60        if (isRunning()) {61            player.showBossBar(bossBar);62        }63    }64 65    private void tick() {66        if (secondsLeft == 0) {67            finish();68            return;69        }70 71        boolean lastSeconds = secondsLeft <= 3;72        NamedTextColor color = lastSeconds ? NamedTextColor.RED : NamedTextColor.YELLOW;73 74        bossBar.progress((float) secondsLeft / totalSeconds);75        bossBar.color(lastSeconds ? BossBar.Color.RED : BossBar.Color.YELLOW);76        bossBar.name(MINI_MESSAGE.deserialize(77            "<white>Game starts in <urgency><seconds>",78            Placeholder.styling("urgency", color),79            Placeholder.unparsed("seconds", String.valueOf(secondsLeft))));80 81        Title title = Title.title(82            Component.text(secondsLeft, color, TextDecoration.BOLD),83            Component.text("Get ready!", NamedTextColor.GRAY),84            ONE_SECOND_TIMES);85        plugin.getServer().showTitle(title);86 87        float pitch = lastSeconds ? 2.0f : 1.0f;88        plugin.getServer().playSound(89            Sound.sound(org.bukkit.Sound.BLOCK_NOTE_BLOCK_PLING, Sound.Source.MASTER, 1.0f, pitch));90 91        secondsLeft--;92    }93 94    private void finish() {95        Title title = Title.title(96            Component.text("GO!", NamedTextColor.GREEN, TextDecoration.BOLD),97            Component.empty(),98            Title.Times.times(Duration.ZERO, Duration.ofSeconds(1), Duration.ofMillis(500)));99        plugin.getServer().showTitle(title);100        plugin.getServer().playSound(101            Sound.sound(org.bukkit.Sound.ENTITY_PLAYER_LEVELUP, Sound.Source.MASTER, 1.0f, 1.0f));102        stop();103    }104}
  1. The title timing, made once. No fade, shown for 0.9 seconds, so each new number replaces the old one before the next tick.
  2. The one shared bar. Name and progress are filled in on every tick, so the starting values do not matter.
  3. A handle to the repeating task. When it is null, no countdown is running. BukkitTask is what the scheduler gives back when you start a task.
  4. How many seconds are still to go.
  5. Other classes ask this before starting a second countdown.
  6. Starting values: the bar is full and yellow, because a previous countdown may have left it red.
  7. Everyone who is online right now sees the bar. Players who join later are handled by a listener.
  8. A method reference: "call my tick method". It is a shorter way to write () -> tick().
  9. Start now, then repeat every 20 ticks, which is every second.
  10. Stops the repeating task. Then the field is set to null so isRunning() says false again.
  11. Removes the bar from every player.
  12. True in the last three seconds. Used for the color, the bar color and the pitch.
  13. The progress, for example 7 of 10 seconds left is 0.7. The (float) turns the number into a decimal first. Without it, Java would do whole-number division and the answer would be 0.
  14. A MiniMessage placeholder that applies a color, so <urgency> means "yellow, or red in the last seconds". MiniMessage explains placeholders.
  15. The server is an audience, so this shows the title to every player at once.
  16. "If it is the last seconds, use 2.0, otherwise 1.0". The tick sound goes up in pitch to build tension.
  17. Counts down by one. This is the same as secondsLeft = secondsLeft - 1.
  18. Called when the number reaches zero. It shows "GO!" and cleans up.

This is what players see in the last seconds:

3 Get ready!
Game starts in 3

And at zero:

GO!

The command

CountdownCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livestitles-actionbar-bossbar-sounds-countdownsrcmainjavacomexampletitlesactionbarbossbarsoundscountdownCountdownCommand.java

The package com.example.titlesactionbarbossbarsoundscountdown is the folder path com/example/titlesactionbarbossbarsoundscountdown inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • titles-actionbar-bossbar-sounds-countdown/
    • src/main/
      • java/com/example/titlesactionbarbossbarsoundscountdown/Package com.example.titlesactionbarbossbarsoundscountdown
        • Countdown.javaHelper class
        • CountdownCommand.javayou are hereCommand (BasicCommand)
        • CountdownJoinListener.javaListener: reacts to events
        • CountdownPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

18@Override19public void execute(CommandSourceStack source, String[] args) {20    CommandSender sender = source.getSender();21 22    if (args.length > 0 && args[0].equalsIgnoreCase("stop")) {23        if (countdown.isRunning()) {24            countdown.stop();25            sender.sendRichMessage("<yellow>Countdown stopped.");26        } else {27            sender.sendRichMessage("<red>No countdown is running.");28        }29        return;30    }31 32    int seconds = 10;33    if (args.length > 0) {34        try {35            seconds = Integer.parseInt(args[0]);36        } catch (NumberFormatException exception) {37            sender.sendRichMessage("<red>Usage: /countdown [seconds] or /countdown stop");38            return;39        }40    }41    if (seconds < 1 || seconds > MAX_SECONDS) {42        sender.sendRichMessage("<red>Pick a number of seconds from 1 to <max>.",43            Placeholder.unparsed("max", String.valueOf(MAX_SECONDS)));44        return;45    }46    if (countdown.isRunning()) {47        sender.sendRichMessage("<red>A countdown is already running. Use /countdown stop first.");48        return;49    }50 51    countdown.start(seconds);52}
  1. The player typed /countdown stop. equalsIgnoreCase ignores capital letters.
  2. The default when the player types only /countdown.
  3. Turning text into a number can fail. Whatever goes wrong inside the braces after try is caught below instead of crashing the command.
  4. Converts text such as "10" into the number 10. If the player typed abc, it throws a NumberFormatException.
  5. The player did not type a number. Show the usage and stop. Null, exceptions and stack traces explains try and catch.
  6. Rejects numbers that are too small or too large. || means "or".
  7. Do not start a second countdown on top of the first.
  8. Everything checked: start.

The main class creates one Countdown, registers a listener and the command, and stops the countdown if the server shuts down:

CountdownPlugin.javaCompiles on Paper 26.3Compile Lab
Where this file livestitles-actionbar-bossbar-sounds-countdownsrcmainjavacomexampletitlesactionbarbossbarsoundscountdownCountdownPlugin.java

The package com.example.titlesactionbarbossbarsoundscountdown is the folder path com/example/titlesactionbarbossbarsoundscountdown inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • titles-actionbar-bossbar-sounds-countdown/
    • src/main/
      • java/com/example/titlesactionbarbossbarsoundscountdown/Package com.example.titlesactionbarbossbarsoundscountdown
        • Countdown.javaHelper class
        • CountdownCommand.javaCommand (BasicCommand)
        • CountdownJoinListener.javaListener: reacts to events
        • CountdownPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.titlesactionbarbossbarsoundscountdown;2 3import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;4import org.bukkit.plugin.java.JavaPlugin;5 6public final class CountdownPlugin extends JavaPlugin {7 8    private Countdown countdown;9 10    @Override11    public void onEnable() {12        countdown = new Countdown(this);13 14        getServer().getPluginManager().registerEvents(new CountdownJoinListener(countdown), this);15        getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event ->16            event.registrar().register("countdown", "Start or stop a countdown", new CountdownCommand(countdown)));17    }18 19    @Override20    public void onDisable() {21        countdown.stop();22    }23}
  1. The one Countdown that the command and the listener share. It gets the plugin because the scheduler needs it.
  2. Runs when the server stops or the plugin is disabled. We cancel the task and hide the bar so nothing is left behind.

The last piece shows the bar to players who join in the middle of a countdown:

CountdownJoinListener.javaCompiles on Paper 26.3Compile Lab
Where this file livestitles-actionbar-bossbar-sounds-countdownsrcmainjavacomexampletitlesactionbarbossbarsoundscountdownCountdownJoinListener.java

The package com.example.titlesactionbarbossbarsoundscountdown is the folder path com/example/titlesactionbarbossbarsoundscountdown inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • titles-actionbar-bossbar-sounds-countdown/
    • src/main/
      • java/com/example/titlesactionbarbossbarsoundscountdown/Package com.example.titlesactionbarbossbarsoundscountdown
        • Countdown.javaHelper class
        • CountdownCommand.javaCommand (BasicCommand)
        • CountdownJoinListener.javayou are hereListener: reacts to events
        • CountdownPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

15@EventHandler16public void onJoin(PlayerJoinEvent event) {17    countdown.showTo(event.getPlayer());18}
  1. If a countdown is running, this adds the joining player as a viewer of the shared bar.
  • titles-actionbar-bossbar-sounds-countdown/
    • src/
      • main/
        • java/
          • com/example/titlesactionbarbossbarsoundscountdown/
            • CountdownPlugin.javaCreates the Countdown, registers the command and the listener
            • Countdown.javaThe boss bar, the repeating task and the title and sounds for each second
            • CountdownCommand.java/countdown [seconds] and /countdown stop
            • CountdownJoinListener.javaShows the bar to players who join mid-countdown
        • resources/
          • plugin.ymlPlugin name, version and the titlescountdown.use permission

You can download this project and run it. Join your test server, give yourself operator status (the permission defaults to op), and type /countdown 10.

Mistakes to avoid

  • Forgetting to hide a boss bar. It stays on the screen. Hide it when you are done and in onDisable.
  • Progress outside 0 to 1. Computing done / total with whole numbers gives 0 or 1, and rounding can push a value to 1.0000001, which throws an exception. Use (float) done / total and Math.min or Math.max to keep it in range.
  • Forgetting to cancel the repeating task. A task that is never canceled keeps running and keeps sending packets after the countdown is over.
  • Mixing up the two Sound classes. If you import org.bukkit.Sound and also Adventure's Sound, Java refuses. Import one and write the other with its full name.
  • Setting a huge volume to make a sound louder. Volume above 1.0 only makes it audible from farther away.
  • Showing a title for something small. A title blocks the middle of the screen. Use the action bar for small things.
  • Sending an action bar once and expecting it to stay. It fades after about three seconds. Resend it while it matters.
Try it

Health in the action bar

Write a command /healthbar that shows the player's health in the action bar, like Health: 20 / 20, updating twice a second for ten seconds.

Hint 1

You need a repeating task that runs every 10 ticks. Count how many times it ran, and call cancel() when it reaches 20.

Hint 2

Maximum health is an attribute: player.getAttribute(Attribute.MAX_HEALTH) can be null, so check before using it. Current health is player.getHealth().

Show the solution

The command starts the task. The task class extends BukkitRunnable, a ready-made repeating task, and cancels itself when it has run 20 times or the player has left:

HealthBarTask.javaCompiles on Paper 26.3Compile Lab
Where this file livestitles-actionbar-bossbar-sounds-exercisesrcmainjavacomexampletitlesactionbarbossbarsoundsexerciseHealthBarTask.java

The package com.example.titlesactionbarbossbarsoundsexercise is the folder path com/example/titlesactionbarbossbarsoundsexercise inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • titles-actionbar-bossbar-sounds-exercise/
    • src/main/
      • java/com/example/titlesactionbarbossbarsoundsexercise/Package com.example.titlesactionbarbossbarsoundsexercise
        • ExercisePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • HealthBarCommand.javaCommand (BasicCommand)
        • HealthBarTask.javayou are hereHelper class
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

21@Override22public void run() {23    if (runsLeft == 0 || !player.isOnline()) {24        cancel();25        return;26    }27    runsLeft--;28 29    AttributeInstance maxHealth = player.getAttribute(Attribute.MAX_HEALTH);30    double max = maxHealth == null ? 20.0 : maxHealth.getValue();31 32    player.sendActionBar(MiniMessage.miniMessage().deserialize(33        "<red>Health: <white><health></white> <gray>/</gray> <white><max></white>",34        Placeholder.unparsed("health", String.valueOf(Math.round(player.getHealth()))),35        Placeholder.unparsed("max", String.valueOf(Math.round(max)))));36}
  1. Stop after 20 runs (ten seconds), or when the player has quit, so the task never talks to someone who is gone.
  2. The maximum health can be missing, which is why the next line falls back to 20.
  3. Health is a decimal such as 18.5. This turns it into a whole number for display.
HealthBarCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livestitles-actionbar-bossbar-sounds-exercisesrcmainjavacomexampletitlesactionbarbossbarsoundsexerciseHealthBarCommand.java

The package com.example.titlesactionbarbossbarsoundsexercise is the folder path com/example/titlesactionbarbossbarsoundsexercise inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • titles-actionbar-bossbar-sounds-exercise/
    • src/main/
      • java/com/example/titlesactionbarbossbarsoundsexercise/Package com.example.titlesactionbarbossbarsoundsexercise
        • ExercisePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • HealthBarCommand.javayou are hereCommand (BasicCommand)
        • HealthBarTask.javaHelper class
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

22new HealthBarTask(player).runTaskTimer(plugin, 0L, 10L);
  1. Start now and repeat every 10 ticks, which is twice a second.
Health: 18 / 20
Try it

A warning sound

In the countdown, play a deep, low ENTITY_ENDER_DRAGON_GROWL sound when the countdown is started, using the Adventure Sound.sound method.

Hint

Add it to the end of start(int seconds). Use a pitch below 1.0 for a deeper growl, and play it on plugin.getServer().

Show the solution
Countdown.java (new lines at the end of start)
1plugin.getServer().playSound(2    Sound.sound(org.bukkit.Sound.ENTITY_ENDER_DRAGON_GROWL, Sound.Source.MASTER, 0.5f, 0.7f));

Volume 0.5 keeps it from being too loud, and pitch 0.7 makes it deeper.

Recap

  • Titles use player.showTitle(Title.title(title, subtitle, Title.Times.times(fadeIn, stay, fadeOut))). Without times, the defaults are 0.5, 3.5 and 1 second. clearTitle() hides it.
  • The action bar is player.sendActionBar(component). It fades after about three seconds, so resend it for changing values, at most twice a second.
  • A boss bar is a shared object: create it with BossBar.bossBar(name, progress, color, overlay), show it with showBossBar, change it with progress, name and color, and always hideBossBar it.
  • Progress is a float from 0.0 to 1.0.
  • Sounds: player.playSound(Sound.sound(org.bukkit.Sound.X, Sound.Source.MASTER, volume, pitch)), or the classic location version with a SoundCategory. Pitch 1.0 is normal; volume above 1.0 only widens the range.
  • For things that change over time, use a repeating scheduler task and cancel it when you are done.

Quick quiz

  1. You show a boss bar for an event. When the event ends, you forget to call hideBossBar. What happens?

  2. Which progress value throws an IllegalArgumentException?

  3. How long does an action bar message stay visible if you never send another one?

  4. What does a pitch of 2.0 do compared to 1.0?

  5. Why do the two lines import org.bukkit.Sound; and import net.kyori.adventure.sound.Sound; in the same file not work?

Next steps