6、关键操作校验:移动速度校验、瞬移检测、技能释放频率与范围校验

好,咱们接着聊反外挂的核心防线。

前面几章我们讲了数据包加密、内存保护这些基础工作。但说实话,这些都属于「防君子不防小人」的手段。真正让外挂开发者头疼的,是行为层面的校验

什么意思呢?你加密做得再好,内存保护得再严,外挂照样可以模拟合法客户端发送数据包。它不读你的内存,它直接伪造操作指令。这时候你怎么防?

答案就是:校验操作本身是否合理

这一章,我重点讲三个最容易被外挂利用的操作维度——移动速度、瞬移、技能释放。这三个点,几乎覆盖了市面上 80% 的移动类和战斗类外挂。


6.1 移动速度校验:别让角色跑得比飞机还快

先问一个问题:你的游戏里,角色最大移动速度是多少?

很多新手策划会拍脑袋给个数值,比如「每秒 10 米」。但外挂开发者拿到这个数值后,直接让客户端发包说「我每秒跑了 100 米」,服务器如果不校验,那角色就真的飞起来了。

核心思路很简单:服务器必须独立计算合法速度范围。

6.1.1 速度校验的基本公式

我个人习惯用这个公式做基础校验:

// 伪代码:移动速度校验
bool ValidateMoveSpeed(Vector3 oldPos, Vector3 newPos, float deltaTime) {
    float distance = Vector3.Distance(oldPos, newPos);
    float speed = distance / deltaTime;  // 实际速度
    
    float maxSpeed = GetMaxMoveSpeed(player);  // 从配置表读取
    
    // 留一点误差容忍(网络抖动、浮点误差)
    float tolerance = 1.2f;  // 允许 20% 的超速
    
    if (speed > maxSpeed * tolerance) {
        LogWarning("玩家 " + player.Id + " 移动速度异常: " + speed);
        return false;
    }
    return true;
}

嗯,这里要注意:容忍度不能给太大。我曾经见过一个项目,容忍度设了 2.0(允许超速 100%),结果外挂直接跑出两倍速度,玩家举报说「这游戏有火箭靴外挂」。后来我们改成了 1.15,配合平滑移动算法,基本就堵住了。

6.1.2 速度校验的进阶:加速度校验

光校验速度还不够。外挂可以做到「每帧都卡在最大速度」,但现实中玩家操作是有加速度的。你想想看,一个角色从静止到全速奔跑,总得有个加速过程吧?

所以我会再加一层加速度校验:

// 加速度校验
bool ValidateAcceleration(Vector3 oldPos, Vector3 newPos, 
                          Vector3 prevPos, float deltaTime) {
    float currentSpeed = (newPos - oldPos).magnitude / deltaTime;
    float prevSpeed = (oldPos - prevPos).magnitude / deltaTime;
    float acceleration = (currentSpeed - prevSpeed) / deltaTime;
    
    float maxAccel = GetMaxAcceleration(player);  // 比如 20 m/s²
    
    if (Mathf.Abs(acceleration) > maxAccel * 1.5f) {
        LogWarning("加速度异常: " + acceleration);
        return false;
    }
    return true;
}

这个校验能防住什么?防那种「瞬间从静止到满速」的外挂。正常玩家按 W 键,角色需要 0.3~0.5 秒才能达到最大速度。外挂直接一步到位,加速度校验就能把它揪出来。

重要提示:速度校验一定要在服务器端做,不要在客户端做。客户端做的校验,外挂可以直接跳过。我在项目中见过有人把校验写在客户端,结果外挂直接 hook 了那个函数,永远返回 true。


6.2 瞬移检测:别让玩家「闪现」到 Boss 脸上

瞬移外挂,说白了就是角色位置在极短时间内发生了巨大变化。比如上一帧还在新手村,下一帧就到了副本 Boss 面前。

这种外挂对游戏平衡性破坏极大。PVP 里直接贴脸,PVE 里跳过所有小怪。怎么防?

6.2.1 基于距离的瞬移检测

