Project: Live sidebar scoreboard
A per-player sidebar with kills, deaths, playtime and online count that updates every second.
In this project you build a live sidebar on the right side of the screen. It shows each player's kills, deaths, playtime and the number of players online, it updates every second, it remembers the numbers after a restart, and players can switch it off with /sidebar.
What you will build
Almost every server has a sidebar, and players love numbers that go up. This project is a good test of what you know, because the sidebar looks simple but touches many parts of a plugin: events to count things, saved data to remember them, the scheduler to refresh the screen, and the scoreboard API to draw it. This is what a player sees:
Every line has a different source. The table also shows which of the numbers the plugin has to remember itself:
| Line | Where the number comes from | Saved by |
|---|---|---|
| Kills | Counted by a listener each time the player kills something | The plugin, in the player's persistent data |
| Deaths | Counted by a listener each time the player dies | The plugin, in the player's persistent data |
| Playtime | Minecraft's own statistic PLAY_ONE_MINUTE | Minecraft itself |
| Online | The size of the server's list of online players | Nothing: it is always current |
Players can hide the sidebar and bring it back with a command. The plugin remembers that choice too, so a player who hid it does not see it again after the next login:
The plan
The sidebar needs data from three places and one task that puts it all on the screen. This picture shows the flow of the numbers:
The pieces, with the chapter that teaches each idea:
| Requirement | The piece that does it | Chapter |
|---|---|---|
| Count kills and deaths | StatsListener, with two event handlers | Events and listeners |
| Remember them for good | PlayerStats, using the player's PersistentDataContainer | Saving data on things |
| Show a clean sidebar | Sidebar: an objective with custom line names and hidden numbers | Scoreboards |
| One sidebar per player | BoardManager: a map from UUID to sidebar | Collections |
| Update every second | A repeating task every 20 ticks (a tick is one twentieth of a second) | The scheduler |
| Hide and show | SidebarCommand, a BasicCommand | Commands, part 1 |
The build steps are:
Create the project and plugin.yml
Name the plugin and declare the toggle permission.
Save kills and deaths on the player
A small class that reads and writes numbers in the persistent data container.
Count kills and deaths
A listener with two handlers, and one surprise about how events inherit.
Turn ticks into a playtime text
A tiny formatter, tried out in a plain Java program first.
Build the sidebar for one player
A title with a gradient, hidden numbers and lines that change in place.
Manage every player's sidebar
Create on join, forget on quit, toggle on command, refresh for everyone.
Wire it up and test
Listeners, command, the repeating task, and a testing checklist.
If you have not read Scoreboards yet, do that first: this page builds the same kind of sidebar, and it explains the scoreboard parts (objective, score, entry) more slowly than we can here.
Step 1: create the project and plugin.yml
Create a project as in the earlier projects (copy the starter, or run new-plugin.cmd -Name SidebarStats from the paper-templates folder), with the package com.example.projectscoreboard. The plugin needs one permission:
Where this file livesproject-scoreboardsrcmainresourcesplugin.yml
Files in src/main/resources are copied into the jar exactly as they are. Paper looks for plugin.yml at the top of the jar.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javaHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javaHelper class
- Playtime.javaHelper class
- Sidebar.javaHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlyou are hereTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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.
1name: SidebarStats2version: '1.0.0'3main: com.example.projectscoreboard.SidebarPlugin4api-version: '26.3'5description: A live sidebar with kills, deaths, playtime and online players, saved per player, with a toggle command.6permissions:7 sidebarstats.toggle:8 description: Lets a player hide and show their own sidebar with /sidebar.9 default: true- The permission for
/sidebar. It defaults totrue, which means every player has it, because hiding your own sidebar is harmless.
Step 2: save kills and deaths on the player
The first question of every stats plugin is: where do I keep the numbers? A HashMap in memory is the easy answer, but it forgets everything when the server restarts. A file or database works, but needs a lot of code. For a few numbers per player there is a third way: the player itself can carry them.
Every player has a persistent data container, a little notebook that Minecraft saves with the player's data. You write a number into it under a name, and the number is still there after a relog or a restart. You never open a file. All our numbers go in this notebook: kills, deaths and also the choice to hide the sidebar.
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardPlayerStats.java
The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javaHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javayou are hereHelper class
- Playtime.javaHelper class
- Sidebar.javaHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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.
15PlayerStats(Plugin plugin) {16 this.killsKey = new NamespacedKey(plugin, "kills");17 this.deathsKey = new NamespacedKey(plugin, "deaths");18 this.hiddenKey = new NamespacedKey(plugin, "sidebar_hidden");19}- A
namespaced key is the name a value is saved under. It has two parts: your plugin's name and the wordkills, so it can never clash with another plugin'skills. - Keys may only contain lowercase letters, digits and the characters
_ - . /. A capital letter or a space would throw an error.
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardPlayerStats.java
The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javaHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javayou are hereHelper class
- Playtime.javaHelper class
- Sidebar.javaHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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.
21int kills(Player player) {22 return player.getPersistentDataContainer().getOrDefault(killsKey, PersistentDataType.INTEGER, 0);23}- Opens the player's notebook.
- Reads the number saved under the key.
PersistentDataType.INTEGERsays "it is a whole number". If the player has never killed anything, nothing is saved yet, and the last argument,0, is returned instead.
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardPlayerStats.java
The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javaHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javayou are hereHelper class
- Playtime.javaHelper class
- Sidebar.javaHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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.
45private static void increase(PersistentDataContainer container, NamespacedKey key) {46 int current = container.getOrDefault(key, PersistentDataType.INTEGER, 0);47 container.set(key, PersistentDataType.INTEGER, current + 1);48}- Reads the current count, or 0 for a new player.
- Writes the new count. Saving it again under the same key replaces the old value.
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardPlayerStats.java
The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javaHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javayou are hereHelper class
- Playtime.javaHelper class
- Sidebar.javaHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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.
37boolean isSidebarHidden(Player player) {38 return player.getPersistentDataContainer().getOrDefault(hiddenKey, PersistentDataType.BOOLEAN, false);39}- The same notebook can hold yes-or-no values. A new player has no entry, so the default is
false: the sidebar is shown.
One class owns every read and write. The rest of the plugin says stats.kills(player) and never needs to know about keys or types. If you later move the numbers into a database, only PlayerStats changes.
Step 3: count kills and deaths
Counting needs two events. When something dies, Paper fires an EntityDeathEvent. When the dead thing is a player, Paper fires a PlayerDeathEvent, which is a more specific version of it:
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardStatsListener.java
The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javaHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javaHelper class
- Playtime.javaHelper class
- Sidebar.javaHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.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
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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.
17@EventHandler18public void onEntityDeath(EntityDeathEvent event) {19 Player killer = event.getEntity().getKiller();20 if (killer != null) {21 stats.addKill(killer);22 }23}- Called whenever anything dies: a zombie, a cow, a villager, or a player.
- The player who dealt the final blow, or
nullwhen nobody did: a creeper explosion, falling, lava, or a mob killing another mob. - Always check for
nullhere. Most deaths in the world have no player killer, and calling a method onnullwould crash the handler. - Adds one to the killer's saved kill count.
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardStatsListener.java
The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javaHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javaHelper class
- Playtime.javaHelper class
- Sidebar.javaHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.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
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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.
25@EventHandler26public void onPlayerDeath(PlayerDeathEvent event) {27 stats.addDeath(event.getPlayer());28}- Called only when a player dies.
- The player who died. Their death count goes up by one.
Here is the part that surprises beginners: a PlayerDeathEvent is also an EntityDeathEvent, because it is a more specific kind of the same event (see Inheritance and interfaces). That means when Alex kills Steve, both handlers run: onEntityDeath gives Alex a kill, and onPlayerDeath gives Steve a death. We never wrote a special case for player-versus-player fights, and nothing is counted twice. Our "kills" number includes players and mobs.
Step 4: turn ticks into a playtime text
Playtime needs no counting from us, because Minecraft already tracks it. The statistic is called Statistic.PLAY_ONE_MINUTE. Despite the name, its value is the time played in ticks, and there are 20 ticks in a second. A player with 78,000 ticks has played for 3,900 seconds, which is 1 hour and 5 minutes.
Players do not want to read 78000, so we turn it into text like 1h 5m. The logic is plain Java, so here is the same method in a small program you can run before it goes into the plugin. Press the green arrow:
1import java.time.Duration;2 3String format(int ticks) {4 Duration played = Duration.ofSeconds(ticks / 20);5 if (played.toHours() > 0) {6 return played.toHours() + "h " + played.toMinutesPart() + "m";7 }8 return played.toMinutesPart() + "m " + played.toSecondsPart() + "s";9}10 11void main() {12 int[] samples = {0, 90, 1200, 72000, 78000, 1500000};13 for (int ticks : samples) {14 IO.println(ticks + " ticks = " + format(ticks));15 }16}- Divides the ticks by 20 to get seconds (whole-number division drops the leftover ticks) and wraps them in a
Duration, a Java object that knows how to split seconds into hours, minutes and seconds. - Plays for an hour or more? Then show hours and minutes, and drop the seconds, which would only flicker.
- The minutes left over after the whole hours are taken out: 0 to 59.
toMinutes()without "Part" would give the total. - A list of test values: no time, a few seconds, exactly one minute, exactly one hour, and so on. Try your own.
The plugin uses the same method inside a small helper class:
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardPlaytime.java
The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javaHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javaHelper class
- Playtime.javayou are hereHelper class
- Sidebar.javaHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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.
12static String format(int ticks) {13 Duration played = Duration.ofSeconds(ticks / TICKS_PER_SECOND);14 if (played.toHours() > 0) {15 return played.toHours() + "h " + played.toMinutesPart() + "m";16 }17 return played.toMinutesPart() + "m " + played.toSecondsPart() + "s";18}- A
staticmethod belongs to the class, not to an object, so you call it asPlaytime.format(ticks).
Step 5: build the sidebar for one player
Now the part players actually see. Each player gets their own Sidebar object, which owns a scoreboard, because every player has different numbers. The scoreboard is created together with the object:
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardSidebar.java
The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javaHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javaHelper class
- Playtime.javaHelper class
- Sidebar.javayou are hereHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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.
22private final Scoreboard scoreboard = Bukkit.getScoreboardManager().getNewScoreboard();- Creates a brand new scoreboard that only this player will see. The server's main scoreboard stays untouched.
Then the constructor sets the objective up:
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardSidebar.java
The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javaHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javaHelper class
- Playtime.javaHelper class
- Sidebar.javayou are hereHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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.
26Sidebar() {27 objective = scoreboard.registerNewObjective("sidebar", Criteria.DUMMY, MINI_MESSAGE.deserialize(TITLE));28 objective.setDisplaySlot(DisplaySlot.SIDEBAR);29 objective.numberFormat(NumberFormat.blank());30 for (int index = 0; index < LINE_COUNT; index++) {31 objective.getScore(entryName(index)).setScore(LINE_COUNT - index);32 }33 setLine(0, Component.space());34 setLine(5, Component.space());35 setLine(6, MINI_MESSAGE.deserialize("<yellow>play.example.com"));36}- An
objective is the thing the sidebar displays. We name itsidebar, give it theDUMMYcriterion (a number that only our plugin changes), and a title. - Turns the title text into a component. The
<gradient:#f7971e:#ffd200>tag fades the letters from orange to yellow, as in MiniMessage. - Says where the objective is shown: the right side of the screen.
- Hides the red numbers on the right of each line. We only need them to sort the lines, not to read.
- The sidebar sorts lines from the highest score down. Line 0 gets the biggest score, so it appears on top.
- An empty line, as a gap between the title and the numbers. The last line is the server address.
A sidebar line is really a score entry, a name with a number. Our entry names are line-0, line-1 and so on, and we never show them. Instead we give each entry a customName, which is the text players really see. That lets us use colors and change the text without ever renaming the entry, which is what makes the update flicker-free.
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardSidebar.java
The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javaHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javaHelper class
- Playtime.javaHelper class
- Sidebar.javayou are hereHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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.
42void update(int kills, int deaths, String playtime, int online) {43 setLine(LINE_KILLS, MINI_MESSAGE.deserialize("<gray>Kills: <green>" + kills));44 setLine(LINE_DEATHS, MINI_MESSAGE.deserialize("<gray>Deaths: <red>" + deaths));45 setLine(LINE_PLAYTIME, MINI_MESSAGE.deserialize("<gray>Playtime: <white>" + playtime));46 setLine(LINE_ONLINE, MINI_MESSAGE.deserialize("<gray>Online: <aqua>" + online));47}- The sidebar receives plain numbers. It does not know where they come from, which keeps this class about drawing only.
- Builds the line from a MiniMessage text with the number attached. Gray label, green number.
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardSidebar.java
The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javaHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javaHelper class
- Playtime.javaHelper class
- Sidebar.javayou are hereHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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.
49private void setLine(int index, Component text) {50 if (text.equals(shownLines[index])) {51 return;52 }53 shownLines[index] = text;54 objective.getScore(entryName(index)).customName(text);55}- The task runs every second, but a kill count changes only now and then. If the new text equals what the line already shows, there is nothing to do. Skipping needless updates keeps the sidebar from flickering and saves network traffic.
- Remembers what is on screen now, for the next comparison.
- Changes the visible text of that line in place.
Step 6: manage every player's sidebar
BoardManager keeps one Sidebar per player in a map, and everything else in the plugin goes through it. The map's key is the player's UUID, the permanent unique id (see Time, UUIDs and randomness), not the Player object, which stops being valid when the player leaves.
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardBoardManager.java
The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javayou are hereHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javaHelper class
- Playtime.javaHelper class
- Sidebar.javaHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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.
55private void show(Player player) {56 Sidebar sidebar = new Sidebar();57 sidebars.put(player.getUniqueId(), sidebar);58 refresh(player, sidebar, Bukkit.getOnlinePlayers().size());59 player.setScoreboard(sidebar.scoreboard());60}- A new sidebar with the title and layout.
- Stores it under the player's UUID so the task can find it later.
- Fills in the real numbers before showing the sidebar, so it never appears empty for a moment.
- The moment the player sees the sidebar. A player can look at one scoreboard at a time.
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardBoardManager.java
The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javayou are hereHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javaHelper class
- Playtime.javaHelper class
- Sidebar.javaHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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.
62private void refresh(Player player, Sidebar sidebar, int online) {63 String playtime = Playtime.format(player.getStatistic(Statistic.PLAY_ONE_MINUTE));64 sidebar.update(stats.kills(player), stats.deaths(player), playtime, online);65}- Minecraft's own playtime counter, in ticks.
- Reads the saved kills from the player's persistent data.
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardBoardManager.java
The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javayou are hereHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javaHelper class
- Playtime.javaHelper class
- Sidebar.javaHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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.
45void refreshAll() {46 int online = Bukkit.getOnlinePlayers().size();47 for (Player player : Bukkit.getOnlinePlayers()) {48 Sidebar sidebar = sidebars.get(player.getUniqueId());49 if (sidebar != null) {50 refresh(player, sidebar, online);51 }52 }53}- The number of players online. We ask once and hand the same number to every sidebar.
- Looks for this player's sidebar. A player who hid it has none, so
nullcomes back and we skip them.
Hiding and showing are two sides of one toggle. The player's choice is saved in the same persistent data, so it survives a relog:
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardBoardManager.java
The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javayou are hereHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javaHelper class
- Playtime.javaHelper class
- Sidebar.javaHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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.
25boolean toggle(Player player) {26 if (sidebars.containsKey(player.getUniqueId())) {27 stats.setSidebarHidden(player, true);28 hide(player);29 return false;30 }31 stats.setSidebarHidden(player, false);32 show(player);33 return true;34}- If the player has a sidebar right now, the toggle hides it. Otherwise it shows it.
- Saves the choice, so the sidebar stays hidden on the next login.
- The method answers "is the sidebar visible now?". The command uses the answer to pick the message.
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardBoardManager.java
The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javayou are hereHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javaHelper class
- Playtime.javaHelper class
- Sidebar.javaHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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.
36void hide(Player player) {37 sidebars.remove(player.getUniqueId());38 player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());39}- Forgets the sidebar object, so the task stops updating it.
- Gives the player the server's main scoreboard back. Without a sidebar objective there, the sidebar disappears.
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardBoardManager.java
The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javayou are hereHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javaHelper class
- Playtime.javaHelper class
- Sidebar.javaHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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.
41void forget(Player player) {42 sidebars.remove(player.getUniqueId());43}- Called when a player quits. It only drops the map entry. The player is gone, so there is no screen left to clean.
Step 7: wire it up and test
The connection listener creates the sidebar when a player joins and drops it when they quit. On join we also refresh everyone at once, so the "Online" number goes up for the players who were already there:
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardConnectionListener.java
The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javaHelper class
- ConnectionListener.javayou are hereListener: reacts to events
- PlayerStats.javaHelper class
- Playtime.javaHelper class
- Sidebar.javaHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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.projectscoreboard;2 3import org.bukkit.event.EventHandler;4import org.bukkit.event.Listener;5import org.bukkit.event.player.PlayerJoinEvent;6import org.bukkit.event.player.PlayerQuitEvent;7 8final class ConnectionListener implements Listener {9 10 private final BoardManager boards;11 12 ConnectionListener(BoardManager boards) {13 this.boards = boards;14 }15 16 @EventHandler17 public void onJoin(PlayerJoinEvent event) {18 boards.showIfWanted(event.getPlayer());19 boards.refreshAll();20 }21 22 @EventHandler23 public void onQuit(PlayerQuitEvent event) {24 boards.forget(event.getPlayer());25 }26}- Shows the sidebar unless the player turned it off earlier.
- Updates everybody's online count.
- When a player leaves, we forget their sidebar. Their count drops on everyone else's screen within a second, at the next refresh.
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardSidebarCommand.java
The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javaHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javaHelper class
- Playtime.javaHelper class
- Sidebar.javaHelper class
- SidebarCommand.javayou are hereCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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 execute(CommandSourceStack source, String[] args) {17 if (!(source.getExecutor() instanceof Player player)) {18 source.getSender().sendRichMessage("<red>Only players have a sidebar.");19 return;20 }21 if (boards.toggle(player)) {22 player.sendRichMessage("<green>Sidebar shown.");23 } else {24 player.sendRichMessage("<gray>Sidebar hidden. Type <yellow>/sidebar</yellow> to bring it back.");25 }26}- Only a player can have a sidebar. The console gets a polite refusal.
- Does the work and tells us which way it went.
The main class puts the pieces together and starts the repeating task:
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardSidebarPlugin.java
The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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.
- project-scoreboard/
- src/main/
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- BoardManager.javaHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javaHelper class
- Playtime.javaHelper class
- Sidebar.javaHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboard/Package com.example.projectscoreboard
- 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
- src/main/
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.projectscoreboard;2 3import org.bukkit.Bukkit;4import org.bukkit.entity.Player;5import org.bukkit.plugin.java.JavaPlugin;6import org.bukkit.scheduler.BukkitTask;7 8public final class SidebarPlugin extends JavaPlugin {9 10 private static final long TICKS_PER_SECOND = 20L;11 12 private BoardManager boards;13 private BukkitTask refreshTask;14 15 @Override16 public void onEnable() {17 PlayerStats stats = new PlayerStats(this);18 boards = new BoardManager(stats);19 20 getServer().getPluginManager().registerEvents(new StatsListener(stats), this);21 getServer().getPluginManager().registerEvents(new ConnectionListener(boards), this);22 registerCommand("sidebar", "Show or hide your sidebar", new SidebarCommand(boards));23 24 refreshTask = getServer().getScheduler().runTaskTimer(25 this, boards::refreshAll, TICKS_PER_SECOND, TICKS_PER_SECOND);26 27 for (Player player : Bukkit.getOnlinePlayers()) {28 boards.showIfWanted(player);29 }30 }31 32 @Override33 public void onDisable() {34 refreshTask.cancel();35 for (Player player : Bukkit.getOnlinePlayers()) {36 boards.hide(player);37 }38 }39}- One stats object, shared by the listener and the manager, so both use the same keys.
- Starts counting kills and deaths. Two listeners are registered, because they do different jobs.
- Registers
/sidebar. The permission comes from the command class. - Runs
boards::refreshAllagain and again. The firstTICKS_PER_SECONDis the delay before the first run, and the second is the gap between runs: 20 ticks is one second. See The scheduler. - A method reference: "call
refreshAllonboards". - When the plugin starts while players are already online, such as when the plugin is enabled while somebody is logged in, those players never trigger a join event. This loop gives them their sidebar.
- Stops the repeating task when the plugin shuts down. A task that is never stopped would keep running after the plugin is gone.
- Gives every player the normal scoreboard back. Otherwise our sidebar stays on their screens until they relog.
- project-scoreboard/
- src/
- main/
- java/
- com/example/projectscoreboard/
- SidebarPlugin.javaMain class: creates everything and starts the one-second task
- PlayerStats.javaReads and writes kills, deaths and the hidden choice in the player's data
- StatsListener.javaCounts kills and deaths
- Playtime.javaTurns ticks into text like 1h 5m
- Sidebar.javaOne player's scoreboard: title, lines and in-place updates
- BoardManager.javaMap from UUID to sidebar: show, hide, toggle, forget, refresh
- ConnectionListener.javaShows the sidebar on join and forgets it on quit
- SidebarCommand.java/sidebar toggles the sidebar
- com/example/projectscoreboard/
- resources/
- plugin.ymlName, main class and the toggle permission
- java/
- main/
- src/
You can download the finished project and compare it with yours.
Testing checklist
Build, start the test server and join. A second player or a second account makes a few checks easier, but you can do most alone.
- The sidebar appears as soon as you join, with 0 kills, 0 deaths, a small playtime and 1 online. The title fades from orange to yellow.
- Watch the playtime. It should grow every second, and nothing else on the sidebar should blink.
- Spawn a mob with
/summon zombieand kill it by hand. Kills goes to 1 within a second. Kill a mob with/kill @e[type=zombie]instead: kills stays the same, because no player dealt the blow. - Run
/killon yourself. Deaths goes to 1. - Quit and rejoin. Kills and deaths are still there: they were saved in your player data.
- Restart the server and join again. The numbers are still there.
- Run
/sidebar. It disappears and you get the gray message. Run it again and it comes back. - Hide the sidebar, then quit and rejoin. It stays hidden, and
/sidebarbrings it back. - With two players, let one quit. The other's "Online" drops to 1 within a second, and the console shows no errors.
- Let one player kill the other. The killer's kills and the victim's deaths each go up by exactly one.
- Stop the server with
stopand look at the console: no errors from SidebarStats.
Mistakes to avoid
- Counting in a
HashMaponly. The numbers vanish on every restart. Anything players care about must be saved. - Calling
getKiller()without anullcheck. Most deaths have no player killer. - Rebuilding the whole scoreboard every second. It flickers. Change the existing lines in place, and skip lines whose text did not change.
- Sharing one scoreboard for everyone. Then every player sees the same numbers. Each player needs their own scoreboard to have their own kills.
- Keeping players in a map forever. Remove them on quit.
- Forgetting to stop the repeating task. Cancel it in
onDisable. - Using a capital letter in a key.
new NamespacedKey(plugin, "Kills")throws an error. Keys are lowercase. - Reading the playtime as seconds. The statistic is in ticks. Divide by 20 first.
Exercises
Add a world line
Add a line to the sidebar that shows the name of the world the player is in, such as World: world_nether. It goes under "Online", and the blank line and the server address move down one place.
Hint 1
The sidebar has a fixed number of lines, LINE_COUNT. Make it one bigger, add a constant for the new line number, and move the two lines after it.
Hint 2
player.getWorld().getName() gives the world name. BoardManager collects the numbers, so it passes the name to Sidebar.update as a new parameter.
Show the solution
Three small changes. The sidebar has eight lines now, the new line is number 5, the blank line moves to 6 and the address to 7:
Where this file livesproject-scoreboard-exercisesrcmainjavacomexampleprojectscoreboardexerciseSidebar.java
The package com.example.projectscoreboardexercise is the folder path com/example/projectscoreboardexercise 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.
- project-scoreboard-exercise/
- src/main/
- java/com/example/projectscoreboardexercise/Package com.example.projectscoreboardexercise
- BoardManager.javaHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javaHelper class
- Playtime.javaHelper class
- Sidebar.javayou are hereHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboardexercise/Package com.example.projectscoreboardexercise
- 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
- src/main/
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.
27Sidebar() {28 objective = scoreboard.registerNewObjective("sidebar", Criteria.DUMMY, MINI_MESSAGE.deserialize(TITLE));29 objective.setDisplaySlot(DisplaySlot.SIDEBAR);30 objective.numberFormat(NumberFormat.blank());31 for (int index = 0; index < LINE_COUNT; index++) {32 objective.getScore(entryName(index)).setScore(LINE_COUNT - index);33 }34 setLine(0, Component.space());35 setLine(6, Component.space());36 setLine(7, MINI_MESSAGE.deserialize("<yellow>play.example.com"));37}- The blank line moved one place down.
- The address is last, at the new bottom.
Where this file livesproject-scoreboard-exercisesrcmainjavacomexampleprojectscoreboardexerciseSidebar.java
The package com.example.projectscoreboardexercise is the folder path com/example/projectscoreboardexercise 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.
- project-scoreboard-exercise/
- src/main/
- java/com/example/projectscoreboardexercise/Package com.example.projectscoreboardexercise
- BoardManager.javaHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javaHelper class
- Playtime.javaHelper class
- Sidebar.javayou are hereHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboardexercise/Package com.example.projectscoreboardexercise
- 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
- src/main/
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.
43void update(int kills, int deaths, String playtime, int online, String world) {44 setLine(LINE_KILLS, MINI_MESSAGE.deserialize("<gray>Kills: <green>" + kills));45 setLine(LINE_DEATHS, MINI_MESSAGE.deserialize("<gray>Deaths: <red>" + deaths));46 setLine(LINE_PLAYTIME, MINI_MESSAGE.deserialize("<gray>Playtime: <white>" + playtime));47 setLine(LINE_ONLINE, MINI_MESSAGE.deserialize("<gray>Online: <aqua>" + online));48 setLine(LINE_WORLD, MINI_MESSAGE.deserialize("<gray>World: <light_purple>" + world));49}- The new parameter: the world name arrives as plain text.
- Sets the new line. The same in-place rule applies, so it never flickers.
Where this file livesproject-scoreboard-exercisesrcmainjavacomexampleprojectscoreboardexerciseBoardManager.java
The package com.example.projectscoreboardexercise is the folder path com/example/projectscoreboardexercise 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.
- project-scoreboard-exercise/
- src/main/
- java/com/example/projectscoreboardexercise/Package com.example.projectscoreboardexercise
- BoardManager.javayou are hereHelper class
- ConnectionListener.javaListener: reacts to events
- PlayerStats.javaHelper class
- Playtime.javaHelper class
- Sidebar.javaHelper class
- SidebarCommand.javaCommand (BasicCommand)
- SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- StatsListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectscoreboardexercise/Package com.example.projectscoreboardexercise
- 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
- src/main/
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.
62private void refresh(Player player, Sidebar sidebar, int online) {63 String playtime = Playtime.format(player.getStatistic(Statistic.PLAY_ONE_MINUTE));64 sidebar.update(stats.kills(player), stats.deaths(player), playtime, online, player.getWorld().getName());65}- Reads the world each time. When the player walks through a portal, the line changes within a second.
The constants at the top also change: LINE_COUNT becomes 8 and LINE_WORLD is 5.
Change the update speed
Change both TICKS_PER_SECOND values in runTaskTimer so the sidebar refreshes every 5 seconds instead (100 ticks). What do you notice about the playtime line, and about how fast a new kill shows up?
Hint
You do not need a new constant: replace the arguments by TICKS_PER_SECOND times 5, or just write 100L for the gap between runs.
Show the solution
Playtime now jumps in steps of five seconds and a new kill takes up to five seconds to show. Nothing breaks, because each refresh reads the real numbers again. The sidebar is cheaper to run, though: players barely notice a 5-second delay for kills, but they do notice a clock that moves in jumps. A good rule is to refresh as slowly as players will accept. For a sidebar with a seconds counter, one second is right.
Count mob kills and player kills separately
Show two lines instead of one: Mobs: 7 and Players: 2. Which classes must change? List them before you start.
Hint
You need a second key in PlayerStats, a check event.getEntity() instanceof Player in the listener, and one more line in the sidebar.
Show the solution
Four places change: PlayerStats gets a second key with its own getter and adder; StatsListener.onEntityDeath calls addPlayerKill when the dead entity is a Player and addMobKill otherwise; Sidebar gets one more line, as in the world-line exercise above; and BoardManager.refresh passes both numbers. Notice that the design pays off here: the listener only counts, the stats class only saves, and the sidebar only draws, so each change is small.
Extension ideas
- Per-player settings. Let each player choose which lines they see, and save the choice in the persistent data container.
- A config file. Move the title, the server address and the line texts to
config.yml. See Configuration files. - A kill streak. Add a line with the current streak and reset it on death.
- Health below the name. Add a second objective that shows hearts under players' names, as in Scoreboards.
- A leaderboard. Show the top three killers. The Leaderboard page shows how to keep a sorted list.
- Placeholders. Offer the numbers to other plugins. See Working with other plugins.
- Animated title. Change the gradient a little each second, to make the title shimmer.
The next project, Chat formatter and filter, changes what players see in chat.
Recap
- A sidebar is an objective whose entries are the lines. Give each entry a fixed name and change its
customNameto change the text. NumberFormat.blank()hides the red numbers on the right of the lines.- Each player needs their own scoreboard to see their own numbers, so keep one
Sidebarper UUID in a map, and remove it on quit. - The player's persistent data container stores small values that survive relogs and restarts, under a namespaced key.
PlayerDeathEventis a kind ofEntityDeathEvent.getKiller()isnullwhen no player dealt the blow, so always check.Statistic.PLAY_ONE_MINUTEcounts ticks. Divide by 20 for seconds.- A repeating task with a 20-tick gap runs once a second. Cancel it in
onDisable. - Only redraw a line when its text changed. That removes flicker.
Quick quiz
Why does the plugin save kills in the player's persistent data container instead of a
HashMap?AHashMapstores numbers fine, but it only lives in memory. The persistent data container is written to disk together with the player's data.A zombie falls into lava and dies with no player nearby. What does
event.getEntity().getKiller()return?getKiller()only returns a player when a player caused the death. For lava, falls and explosions without a player it returnsnull.Alex kills Steve. Which handlers of
StatsListenerrun?APlayerDeathEventis also anEntityDeathEvent, so listeners for both types are called.Why does
setLinecompare the new text withshownLines[index]before changing the line?Setting the same text again would be harmless but wasteful. The task runs every second and most lines stay the same for a long time.What happens if
BoardManager.forgetis never called when players quit?AHashMapholds on to its entries until you remove them. Java does not guess that a player is gone.
Next steps
- Project: Chat formatter and filter: change how chat looks and what it says.
- Scoreboards: teams, colors and hearts under names.
- Saving player data: files and databases for bigger data.
- Practice Arena: challenges for scoreboards, tasks and events.