UE5第一人称射击游戏开发:从角色移动到AI追击的完整实践
2026/9/23 20:37:45 网站建设 项目流程

简介:这是一份UE5第一人称射击游戏开发实战教程,适合具备一定UE5基础、希望系统掌握FPS核心玩法的初学者和中级程序员。PDF围绕完整开发流程展开:先进行环境搭建与项目设置,再依次实现第一人称视角、玩家移动与交互、射击与敌人AI、UI界面展示,每一步都给出技术思路和可运行C++片段,例如玩家控制器与角色类的编写、增强输入映射等。资源包仅有1个PDF文件,779KB,体量紧凑但信息密度高。读者跟随章节边读边练,可理解UE5动作游戏的项目组织方式,掌握角色移动控制、射击判定、简单AI及HUD实现的具体做法,最终独立搭建出一个带操作界面的FPS游戏原型。目前已有2198人学习下载,适合作为日常练习与功能查漏的参考资料。

1. 做 UE5 第一人称射击游戏,先把这些默认项改掉

如果你用 UE5 做第一人称射击游戏,最省事的路线不是直接套第三人称模板,而是把输入、相机、碰撞和动画链路在一个空项目里重新连一遍。第三人称模板里那套弹簧臂跟随逻辑,转到第一人称后会变成不停占用你时间的干扰项。这个案例教程要带你把一条完整玩法链路走通:角色移动和镜头控制、追踪式射击、能听枪声并主动追击的敌人 AI、动画重定向后的武器握持动作,最后再用一组可复现的验证方法检查命中反馈和延迟。

整个过程不依赖付费商城模板,主要用 UE5 自带的 Enhanced Input、Line Trace、Behavior Tree 和 IK Retargeter,指定角色和武器用少量 C++ 实现。这套改法适合刚学完 UE5 入门、想用一整个玩法验证实力的开发者,也适合给策划做射击手感验证的原型工程。看完之后你再回到第三人称模板,反而能看懂哪些地方要拆、哪些地方要留。

2. 用 UE5 模板和 Enhanced Input 搭出第一人称角色移动层

2.1 第三人称模板和自建 Pawn 的取舍

直接修改 UE5 的第三人称模板是很多人一开始的选择,但随后会绕进三个坑:SpringArm 造成的镜头惯性、角色转身带动身体旋转、相机贴墙后退。第一人称射击游戏的摄像机一般挂在头部骨骼或角色根组件上,让视角跟随 Controller 而不是跟随角色骨骼。

我的做法是从Character子类新建FPSPlayerCharacter,在构造函数里打开两个旋转开关:

bUseControllerRotationPitch = true; bUseControllerRotationYaw = true;

注意不能打开bUseControllerRotationRoll,第一人称镜头长期带 Roll 角会让人眩晕。打开这两个值的含义是:视野方向完全由 Controller 维持,角色动画层只做反馈,不再负责朝向。很多第一人称射击项目都没把角色自转关干净,导致射击时准心在命中判定里跑得比视觉快半拍。

CharacterMovementComponent 里有一组值得在原型期就改掉的默认值,我常用的参数参考如下:

参数默认值FPS 推荐作用
Max Walk Speed600600~750决定走路和冲刺的基础移速
Max Acceleration20482048~4096越大响应越快,急转身更跟手
Braking Deceleration20484096松开键位的减速强度
Air Control0.050.3~0.5空中转向手感,跳跃射击时需要
Ground Friction88~12配合刹车调整起步惯性

修改位置在构造函数或BeginPlay均可。单人原型直接在构造函数写最简单,后续如果项目要开放网络同步,移动相关属性要交给服务器侧统一更新,不能在客户端只改本机数值。

2.2 Enhanced Input 的动作映射怎么组织战斗键位

UE5 默认推荐 Enhanced Input,替代旧版 InputAxis。第一人称射击至少需要三组 InputAction:Move(2D 向量)、Look(2D 向量)、Attack(布尔)。Attack 单独拆成一个动作,因为开火和松开都要监听,用于半自动和按住连发切换。

右键新建 Input Action 后,在编辑器的 Mapping Context 里把 WASD、鼠标移动、鼠标左键放进同一组。鼠标上下移动记得开启 Y 轴反转,否则很多从主机转过来的玩家第一反应就会觉得镜头反向。

绑定 C++ 代码如下:

void AFPPlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) { if (UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(PlayerInputComponent)) { EIC->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AFPPlayerCharacter::Move); EIC->BindAction(LookAction, ETriggerEvent::Triggered, this, &AFPPlayerCharacter::Look); EIC->BindAction(AttackAction, ETriggerEvent::Started, this, &AFPPlayerCharacter::StartAttack); EIC->BindAction(AttackAction, ETriggerEvent::Completed, this, &AFPPlayerCharacter::StopAttack); } }