最基础的做法:计算两帧之间的位移距离,如果超过阈值,直接判定为瞬移。

bool DetectTeleport(Vector3 oldPos, Vector3 newPos, float deltaTime) {
    float distance = Vector3.Distance(oldPos, newPos);
    
    // 最大合法移动距离 = 最大速度 * 时间间隔 * 容忍系数
    float maxDistance = GetMaxMoveSpeed(player) * deltaTime * 1.2f;
    
    if (distance > maxDistance) {
        LogWarning("检测到疑似瞬移: 距离=" + distance + 
                   " 最大允许=" + maxDistance);
        return true;
    }
    return false;
}

这个逻辑看起来简单,但有个坑:网络延迟会导致位置跳跃。比如玩家 A 在客户端走了 2 秒,但因为网络卡顿,服务器收到的是 2 秒后的位置,这时候距离就会很大。

怎么解决?我建议做时间戳对齐

  • 客户端发包时带上本地时间戳
  • 服务器计算时,用「当前服务器时间 - 客户端时间戳」估算延迟
  • 如果延迟较高,适当放宽瞬移阈值

避坑指南:我曾经遇到过一种外挂,它不直接瞬移,而是「加速移动」——每帧移动距离比正常大 30%,但不超过瞬移阈值。这种外挂用速度校验就能防住,所以瞬移检测和速度校验要配合使用。

6.2.2 路径连续性校验

更高级的瞬移检测,是校验移动路径是否连续。说白了,就是看角色有没有穿过「不可通行区域」。

举个例子:

  • 角色从 A 点移动到 B 点
  • 中间有一堵墙
  • 正常玩家必须绕路
  • 外挂直接穿墙瞬移

这时候,服务器可以做射线检测

bool CheckPathContinuity(Vector3 from, Vector3 to) {
    // 从 from 到 to 做射线检测
    RaycastHit hit;
    if (Physics.Raycast(from, to - from, out hit, 
                        Vector3.Distance(from, to))) {
        // 如果射线击中了障碍物,说明路径不连续
        LogWarning("路径被阻挡: " + hit.collider.name);
        return false;
    }
    return true;
}

嗯,这个方案在 3D 游戏里很有效。但要注意性能问题——每帧对所有移动玩家做射线检测,服务器扛不住。我一般建议抽样检测

  • 只对「位移距离超过正常值 50%」的玩家做路径检测
  • 或者每 5 帧检测一次,而不是每帧都检测

6.3 技能释放频率与范围校验

移动类外挂讲完了,咱们聊聊战斗类外挂。技能释放频率和范围,是外挂最常动手脚的地方。

6.3.1 技能释放频率校验

你想想看,一个技能冷却时间是 5 秒,外挂直接改成 0.5 秒,那还得了?

核心做法:服务器独立维护技能冷却状态

// 技能冷却校验
bool ValidateSkillCooldown(Player player, int skillId) {
    SkillCooldown cd = player.GetSkillCooldown(skillId);
    float now = GetServerTime();
    
    if (now < cd.nextAvailableTime) {
        LogWarning("技能冷却未结束: skillId=" + skillId + 
                   " 剩余=" + (cd.nextAvailableTime - now));
        return false;
    }
    
    // 更新冷却时间
    SkillConfig config = GetSkillConfig(skillId);
    cd.nextAvailableTime = now + config.cooldown;
    return true;
}

这里有个关键点:冷却时间必须在服务器端计算。客户端发来的「技能已冷却」信息,服务器不能信。我见过一个项目,客户端告诉服务器「我的技能冷却好了」,服务器就直接放行,结果外挂把冷却时间改成了 0。

警告:不要相信客户端的任何冷却状态。服务器必须自己计时。如果担心服务器时间与客户端不同步,可以让客户端在释放技能时带上「技能就绪时间戳」,服务器做交叉验证。

6.3.2 技能范围校验

技能范围外挂,常见的有两种:

  1. 扩大攻击范围:比如一个近战技能,外挂改成全屏攻击
  2. 无视距离:技能明明打不到目标,外挂强行造成伤害

