玩法奖励池机制设计文档

玩法奖励池机制设计:客户端用 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. 待确认

  1. rewardCount 固定为 5 是否合适?
  2. 宝箱层范围 5~55 层?还是其他区间?
  3. session 超时时间(建议 30 分钟)
  4. distance 上限(建议 ≤ 999999)
  • 留下精彩的评论吧~(0条)

所有内容仅供学习与交流,转载须标明链接。未经同意,禁止作为商业用途,有特殊需求请联系站长。