Paper Plugin Guide
File mode0

Paper essentials

Scoreboards, sidebars and teams

Sidebars with live stats, name colors and teams.

Intermediate37 min read

The panel on the right side of the screen with a title and a list of lines is a scoreboard sidebar. On this page you will build one that updates live without flickering, give players colored name tags with teams, and show hearts under their heads.

What is a scoreboard?

Minecraft has a built-in system for keeping track of numbers and groups of players. It is called the scoreboard. Vanilla players meet it through the /scoreboard and /team commands. Plugins use the same system, and that is why almost every server sidebar, colored rank tag and "hearts under the name" display you have ever seen is a scoreboard in disguise.

A scoreboard has a few parts, and each one has a job:

The parts of a scoreboard A Scoreboard holds Objectives and Teams. An Objective such as kills holds Scores, one per entry. An objective set to DisplaySlot.SIDEBAR is drawn on the right of the screen. A Team groups entries and styles their names. Scoreboard: the whole board Objective kills a list of scores Steve Score 12 Alex Score 7 Sam Score 3 Team red members: Steve, Alex gives them a name color, a prefix, and friendly fire rules DisplaySlot .SIDEBAR What the player sees kills Steve 12 Alex 7 Sam 3 (the sidebar, right of screen) A player sees one board at a time. Give it to them: player.setScoreboard(board)
The parts of a scoreboard: objectives hold scores for entries, teams group entries, and display slots decide where an objective is shown.
  • An objective is one list of numbers with a name, for example "kills" or "sidebar". It has a title and a criteria, which says what changes the numbers. The criteria Criteria.DUMMY means "only my plugin changes them". Others, such as Criteria.HEALTH or Criteria.DEATH_COUNT, are updated by the game itself.
  • An entry is whatever owns a number. Usually that is a player's name, but it can be any text, such as "line-1".
  • A score is the number an entry has in an objective.
  • A team is a group of entries that share a look and some rules: a name prefix, a color, whether name tags are visible, whether members push each other.
  • A display slot is a place on the screen where one objective is shown: the SIDEBAR, BELOW_NAME (under every player's name tag) or PLAYER_LIST (the tab list).

One scoreboard for everyone, or one per player

Paper gives you two ways to get a scoreboard from the scoreboard manager:

Java
1ScoreboardManager manager = Bukkit.getScoreboardManager();2Scoreboard main = manager.getMainScoreboard();3Scoreboard mine = manager.getNewScoreboard();
  1. The one scoreboard every player starts on. It is saved with the world, and the vanilla /scoreboard and /team commands change it.
  2. A fresh, empty scoreboard that only exists while the server runs. Nobody sees it until you give it to a player.

Each player looks at exactly one scoreboard at a time. player.setScoreboard(board) switches which one. That gives you two designs:

Shared: the main scoreboard Per player: getNewScoreboard() Steve Alex Sam Main scoreboard one sidebar one set of teams saved with the world Steve Alex Sam Board A Kills: 12 Board B Kills: 3 Board C Kills: 0 player.setScoreboard(board)
Shared main scoreboard on the left, one scoreboard per player on the right.
  • Shared main scoreboard. Easy, and survives restarts, but everyone sees the same sidebar. Good for team colors and rules that are the same for everybody.
  • One new scoreboard per player. Every player can have their own lines ("Your kills: 12"). This is what most sidebars use, and it is what the example plugin does.

Bukkit.getScoreboardManager() returns null before the first world has loaded. Calling it inside onEnable or inside an event handler is fine, because Paper loads plugins after the worlds by default.

Your first sidebar

Here is a listener that gives every joining player a small sidebar. Read the notes, then compare the picture below the code with what the code does line by line.

SidebarOnJoin.javaCompiles on Paper 26.3Compile Lab
Where this file livesscoreboards-first-sidebarsrcmainjavacomexamplescoreboardsfirstsidebarSidebarOnJoin.java

The package com.example.scoreboardsfirstsidebar is the folder path com/example/scoreboardsfirstsidebar 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.

  • scoreboards-first-sidebar/
    • src/main/
      • java/com/example/scoreboardsfirstsidebar/Package com.example.scoreboardsfirstsidebar
        • FirstSidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • SidebarOnJoin.javayou are hereListener: reacts to events
      • 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.

20@EventHandler21public void onJoin(PlayerJoinEvent event) {22    Player player = event.getPlayer();23 24    Scoreboard scoreboard = Bukkit.getScoreboardManager().getNewScoreboard();25 26    Objective sidebar = scoreboard.registerNewObjective(27        "sidebar", Criteria.DUMMY, miniMessage.deserialize("<gold><bold>My Server"));28    sidebar.setDisplaySlot(DisplaySlot.SIDEBAR);29    sidebar.numberFormat(NumberFormat.blank());30 31    sidebar.getScore("line-name").setScore(3);32    sidebar.getScore("line-name").customName(miniMessage.deserialize(33        "<gray>Player: <white><name>", Placeholder.unparsed("name", player.getName())));34 35    sidebar.getScore("line-gap").setScore(2);36    sidebar.getScore("line-gap").customName(miniMessage.deserialize(" "));37 38    sidebar.getScore("line-ip").setScore(1);39    sidebar.getScore("line-ip").customName(miniMessage.deserialize("<yellow>play.example.com"));40 41    player.setScoreboard(scoreboard);42}
  1. A brand new scoreboard just for this player. Using the main one here would give everyone the same lines.
  2. Creates the objective. "sidebar" is its internal name, Criteria.DUMMY means only we change its numbers, and the last argument is the title shown on screen, written in MiniMessage.
  3. Pins the objective to the sidebar. An objective with no display slot exists but nobody sees it.
  4. Hides the red numbers that normally appear at the right end of every line. They are the scores themselves, and in a text sidebar they are just noise.
  5. Gives the entry "line-name" the score 3. The sidebar sorts lines from the highest score at the top to the lowest at the bottom.
  6. Replaces the text shown for this entry. Without it the sidebar would print the entry name, line-name, instead of your text. Placeholder.unparsed inserts the player's name as plain text, so odd characters in it can never be read as MiniMessage tags.
  7. Hands the finished scoreboard to the player. Until this line runs, they still see the main scoreboard.
Player: Steve

play.example.com

The main class only registers the listener, exactly like in Events and listeners:

FirstSidebarPlugin.javaCompiles on Paper 26.3Compile Lab
Where this file livesscoreboards-first-sidebarsrcmainjavacomexamplescoreboardsfirstsidebarFirstSidebarPlugin.java

The package com.example.scoreboardsfirstsidebar is the folder path com/example/scoreboardsfirstsidebar 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.

  • scoreboards-first-sidebar/
    • src/main/
      • java/com/example/scoreboardsfirstsidebar/Package com.example.scoreboardsfirstsidebar
        • FirstSidebarPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
        • SidebarOnJoin.javaListener: reacts to events
      • 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.scoreboardsfirstsidebar;2 3import org.bukkit.plugin.java.JavaPlugin;4 5public final class FirstSidebarPlugin extends JavaPlugin {6 7    @Override8    public void onEnable() {9        getServer().getPluginManager().registerEvents(new SidebarOnJoin(), this);10    }11}

Lines, scores and custom names

A sidebar has no real "lines". It is a list of entries sorted by score, so a line's position comes from its number:

PartWhat it does for a sidebar line
Entry name ("line-name")Identifies the line. It must be unique, and you use it to find the line again later. Players never see it when a custom name is set.
Score (setScore(3))Decides the position. Higher numbers are higher up. Give the top line the biggest number.
Custom name (customName(component))The text players actually read. It is a component, so colors and styles work.
Number format (NumberFormat.blank())Hides, restyles or replaces the number at the right end of the line.

Two rules come from Minecraft itself, not from Paper. The sidebar shows at most 15 lines, the 15 with the highest scores. And every entry must be a different string, so a blank spacer line needs its own entry name ("line-gap"), even though its custom name is just a space.

Making it live

A sidebar that never changes is not very useful. A good sidebar shows kills, deaths, how many players are online and how long you have played, and keeps those numbers fresh. The plan is a repeating task, as you learned in the scheduler chapter: once per second, compute the new text and put it on every player's sidebar.

ScoreboardsDemoPlugin.javaCompiles on Paper 26.3Compile Lab
Where this file livesscoreboards-demosrcmainjavacomexamplescoreboardsdemoScoreboardsDemoPlugin.java

The package com.example.scoreboardsdemo is the folder path com/example/scoreboardsdemo 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.

  • scoreboards-demo/
    • src/main/
      • java/com/example/scoreboardsdemo/Package com.example.scoreboardsdemo
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerBoard.javaHelper class
        • Playtime.javaHelper class
        • ScoreboardsDemoPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
        • SidebarCommand.javaCommand (BasicCommand)
      • 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@Override16public void onEnable() {17    boards = new BoardManager();18    getServer().getPluginManager().registerEvents(new ConnectionListener(boards), this);19    registerCommand("sidebar", "Show or hide your sidebar", new SidebarCommand(boards));20    refreshTask = getServer().getScheduler().runTaskTimer(21        this, boards::refreshAll, TICKS_PER_SECOND, TICKS_PER_SECOND);22}
  1. One object that remembers every player's scoreboard. Section "The complete demo" shows it.
  2. Registers /sidebar, so a player can hide the sidebar if they dislike it. Always give players an off switch.
  3. Runs refreshAll on the main thread, first after 20 ticks and then every 20 ticks. Twenty ticks are one second, so this is the "update every second" loop. boards::refreshAll is a method reference: a short way to say "call this method".

Avoid flicker: change lines in place

The tempting way to update a sidebar is to wipe it and write it again every second. That makes lines vanish for a moment and come back, which players see as flicker. The calm way is to create each line once and only change its custom name afterwards.

Flickers: wipes every line, every secondHas a mistake
1for (int index = 0; index < lines.size(); index++) {2    scoreboard.resetScores("line-" + index);3    sidebar.getScore("line-" + index).setScore(lines.size() - index);4    sidebar.getScore("line-" + index).customName(lines.get(index));5}

The example does the calm version. Every line is created with its score once, in the constructor, and then setLine only touches a line when its text really changed:

PlayerBoard.javaCompiles on Paper 26.3Compile Lab
Where this file livesscoreboards-demosrcmainjavacomexamplescoreboardsdemoPlayerBoard.java

The package com.example.scoreboardsdemo is the folder path com/example/scoreboardsdemo 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.

  • scoreboards-demo/
    • src/main/
      • java/com/example/scoreboardsdemo/Package com.example.scoreboardsdemo
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerBoard.javayou are hereHelper class
        • Playtime.javaHelper class
        • ScoreboardsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • SidebarCommand.javaCommand (BasicCommand)
      • 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.

91private void setLine(int index, Component text) {92    if (text.equals(shownLines[index])) {93        return;94    }95    shownLines[index] = text;96    sidebar.getScore(entryName(index)).customName(text);97}
  1. Components know how to compare themselves. If the new text is the same as what is already on screen, do nothing, so no packet is sent. A sidebar for 100 players that did not change costs almost nothing.
  2. Remember what the line shows now, to compare with next second.
  3. The only call that reaches the player. The entry and its score stay exactly where they are, so nothing blinks.

The method that builds the new text for one player reads their numbers from Minecraft's built-in statistics. Statistics are numbers the game already tracks for every player, so you do not have to count kills yourself:

PlayerBoard.javaCompiles on Paper 26.3Compile Lab
Where this file livesscoreboards-demosrcmainjavacomexamplescoreboardsdemoPlayerBoard.java

The package com.example.scoreboardsdemo is the folder path com/example/scoreboardsdemo 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.

  • scoreboards-demo/
    • src/main/
      • java/com/example/scoreboardsdemo/Package com.example.scoreboardsdemo
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerBoard.javayou are hereHelper class
        • Playtime.javaHelper class
        • ScoreboardsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • SidebarCommand.javaCommand (BasicCommand)
      • 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.

64void refresh(Player viewer, Collection<? extends Player> online) {65    int kills = viewer.getStatistic(Statistic.MOB_KILLS) + viewer.getStatistic(Statistic.PLAYER_KILLS);66    int deaths = viewer.getStatistic(Statistic.DEATHS);67    String playtime = Playtime.format(viewer.getStatistic(Statistic.PLAY_ONE_MINUTE));68 69    setLine(LINE_KILLS, miniMessage.deserialize("<gray>Kills: <green>" + kills));70    setLine(LINE_DEATHS, miniMessage.deserialize("<gray>Deaths: <red>" + deaths));71    setLine(LINE_ONLINE, miniMessage.deserialize("<gray>Online: <aqua>" + online.size()));72    setLine(LINE_PLAYTIME, miniMessage.deserialize("<gray>Playtime: <white>" + playtime));73 74    for (Player player : online) {75        hearts.getScore(player.getName()).setScore((int) Math.ceil(player.getHealth()));76        Team team = player.hasPermission("scoreboardsdemo.vip") ? vipTeam : memberTeam;77        if (!team.hasEntry(player.getName())) {78            team.addEntry(player.getName());79        }80    }81}
  1. Reads a statistic. Kills are split into mob kills and player kills, so the sidebar adds them. Statistics survive restarts because the game saves them.
  2. The total time played, counted in ticks even though the name says "minute". Playtime.format turns ticks into text like 1h 05m.
  3. Builds a MiniMessage string from the number. Numbers cannot contain tags, so joining them in is safe.
  4. How many players are online right now. Every player's sidebar shows the same value.

After you build and join, this is what the finished sidebar looks like:

Kills: 12
Deaths: 3
Online: 5
Playtime: 1h 05m

play.example.com

Teams: colors, prefixes and rules

A team is the part of the scoreboard that styles players. Anyone on a team gets the team's look wherever their name appears: the name tag above their head, the tab list and the death messages. Teams also carry rules, like "members cannot hurt each other".

TeamSnippets.javaCompiles on Paper 26.3Compile Lab
Where this file livesscoreboards-snippetssrcmainjavacomexamplescoreboardssnippetsTeamSnippets.java

The package com.example.scoreboardssnippets is the folder path com/example/scoreboardssnippets 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.

  • scoreboards-snippets/
    • src/main/java/com/example/scoreboardssnippets/Package com.example.scoreboardssnippets
      • TeamSnippets.javayou are hereHelper 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.

20public static Team createRedTeam() {21    Scoreboard scoreboard = Bukkit.getScoreboardManager().getMainScoreboard();22    Team red = scoreboard.getTeam("red");23    if (red == null) {24        red = scoreboard.registerNewTeam("red");25    }26    red.displayName(Component.text("Red team", NamedTextColor.RED));27    red.prefix(MiniMessage.miniMessage().deserialize("<red>[Red] "));28    red.color(NamedTextColor.RED);29    red.setAllowFriendlyFire(false);30    red.setCanSeeFriendlyInvisibles(true);31    return red;32}
  1. Teams belong to one scoreboard. This one lives on the main scoreboard, which is saved with the world, so it still exists after a restart.
  2. Because of that saving, always ask for the team first and only create it when it is missing. Registering a name that already exists throws an IllegalArgumentException.
  3. Text placed in front of the player's name. It is a component, so any MiniMessage works.
  4. The team color. It colors the player's name and the glowing outline. It only accepts the 16 named colors; for any other color use a prefix.
  5. Team members cannot damage each other with melee or arrows.
  6. Teammates under an invisibility effect show up as faint outlines to each other.

Adding a player to a team is one line, team.addPlayer(player). A player can be on only one team per scoreboard, so adding them to a second team quietly removes them from the first. Under the hood a team stores entries (text), and for a player the entry is their name. team.addEntry(String) does the same thing with a plain string, and team.addEntity(entity) puts a mob on a team.

Team options

Three behaviors are options. You set each one with team.setOption(option, status). The status says who the option applies to:

OptionWhat it controls
Team.Option.NAME_TAG_VISIBILITYWho can see the name above a member's head.
Team.Option.DEATH_MESSAGE_VISIBILITYWho gets to read the death messages of members.
Team.Option.COLLISION_RULEWhether members push others and get pushed.
StatusName tags and death messagesCollisions
ALWAYSEveryone sees them.Members push and get pushed by everyone.
NEVERNobody sees them.Nobody pushes members, and members push nobody.
FOR_OTHER_TEAMSHidden from players on other teams, so only teammates see them.Members collide only with players on other teams.
FOR_OWN_TEAMHidden from teammates, so only players on other teams see them.Members collide only with their own teammates.
TeamSnippets.javaCompiles on Paper 26.3Compile Lab
Where this file livesscoreboards-snippetssrcmainjavacomexamplescoreboardssnippetsTeamSnippets.java

The package com.example.scoreboardssnippets is the folder path com/example/scoreboardssnippets 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.

  • scoreboards-snippets/
    • src/main/java/com/example/scoreboardssnippets/Package com.example.scoreboardssnippets
      • TeamSnippets.javayou are hereHelper 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.

38public static void hideNameTag(Team team) {39    team.setOption(Team.Option.NAME_TAG_VISIBILITY, Team.OptionStatus.NEVER);40}

The snippet above hides the name tag from everybody with NEVER. Hiding name tags is a classic hide-and-seek trick. A team of hiders with FOR_OTHER_TEAMS keeps tags visible to each other but hides them from the seekers. Setting COLLISION_RULE to NEVER stops players from blocking each other in lobbies.

Hearts under the name and in the tab list

Two display slots besides the sidebar show a number for every player at once:

  • DisplaySlot.BELOW_NAME: the number (or hearts) under each player's name tag.
  • DisplaySlot.PLAYER_LIST: the number at the right end of each row of the tab list.

Their objectives work exactly like the sidebar one, except that the entries are player names. Setting RenderType.HEARTS shows the number as hearts instead of digits, where 2 points are one heart.

TeamSnippets.javaCompiles on Paper 26.3Compile Lab
Where this file livesscoreboards-snippetssrcmainjavacomexamplescoreboardssnippetsTeamSnippets.java

The package com.example.scoreboardssnippets is the folder path com/example/scoreboardssnippets 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.

  • scoreboards-snippets/
    • src/main/java/com/example/scoreboardssnippets/Package com.example.scoreboardssnippets
      • TeamSnippets.javayou are hereHelper 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.

50public static void showHealthInTabList(Scoreboard scoreboard) {51    Objective health = scoreboard.registerNewObjective(52        "tabhealth", Criteria.HEALTH, Component.text("Health"), RenderType.HEARTS);53    health.setDisplaySlot(DisplaySlot.PLAYER_LIST);54}
  1. A game-controlled criteria: the game keeps these scores equal to each player's health on that scoreboard, so you do not write any update code.
  2. Draws hearts instead of a number. RenderType.INTEGER is the plain number.
  3. Shows it in the tab list. Use BELOW_NAME for the line under the name tag.

The demo plugin takes the second route: a DUMMY objective whose scores it sets by hand every second, in the loop at the end of refresh. That way the code behaves the same on every player's own scoreboard, and it also shows how to write numbers for other players:

PlayerBoard.javaCompiles on Paper 26.3Compile Lab
Where this file livesscoreboards-demosrcmainjavacomexamplescoreboardsdemoPlayerBoard.java

The package com.example.scoreboardsdemo is the folder path com/example/scoreboardsdemo 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.

  • scoreboards-demo/
    • src/main/
      • java/com/example/scoreboardsdemo/Package com.example.scoreboardsdemo
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerBoard.javayou are hereHelper class
        • Playtime.javaHelper class
        • ScoreboardsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • SidebarCommand.javaCommand (BasicCommand)
      • 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.

34PlayerBoard() {35    sidebar = scoreboard.registerNewObjective(36        "sidebar", Criteria.DUMMY, miniMessage.deserialize("<gold><bold>My Server"));37    sidebar.setDisplaySlot(DisplaySlot.SIDEBAR);38    sidebar.numberFormat(NumberFormat.blank());39    for (int index = 0; index < LINE_COUNT; index++) {40        sidebar.getScore(entryName(index)).setScore(LINE_COUNT - index);41    }42    setLine(0, Component.space());43    setLine(5, Component.space());44    setLine(6, miniMessage.deserialize("<yellow>play.example.com"));45 46    hearts = scoreboard.registerNewObjective(47        "hearts", Criteria.DUMMY, Component.text("HP"), RenderType.HEARTS);48    hearts.setDisplaySlot(DisplaySlot.BELOW_NAME);49 50    vipTeam = scoreboard.registerNewTeam("vip");51    vipTeam.prefix(miniMessage.deserialize("<gold>[VIP] "));52    vipTeam.color(NamedTextColor.GOLD);53    vipTeam.setOption(Team.Option.COLLISION_RULE, Team.OptionStatus.NEVER);54 55    memberTeam = scoreboard.registerNewTeam("member");56    memberTeam.color(NamedTextColor.GRAY);57    memberTeam.setOption(Team.Option.COLLISION_RULE, Team.OptionStatus.NEVER);58}
  1. Creates every sidebar entry once, with its score. Index 0 gets the highest score, 7, so it is the top line.
  2. Spacer lines are real lines with an empty-looking custom name.
  3. An objective that draws hearts. Its scores are written by refresh.
  4. The hearts appear under every player's name tag.
  5. The VIP team: gold prefix and gold name.
  6. With NEVER nobody on this team can be pushed. It feels good in lobbies.
[VIP] Steve
Alex
Sam

That preview shows three names the way teams style them: Steve has the scoreboardsdemo.vip permission and is on the gold team, Alex and Sam are on the gray team. The rest of refresh makes sure every online player is on the correct team on this viewer's board:

PlayerBoard.javaCompiles on Paper 26.3Compile Lab
Where this file livesscoreboards-demosrcmainjavacomexamplescoreboardsdemoPlayerBoard.java

The package com.example.scoreboardsdemo is the folder path com/example/scoreboardsdemo 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.

  • scoreboards-demo/
    • src/main/
      • java/com/example/scoreboardsdemo/Package com.example.scoreboardsdemo
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerBoard.javayou are hereHelper class
        • Playtime.javaHelper class
        • ScoreboardsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • SidebarCommand.javaCommand (BasicCommand)
      • 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.

74for (Player player : online) {75    hearts.getScore(player.getName()).setScore((int) Math.ceil(player.getHealth()));76    Team team = player.hasPermission("scoreboardsdemo.vip") ? vipTeam : memberTeam;77    if (!team.hasEntry(player.getName())) {78        team.addEntry(player.getName());79    }80}
  1. Health is a decimal like 17.5. Scores are whole numbers, so round up: a half heart still shows as a full point.
  2. The team depends on a permission, which means giving someone the permission with a permissions plugin changes their team within a second. See Permissions.
  3. Only call addEntry when the player is not already there, so a second with no changes does no work.

The complete demo plugin

Everything above is one plugin, scoreboards-demo. It has small classes with one job each, which is a good habit for any plugin that grows:

  • scoreboards-demo/
    • src/main/
      • java/com/example/scoreboardsdemo/
        • ScoreboardsDemoPlugin.javaStarts the refresh timer, registers the listener and the command, cleans up on disable
        • ConnectionListener.javaGives a player a sidebar when they join and cleans up when they quit
        • BoardManager.javaRemembers one PlayerBoard per player and refreshes all of them
        • PlayerBoard.javaOne player's scoreboard: sidebar lines, hearts, teams
        • Playtime.javaTurns ticks into text like 1h 05m
        • SidebarCommand.java/sidebar hides or shows the sidebar
      • resources/
        • plugin.ymlDeclares the plugin and its two permissions
BoardManager.javaCompiles on Paper 26.3Compile Lab
Where this file livesscoreboards-demosrcmainjavacomexamplescoreboardsdemoBoardManager.java

The package com.example.scoreboardsdemo is the folder path com/example/scoreboardsdemo 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.

  • scoreboards-demo/
    • src/main/
      • java/com/example/scoreboardsdemo/Package com.example.scoreboardsdemo
        • BoardManager.javayou are hereHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerBoard.javaHelper class
        • Playtime.javaHelper class
        • ScoreboardsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • SidebarCommand.javaCommand (BasicCommand)
      • 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.scoreboardsdemo;2 3import java.util.Collection;4import java.util.HashMap;5import java.util.Map;6import java.util.UUID;7import org.bukkit.Bukkit;8import org.bukkit.entity.Player;9 10final class BoardManager {11 12    private final Map<UUID, PlayerBoard> boards = new HashMap<>();13 14    void show(Player player) {15        PlayerBoard board = boards.computeIfAbsent(player.getUniqueId(), id -> new PlayerBoard());16        player.setScoreboard(board.scoreboard());17    }18 19    void hide(Player player) {20        player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());21    }22 23    boolean toggle(Player player) {24        PlayerBoard board = boards.get(player.getUniqueId());25        if (board == null) {26            return false;27        }28        if (player.getScoreboard() == board.scoreboard()) {29            hide(player);30            return false;31        }32        show(player);33        return true;34    }35 36    void remove(Player player) {37        boards.remove(player.getUniqueId());38        for (PlayerBoard other : boards.values()) {39            other.forget(player.getName());40        }41    }42 43    void refreshAll() {44        Collection<? extends Player> online = Bukkit.getOnlinePlayers();45        for (Player viewer : online) {46            PlayerBoard board = boards.get(viewer.getUniqueId());47            if (board != null) {48                board.refresh(viewer, online);49            }50        }51    }52}
  1. A map from player ID to that player's board. UUIDs are stable even if the player changes their name.
  2. Returns the board for this player, creating one the first time. A returning player keeps their board.
  3. "Hiding" the sidebar means switching back to the main scoreboard. The player's own board stays alive in the map, ready to come back.
  4. Compares the two objects themselves, not their contents. That is the right test for "is this the very same board?".
  5. When a player leaves, remove their name from everyone else's board, so old names do not pile up in teams and scores.