怎么防?服务器必须做距离校验

bool ValidateSkillRange(Player caster, Player target, int skillId) {
    SkillConfig config = GetSkillConfig(skillId);
    float distance = Vector3.Distance(caster.position, target.position);
    
    // 技能最大有效距离
    float maxRange = config.range;
    
    // 加上一点误差容忍(网络延迟导致的位置偏差)
    float tolerance = 1.1f;  // 允许 10% 的误差
    
    if (distance > maxRange * tolerance) {
        LogWarning("技能超出范围: skillId=" + skillId + 
                   " 距离=" + distance + " 最大=" + maxRange);
        return false;
    }
    return true;
}

但光校验距离还不够。有些技能是扇形范围矩形范围,这时候需要做更精细的碰撞检测。

我建议用服务器端的碰撞检测库,比如 Unity 的 Physics 或者自研的 2D/3D 碰撞检测。客户端发来技能释放请求,服务器在服务端重新计算一次技能命中范围,而不是直接相信客户端说「我打中了谁」。

核心原则:服务器永远不要相信客户端发来的「命中结果」。服务器应该根据客户端发来的「技能释放位置和方向」,自己重新计算哪些目标被命中。这才是安全的做法。


6.4 本章知识体系总览

说了这么多,我画了一张图,把这一章的核心逻辑串起来。你一看就明白了:

关键操作校验体系 移动速度校验 • 速度公式校验 • 加速度校验 • 容忍度控制 • 时间戳对齐 • 网络延迟补偿 瞬移检测 • 距离阈值检测 • 路径连续性校验 • 射线检测 • 抽样检测优化 • 不可通行区域 技能释放校验 • 冷却时间校验 • 服务器独立计时 • 技能范围校验 • 碰撞检测重算 • 扇形/矩形范围 核心原则:服务器独立校验,不信任客户端任何操作数据

这张图把三个校验维度放在一起对比。你看,左边是移动速度,中间是瞬移,右边是技能。它们虽然检测方法不同,但核心思想是一样的——服务器自己做计算,不依赖客户端的结果


6.5 实战中的组合策略

最后,我分享一个实战中的组合策略。这三个校验不是孤立的,它们需要配合使用:

外挂类型 速度校验 瞬移检测 技能校验 推荐处理方式
加速跑 ✅ 必检 ✅ 辅助 ❌ 无关 踢下线 + 封号 1 小时
瞬移穿墙 ✅ 辅助 ✅ 必检 ❌ 无关 回滚位置 + 警告
无CD技能 ❌ 无关 ❌ 无关 ✅ 必检 技能无效 + 记录日志
全屏攻击 ❌ 无关 ❌ 无关 ✅ 必检 伤害无效 + 踢下线
组合外挂 ✅ 必检 ✅ 必检 ✅ 必检 直接封号 + 上报

你看,不同的外挂行为,触发的校验点不一样。我建议在服务器端做一个综合评分系统

  • 每次检测到异常,给玩家加一个「嫌疑分」
  • 嫌疑分累积到阈值,自动触发封禁
  • 不同校验点的权重不同(比如瞬移权重高,速度超限权重低)

这样做的好处是:误判率低,打击精准。偶尔的网络抖动只会加一点点嫌疑分,不会直接封号。但真正的外挂,会在多个维度同时触发异常,嫌疑分快速累积,很快就会被封掉。

个人经验:我曾经在项目里遇到过一种外挂,它只超速 10%,而且只在人少的地方用。速度校验和瞬移检测都抓不到它。后来我加了「行为模式分析」——发现这个玩家总是在固定时间段、固定地图超速,而且从不与其他玩家交互。最终通过人工审核封掉了。所以,自动化校验 + 人工审核,才是最佳组合。


好了,这一章的内容就到这里。关键操作校验,说白了就是让服务器「多算一步」。你多算一步,外挂就多费十步。记住:服务器永远不要相信客户端发来的任何「结果」,只相信客户端发来的「原始输入」。这个原则,贯穿整个反外挂架构设计。

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