UE5 纯蓝图 FPS 实战笔记:命中判定、AI 追击与带冲量的布娃娃死亡

一个用 UE 5.8 纯蓝图(零 C++)完成的 FPS 练习项目。本文梳理三个核心系统的实现思路:武器命中判定、Whisper AI 追击,以及带冲量的布娃娃死亡效果。文中的蓝图节点名均可在编辑器里直接搜索复现。

一、先看整体架构

项目逻辑全部放在 Content/Code/ 下,类关系非常简单:

1
2
3
4
BP_FPSPlayer (Character)  ──BI_Fire接口──▶  BP_WeaponBase (Actor) ──继承──▶ BP_VIRTUS
│ │
│ 射线检测 │ 命中后 Cast 并调用
└──────────────────▶ BP_Whisper (Character, AI 敌人) ◀──────────────┘

三个系统围绕”一颗子弹”串联起来:扣下扳机 → 接口调用开火 → 射线检测 → 物理材质分流 → 命中敌人 → 传递骨骼名与子弹方向 → 伤害计算 → 受击蒙太奇 / 布娃娃死亡。理解了这条链路,整个项目的设计思路就清楚了。

二、命中判定:Hitscan + 物理材质分流

1. 射线检测基础

开火逻辑在武器基类 BP_WeaponBase 里,核心就是一条 LineTraceSingle:

  • 使用自定义 Trace 通道 TraceTypeQuery1,和默认的 Visibility/Camera 通道隔离开,避免被其他系统的射线干扰;
  • 打开 bTraceComplex,对复杂碰撞体做精确检测,保证打在骨骼网格的轮廓上而不是胶囊体上;
  • BreakHitResult 拆出所有需要的信息:ImpactPointImpactNormalHitBoneNameDistancePhysMatHitActor

其中子弹方向直接由数学算出来:Normal(TraceEnd - TraceStart),归一化后传给敌人——这一步很关键,后面布娃娃死亡要复用这个方向,后面会讲。

2. 用物理材质做表面判定,而不是一堆 Cast

命中后要决定”打到了什么、该播什么特效”。常见做法是 Cast To 各种 Actor 类,但这里换了一个更优雅的思路:用 PhysicalMaterial 的表面类型(Physical Surface)做分流

项目里定义了四种物理材质:

物理材质 表面类型 用途
PM_Dirt / PM_Metal / PM_Wood SurfaceType1~3 地形与场景物体
PM_Enemy SurfaceType4 敌人身体专用标记

射线命中后拿到 PhysMat,一个 SwitchEnum 按表面类型分流:

  • 水泥/泥土/金属/木头 → 对应的弹着 VFX(FX_SquibConcrete/Dirt/Metal/Wood)+ 撞击音效 Cue + 弹孔贴花(BP_BulletDecal,按表面切换贴花材质);
  • PM_Enemy → 播放血液 Niagara(Impact_Blood),进入敌人伤害流程。

为什么这个设计值得记一笔:物理材质是碰撞查询的免费副产品——BreakHitResult 里的 PhysMat 不需要任何额外查询或 Cast,零开销就拿到了”这是什么表面”。美术在网格上设置物理材质后,逻辑零改动即可生效;以后加新表面类型也只是加一个枚举分支和一套表现资产。相比”把场景里每种物体都 Cast 一遍”,这套方案更干净、更好扩展。

3. 命中敌人后的传递链

命中 BP_Whisper 时,武器做三件事:

  1. Cast To BP_Whisper;
  2. 调用敌人的两个自定义事件:
    • Impact Bone(Name)——命中骨骼名;
    • Bullet Direction(Vector)——上面算出的子弹方向;
  3. ApplyDamage(BaseDamage) 结算伤害。

一次 Trace 的所有信息就这样”喂”给了敌人,伤害计算、受击表现、死亡表现共用同一份命中数据,不会有二次查询。

4. 射速与后坐力

  • 射速:SetTimerDelegate 计时器循环触发开火(间隔约 0.1s,即 600rpm 档),IsFiring 标志控制启停;
  • 后坐力:AddControllerPitchInput / AddControllerYawInput 配合 RandomFloatInRange 在随机范围内抖动摇杆,而不是固定偏移——连续点射时枪口跳动更自然;
  • 开火表现由玩家/武器双视角蒙太奇 + 动画通知 MCX-VIRTUS_Fire 同步,逻辑时机跟着动画走。