Mistakes to avoid

  • Rewriting the whole sidebar each second. It flickers. Create the lines once, then change custom names, and only when the text changed.
  • Reusing an entry name. Two lines with the same entry are one line. Give every line, even blank ones, a different name.
  • Registering the same objective or team twice. registerNewObjective and registerNewTeam throw if the name already exists. On the main scoreboard, which is saved, check with getObjective or getTeam first.
  • Forgetting that teams live on one scoreboard. A team you made on the main scoreboard does nothing for a player who is looking at their own scoreboard.
  • Leaking boards. If you keep one board per player in a map, remove it on PlayerQuitEvent.
  • Using a long sidebar. More than 15 lines are cut off by the game. Players also hate walls of text; aim for 6 to 10 short lines.
  • Running the update off the main thread. Scoreboard changes are Bukkit API calls. Use the normal scheduler, not an async task, as the scheduler chapter explains.

Try it yourself

Try it

Show the world name

Add a line under "Playtime" that shows the name of the world the player is in, for example World: world_nether.

Hint 1

Put another entry in the PlayerBoard constructor loop by raising LINE_COUNT and moving the ip line down, then call setLine in refresh.

Hint 2

The world is viewer.getWorld() and its name is getName().

Show the solution

Make room for one more line: raise LINE_COUNT to 8, add a constant LINE_WORLD = 5, and move the spacer and the address down to indexes 6 and 7 in the constructor. Then add this to refresh:

