4、数值校验体系:游戏内经济系统校验、属性增长曲线校验、概率类事件校验
数值校验,说白了就是给游戏世界里的数字上把锁。
我见过太多项目,上线第一天就被刷子们把经济系统搞崩了。嗯,这里要说的不是简单的「数值范围检查」,而是一整套校验体系。它要能挡住脚本、堵住逻辑漏洞、还能在性能开销和安全性之间找到平衡点。
4.1 经济系统校验:别让金币变成废纸
游戏经济系统一旦崩了,玩家信任也就没了。我经历过一个MMO项目,上线第三天就发现某个副本产出金币异常——原因是客户端提交的掉落数据没做二次校验,脚本直接改了掉落包。
经济系统校验,核心要盯住三个环节:
- 产出校验:每个金币来源都要有「合法身份」。任务奖励、怪物掉落、交易收入,各自有独立的产出ID和上限。
- 流通校验:玩家之间交易时,物品和金币的流向必须可追溯。我习惯在交易完成时记录一条「交易流水」,包含双方ID、物品、金额、时间戳。
- 总量监控:服务器端定时统计全服金币总量。如果某小时增量超过阈值(比如正常产出的3倍),立刻触发告警。
举个例子,玩家出售物品给商店:
// 伪代码:服务器端出售校验
function sellItem(player, itemId, quantity) {
// 1. 检查物品是否存在
item = getItemFromDB(player.id, itemId);
if (!item || item.quantity < quantity) return ERROR_ITEM_NOT_FOUND;
// 2. 计算总价(服务器算,不用客户端传的)
price = getItemSellPrice(itemId) * quantity;
// 3. 检查玩家当日出售次数上限
if (getDailySellCount(player.id) >= MAX_DAILY_SELL) return ERROR_SELL_LIMIT;
// 4. 扣除物品,增加金币
removeItem(player.id, itemId, quantity);
addGold(player.id, price);
// 5. 记录流水
logTransaction(player.id, "sell", itemId, quantity, price);
}
你想想看,如果第2步用了客户端传过来的价格,那脚本直接传个天价,经济系统瞬间就崩了。
4.2 属性增长曲线校验:别让角色变成超人
属性增长曲线,说白了就是角色从1级到满级,各项属性应该怎么涨。这个曲线一旦被篡改,就会出现「1级号一刀秒BOSS」的荒唐事。
我建议在服务器端维护一份「属性增长基准表」:
| 等级区间 | 攻击力基准 | 防御力基准 | 血量基准 | 允许偏差 |
|---|---|---|---|---|
| 1-10 | 10-50 | 5-25 | 100-500 | ±5% |
| 11-30 | 50-200 | 25-100 | 500-2000 | ±3% |
| 31-60 | 200-800 | 100-400 | 2000-8000 | ±2% |
| 61-100 | 800-3000 | 400-1500 | 8000-30000 | ±1% |
每次玩家属性发生变化(升级、装备更换、技能学习),服务器都要做一次「属性合规检查」:
function validateAttributes(player) {
expected = getBaseAttributes(player.level);
actual = player.getAttributes();
// 检查每个属性是否在允许范围内
for (attr in ["attack", "defense", "hp"]) {
deviation = abs(actual[attr] - expected[attr]) / expected[attr];
if (deviation > ALLOWED_DEVIATION[player.level]) {
// 触发异常,回滚属性
logCheatAttempt(player.id, "attribute_overflow", attr, deviation);
rollbackAttributes(player);
return false;
}
}
return true;
}
我曾经遇到过一个案例:外挂通过修改内存中的装备强化等级,把+5的武器改成了+20。因为服务器只在装备穿戴时校验一次,结果这个假+20武器在玩家手里用了三天才被发现。后来我们改成「每次战斗前校验装备属性」,才堵住这个漏洞。
4.3 概率类事件校验:别让抽卡变成必中
概率类事件,比如抽卡、暴击、掉落,是外挂的重灾区。外挂通常通过两种方式作弊:
- 重放攻击:抽到好东西就提交,没抽到就断线重连
- 篡改随机种子:直接修改客户端随机数,让「1%概率」变成「100%概率」
我的做法是:所有概率事件的结果,由服务器生成,客户端只负责展示。
具体流程是这样的:
- 客户端发起抽卡请求,带上本次操作的「流水号」
- 服务器用流水号+玩家ID+时间戳,生成一个「确定性随机数」
- 服务器根据这个随机数,决定抽卡结果
- 结果直接写入数据库,再返回给客户端
// 服务器端抽卡逻辑
function drawCard(player, poolId) {
// 1. 检查抽卡次数和资源
if (!canDraw(player, poolId)) return ERROR_LIMIT;
// 2. 生成确定性随机数
seed = hash(player.id + "_" + poolId + "_" + getDrawCount(player, poolId));
random = pseudoRandom(seed);
// 3. 根据概率表决定结果
result = getResultByProbability(poolId, random);
// 4. 发放奖励,记录日志
giveReward(player, result);
logDraw(player.id, poolId, result, seed);
return result;
}
另外,概率事件还要做「宏观校验」。比如某个SSR的出货率设定为1%,如果服务器统计发现某玩家连续1000抽都没出,虽然单次概率没问题,但整体概率已经偏离了预期。这时候需要触发「保底机制」或者告警。
4.4 数值校验体系架构图
下面这张图展示了数值校验体系的整体架构。你可以看到,所有客户端请求都要经过三层校验,才能到达游戏逻辑层。
这张图里,三层校验是串行的。任何一层校验失败,请求都会被拦截,不会到达游戏逻辑层。这样做的好处是:即使某一层有漏洞,还有后面两层兜底。
嗯,数值校验这块就聊这么多。记住一句话:服务器永远不要相信客户端传来的任何数值。所有数字,自己算一遍,准没错。