这里的关键是Triggered而不是Started。Move 和 Look 需要每一帧持续读取当前输入值,Triggered会在动作保持触发时持续回调;Started只在按键按下瞬间触发一次。Attack 动作绑定StartedCompleted,这样半自动武器点一下就停,全自动武器按住时在 Tick 里检测bIsFiring即可持续射击。

Move的实现要特别注意方向来源:

void AFPPlayerCharacter::Move(const FInputActionValue& Value) { FVector2D Axis = Value.Get<FVector2D>(); AddMovementInput(UKismetMathLibrary::GetForwardVector(GetControlRotation()), Axis.X); AddMovementInput(UKismetMathLibrary::GetRightVector(GetControlRotation()), Axis.Y); }

这里必须用GetControlRotation(),如果写成GetActorRotation(),玩家转动视角后角色仍然按初始朝向移动,转头越大幅度,方向偏差越大。第一人称射击的移动方向永远由当前相机朝向决定,这是一个看似基础但最容易出错的地方。

移动端调试时还要考虑双指触摸蓝图那类输入布局:左半屏触发 MoveAction,右半屏触发 LookAction 和 AttackAction。Enhanced Input 的多指识别在 Mapping Context 的修饰器里按 Finger 索引区分触点,如果不区分,两指触控会被系统按触摸顺序分配,容易出现滑动手势抢镜头的漂移问题。

2.3 冲刺和视角 FOV 联动怎么做

射击游戏常用冲刺时拉大 FOV 来制造速度感。常见做法是维护一个 FOV 目标值,默认 90,冲刺时改成 104,再用FInterpTo做平滑过渡:

float TargetFOV = bWantsToSprint ? SprintFOV : BaseFOV; CurrentFOV = FMath::FInterpTo(CurrentFOV, TargetFOV, DeltaSeconds, 8.f); GetCameraComponent()->SetFieldOfView(CurrentFOV);

插值速度从 8 起步。调慢了没有冲刺反馈,调快了容易发晕。注意到这里每一帧都在调用 SetFieldOfView,所以应该加一个小于 0.1 度的差值判断,数值不变时跳过写入。这个细节在移动端和低端 PC 上能省下不少内部状态更新开销。

3. 用 Line Trace 实现 UE5 的第一人称射击命中判定

3.1 Hitscan 和 Projectile 的选型

射击命中判定要先决定武器模型。Hitscan 扫描和 Projectile 投射物的差别会直接影响网络同步、射击手感和性能预算:

维度Hitscan 扫描Projectile 投射物
命中时刻开火瞬间飞行一段时间后
网络同步延迟补偿和命中通知更简单需要插值和服务器回滚
性能一次通道检测多次碰撞和物理计算
典型武器步枪、手枪火箭筒、榴弹发射器

第一人称射击游戏的核心手感和“开火后立刻看到命中反馈”强相关。飞行时间再短,也会让拉枪瞬间感觉慢半拍。大多数项目的主武器都优先做 Hitscan,需要弹道下坠的武器再单独实现投射物。原型阶段我一般只写一个 Hitscan 接口,预留武器基类,等玩法确认需要抛物线武器时再扩展。

3.2 从相机发射 Trace 还是从枪口发射

很多新手第一版从枪口静态组件发射射线,结果看到弹着点明显偏离准心,尤其贴墙时枪口埋进墙体,子弹直接打在地上。正确的做法是从 Camera 视线起点发射,枪口只作为特效显示位置。这样准心指向哪里,子弹就命中哪里。

FHitResult AFPPlayerCharacter::FireHitScan() { FVector CameraLoc; FRotator CameraRot; GetActorEyesViewPoint(CameraLoc, CameraRot); FVector ShotDir = CameraRot.Vector(); FVector TraceEnd = CameraLoc + ShotDir * FireDistance; FCollisionQueryParams Params; Params.AddIgnoredActor(this); Params.bReturnPhysicalMaterial = true; FHitResult Hit; GetWorld()->LineTraceSingleByChannel(Hit, CameraLoc, TraceEnd, ECC_Visibility, Params); return Hit; }

AddIgnoredActor(this)是这串逻辑里的保命键。如果不忽略自己,射线会命中角色胶囊体,枪械永远打不出去。bReturnPhysicalMaterial打开后,命中金属、木头、血肉时能拿到不同的 PhysicalMaterial,后续接脚步声和弹孔材质反馈都靠它。