PlayerBoard.java (new lines in refresh)
1setLine(LINE_WORLD, miniMessage.deserialize(2    "<gray>World: <white><world>",3    Placeholder.unparsed("world", viewer.getWorld().getName())));

Everything else already works, because the constructor loop creates entries for every index from 0 up to LINE_COUNT. World names can contain odd characters, so the example inserts the name with Placeholder.unparsed instead of gluing it into the MiniMessage string. You also need import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;.

Try it

Ghost mode

Write a plugin where every player who joins cannot be pushed by other players and has no name tag. You do not need a sidebar. Use the main scoreboard.

Hint 1

You need one team with two options set. Team.Option.COLLISION_RULE and Team.Option.NAME_TAG_VISIBILITY, both with OptionStatus.NEVER.

Hint 2

Remember that the main scoreboard is saved. Ask for the team with getTeam before you register it.

Show the solution

The listener creates the team the first time anyone joins, sets both options once, and adds the player every time:

GhostListener.javaCompiles on Paper 26.3Compile Lab
Where this file livesscoreboards-exercisesrcmainjavacomexamplescoreboardsexerciseGhostListener.java

The package com.example.scoreboardsexercise is the folder path com/example/scoreboardsexercise 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.

  • scoreboards-exercise/
    • src/main/
      • java/com/example/scoreboardsexercise/Package com.example.scoreboardsexercise
        • GhostListener.javayou are hereListener: reacts to events
        • GhostPlugin.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.

