Skip to content

排行榜与成就 ​

排行榜制造竞争,成就制造目标——都是驱动玩家长期活跃的系统。这一篇讲数据聚合和展示。

排行榜 ​

核心问题:排序 ​

排行榜本质是"按某字段给所有玩家排序"。数据量大时,每次请求都全量排序很慢。

小服:内存排序 ​

玩家不多(几百),直接在内存维护排序后的列表:

java
public class Leaderboard {
    private final Map<UUID, Double> scores = new HashMap<>();
    private List<Map.Entry<UUID, Double>> sorted; // 缓存排序结果

    public void update(UUID id, double score) {
        scores.put(id, score);
        sorted = null; // 标记需重排
    }

    public List<Map.Entry<UUID, Double>> top(int n) {
        if (sorted == null) {
            sorted = scores.entrySet().stream()
                .sorted(Map.Entry.<UUID, Double>comparingByValue().reversed())
                .collect(Collectors.toList());
        }
        return sorted.subList(0, Math.min(n, sorted.size()));
    }
}

缓存排序结果

别每次查排行榜都重排。分数变了才重排(sorted = null),查询时直接返回缓存。玩家查排行榜是高频操作。

大服:Redis 有序集合 ​

玩家上万时,内存排序也慢。用 Redis 的 Sorted Set(ZSET)——它天生有序、增删改 O(log N):

java
// 伪代码
jedis.zadd("leaderboard:coins", playerCoins, playerId.toString());
// 取前 10 名
Set<Tuple> top = jedis.zrevrangeWithScores("leaderboard:coins", 0, 9);
// 查某玩家排名
Long rank = jedis.zrevrank("leaderboard:coins", playerId.toString());

Redis ZSET 是排行榜神器

Sorted Set 专为排行榜设计。分数变更自动重排、能取 Top N、能查个人排名。中型以上服强烈推荐。见数据持久化的 Redis 部分。

多维度排行榜 ​

别只做一个金币榜。常见维度:

  • 等级榜、战力榜、金币榜、击杀榜、在线时长榜、公会榜

每个维度一个 key(leaderboard:level、leaderboard:kills…)。

展示 ​

用箱子 GUI 展示排行榜最直观:

java
public void showLeaderboard(Player p) {
    Inventory inv = Bukkit.createInventory(null, 27, "金币排行榜");
    List<Map.Entry<UUID, Double>> top = leaderboard.top(27);
    for (int i = 0; i < top.size(); i++) {
        ItemStack skull = createSkull(top.get(i).getKey(), i + 1, top.get(i).getValue());
        inv.setItem(i, skull);
    }
    p.openInventory(inv);
}

也可用 /leaderboard 命令输出文字、或 BossBar 滚动展示前三。

实时性 ​

别实时全量刷新

玩家每打一个怪就更新排行榜并重排?太重。做法:分数实时改内存,排行榜缓存每隔几分钟(或定时任务)重算一次。即时排名查询用 Redis ZSET 才划算。

成就系统 ​

成就 = 检测条件达成 + 发奖励。结构:

成就定义 ​

java
public class Achievement {
    private String id;          // "first_kill"
    private String name;        // "初出茅庐"
    private String description; // "击杀第一只怪物"
    private Material icon;      // 展示图标
    private double reward;      // 奖励金币
}

成就静态定义在配置里,便于增删不改代码。

玩家进度 ​

每个玩家记录已解锁的成就:

java
public class PlayerData {
    private Set<String> unlocked = new HashSet<>(); // 成就ID集合
}

检测触发 ​

在各事件里检测条件。比如击杀成就:

java
@EventHandler
public void onMobDeath(EntityDeathEvent e) {
    Player killer = e.getEntity().getKiller();
    if (killer == null) return;
    UUID id = killer.getUniqueId();

    // 首杀成就
    checkAchievement(id, "first_kill", () -> getKillCount(id) >= 1);
    // 百杀成就
    checkAchievement(id, "kill_100", () -> getKillCount(id) >= 100);
}

private void checkAchievement(UUID id, String achId, BooleanSupplier condition) {
    PlayerData data = getData(id);
    if (data.getUnlocked().contains(achId)) return; // 已解锁
    if (condition.getAsBoolean()) {
        unlock(id, achId);
    }
}

private void unlock(UUID id, String achId) {
    Achievement ach = achievements.get(achId);
    getData(id).getUnlocked().add(achId);
    econ.deposit(id, ach.getReward()); // 发奖
    Player p = Bukkit.getPlayer(id);
    if (p != null) {
        p.sendTitle(ChatColor.GOLD + "成就解锁", ach.getName(), 10, 60, 10);
        p.playSound(p.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1, 1);
    }
}

成就分类 ​

  • 累计型:击杀 100 怪、采集 1000 矿
  • 探索型:到达某坐标、进入某世界
  • 一次性:首次进服、首次交易
  • 隐藏成就:不显示条件,解锁才有惊喜

进度型成就 ​

"击杀 100 怪"想显示进度(73/100),得存进度而非布尔:

java
private Map<String, Integer> progress = new HashMap<>(); // 成就ID -> 进度

展示 ​

成就墙用箱子 GUI 分类展示:已解锁亮色、未解锁灰色 + 问号、进度型显示 x/y。

常见坑 ​

  • 排行榜只算在线玩家:离线玩家数据要一起算(从数据库读),否则不准
  • 成就重复触发:解锁后要加入 unlocked 集合,避免重复发奖
  • 条件检测性能:每个事件都跑一堆条件判断,注意别太重(已解锁的快速跳过)

下一篇跨服与大厅网络。