FPS 开火流程详解

📅 2026/8/20 15:50:31
FPS 开火流程详解
开火看似按一下鼠标实际上是一条横跨输入、逻辑、表现、网络四层的复杂管线。我按照一次开火从按下到结算的完整时间线来讲并且分单机流程和联网流程两层因为联网才是真正的难点。一、先建立全局时间线一次开火在联网环境下的完整旅程客户端 服务器 其他客户端 │ 按下开火 │ 本地门禁检查 ───────────────────────────────────────────── │ 本地预测表现立即出枪火/音效/后坐力 │ 本地弹道计算 命中预测 │ ├──── 上报开火请求 ────────► 接收请求 │ (时间戳/方向/序号) │ │ 门禁二次校验 │ 回滚目标位置 │ 权威命中判定 │ 伤害结算 │ ◄─── 命中确认/伤害结果 ────┤ │ │──── 广播开火事件 ──────► 播放他人开火表现 本地校正如误判则修正记住一个原则客户端立刻表现是为了手感服务器最终裁决是为了公平。下面按阶段拆解。二、阶段一输入采集开火输入不是简单的if (Input.GetMouseButton(0))要处理几个细节publicstructFireInput{publicboolIsPressed;// 本帧是否按下publicboolIsHeld;// 是否持续按住publicfloatPressTimestamp;// 按下的精确时间戳网络要用}要处理的问题区分按下和按住半自动枪只认按下的那一帧松开再按才能再开全自动枪认持续按住。这个区别交给 FireMode 模块输入层只如实上报状态。输入缓冲Input Buffer玩家在换弹快结束时按了开火是丢弃还是缓存到换弹完成后立刻开火大厂通常做短暂缓冲约 100-200ms手感更跟手。时间戳记录精确按下时间这是后面服务器延迟补偿的关键依据必须在输入层就采集。三、阶段二门禁检查能不能开火这是开火管线的第一道闸门任何一项不通过就直接终止。抽成独立方法逻辑清晰且方便网络层复用同一套规则。privateFireBlockReasonCheckCanFire(){// 1. 射速限制距上次开火是否够久floatinterval60f/Config.RPM;if(Time.time_lastFireTimeinterval)returnFireBlockReason.RateLimited;// 2. 弹药检查if(_ammo.Current0)returnFireBlockReason.NoAmmo;// 触发空仓提示音/自动换弹// 3. 状态检查if(State.IsReloading)returnFireBlockReason.Reloading;if(State.IsSwitching)returnFireBlockReason.Switching;if(State.IsSprinting)returnFireBlockReason.Sprinting;// 疾跑不能开火// 4. 开火模式检查半自动是否已松开过if(!_fireMode.CanFireNow(_input))returnFireBlockReason.FireModeBlocked;returnFireBlockReason.None;// 放行}关键点射速控制用 RPM 换算成时间间隔而不是每帧计数。因为帧率不稳定时按帧计数会导致射速随帧率波动这是新手常犯的错误。四、阶段三开火模式FireMode不同枪的一次开火消耗几发、怎么触发由这个模块决定。用策略模式每种模式独立。publicinterfaceIFireMode{boolCanFireNow(FireInputinput);intShotsPerTrigger{get;}// 本次触发打几发}三种典型模式全自动按住持续开火每到射速间隔就打一发。publicboolCanFireNow(FireInputinput)input.IsHeld;半自动必须松开再按。用一个标志位记录是否已松开过。publicboolCanFireNow(FireInputinput){if(!input.IsHeld){_releasedtrue;returnfalse;}if(_releasedinput.IsPressed){_releasedfalse;returntrue;}returnfalse;}三连发Burst一次触发打固定 3 发中间有内部小间隔打完才响应下次触发。这个需要一个内部协程或计数器状态机来控制连发节奏。五、阶段四确定射击方向含散布门禁通过后计算这一发子弹真正飞的方向。基础方向是准星朝向相机中心再叠加散布偏移。Vector3GetFireDirection(){// 基础方向从相机中心射出不是从枪口Vector3baseDir_camera.forward;// 应用散布在一个圆锥内随机偏移floatspread_spread.CurrentSpread();// 见架构篇散布计算Vector2offsetRandom.insideUnitCircle*spread;Vector3dirQuaternion.AngleAxis(offset.x,_camera.up)*Quaternion.AngleAxis(offset.y,_camera.right)*baseDir;returndir.normalized;}一个重要的视觉技巧——枪口与准星分离判定射线从相机中心发出保证准星对哪打哪符合玩家预期枪口火焰特效从武器模型的枪口发出视觉正确两者起点不同所以近距离贴脸时子弹轨迹和枪口朝向会略有差异这是行业通用做法玩家几乎无感但判定更符合直觉。六、阶段五弹道与命中检测根据弹道模型分两条路。Hitscan瞬中HitResultFireHitscan(Vector3origin,Vector3dir){if(Physics.Raycast(origin,dir,outRaycastHithit,Config.MaxRange,_hitMask)){returnnewHitResult{Hittrue,Pointhit.point,Distancehit.distance,Hitboxhit.collider.GetComponentHitbox(),// 部位Targethit.collider.GetComponentInParentDamageable()};}returnHitResult.Miss;}要处理的进阶问题穿透Penetration子弹穿墙、穿多个敌人。用RaycastAll或递归Raycast每穿透一层材质衰减一次伤害直到能量耗尽。// 穿透逻辑简化示意floatenergy1.0f;foreach(varhitinPhysics.RaycastAll(...).OrderBy(hh.distance)){ProcessHit(hit,energy);energy-GetMaterialResistance(hit);// 木板扣得少钢板扣得多if(energy0)break;}霰弹枪一次开火发射多条射线如 8-12 颗弹丸每颗独立散布、独立判定。Projectile实弹生成一个子弹实体每帧用线段检测推进见架构篇防高速穿墙。七、阶段六后坐力应用开火成功后立刻施加后坐力。注意后坐力要作用在多个地方voidApplyRecoil(){Vector2recoil_recoil.NextRecoil(_continuousShots);// 1. 镜头上抬影响准星指向玩家需下压鼠标补偿_cameraController.AddRecoil(recoil);// 2. 武器模型后坐动画纯表现不影响判定_weaponAnimator.PlayRecoilKick();// 3. 散布增加连发越久越散_continuousShots;}手感关键镜头后坐力和武器模型后坐力用不同曲线武器模型可以晃得夸张些增强打击感但镜头影响实际弹着要按预设图案精确控制让高手能背板压枪。八、阶段七反馈表现事件驱动逻辑层不直接调用表现而是广播一个开火事件由各表现模块自己监听。这样逻辑和美术彻底解耦。// 逻辑层只发事件EventBus.Publish(newWeaponFiredEvent{Weaponthis,OriginmuzzlePos,HitPointhitResult.Point,HitTypehitResult.SurfaceType});// 各表现模块独立监听// - 枪口火焰系统 → 播放 muzzle flash// - 音效系统 → 播放开火音近距离3D音 远距离衰减音// - 抛壳系统 → 弹出弹壳对象池// - 弹孔系统 → 在命中点生成弹孔贴花// - UI系统 → 准星扩散动画、弹药数刷新// - 命中反馈 → 击中敌人的红色 hitmarker、爆头音这种设计的好处加一个新表现比如开火时手柄震动只需新增一个监听者完全不碰开火逻辑。九、联网核心客户端预测 服务器裁决这才是 FPS 开火最难的部分。单机上面就够了联网必须重构。9.1 客户端做什么客户端立即预测表现不等服务器voidOnFireLocal(){if(CheckCanFire()!FireBlockReason.None)return;// 立即本地表现预测——手感的关键ApplyRecoil();PlayFireEffects();_ammo.Consume();// 本地先扣弹// 本地也算一次命中用于即时 hitmarkervarlocalHitFireHitscan(...);if(localHit.Hit)ShowHitmarker();// 先给玩家反馈// 上报服务器关键带时间戳和序号_network.SendFireRequest(newFireRequest{Sequence_fireSequence,Timestamp_input.PressTimestamp,// 客户端开火时刻Origin_camera.position,DirectionfireDir,});}9.2 服务器做什么权威裁决服务器收到请求后不信任客户端说的命中自己重算voidOnReceiveFireRequest(Playershooter,FireRequestreq){// 1. 门禁二次校验防加速外挂if(!ValidateFireRate(shooter,req.Timestamp)){KickOrFlag(shooter);return;}// 2. 延迟补偿把所有敌人回滚到 shooter 开火那一刻floatrewindTimereq.Timestamp;// 已含 RTT 补偿varsnapshot_rewindSystem.RewindAllPlayers(rewindTime);// 3. 用服务器权威数据重算命中varhitServerRaycast(req.Origin,req.Direction,snapshot);// 4. 命中则结算伤害if(hit.Hit){floatdmgDamageSystem.Calculate(hit,shooter.Weapon);hit.Target.TakeDamage(dmg,shooter);// 通知双方SendHitConfirm(shooter,hit);SendDamageReceived(hit.Target,dmg);}// 5. 广播开火事件给其他人播放开火表现BroadcastFireEvent(shooter,req);}9.3 延迟补偿Lag Compensation—— 最精妙的一环问题场景你屏幕上敌人站在门口你瞄准爆头开枪。但你的网络包到服务器要 50ms这期间敌人已经跑进门后了。如果服务器用当前位置判定你明明打中了却算 miss——玩家会骂这游戏判定有问题。解决方案服务器为每个玩家保存一份历史位置快照环形缓冲存最近约 1 秒。收到开火请求时根据时间戳把敌人倒带回你开枪那一刻的位置再判定。publicclassRewindSystem{// 每个玩家的历史 hitbox 快照DictionaryPlayer,RingBufferHitboxSnapshot_history;publicvoidRecordSnapshot()// 每个服务器帧记录{foreach(varpin_players)_history[p].Add(newHitboxSnapshot{TimeServerTime,Hitboxesp.CaptureHitboxTransforms()// 头/胸/四肢位置});}publicHitboxSnapshotRewind(Playertarget,floatclientTime){// 找到 clientTime 前后两个快照插值出精确位置var(before,after)_history[target].FindSurrounding(clientTime);returnHitboxSnapshot.Lerp(before,after,clientTime);}}代价与平衡延迟补偿让开枪方打哪中哪很爽但会导致被打方出现我明明躲进掩体了还是被打死的情况因为服务器把他倒带回了掩体外。这是 FPS 的经典权衡——优先保证开枪方体验因为射击的即时反馈更影响手感。补偿有时间上限通常 200-250ms超过就不补偿防止高延迟玩家滥用。9.4 客户端校正如果服务器判定和本地预测不一致本地以为中了服务器说没中客户端要静默修正——撤销 hitmarker、回滚弹药数等。多数情况预测是准的校正很少发生。十、完整流程图总览┌─────────────────── 客户端 ───────────────────┐ │ 输入采集(带时间戳) │ │ ↓ │ │ 门禁检查 ──不通过──► 反馈(空仓音/无动作) │ │ ↓ 通过 │ │ FireMode 判定(全自动/半自动/连发) │ │ ↓ │ │ 计算方向(基础朝向 散布) │ │ ↓ │ │ ┌── 立即本地预测 ──┐ │ │ │ 后坐力 / 特效 │ │ │ │ 音效 / 抛壳 │ │ │ │ 本地命中 hitmarker│ │ │ └──────────────────┘ │ │ ↓ │ │ 上报开火请求 ─────────────┐ │ └───────────────────────────┼───────────────────┘ ↓ ┌─────────────────── 服务器 ─────────────────────┐ │ 射速/合法性二次校验(反作弊) │ │ ↓ │ │ 延迟补偿回滚所有玩家到开火时刻 │ │ ↓ │ │ 权威射线/弹道重算 │ │ ↓ │ │ 命中 → 伤害结算 → 通知双方 │ │ ↓ │ │ 广播开火事件给其他客户端 │ └─────────────────────────────────────────────────┘十一、各阶段最容易踩的坑阶段常见错误正确做法射速控制按帧计数随帧率波动用时间戳 RPM 间隔射击方向从枪口发射线导致准星不准判定从相机中心特效从枪口表现调用逻辑里直接调播放动画事件驱动表现层监听弹药扣除只在服务器扣本地有延迟感本地预测扣 服务器校正命中判定信任客户端上报的命中服务器权威重算网络延迟不做补偿玩家骂判定延迟补偿倒带高速子弹位置点检测穿墙线段检测小结开火流程的设计精髓门禁把关 → 方向计算 → 弹道判定 → 后坐力 → 事件反馈是清晰的单机主干联网后套上本地预测手感 服务器权威公平 延迟补偿体验三层就是完整的工业级方案。