三、Whisper AI:没有行为树的纯蓝图追击

1. 异步 MoveTo + 委托循环

Whisper 继承 Character,但没有使用 AIController 类,也没有行为树,整个 AI 就是一张事件图。

核心节点是 AIBlueprintHelperLibraryCreateMoveToProxyObject——一个异步 AIMoveTo 代理节点,用法和时序节点类似:

  • 输入:目标 Actor(玩家)、AcceptanceRadius(约 50);
  • 输出:OnSuccess / OnFail 两个委托,绑到自定义事件 MovementResult;
  • 在回调里重新发起下一次 MoveTo,就形成了”追到/追丢了都继续追”的循环。

配合 LessEqual 距离判定(玩家进入约 5000 范围内才发起追击)和追击时的 MaxWalkSpeed 调整,一个”巡逻发呆 → 发现玩家 → 加速追击”的简单 AI 就有了。对于单敌人练习场景,这个方案比搭一套行为树轻得多,而且事件流一目了然、容易调试。

2. 状态枚举:谁打你,谁告诉你演什么

BP_Whisper 内定义了状态枚举:E_Chase / E_Death / E_HitMontage / E_Whisper_Damage

有意思的是,这个枚举不是 AI 自己维护的——武器命中时直接设置敌人的状态。射击系统本来就掌握着最准确的命中信息(部位、伤害量、致死与否),由它来”导演”敌人的反应,AI 侧只负责响应,职责划分很清晰。

四、受击反馈:一个 Map 驱动的蒙太奇系统

1. 骨骼名 → 蒙太奇映射

受击表现的核心是一个蓝图变量:

1
HitAnimMontages : Map<Name, AnimMontage>
  • 躯干骨骼(spine_01 / spine_02 / spine_03)→ Anim_Whisper_Taking_Bodyshot_Montage;
  • 其他部位(头、颈、四肢)→ Anim_Whisper_Taking_Headshot_Montage

收到 Impact BoneMap Find 查表,PlayAnimMontage 直接播放,还支持 StartSectionName 从指定段起播。用数据映射替代 if-else 硬编码,以后加部位、加表现只需要改这个 Map。

2. 伤害倍率

同一份骨骼名还参与伤害计算:EqualEqual(Name) 比较命中骨骼,身体与头部分走不同倍率(示例值约 2x / 3x),Multiply 后从 Health 里扣除。也就是说:骨骼名同时驱动了伤害数值和受击动画,两个系统共享一个输入。

3. 动画蓝图的分层结构

ABP_Whisper 的设计也值得一提:

  • Locomotion 状态机:只有 Idle/Walk 一个状态,由 Walk Speed 参数驱动 1D 混合空间 BS1D_Whisper(Idle1 ↔ Walk1,三次插值)——移动逻辑保持极简;
  • DefaultSlot 蒙太奇槽位 + LayeredBoneBlend:受击/死亡蒙太奇通过槽位叠加在基础移动动画之上,互不打断。

“基础运动永远在播,一次性表现走蒙太奇槽位”是角色动画的经典分层思路,这里体现得很标准。

五、带冲量的布娃娃死亡

这是整个项目最”爽”的部分:敌人中弹后不是播放死亡动画,而是沿子弹方向被物理打飞

死亡流程在 BP_Whisper 内,关键四步:

  1. 血量归零判定(Health - Damage ≤ 0);
  2. DisableMovement——立刻停掉 AI 移动。这一步不能省:追击逻辑还在跑的话,CharacterMovement 会和物理模拟抢控制权,布娃娃会被”拽”着走;
  3. SetAllBodiesSimulatePhysics(true)——全身骨骼切到物理模拟,进入布娃娃;
  4. AddImpulse 施加冲量,三个参数是精髓:
    • BoneName = 命中骨骼(来自武器传的 Impact Bone)——冲量只作用在被打中的部位;
    • Impulse = 子弹方向 × 冲量系数(约 5000,由 Conv_DoubleToVector 标量广播 + 向量乘法实现)——方向与枪线一致;
    • bVelChange = true——用”速度变化”模式而非”力”模式。力模式受质量影响,布娃娃各部位质量不同,同样的力打在不同部位表现差异巨大;速度变化模式直接指定速度增量,保证爆头和打腿的”手感”一致。

