Scoreboards

Track scores and show per-player sidebars, name tags, and list panels.

A scoreboard holds named objectives, and each objective holds a score per entry - a player, an entity, or any plain string you choose. Display an objective in a slot and the client draws it: a sidebar on the right, a number under every player's name tag, or a column in the pause-screen player list.

Display an objective

The server keeps one primary scoreboard at getServer().getScoreboard(). Add an objective to it, drop the objective into a slot, and set scores - here a "blocks mined" tally in the sidebar that climbs as players dig:

include/my_plugin.h
#include <endstone/endstone.hpp>

namespace es = endstone;

class MyPlugin : public es::Plugin {
public:
    void onEnable() override
    {
        objective_ = getServer().getScoreboard()->addObjective(
            "mined", es::Criteria::Type::Dummy, "Blocks Mined");
        objective_->setDisplay(es::DisplaySlot::SideBar, es::ObjectiveSortOrder::Descending);
        registerEvent(&MyPlugin::onBlockBreak, *this);
    }

    void onBlockBreak(es::BlockBreakEvent &event)
    {
        auto score = objective_->getScore(&event.getPlayer());
        score->setValue(score->getValue() + 1);
    }

    void onDisable() override
    {
        objective_->unregister();
    }

private:
    std::unique_ptr<es::Objective> objective_;
};

The pieces:

  • Criteria::Type::Dummy means the server never touches the score - your plugin owns it. It's the criteria you want for anything you drive yourself.
  • getScore(entry) returns a std::unique_ptr<Score> whose value you read with getValue and write with setValue. The entry is a ScoreEntry - a Player *, an Actor *, or a std::string.
  • setDisplay puts the objective in a slot - SideBar, BelowName, or PlayerList - with a sort order. unregister() removes the objective entirely; do it in onDisable so a reload starts clean.

If you've used the in-game /scoreboard command, the API maps onto it one-to-one - each call above is the programmatic form of a command you could type:

C++ API/scoreboard command
addObjective("mined", Criteria::Type::Dummy, "Blocks Mined")/scoreboard objectives add mined dummy "Blocks Mined"
setDisplay(DisplaySlot::SideBar, ObjectiveSortOrder::Descending)/scoreboard objectives setdisplay sidebar mined descending
getScore(&player)->setValue(value + 1)/scoreboard players add @s mined 1
objective->unregister()/scoreboard objectives remove mined

Players, entities, and text rows

An entry is whatever a score hangs off. getScore accepts three kinds, and the type you pass decides what gets tracked:

objective_->getScore(&player)->setValue(10);          // a real player, by identity
objective_->getScore(&zombie)->setValue(3);           // any Actor (mob, entity)
objective_->getScore("Server Total")->setValue(999);  // plain text - a free-standing label row
  • A Player * is tracked by the player's identity, so the score follows them across reconnects and renames, and shows under their name tag in the BelowName slot.
  • An Actor * works the same way for entities - useful for per-mob counters.
  • A std::string is just text: a standalone named row tied to nobody. These are how you draw arbitrary text lines, like the "Coins" and "Level" rows in the next section.

Pass the player pointer &player, not player.getName(). getScore(&player) tracks the actual player; getScore(player.getName()) quietly creates a plain text row that merely shares the same characters - a separate entry that won't follow the real player, won't appear under their name tag, and drifts away from their real score the moment they rename. Only pass a string when you genuinely want a text row.

A different scoreboard for each player

This is where Endstone goes beyond what you can do from add-ons. The vanilla script API scoreboard and the /scoreboard command share one world scoreboard - every player necessarily sees the same objective and the same values. Endstone instead lets you build a scoreboard per player and assign it individually:

auto board = getServer().createScoreboard();  // a fresh, independent scoreboard
auto objective = board->addObjective("hud", es::Criteria::Type::Dummy, "Your Stats");
objective->setDisplay(es::DisplaySlot::SideBar, es::ObjectiveSortOrder::Ascending);

objective->getScore("Coins")->setValue(coins);  // plain strings make label rows
objective->getScore("Level")->setValue(level);

player.setScoreboard(*board);                   // only this player sees it

createScoreboard() hands back a standalone std::shared_ptr<Scoreboard>, and setScoreboard swaps which one that single player renders - so two players can be looking at completely different sidebars at the same moment. Because score entries can be arbitrary strings, each row is really a line of text you control, and the number on the right is just another field to drive.

Keep the std::shared_ptr<Scoreboard> alive for as long as the player should see it - store it in a member (for example, a map keyed by player). When the last reference drops, the scoreboard goes with it and the player falls back to the primary one.

Per-player scoreboards are the backbone of advanced JSON UI work. The sidebar is one of the few server-driven surfaces a resource pack can restyle, so pairing per-player objectives (the live data) with custom JSON UI (the look) is how server-side plugins build rich, personalised HUDs - health bars, quest trackers, shop menus - without a client mod. Drive the values from C++; let the resource pack render them. See the Bedrock Wiki's Intro to JSON UI for how the resource-pack side works.

To send a player back to the shared view, assign the primary scoreboard again:

player.setScoreboard(*getServer().getScoreboard());

On this page