14@EventHandler15public void onJoin(PlayerJoinEvent event) {16    Scoreboard scoreboard = Bukkit.getScoreboardManager().getMainScoreboard();17    Team ghosts = scoreboard.getTeam(TEAM_NAME);18    if (ghosts == null) {19        ghosts = scoreboard.registerNewTeam(TEAM_NAME);20        ghosts.setOption(Team.Option.COLLISION_RULE, Team.OptionStatus.NEVER);21        ghosts.setOption(Team.Option.NAME_TAG_VISIBILITY, Team.OptionStatus.NEVER);22    }23    ghosts.addPlayer(event.getPlayer());24}
  1. The options are set only when the team is created. A saved team keeps them after a restart.
  2. Puts the joining player on the team. Teams hold entries, and addPlayer adds the player's name for you.

The tiny main class that registers it is GhostPlugin, exactly like FirstSidebarPlugin.

When you want to build a polished sidebar from scratch, with its own commands and configurable text, continue with Project: Live sidebar scoreboard.

Recap

  • A scoreboard has objectives (lists of scores for entries), teams, and display slots that decide where an objective appears.
  • getMainScoreboard() is shared and saved with the world. getNewScoreboard() makes a private board you give to one player with setScoreboard.
  • A sidebar is a DUMMY objective in DisplaySlot.SIDEBAR. Each line is an entry with a score for its position and a customName for its text, and NumberFormat.blank() hides the numbers.
  • To update without flicker, create lines once and then change only their custom names, and skip the change when nothing is different.
  • Teams give prefixes, colors, and rules for name tag visibility, collisions and friendly fire. A team lives on exactly one scoreboard.
  • BELOW_NAME and PLAYER_LIST show one number or hearts for every player; RenderType.HEARTS draws hearts.

Quick quiz

  1. You want every player to see their own kill count in the sidebar. Which scoreboard do you give each player?

  2. What decides the order of lines in a sidebar?

  3. Your sidebar flickers every second. What is the best fix?

  4. You register a team named "red" on the main scoreboard in onEnable. After a restart, the plugin throws an IllegalArgumentException. Why?

  5. Which setting stops team members from pushing each other?

Next steps