最后把状态切到 E_Death,收尾。

效果:爆头向后仰、打腿前扑、侧面命中横飞,方向永远和枪线一致——玩家的每一次射击都能”看到”自己的命中结果,这是手感正反馈的重要来源。

配套资产 Phy_Whisper_PhysicsAsset 用了 ConeConstraint(圆锥约束)连接骨骼,限制关节旋转自由度,让布娃娃的动作像”人体”而不是”面条”。

六、让系统跑起来的其他细节

1. BI_Fire 蓝图接口

玩家不持有武器具体类引用,统一通过 BI_Fire 接口的 StartWeaponFire / StopFire 调用武器。换武器 = 换一个实现该接口的 Actor,玩家逻辑零改动。这是蓝图里最值得养成的习惯之一:用接口而非具体类通信。

2. 武器继承体系

  • BP_WeaponBase 承载所有通用逻辑:命中判定、弹药、弹壳、贴花、表面分流;
  • BP_VIRTUS 子类只做数据配置:BaseDamageFireRateMagSizeTotalAmmo,以及自己的蒙太奇/音效引用;
  • 武器本身是模块化部件拼装:Weapon / Magazine(弹匣)/ Sight(机瞄)/ Scope(瞄具)/ Stock(枪托)/ Barrel(枪管)分部件挂载。

新枪 = 新子类 + 换部件网格,通用逻辑一行不改。

3. 弹药与换弹

  • 三变量模型:MagSize(弹匣容量)/ CurrentAmmo(当前弹匣)/ TotalAmmo(备弹),开火 DecrementInt,打空有专属干火音效 Dead_Trigger_Hand-Guns;
  • 换弹蒙太奇上放置自定义动画通知 AN_Reload:动画播到”填弹”那一帧时回调武器蓝图执行 ReloadAmmo——逻辑时机由动画驱动,而不是用延迟或计时器猜;
  • PlayMontageCallbackProxy 的五个回调(OnBlendOut / OnCompleted / OnInterrupted / OnNotifyBegin / OnNotifyEnd)管理换弹状态机,换弹途中开火被禁用。

4. 弹壳系统 BP_Casing

小物件也有讲究:

  • ProjectileMovementComponent:初速 + 重力 + bShouldBounce 落地弹跳;
  • RotatingMovementComponent + 随机转速:每颗弹壳的落地姿态都不同;
  • OnComponentHit(开启 bNotifyRigidBodyCollision)落地播放弹壳音效,DoOnce 防重复触发;
  • InitialLifeSpan 到时自动销毁,不留垃圾。

5. Enhanced Input 与瞄准手感

  • IMC_FPSInput 映射上下文 + 6 个 Input Action(Move / Look / Fire / Aim / Reload / Run),IA_Fire 绑定 Triggered/Completed 支持按住连发;
  • 瞄准手感三件套:FInterpTo 平滑 FOV 缩放(默认 75)、瞄准时灵敏度联动(UpdateScopeSensitivity)、走/跑速度平滑切换(400 ↔ 600)——全部插值过渡,没有生硬的瞬变;
  • E_PlayerState / E_WeaponState 枚举驱动玩家与武器两套动画状态机。

七、总结

回头看这个项目,几条值得记住的设计原则:

  1. 一次 Trace 喂饱所有系统——命中位置、骨骼、方向、物理材质一次性拿到,伤害、表现、死亡共用一份数据;
  2. 数据驱动替代硬编码——表面类型枚举、骨骼名→蒙太奇映射表,加内容不写逻辑;
  3. 动画通知驱动逻辑时机——开火、换弹填弹的时机都跟着动画走;
  4. 接口解耦 + 继承分层——玩家不认识具体武器,武器基类不认识具体枪型;
  5. 表现优先的小细节(布娃娃冲量、弹壳随机旋转、插值手感)才是 FPS 手感的来源。

已知的迭代方向:攻击动画资源(Anim_Whisper_Attack1/2/3)已经备好但尚未接入 AI 流程,下一步可以让 Whisper 在贴近玩家时发起近战攻击;死亡蒙太奇与布娃娃的衔接也可以打磨得更顺滑。