8、战斗逻辑校验:伤害计算公式服务器重算、暴击/命中判定服务器校验、技能连招合法性

战斗逻辑校验,说白了就是让服务器当一回“裁判”。

客户端打得再花哨,服务器不认,那就等于零。我见过太多游戏,客户端算得飞起,结果被脱机挂一锅端。嗯,这里的关键在于——服务器必须拥有最终解释权

8.1 伤害计算公式服务器重算

客户端提交一次攻击,服务器不能直接信。你得重新算一遍伤害。

为什么?因为客户端可能被修改。攻击力改成一亿,暴击率改成100%,这种事我遇到过不止一次。

服务器重算伤害,核心逻辑是这样的:

  1. 接收攻击请求:客户端上报“我打了目标一下”,附带攻击者ID、技能ID、目标ID。
  2. 校验攻击者状态:攻击者是否存活?是否在攻击范围内?技能冷却是否好了?
  3. 获取双方属性:服务器从数据库或缓存中拉取攻击者的攻击力、穿透、暴击率,以及目标的防御、韧性、减伤等。
  4. 执行公式:用服务器端的公式重新计算伤害,完全不信任客户端传来的任何数值。
  5. 返回结果:将最终伤害值同步给客户端,用于播放特效和飘字。

核心原则:客户端上报的伤害值、暴击标志、命中结果,服务器一律无视。服务器只认自己算出来的。

举个例子,一个简单的伤害公式:

// 服务器端伪代码
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 整体校验流程

把上面三个点串起来,战斗逻辑校验的完整流程是这样的:

客户端发起攻击请求
    ↓
服务器接收请求,解析攻击者、技能、目标
    ↓
校验攻击者状态(存活、范围、冷却)
    ↓
校验技能连招合法性(顺序、时间窗口)
    ↓
执行命中判定(服务器随机数)
    ↓
执行暴击判定(服务器随机数)
    ↓
执行伤害计算(服务器公式)
    ↓
返回结果给客户端
    ↓
客户端播放表现

这个流程里,每一步都不能跳过。我曾经见过一个项目,只做了伤害重算,没做连招校验,结果被外挂用“无前摇技能”刷爆了排行榜。

总结一句话:战斗逻辑校验,就是让服务器当那个“不讲情面”的裁判。客户端可以演,但服务器必须较真。

战斗逻辑校验核心流程 客户端发起攻击请求 服务器接收请求,解析数据 校验攻击者状态 校验技能连招合法性 校验冷却/时间窗口 命中判定 + 暴击判定(服务器) 伤害计算(服务器公式) 返回结果给客户端

这张图把整个流程串起来了。你可以看到,所有关键判定都集中在服务器端。客户端只负责发起请求和播放表现。

嗯,最后说一句。战斗逻辑校验做得好不好,直接决定游戏能不能活。我见过太多游戏,上线第一天就被外挂打穿,就是因为服务器太“信任”客户端了。记住:永远不要相信客户端传来的任何战斗数据

公众号:蓝海资料掘金营,微信deep3321