还要处理枪口视角遮挡的问题。如果只从相机发射,贴墙时会出现枪管穿墙但命中点还在墙外的穿模感。我一般会在开火前多打一次相机到枪口位置的短距离 Trace,检测到墙就把枪口特效移到墙面附近,视觉和命中点才不会互相拆台。

3.3 散射、后坐力与命中反馈

无扩散的 Hitscan 会让全自动武器显得过于精准,所以腰射时要加随机散射。散射量通常和移动速度、连射次数挂钩:

float SpreadDeg = FMath::Clamp(BaseSpread + FMath::Abs(GetVelocity().Size()) * VelocitySpreadFactor, MinSpread, MaxSpread); FVector2D RandomOffset = FMath::RandUnitVector() * SpreadDeg; ShotDir = (ShotDir + CameraRot.Vector() * RandomOffset.X + CameraRot.RightVector() * RandomOffset.Y).GetSafeNormal();

FMath::RandUnitVector返回的是圆盘内均匀分布的随机点,比两个独立随机浮点数组合更均匀。准心扩散范围要限制在MinSpreadMaxSpread之间,否则移动中射击的弹道会散得完全看不懂。

后坐力不建议直接在一帧里调AddControllerPitchInput,那样会把视角永久抬高一截,玩家还得手动拉回来。常见做法是播放一段很短的 CameraShake,让镜头先向上推再回中;这类效果可以用UCameraShakeBase的 Oscillation 参数完成,不污染输入轴,也不会被跳跃状态打断。枪口火光和命中反馈放到武器动画蒙太奇里指定帧的山级事件触发,什么时候冒出火花、什么时候播放弹孔音效,都以这个 Notify 位置为准。

3.4 命中目标后的通用接口设计

武器命中后不要直接依赖敌人类型。定义一个IDamageableInterface,所有能被子弹打中的角色或物资产实它:

if (Hit.GetActor() && Hit.GetActor()->GetClass()->ImplementsInterface(UDamageableInterface::StaticClass())) { IDamageableInterface* Target = Cast<IDamageableInterface>(Hit.GetActor()); Target->ApplyDamage(CurrentWeaponDamage, this, GetController(), Hit.Location); }

这样比Cast<AEnemyBase>通用得多。后面加木箱、靶子、油桶都只需要实现同一个接口,武器侧不用反复加分支。敌人体内的血量和死亡逻辑各自维护,武器只负责传出“谁打的、在哪打的、打了多少”这三个信息。

4. 用 UE5 的 Behavior Tree 和感知事件给敌人 AI 做还手能力

4.1 AI Controller 与 Behavior Tree 的最小组合

用 UE5 做 NPC AI Agent 的开发方式和后端智能体不同,这里更看重帧级响应。感知、决策、行动三步需要在几十毫秒内完成,Behavior Tree 的天然事件驱动特性正好合适,它不会每帧跑完整棵树。

创建敌人角色后,在它的AIControllerClass指定一个自定义 AIController,重写OnPossess并运行行为树:

void AFPSEnemyController::OnPossess(APawn* InPawn) { Super::OnPossess(InPawn); if (UBehaviorTree* BT = LoadObject<UBehaviorTree>(nullptr, TEXT("/Game/AI/BT_FPSEnemy.BT_FPSEnemy"))) { RunBehaviorTree(BT); } }

RunBehaviorTree返回 false 的常见原因是关卡里没有烘焙 NavMesh。我在这种情况下会先检查 P 键呼出的 Navigation 显示,看敌人脚下是否出现绿格子。路径写死用LoadObject只适合原型验证,正式工程请把 BehaviorTree 资产以引用变量暴露在蓝图中,避免资产路径变更后找不到。

行为树结构我通常这样组织:

FPSEnemyBT ├─ Combat (Selector) │ ├─ AttackPlayer (Sequence, 条件: 在视野和射程内) │ │ ├─ Set Focus (黑板变量) │ │ ├─ Task PlayMontage (射击动作) │ │ └─ Wait 0.5 (射击硬直) │ └─ MoveToPlayer (Task MoveTo, 目标黑板变量) ├─ Investigate (Sequence, 条件: 听到枪声) │ └─ MoveToNoiseLocation (黑板变量) └─ Patrol └─ MoveToRandomPoint (Task FindPatrolLocation)

Selector从上往下试,一旦某个分支条件满足,后面的巡逻分支就不会执行。这里最值得调的是MoveTo的 AcceptanceRadius。设成 10 敌人会抖着找目标,设成 250 又会在远处左右滑步。一般持枪距离 1500 时,我会把这个半径放在 120~200 之间,敌人既能站定射击,又不会在目标周围来回抽动。

