一、两种武器判定模型对比
Hitscan(瞬时命中) Projectile(弹道飞行) ───────────────────── ───────────────────── 开枪瞬间发射射线 开枪生成一个真实飞行的"弹丸实体" 无限速,立即到达 有速度,需要时间飞行 一次射线检测出结果 每帧推进 + 每帧碰撞检测 无重力/风偏 受重力、空气阻力、风力影响 代表武器: 代表武器: - CS 步枪/手枪 - 火箭筒 RPG - Valorant - 榴弹发射器 - 大多数即时命中枪械 - 弓箭 / 弩 - 坦克炮弹 - PUBG 狙击子弹(远距离)核心差异:Hitscan 是"数学问题",Projectile 是"物理仿真问题"。
二、Projectile 的核心挑战
Hitscan 延迟补偿只需回溯一个瞬间。而 Projectile 是持续存在、跨越多帧的实体,它带来了全新的问题:
时间轴问题: t=0 玩家发射火箭 [🚀] ──────────────> t=1 火箭飞行中 [🚀] ────────────> t=2 火箭飞行中 [🚀] ─────────> t=3 火箭击中敌人 [🚀]💥[敌人] 问题:整个飞行过程中,是否每一帧都要做延迟补偿? 敌人在这几帧里也在移动,该和"哪个时刻"的敌人做碰撞?三、两种主流实现架构
方案 A:服务器权威弹道(Server-Authoritative)
服务器完全掌控弹丸,客户端只做视觉表现:
/** * 服务器权威弹道实体 */classServerProjectile{Vector3position;Vector3velocity;longspawnTime;// 发射时刻longownerRewindTime;// 发射者开火时的回溯基准时间Playerowner;doublegravity=9.8;doublemass;/** * 每个服务器 tick 推进弹道 */voidupdate(doubledeltaTime){// ① 物理积分:更新速度和位置velocity.y-=gravity*deltaTime;// 重力velocity=applyAirDrag(velocity,deltaTime);// 空气阻力Vector3nextPos=position.add(velocity.multiply(deltaTime));// ② 连续碰撞检测(CCD)—— 用线段而非点,防止高速穿透HitResulthit=sweepTest(position,nextPos,deltaTime);if(hit!=null){onImpact(hit);// 爆炸/伤害destroy();}else{position=nextPos;}}}方案 B:客户端预测弹道(Client-Predicted)
客户端立即生成弹丸给玩家看(手感好),服务器验证:
客户端 服务器 │ │ │ 开火,立即生成本地弹丸 🚀 │ │ (玩家马上看到火箭飞出) │ │ │ │───── 发射请求(方向+时间戳) ──────>│ 收到,生成权威弹丸 │ │ (用回溯时间校准起点) │ 本地弹丸继续飞行(预测) │ 权威弹丸并行飞行 │ │ │<──── 弹道校正/命中结果 ───────────│ 判定命中 │ 若偏差大 → 平滑修正视觉 │四、Projectile 的延迟补偿:关键差异点
差异 1:发射起点的时间校准
/** * Projectile 发射时的延迟补偿 * 与 Hitscan 不同:补偿的是"发射起点",而非"命中点" */ServerProjectilespawnProjectile(Playershooter,FireCommandcmd,longserverTime){// ① 计算发射者开火时看到的世界时刻longrewindTime=serverTime-shooter.getRtt()/2-shooter.getInterpDelay();// ② 关键:弹丸要"补飞"这段延迟期间应该走过的距离longcompensationMs=serverTime-rewindTime;ServerProjectileproj=newServerProjectile();proj.position=cmd.origin;proj.velocity=cmd.direction.multiply(cmd.muzzleSpeed);proj.spawnTime=rewindTime;// ③ 快进弹丸,让它追上"当前服务器时间"// 因为在网络传输期间,弹丸本应已经飞了一段proj.fastForward(compensationMs/1000.0);returnproj;}💡核心理解:Hitscan 回溯的是敌人;Projectile 除了要考虑敌人,还要把弹丸自身向前快进,补偿网络延迟期间弹丸本该飞过的路程。
差异 2:飞行期间的碰撞判定策略
这是最大的争议点——每帧碰撞检测时,要不要对敌人做回溯?
/** * 三种命中判定策略 */enumProjectileHitMode{// 策略1:实时判定(无飞行中补偿)// 弹丸飞行的每一帧,都用敌人的"当前真实位置"判定// 优点:简单、公平(弹丸一旦发射就活在服务器时间)// 缺点:对发射瞬间的瞄准不完全补偿REALTIME,// 策略2:全程回溯(Full Rewind)// 弹丸每一帧都把敌人回滚到"发射者时间线"// 优点:手感极好// 缺点:极其复杂,容易出现"子弹拐弯"的极端情况FULL_REWIND,// 策略3:混合模式(业界主流)// 发射瞬间用回溯校准起点,之后弹丸活在服务器实时时间线// 平衡了手感和公平性HYBRID}主流选择:混合模式(HYBRID)
voidupdate(doubledeltaTime){Vector3nextPos=integratePhysics(deltaTime);// 弹丸发射后,就活在服务器"当前"时间线// 不再对敌人做回溯,用敌人真实位置判定// 因为弹丸有飞行时间,玩家本就需要"预判提前量"HitResulthit=sweepTest(position,nextPos,getCurrentEntities());if(hit!=null)onImpact(hit);elseposition=nextPos;}🎯为什么飞行中不回溯?
因为弹道武器本身就要求玩家打提前量。子弹飞行需要时间,玩家瞄准的是敌人"未来会到的位置"。如果飞行中还回溯敌人,反而破坏了这种预判玩法。
差异 3:连续碰撞检测(CCD)
高速弹丸一帧可能飞过整个敌人,普通点检测会穿透漏判:
/** * 连续碰撞检测 —— Projectile 必须解决的问题 */HitResultsweepTest(Vector3from,Vector3to,doubledeltaTime){doubledistance=from.distance(to);doublespeed=distance/deltaTime;// 高速弹丸:一帧移动距离 > 敌人厚度 → 会穿透!// 火箭速度 40m/s,tick=64Hz → 每帧移动 0.6m// 狙击子弹 900m/s,每帧移动 14m!远超人体厚度if(distance>MIN_HITBOX_SIZE){// 用"扫掠射线"或"胶囊体扫描"代替点检测Rayray=newRay(from,to.subtract(from).normalize());returnraycast(ray,distance);// 线段检测,不会穿透}else{// 低速弹丸可用球体重叠检测returnsphereOverlap(to,projectileRadius);}}穿透问题图解: ❌ 点检测(错误): 帧1[🚀] [敌人] 帧2[🚀] 弹丸"跳过"了敌人,检测不到碰撞! ✅ 扫掠检测(正确): 帧1[🚀]━━━━━━━━━━━━━━━━━━━━━━━━━[🚀]帧2 └── 用线段检测整条路径 → 命中敌人!五、完整实现示例
/** * 弹道武器完整服务器端实现 */publicclassProjectileWeaponSystem{privatefinalList<ServerProjectile>activeProjectiles=newArrayList<>();/** * 玩家开火 */publicvoidonFire(Playershooter,FireCommandcmd,longserverTime){// 1. 计算回溯时间,校准发射起点longrewindTime=serverTime-shooter.getRtt()/2-shooter.getInterpDelay();longcompensationMs=serverTime-rewindTime;// 2. 创建弹丸ServerProjectileproj=newServerProjectile();proj.owner=shooter;proj.position=cmd.muzzlePosition;proj.velocity=cmd.direction.multiply(shooter.weapon.muzzleSpeed);proj.gravity=shooter.weapon.gravity;proj.damage=shooter.weapon.damage;// 3. 快进补偿网络延迟proj.fastForward(compensationMs/1000.0);activeProjectiles.add(proj);}/** * 每个服务器 tick 更新所有弹丸 */publicvoidtick(doubledeltaTime){Iterator<ServerProjectile>it=activeProjectiles.iterator();while(it.hasNext()){ServerProjectileproj=it.next();// 物理积分proj.velocity.y-=proj.gravity*deltaTime;Vector3nextPos=proj.position.add(proj.velocity.multiply(deltaTime));// 连续碰撞检测(防穿透)HitResulthit=sweepTest(proj.position,nextPos,getCurrentEntities());if(hit!=null){applyDamage(hit.target,calculateDamage(proj,hit));spawnExplosion(hit.point);// 爆炸效果it.remove();}elseif(proj.getLifetime()>MAX_LIFETIME){it.remove();// 超时销毁}else{proj.position=nextPos;}}}/** * 爆炸类武器的范围伤害 */privatevoidapplyExplosionDamage(Vector3center,doubleradius,doublemaxDamage){for(Entitye:getEntitiesInRadius(center,radius)){doubledist=e.position.distance(center);// 距离衰减doublefalloff=1.0-(dist/radius);doubledamage=maxDamage*Math.max(0,falloff);// 遮挡检测:墙后的人不受伤害if(!isBlockedByWorld(center,e.position)){applyDamage(e,damage);}}}}六、关键差异总结表
| 维度 | Hitscan | Projectile |
|---|---|---|
| 判定方式 | 单次射线检测 | 每帧物理推进 + 碰撞 |
| 飞行时间 | 无(瞬时) | 有,需要预判提前量 |
| 物理影响 | 无 | 重力/阻力/风力 |
| 延迟补偿对象 | 回溯敌人位置 | 校准弹丸起点+ 快进 |
| 飞行中补偿 | 不适用 | 通常不回溯(保留预判玩法) |
| 穿透风险 | 无 | 高,需 CCD 连续检测 |
| 服务器开销 | 低(一次计算) | 高(持续存在多帧) |
| 网络同步 | 只同步命中结果 | 需同步弹丸生成/位置/销毁 |
| 客户端预测 | 简单 | 复杂(需弹道预测+校正) |
| 作弊验证 | 验证射线合法性 | 验证发射参数+弹道物理 |
七、特殊问题:狙击枪的抉择
远距离狙击是个灰色地带:
近距离(<50m):子弹飞行 <0.05s → 可当作 Hitscan 处理(延迟补偿回溯敌人) 远距离(>200m):子弹飞行明显有延迟 + 下坠 → 必须当作 Projectile(玩家要打提前量+压枪补偿下坠) 许多游戏的选择: - CS/Valorant:全程 Hitscan(快节奏,简化) - PUBG/战地:Projectile(拟真,需要预判和弹道计算)八、工程实现要点
- 发射时补偿起点—— 用回溯时间校准,并快进弹丸补网络延迟
- 飞行中活在服务器时间—— 通常不再回溯敌人,保留预判玩法
- 必须用 CCD—— 扫掠检测防止高速穿透
- 物理积分要稳定—— 固定时间步长,避免帧率影响弹道
- 客户端预测 + 平滑校正—— 兼顾手感和权威性
- 爆炸范围伤害 + 遮挡检测—— 距离衰减 + 墙体阻挡