排行榜与成就
排行榜制造竞争,成就制造目标——都是驱动玩家长期活跃的系统。这一篇讲数据聚合和展示。
排行榜
核心问题:排序
排行榜本质是"按某字段给所有玩家排序"。数据量大时,每次请求都全量排序很慢。
小服:内存排序
玩家不多(几百),直接在内存维护排序后的列表:
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集合,避免重复发奖 - 条件检测性能:每个事件都跑一堆条件判断,注意别太重(已解锁的快速跳过)
下一篇跨服与大厅网络。
