排行榜系统开发文档
排行榜系统服务端+客户端实现,含分数上报、全榜/武器榜、实时排名查询
排行榜系统开发文档
一、需求概述
游戏结束后自动上报分数和武器,服务端记录:
- 总榜:全局前 100 名
- 武器榜:每把武器(2000-2015)各前 100 名
客户端可主动查询任意榜。
二、客户端现状(已确认)
分数系统 ScoreSystem
/Volumes/2TAPFS/UnityProject/NinjaFall/client/Assets/Scripts/System/ScoreSystem.cs
_currentScore— 本局得分(随正确输入累加,baseScore + combo加成)CurrentScore属性 — 对外暴露Settle()— 游戏结束时调用,触发RankingService.Instance.SubmitScore()(当前仅本地)
武器系统 WeaponSystem
/Volumes/2TAPFS/UnityProject/NinjaFall/client/Assets/Scripts/System/WeaponSystem.cs
_currentWeaponId— 当前装备武器 IDCurrentWeaponId属性 — 对外暴露- 初始化时从
InventoryService.Instance.CurrentWeapon读取
结算时机
GameStateManager.cs — OnPlayerDead() → ScoreSystem.Instance.Settle() → 状态切换 GameOver
待改动点
ScoreSystem.Settle()目前只调本地RankingService.SubmitScore(),需新增网络上报- 上报时机:游戏结束(GameOver)时自动触发
- 上报内容:
score = ScoreSystem.Instance.CurrentScore,weapon_id = WeaponSystem.Instance.CurrentWeaponId
三、数据结构
数据库表
character_rank_scores(单局记录)
CREATE TABLE character_rank_scores (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
character_id BIGINT NOT NULL,
username VARCHAR(64) NOT NULL,
weapon_id INT NOT NULL,
score INT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_character (character_id),
INDEX idx_weapon_score (weapon_id, score DESC)
);
global_rank(总榜,冗余表,TOP 100)
CREATE TABLE global_rank (
rank INT PRIMARY KEY,
character_id BIGINT NOT NULL,
username VARCHAR(64) NOT NULL,
weapon_id INT NOT NULL,
score INT NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
weaponrank{id}(各武器榜,TOP 100)
-- 16张表:weapon_rank_2000, weapon_rank_2001, ... weapon_rank_2015
CREATE TABLE weapon_rank_2000 (
rank INT PRIMARY KEY,
character_id BIGINT NOT NULL,
username VARCHAR(64) NOT NULL,
score INT NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- (其余15张表结构相同)
考虑到 SQLite(服务端未定),可用单表 + weapon_id 过滤实现,或 JSON 分表。此处用 MySQL 多表描述。
四、Proto 消息
// 上报分数(游戏结束时自动调用)
message C2S_RankSubmit {
int32 weapon_id = 1; // 当前武器ID(2000-2015)
int32 score = 2; // 本局得分(ScoreSystem.CurrentScore)
}
// 上报结果
message S2C_RankSubmitResult {
int32 code = 1; // 0=成功,其他=错误码
string msg = 2;
int32 rank_in_total = 3; // 总榜排名(-1=未上榜)
int32 rank_in_weapon = 4; // 武器榜排名(-1=未上榜)
}
// 查询排行榜
message C2S_RankQuery {
int32 query_type = 1; // 0=总榜,1=武器榜
int32 weapon_id = 2; // query_type=1 时填武器ID(2000-2015),否则填0
int32 offset = 3; // 起始位置(0=第1名)
int32 limit = 4; // 条数(最大100)
}
// 排行榜条目
message RankEntry {
int32 rank = 1; // 排名(1-100)
string username = 2; // 用户名
int32 score = 3; // 分数
int32 weapon_id = 4; // 武器ID(总榜有,武器榜无)
}
// 查询结果
message S2C_RankQueryResult {
int32 code = 1; // 0=成功
int32 query_type = 2; // 查询类型(0=总榜,1=武器榜)
int32 weapon_id = 3; // 武器ID(查询武器榜时)
repeated RankEntry entries = 4;
int32 my_rank = 5; // 我的排名(-1=未上榜)
int32 my_score = 6; // 我的分数(查询时用的那个)
}
五、错误码
| code | 说明 |
|---|---|
| 0 | 成功 |
| 1 | 角色不存在 |
| 2 | 分数无效(<=0) |
| 3 | 武器ID无效 |
| 10 | 查询类型无效 |
六、完整流程
上报(游戏结束时)
客户端 服务端
| |
| GameStateManager.OnPlayerDead()
| |
|--- C2S_RankSubmit --------->|
| (weapon_id=2000, score=12345)
| |
| | 1. 验证 character_id / weapon_id / score
| | 2. 写入 character_rank_scores
| | 3. 更新 global_rank(重新排序,插入或忽略)
| | 4. 更新 weapon_rank_{id}(重新排序,插入或忽略)
| | 5. 计算 rank_in_total / rank_in_weapon
| |
|<-- S2C_RankSubmitResult ----|
| (code=0, rank_in_total=42, rank_in_weapon=3)
| |
| RankingService.OnRankSubmitResult?.Invoke(result)
查询(玩家主动拉取)
客户端 服务端
| |
|--- C2S_RankQuery ----------->|
| (query_type=0, offset=0, limit=20) // 总榜前20名
| |
|<-- S2C_RankQueryResult ------|
| (code=0, query_type=0, entries=[...])
| |
|--- C2S_RankQuery ----------->|
| (query_type=1, weapon_id=2000, offset=0, limit=20) // 武器2000榜
| |
|<-- S2C_RankQueryResult ------|
| (code=0, query_type=1, weapon_id=2000, entries=[...])
七、服务端实现
新增文件
server/RankService.cs
// 上报处理
public async Task<RankSubmitResult> SubmitScoreAsync(long characterId, string username, int weaponId, int score);
// 查询处理
public async Task<RankQueryResult> QueryRankAsync(long characterId, int queryType, int weaponId, int offset, int limit);
// 内部:重新计算 global_rank 和 weapon_rank_{id}
private void RebuildGlobalRank();
private void RebuildWeaponRank(int weaponId);
server/RankController.cs
- WebSocket 处理(C2S_RankSubmit → S2C_RankSubmitResult)
- WebSocket 处理(C2S_RankQuery → S2C_RankQueryResult)
数据库操作
上报时:
// 插入记录
INSERT INTO character_rank_scores (character_id, username, weapon_id, score) VALUES (?, ?, ?, ?)
// 更新总榜(取 TOP 100)
INSERT INTO global_rank (rank, character_id, username, weapon_id, score)
SELECT COUNT(*) + 1, ?, ?, ?, ?
FROM global_rank WHERE score > ?
ON DUPLICATE KEY UPDATE score=VALUES(score), username=VALUES(username), weapon_id=VALUES(weapon_id), updated_at=NOW()
// 更新武器榜(取 TOP 100)
INSERT INTO weapon_rank_{id} (rank, character_id, username, score)
...
查询时:
SELECT rank, username, score FROM global_rank ORDER BY rank LIMIT ? OFFSET ?
SELECT rank, username, score FROM weapon_rank_{id} ORDER BY rank LIMIT ? OFFSET ?
八、客户端实现
改动点
1. ScoreSystem.cs — Settle() 新增网络上报
public void Settle()
{
bool isNewRecord = RankingService.Instance.SubmitScore(
AuthService.Instance.Username,
_currentScore
);
// 新增:上报到服务器
WebSocketService.Instance.SubmitRank(
WeaponSystem.Instance.CurrentWeaponId,
_currentScore
);
if (isNewRecord) {
EventDispatcher.Instance.Publish(EventID.OnNewRecord, _currentScore);
}
}
2. RankingService.cs — 预留网络回调
public event Action<RankSubmitResult> OnRankSubmitResult;
public void SubmitRankNetwork(int weaponId, int score) {
WebSocketService.Instance.SubmitRank(weaponId, score);
}
3. WebSocketService.cs — 新增
// 上报
public void SubmitRank(int weaponId, int score) {
var req = new C2S_RankSubmit { WeaponId = weaponId, Score = score };
Send(MsgId.C2S_RankSubmit, req);
}
// 查询
public void QueryRank(int queryType, int weaponId, int offset, int limit) {
var req = new C2S_RankQuery { QueryType = queryType, WeaponId = weaponId, Offset = offset, Limit = limit };
Send(MsgId.C2S_RankQuery, req);
}
// 处理结果
case MsgId.S2C_RankSubmitResult:
var submitResult = _decoder.DecodeS2C_RankSubmitResult(pkg.Data);
MainThreadDispatcher.Invoke(() => RankingService.Instance.OnRankSubmitResult?.Invoke(submitResult));
break;
case MsgId.S2C_RankQueryResult:
var queryResult = _decoder.DecodeS2C_RankQueryResult(pkg.Data);
MainThreadDispatcher.Invoke(() => OnRankQueryResult?.Invoke(queryResult));
break;
事件:
public event Action<S2C_RankSubmitResult> OnRankSubmitResult;
public event Action<S2C_RankQueryResult> OnRankQueryResult;
九、注意事项
- 同分数处理:按 timestamp 排序,早提交的排前
- 客户端查榜:排行榜界面打开时拉取,不主动推送
- NETWORK_ENABLED=false 时:本地流程不变,网络上报跳过(GameConfig.NETWORK_ENABLED)
- 升级武器后:武器榜显示的是使用该武器的历史最高分,而非当前武器属性
- TopScore 更新:上报后若分数更高,
InventoryService.Instance.TopScore应同步更新
十、MsgId 分配建议
| 消息 | MsgId |
|---|---|
| C2S_RankSubmit | 60 |
| S2C_RankSubmitResult | 61 |
| C2S_RankQuery | 62 |
| S2C_RankQueryResult | 63 |
十一、已完成清单
- [x] 更新 Ninjiafall1Proto.proto(添加 RankSubmit / RankSubmitResult / RankQuery / RankQueryResult + RankEntry)
- [x] 重新编译 protobuf(build.sh)
- [x] 数据库:character_rank_scores / global_rank / weaponrank* 表
- [x] 服务端:RankService.cs — SubmitScoreAsync / QueryRankAsync,16表自动建
- [x] 服务端:RankController.cs(WebSocket)— HandleRankSubmit / HandleRankQuery
- [x] 服务端:WebSocketServer.cs — 注册 MsgId.C2S_RankSubmit / C2S_RankQuery
- [x] 服务端:Program.cs — DI 注册 RankService
- [x] 客户端:ProtobufTypes.cs — 添加消息类型
- [x] 客户端:ProtobufCodec.cs — 编解码支持
- [x] 客户端:WebSocketService.cs — SubmitRank + QueryRank + 事件
- [x] 客户端:ScoreSystem.Settle() — 新增网络上报调用
- [ ] 测试:上报成功 / 分数被刷新 / 查询总榜 / 查询武器榜(待联调)
所有内容仅供学习与交流,转载须标明链接。未经同意,禁止作为商业用途,有特殊需求请联系站长。