4.2 利用多播委托传递枪声,让 AI 的听觉感知联动

敌人知道玩家在哪,不能靠玩家主动告诉敌人,这样太作弊。常见做法是武器开火时广播一个声源位置,AI 的感知系统把它当刺激处理。UE5 多播委托正好可以当轻量级事件总线用。

在武器基类里声明一个双参数多播委托:

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FWeaponFiredEvent, FVector, FireLocation, float, Volume); UCLASS() class AFPSWeaponBase : public AActor { GENERATED_BODY() public: UPROPERTY(BlueprintAssignable, Category = "Weapon|Event") FWeaponFiredEvent OnWeaponFired; };

开火后调用OnWeaponFired.Broadcast(FireLocation, 1.0f);。广播点不要直接用角色位置,第一人称武器枪口在相机附近,声源如果放在玩家身体中心,AI 会按玩家整个胶囊体判断方向。把枪口世界位置传出去,听觉感知才有方位感。

敌人 AI Controller 在初始化时绑定事件:

Weapon->OnWeaponFired.AddDynamic(this, &AFPSEnemyController::OnGlobalShootEvent);

AI Perception 组件里开启AISense_Hearing即可接收到这类刺激。感知回调会更新黑板里的 LastNoiseLocation,行为树的 Investigate 分支读取这个位置执行 MoveTo。多播委托的绑定生命周期要注意:敌人销毁时如果回调还挂在事件上,下次开火就会访问到已经标记的 Pawn。所以在OnUnPossess里统一调用一次RemoveDynamic,避免 PIE 环境下出现幽灵回调。

4.3 受击反馈和死亡掉落

敌人的受击反馈不能只依赖动画蒙太奇。AI 控制角色在行为树里持续执行 MoveTo 时,播放受伤动画会被下一个移动指令打断。常见做法是受击时强制切到受击分支:

GetCharacterMovement()->MaxWalkSpeed = 150.f; PlayAnimMontage(HitReactMontage); GetWorld()->GetTimerManager().SetTimer(RecoverTimer, [this]() { GetCharacterMovement()->MaxWalkSpeed = OriginalSpeed; }, 0.6f, false);

受击瞬间把移动速度压到 150,播放短促的 HitReact,0.6 秒内恢复。这样既有被打断的感觉,又不会让敌人卡在原地不追玩家。

死亡先不急着开 Ragdoll。把胶囊体碰撞保留,关掉移动输入,播放一段死亡动画,延迟几秒再禁 Tick。直接启用物理布娃娃会让 AI 的 NavMesh 和感知在物理帧里重新计算,低端机容易卡顿,等玩法稳定后再换效果不迟。

5. UE5 动画重定向与武器握持的关键设置

5.1 用 IK Retargeter 把可用骨骼动画搬到自定义角色上

第一人称射击游戏里,敌人和玩家的武器动画大多来自商店或动捕资源,骨骼命名不一定和当前角色匹配。UE5 的 IK Retargeter 流程分三步:在角色骨骼资产上创建 IK Rig,定义骨盆、脊柱、手臂、腿部骨骼链,再创建 IK Retargeter 指定源骨骼和目标骨骼。

重定向完最常见的问题就是脚部悬空或者手臂划大圈。我一般先检查三处:骨盆映射是否指向 pelvis,膝盖的 Pole Vector 是否朝前,脚部骨骼有没有开 Foot Lock。下面是几个高频参数:

Retarget 参数作用常见应用范围
Twist Axis纠正手臂旋转的轴向按目标骨架 Z 或 X 轴微调
Pole Vector保持膝盖朝向,避免膝盖旋转过头0.4~0.7
Retarget Root根节点位移的平顺度模型差异大时用 0.1~0.3
Foot Lock修正脚掌接触地面的滑步开,动画急停时明显改善

第一人称手臂动画不要直接复用第三人称全骨骼重定向,因为第一人称镜头下的手臂参考高度和枪械锚点都和第三人称不同。我更建议单独建一个第一人称手臂骨骼,只保留手部和前臂动画,枪械作为手臂骨骼上的子组件挂在上面,这样切换第一人称时不会把全身骨骼带进镜头。

5.2 AnimBP 槽位让开火和换弹互不打断

第一人称和第三人称的 AnimBP 都要处理同一件事:角色在奔跑时换弹或开火,不希望整条动画被替换。UE5 的 Slot 节点专门解决这种分层混合需求。

在 AnimBP 的 AnimGraph 里加一个 Slot 节点,给它一个比如 UpperBody 的 SlotName。开火和换弹的 AnimMontage 播放时指定到这个 Slot,底层 Locomotion 仍然在跑,上层动画按过渡时间混合进去。

