玩法奖励池机制设计文档
玩法奖励池机制设计:客户端用 seed 预生成宝箱表,结束后上报 passedFloors + MD5,服务器还原校验后写入背包并录入排行榜。Pool 1 专属机制,Pool 2/3 走正常抽奖。
玩法奖励池机制设计文档
1. 概述
模块划分
| 模块 | 归属 | 职责 |
|---|---|---|
GameSessionManager |
玩法系统 | 处理 Pool 1(seed → 宝箱表 → passedFloors + md5) |
LotteryManager |
抽奖系统 | 处理 Pool 2/3(正常请求服务器,实时抽奖) |
Pool 1 对客户端来说是「玩法内置宝箱」,非抽卡系统功能。
- 进入玩法 → 拿 seed → 生成本地宝箱表
- 每层检查 → 有宝箱就显示 → 玩家点开
- 玩法结束 → 上报 passedFloors + md5
服务器收到后还原 Pool 1 奖励,写入背包。
2. 流程总览
[玩法开始] 客户端 ──▶ C2S_GameSessionStart
◀─── S2C_GameSessionStart { sessionId, seed, rewardCount }
[玩法中] 客户端用 seed 预生成 1000 层宝箱表 boxTable[floor]
每到新层 → 检查 boxTable[floor]
非 0 → 显示宝箱,玩家点击
记录 passedFloors(所有经过的层)
[玩法结束] 客户端 ──▶ C2S_GameSessionEnd {
sessionId, time, distance, passedFloors, md5
}
服务器:
1. 用 seed 还原 boxTable
2. 过滤 passedFloors 中的宝箱层 → 奖励列表
3. 校验 MD5(奖励列表) == md5
4. time / max(passedFloors) >= 0.5
5. 全部通过 → 写入背包 + 录入排行榜
◀─── S2C_GameSessionEnd { verified, rewards[] }
3. Proto 消息设计
C2S_GameSessionStart(开始玩法,无参数)
message C2S_GameSessionStart {
option (msgid) = 40;
}
S2C_GameSessionStart(服务器返回)
message S2C_GameSessionStart {
option (msgid) = 41;
int32 code = 1; // 0=成功
string msg = 2;
string session_id = 3; // 会话ID(UUID)
int32 seed = 4; // 随机种子
int32 reward_count = 5; // 奖励数量(宝箱总个数)
}
C2S_GameSessionEnd(结束玩法)
message C2S_GameSessionEnd {
option (msgid) = 42;
string session_id = 1;
int32 time = 2; // 存活时间(秒)
int32 distance = 3; // 分数(用于排行榜)
repeated int32 passed_floors = 4; // 经过的层列表
string md5 = 5; // MD5(宝箱奖励ID列表JSON)
}
S2C_GameSessionEnd(校验结果)
message S2C_GameSessionEnd {
option (msgid) = 43;
int32 code = 1; // 0=成功, 1=session无效, 2=校验失败
string msg = 2;
bool verified = 3; // 是否通过校验
repeated Reward rewards = 4; // 奖励列表(校验通过时)
}
4. 客户端 GameSessionManager
核心职责
- 管理玩法生命周期(开始/结束)
- 用 seed 预生成 1000 层宝箱表
- 记录 passedFloors
- 生成 MD5
数据结构
public class GameSessionManager {
public static GameSessionManager Instance { get; private set; }
private string _sessionId;
private int _seed;
private int _rewardCount;
private int[] _boxTable; // boxTable[floor] = rewardId, 0=无宝箱
private HashSet<int> _passedFloors = new();
private HashSet<int> _earnedRewards = new(); // 已领取的 rewardId
// 外部注入的回调
public event Action<int> OnBoxAppeared; // 宝箱出现(传 rewardId)
public event Action OnSessionEnd; // 玩法结束
}
初始化(SessionStart 后调用)
public void Init(string sessionId, int seed, int rewardCount) {
_sessionId = sessionId;
_seed = seed;
_rewardCount = rewardCount;
_passedFloors.Clear();
_earnedRewards.Clear();
_boxTable = GenerateBoxTable(seed, rewardCount);
}
// 用 seed 确定性生成 1000 层宝箱表
int[] GenerateBoxTable(int seed, int rewardCount) {
var table = new int[1001]; // index 0~1000,0=无宝箱
var rewardIds = GenerateRewardIds(seed, rewardCount); // 从 seed 生成 rewardId 序列
var floors = GenerateBoxFloors(seed, rewardCount); // 从 seed 生成层号序列
for (int i = 0; i < rewardCount; i++) {
table[floors[i]] = rewardIds[i];
}
return table;
}
每层检查
public void OnFloorEntered(int floor) {
_passedFloors.Add(floor);
if (floor <= 1000 && _boxTable[floor] != 0) {
OnBoxAppeared?.Invoke(_boxTable[floor]);
}
}
宝箱点击
public void OnBoxOpened(int floor) {
if (floor <= 1000 && _boxTable[floor] != 0) {
_earnedRewards.Add(_boxTable[floor]);
}
}
玩法结束
public (string sessionId, int time, int distance, string md5) BuildEndReport(int time, int distance) {
// 生成 MD5:所有已领取奖励的 JSON
var earnedList = _earnedRewards.ToList();
var json = JsonSerialize(earnedList);
var md5 = ComputeMD5(json);
return (_sessionId, time, distance, md5);
}
5. 客户端 LotteryManager(抽奖系统)
核心职责
- 处理 Pool 2/3 抽奖
- 余额判断
- 调服务器抽奖
接口
public class LotteryManager {
public static LotteryManager Instance { get; private set; }
public static readonly int[] AVAILABLE_POOLS = new[] { 2, 3 };
public (int costType, int costAmount) GetPoolCost(int poolId);
public bool CanAfford(int poolId);
public void Draw(int poolId, int count = 1); // 调 WebSocketService
}
6. 服务器逻辑
GameSession 数据结构
public class GameSession {
public string SessionId;
public int UserId;
public int Seed;
public int RewardCount;
public List<int> RewardIds; // 奖励ID列表(按 seed 生成)
public List<int> BoxFloors; // 宝箱层列表(按 seed 生成)
public bool Used;
}
C2S_GameSessionEnd 校验
public async Task HandleGameSessionEnd(WebSocketClient client, GameSessionEndRequest req) {
var session = _sessions.GetValueOrDefault(req.SessionId);
if (session == null || session.UserId != client.UserId || session.Used)
return SendError("session无效");
// 用 seed 还原宝箱分布
var (_, rewardIds) = GenerateFromSeed(session.Seed, session.RewardCount);
// 过滤 passedFloors 中的宝箱层 → 实际领取的奖励
var earned = new List<int>();
foreach (var floor in req.PassedFloors) {
if (session.BoxFloors.Contains(floor)) {
int idx = session.BoxFloors.IndexOf(floor);
earned.Add(rewardIds[idx]);
}
}
// MD5 校验
var json = JsonSerialize(earned);
if (ComputeMD5(json) != req.Md5)
return SendError("MD5校验失败");
// 0.5秒/层校验
int maxFloor = req.PassedFloors.Count > 0 ? req.PassedFloors.Max() : 0;
if (maxFloor > 0 && (float)req.Time / maxFloor < 0.5f)
return SendError("时间异常");
// 奖励完整性
if (earned.Count != session.RewardCount)
return SendError("奖励数量不完整");
// 通过 → 写入背包 + 入排行榜
foreach (var rewardId in earned) {
await _db.AddItem(client.UserId, rewardId, 1);
}
await _rankService.SubmitScore(client.UserId, req.Distance);
session.Used = true;
SendSuccess(earned);
}
7. MD5 校验说明
校验内容
奖励 ID 列表的 JSON 字符串(两端生成算法完全一致)。
双端一致性保证
客户端和服务器共用同一套 Deterministic RNG 算法:
int DeterministicRand(int seed, int step) {
return (seed * 1103515245 + step * 12345 + 12347) % (1 << 30);
}
同一 seed → 同 DeterministicRand 序列 → 同 boxTable → 同 earnedRewards → 同 JSON → 同 MD5
示例
seed = 42, rewardCount = 5
step=0 → floor = 5 + rand(42,0)%50 = 47
step=1 → rewardId index = rand(42,1)%pool1Count = 3 → rewardId=103
step=2 → floor = 5 + rand(42,2)%50 = 22
step=3 → rewardId index = rand(42,3)%pool1Count = 1 → rewardId=101
...
客户端 boxTable[47]=103, boxTable[22]=101, ...
服务器 RewardIds[0]=103, RewardIds[1]=101, ...
earnedRewards = [103, 101, ...] // 顺序一致
JSON = "[103, 101, ...]"
MD5 一致 ✓
奖励生成算法(服务器和客户端共用)
// 奖池 1 的 rewardId 列表(来自 GachaPool.csv)
List<int> Pool1RewardIds = LoadPool1RewardIds(); // 从 CSV 读取
// 用 seed 生成 rewardId 序列
(int[] floors, int[] rewardIds) GenerateFromSeed(int seed, int rewardCount) {
floors = new int[rewardCount];
rewardIds = new int[rewardCount];
for (int i = 0; i < rewardCount; i++) {
int floor = 5 + DeterministicRand(seed, i * 2) % 50; // 第5~54层
int rewardIdx = DeterministicRand(seed, i * 2 + 1) % Pool1RewardIds.Count;
floors[i] = floor;
rewardIds[i] = Pool1RewardIds[rewardIdx];
}
return (floors, rewardIds);
}
Random Seed 使用位置
| 阶段 | 客户端 | 服务器 |
|---|---|---|
| SessionStart | 用 seed 生成 1000 层 boxTable | 用 seed 生成 RewardIds + BoxFloors |
| 玩法中 | 每层查 boxTable | 无(不参与) |
| SessionEnd | 生成 md5(earnedRewards) | 重算 md5 校验 |
8. 防作弊汇总
| 作弊方式 | 防御 |
|---|---|
| 伪造 seed | 服务器生成 seed,客户端无法预知 |
| 伪造 passedFloors | 服务器用 seed 还原宝箱层,不依赖客户端 |
| 伪造 md5 | 服务器重算 MD5 比对 |
| 瞬间通关 | time / max(passedFloors) >= 0.5 |
| 跳过宝箱 | earned.Count == RewardCount |
| 重复提交 session | Used 标记 |
| 冒领他人 session | session.UserId 校验 |
8. 待确认
- rewardCount 固定为 5 是否合适?
- 宝箱层范围 5~55 层?还是其他区间?
- session 超时时间(建议 30 分钟)
- distance 上限(建议 ≤ 999999)
所有内容仅供学习与交流,转载须标明链接。未经同意,禁止作为商业用途,有特殊需求请联系站长。
