服务端射线检测必须基于权威状态重建:起点取 player->getposition(),方向由 player->getrotation() 推导,禁用客户端传入的任何射线参数;需用时间戳对齐输入时刻,查历史快照,硬编码射线长度上限,严格过滤碰撞层,确保物理世界完整准确。

用 raycast 做服务端射线检测,但别直接信客户端传来的起点和方向
服务端不能信任客户端发来的任何射线参数——起点、方向、长度全可能被篡改。真实做法是:服务端只接受客户端的输入意图(比如“按鼠标左键+当前视角四元数quat”),然后在服务端用玩家角色位置和朝向重建射线原点与方向。起点必须取自服务端权威位置(如 player->GetPosition()),方向从服务端持有的 player->GetRotation() 推导,而不是解析客户端发来的 vec3。
- 客户端只需上传
input_timestamp、view_quat(压缩为 4 个uint16_t更安全)、是否开镜等状态,不传射线 - 服务端用相同旋转逻辑(如
quat * vec3(0,0,-1))算出前向向量,再加一个固定枪口偏移(如vec3(0.2f, 0.1f, 0.8f))得到最终射线起点 - 若客户端视角更新有延迟,服务端需用插值或回滚时间戳对齐(见下节)
处理时间戳错位:用 server_tick 对齐射线发射时刻
客户端视角和服务器 tick 不同步,直接用当前服务端位置+朝向会打偏。必须把射线“回溯”到客户端输入发生的真实逻辑时刻。常见做法是:客户端在点击瞬间记录本地 tick_count 或毫秒级 input_time_ms,服务端收到后查该时刻的玩家快照(position/rotation)——这要求服务端保留最近 100–200ms 的历史状态(带时间戳的 ring buffer)。
- 不要用
std::chrono::steady_clock::now()直接比对;统一用服务端单调递增的game_tick(如每 16ms 一帧)做索引 - 若请求的
input_tick超出缓存范围,拒绝本次射击(返回ERR_INPUT_TOO_OLD) - 射线长度建议硬编码上限(如
100.0f),禁止客户端指定;过长射线性能差且易被滥用
碰撞检测必须用服务端物理世界,且禁用触发器和射线优化
调用 PhysicsWorld::RaycastClosest() 时,务必确认它查询的是服务端加载的完整静态/动态碰撞体(btBvhTriangleMeshShape 或 btConvexHullShape),而非客户端简化的 LOD 网格。尤其注意:墙、地板、掩体必须是带厚度的实体碰撞体,不能是单面 btStaticPlaneShape——否则子弹会从背面穿出。
- 禁用
btCollisionWorld::setForceUpdateAllAabbs(false)这类优化,确保动态物体(如可移动箱子)的 AABB 实时更新 - 射线过滤层(filter mask)要严格:子弹射线只检测
LAYER_SOLID和LAYER_PLAYER,跳过LAYER_TRIGGER和LAYER_DECAL - 若用 Bullet,避免
btCollisionWorld::rayTest()(仅返回第一个命中),改用btCollisionWorld::rayTestSingle()并检查m_closestHitFraction是否 0.999f(防浮点误差导致穿墙)
自动瞄准(Aimbot)特征识别比射线本身更关键
单纯校验射线是否穿墙只能防“穿墙枪”,防不了“瞬移准心”类外挂。真正要拦的是异常瞄准行为:比如玩家视角在 50ms 内从偏离目标 30° 突然转到偏差
- 记录每个玩家最近 N 帧的
view_delta_yaw/pitch、fire_delay_since_last、target_distance_at_fire - 当
abs(delta_yaw) > 15_deg && fire_delay_since_last 时,触发二级验证:查该帧射线是否击中了“视线不可达但模型包围盒可达”的目标(即穿模但不穿墙) - 不要实时踢人,先标记
suspicion_score += 2,连续 3 次触发才临时禁用射击权限(player->SetCanFire(false)2 秒)
穿墙只是表象,外挂的核心是绕过人类反应延迟。服务端校验的重点从来不是“这条射线合不合理”,而是“这个玩家的操作序列像不像机器”。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