C++ 里播放换弹蒙太奇时只需要一行:

UAnimInstance* Anim = GetMesh()->GetAnimInstance(); if (Anim && ReloadMontage) { Anim->Montage_Play(ReloadMontage, 1.f, EMontagePlayReturnType::MontageLength, 0.f, false); }

蒙太奇资产里的 SlotName 必须和 AnimBP 里的 Slot 名称一致,否则动画会在默认层播放,效果变成全身切换。第一人称开火蒙太奇不要开 Root Motion,第一人称视角下根动画会造成镜头来回晃,而且实际位移不被移动组件认可,射击时角色会莫名滑步。

5.3 枪口特效和材质里的 Panner 用法

Panner 是材质里常用的 UV 平移节点,做枪口火焰贴图滚动很合适。在材质编辑里,把火焰贴图的 TexCoord 输入前接一个 Panner,Speed 节点分成 X 和 Y 两个输入,X 为 0、Y 为 1 表示火焰向上滚动,调大数值会使贴图滚动变快。

火焰质感主要在自发光强度。把噪声贴图连到 Emissive Color,乘以一个较大倍率,再配合 Niagara 粒子里的半透明混合,枪口才会有烧起来的感觉。如果 Panner 用在贴花或 UI 上,边缘会显示出明显的方片,要额外乘 OpacityMask 做透明度裁剪。

武器握把位置如果对齐不了,优先调整手型动画,其次是枪械骨骼上的辅助 IK。许多第一人称射击案例败在模型漂亮但手穿模。碰到这类问题,我一般会在枪械骨骼加两个 Two Bone IK 目标点,在右手和左手分别做瞄准约束,比逐帧手调动画快得多。

6. 交付前的联调:用 TraceDebug 和 stat 命令验证第一人称射击手感

6.1 把命中轨迹和伤害事件都打出来

很多手感问题都出在“看起来打到,却没有伤害反馈”。因为 Trace 起终点和视觉方向之间只要偏离一两个像素,远距离就会差出半个身位。建议在 FireHitScan 前面加一个调试条件,只在 PIE 模式里绘制射线和命中点:

#if WITH_EDITOR if (bDrawDebugTrace && GetWorld()->WorldType == EWorldType::PIE) { DrawDebugLine(GetWorld(), CameraLoc, TraceEnd, FColor::Yellow, false, 0.2f, 0, 1.0f); if (Hit.bBlockingHit) { DrawDebugPoint(GetWorld(), Hit.Location, 8.f, FColor::Red, false, 0.2f); } } #endif

红点表示实际命中位置,弹孔中心应该和红点重叠。如果红点在角色胶囊体上,多半是AddIgnoredActor(this)没生效或者第一人称武器子组件也被算进了命中对象。再打开bTraceComplex,用复杂碰撞检测能看到更细的骨头结构,但物理查询开销会变大,正式版本记得关掉。

6.2 用 stat 命令把资源压力先隔离

手感卡顿和输入延迟通常由性能问题伪装。在编辑器按波浪号打开控制台,输入三条命令组合判断:

  • stat fps:看实时帧率,确认是否低于目标刷新率。
  • stat unit:看 Game/Thread/GPU 三项耗时,哪一项标红就优先查哪一项。
  • stat SceneRendering:看 DrawCall 是否被枪口特效拖垮。

第一人称枪械的 Niagara 粒子如果常驻发射,敌人一多 DrawCall 会高出很多。我一般会把枪口特效组件在开火时瞬间触发,不开火时隐藏,只保留一个极短生命周期的粒子系统。这个改动通常能让stat unit的 GPU 耗时明显下降。移动端还要回到真机跑一遍,双指触摸蓝图的输入布局不能只在 PIE 预览器里验证,真机上触摸事件分派比编辑器更接近硬件延迟,至少跑三十分钟再看触点复用是否漂移。

6.3 给玩家一个快速手感验证流程

FPS 手感最终要人来测,但流程可以固化。站在距离一排标靶 1500 单位的位置,快速点射十发,观察准星跳动是否超过两个像素;移动中射击十发,观察命中反馈是否比开火晚两帧以上;再让一个敌人 AI 触发枪声听觉,记录它在听到枪声后一秒内能不能到达警戒位置。如果反馈晚过两帧,优先检查动画蒙太奇里的 Notify 帧位置,把枪口火光和枪声统一挪到扳机按下的同一帧,再去调 CameraShake 振幅。这个验证场景提前摆好,之后每换一把新枪、每加一种怪物,先跑一遍这一套流程,比反复靠手感猜原因要可靠得多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询