8、战斗逻辑校验:伤害计算公式服务器重算、暴击/命中判定服务器校验、技能连招合法性
战斗逻辑校验,说白了就是让服务器当一回“裁判”。
客户端打得再花哨,服务器不认,那就等于零。我见过太多游戏,客户端算得飞起,结果被脱机挂一锅端。嗯,这里的关键在于——服务器必须拥有最终解释权。
8.1 伤害计算公式服务器重算
客户端提交一次攻击,服务器不能直接信。你得重新算一遍伤害。
为什么?因为客户端可能被修改。攻击力改成一亿,暴击率改成100%,这种事我遇到过不止一次。
服务器重算伤害,核心逻辑是这样的:
- 接收攻击请求:客户端上报“我打了目标一下”,附带攻击者ID、技能ID、目标ID。
- 校验攻击者状态:攻击者是否存活?是否在攻击范围内?技能冷却是否好了?
- 获取双方属性:服务器从数据库或缓存中拉取攻击者的攻击力、穿透、暴击率,以及目标的防御、韧性、减伤等。
- 执行公式:用服务器端的公式重新计算伤害,完全不信任客户端传来的任何数值。
- 返回结果:将最终伤害值同步给客户端,用于播放特效和飘字。
核心原则:客户端上报的伤害值、暴击标志、命中结果,服务器一律无视。服务器只认自己算出来的。
举个例子,一个简单的伤害公式:
// 服务器端伪代码
float CalculateDamage(Attacker a, Defender d, Skill s) {
float baseDamage = a.attack * s.damageRatio;
float defenseReduction = d.defense / (d.defense + 1000); // 防御减伤
float finalDamage = baseDamage * (1 - defenseReduction);
// 暴击判定
if (IsCritical(a.critRate, d.critResist)) {
finalDamage *= a.critDamage;
}
// 浮动修正
finalDamage *= Random.Range(0.95f, 1.05f);
return Mathf.Max(1, finalDamage); // 至少造成1点伤害
}
我个人习惯在服务器端维护一套完整的属性快照。战斗开始时,把双方属性都缓存下来,避免频繁读库。这样重算起来很快,延迟也低。
8.2 暴击/命中判定服务器校验
暴击和命中,是外挂最爱改的两个点。你想想看,如果客户端能自己决定“这一刀暴击了”,那游戏平衡性就彻底崩了。
服务器校验暴击/命中的流程,我总结为三步:
- 随机种子统一:服务器和客户端使用相同的随机种子。战斗开始时,服务器生成一个种子,同步给客户端。后续所有随机判定,都基于这个种子。
- 判定逻辑下沉:暴击判定、命中判定、闪避判定,全部在服务器执行。客户端只负责表现。
- 结果同步:服务器判定完成后,将结果(暴击了/没暴击,命中了/闪避了)推送给客户端。客户端根据结果播放对应的动画和特效。
避坑指南:我曾经遇到过一种情况——客户端和服务器的随机算法不一致。客户端用Unity的Random.Range,服务器用C#的Random.Next,结果两边算出来的暴击序列对不上。后来我统一用相同的算法和种子,才彻底解决。
命中判定的公式一般长这样:
// 命中判定
bool IsHit(Attacker a, Defender d) {
float hitRate = a.hitRate - d.dodgeRate;
hitRate = Mathf.Clamp01(hitRate); // 限制在0~1之间
float roll = Random.Value; // 0~1随机数
return roll < hitRate;
}
这里有个细节:命中率不能低于一个最小值。比如5%,保证即使属性差距再大,也有一定概率命中。否则玩家体验会很差。
8.3 技能连招合法性
技能连招,是动作游戏和MOBA游戏的重灾区。外挂可以做到“一秒五刀”,或者“无CD连招”。服务器必须校验连招的合法性。
校验的核心维度有三个:
| 维度 | 说明 | 校验方式 |
|---|---|---|
| 技能冷却 | 每个技能是否有独立的CD | 服务器记录每个技能的冷却结束时间戳 |
| 技能队列 | 技能释放顺序是否合法 | 校验技能之间的前置/后置关系 |
| 时间窗口 | 两次技能释放的间隔是否合理 | 检查时间戳差值是否大于最小间隔 |
我举个例子。某个角色的连招是“A技能 → B技能 → C技能”。外挂可能直接跳过A和B,狂按C。服务器怎么防?
答案是:维护一个技能状态机。
// 技能状态机示例
class SkillStateMachine {
private Dictionary<int, SkillState> playerStates;
bool CanCastSkill(int playerId, int skillId) {
var state = playerStates[playerId];
// 1. 检查冷却
if (state.cooldowns[skillId] > CurrentTime) {
return false; // 冷却中
}
// 2. 检查连招前置
if (skillChain[skillId].previousSkill != state.lastSkill) {
return false; // 连招顺序错误
}
// 3. 检查时间窗口
if (CurrentTime - state.lastCastTime < MIN_CAST_INTERVAL) {
return false; // 释放太快
}
return true;
}
}
注意:时间窗口的阈值不能设得太死。网络延迟会导致客户端和服务器的计时有偏差。我一般会加一个50~100毫秒的容忍区间。太严了,正常玩家也会被误判。
还有一个常见问题:技能被打断。比如玩家正在释放A技能,被敌人控制了,那么A技能的连招状态应该重置。服务器需要监听战斗事件,及时更新状态机。
8.4 整体校验流程
把上面三个点串起来,战斗逻辑校验的完整流程是这样的:
客户端发起攻击请求
↓
服务器接收请求,解析攻击者、技能、目标
↓
校验攻击者状态(存活、范围、冷却)
↓
校验技能连招合法性(顺序、时间窗口)
↓
执行命中判定(服务器随机数)
↓
执行暴击判定(服务器随机数)
↓
执行伤害计算(服务器公式)
↓
返回结果给客户端
↓
客户端播放表现
这个流程里,每一步都不能跳过。我曾经见过一个项目,只做了伤害重算,没做连招校验,结果被外挂用“无前摇技能”刷爆了排行榜。
总结一句话:战斗逻辑校验,就是让服务器当那个“不讲情面”的裁判。客户端可以演,但服务器必须较真。
这张图把整个流程串起来了。你可以看到,所有关键判定都集中在服务器端。客户端只负责发起请求和播放表现。
嗯,最后说一句。战斗逻辑校验做得好不好,直接决定游戏能不能活。我见过太多游戏,上线第一天就被外挂打穿,就是因为服务器太“信任”客户端了。记住:永远不要相信客户端传来的任何战斗数据。