在 UE5 里做战斗 AI,“狂暴敌人”是特别典型的需求:怪物前半段还是常规追击、试探攻击,血量掉到阈值后突然提速、攻击频率蹿升、招式变化,玩家节奏一下被打乱。很多开发者第一反应是“把攻击间隔改短、把移速调高”,但真正想做得有层次感,核心其实在两套系统上:战斗状态机负责宏观阶段切换,行为树负责微观战术决策。这篇文章就围绕这两套系统展开,先拆解“狂暴”这个需求到底要哪些行为,再给一套可落地的行为树节点设计和状态机定义,最后补齐调试手段和常见坑。
阅读本文你能获得三块东西:一是狂暴敌人从需求拆成技术模块的思路,二是 UE5 行为树 + 状态机 + Blackboard 的具体构建方式,三是敌人 AI 调试和性能观察的实操方法。适合正在做 UE5 单机或网游敌人 AI、对行为树和状态机有基础概念但没完整搭过战斗 AI 的开发者阅读。
1. 狂暴敌人 AI 开发核心能力速览
| 能力项 | 说明 |
|---|---|
| 核心系统 | AI Controller、行为树(Behavior Tree)、黑板(Blackboard)、状态机(State Machine) |
| 主要功能 | 敌人巡逻、追击、攻击、硬直、狂暴模式切换、死亡处理 |
| 战斗逻辑实现 | 状态机管宏观战斗阶段,行为树管单帧内的决策执行 |
| 配套系统 | AIPerception 感知、NavMesh 寻路、Montage 动画通知、Gameplay Tag |
| 调试工具 | Behavior Tree Debugger、AI Debug Draw、可视化 Logger |
| 性能关键点 | 行为树 Tick 频率、AI 感知更新频率、NavMesh 查询开销、同时活跃 AI 数量 |
| 适用场景 | 第三人称动作游戏、ARPG、类魂敌人 AI、竞技场怪物 AI |
需要注意:UE5 引擎本身不限制这套方案的具体版本,行为树模块在 UE4 时期就已经成熟,UE5 沿用了整体框架。不同 UE5 小版本的 AI 感知细节、调试面板位置可能有差异,实际开发时以你本机安装的引擎版本为准。
2. 狂暴敌人逻辑拆解:从“感觉”到可编码行为
“狂暴”在玩法层面给玩家的感受是:敌人变快了、变狠了、打法需要调整。但在工程层面,这必须拆成一系列可编程的行为差异。
2.1 狂暴前后的行为对比
一般战斗 AI 可以按血量或事件触发狂暴模式。假设一个近战敌人,狂暴前后的差异可以这样定义:
| 行为维度 | 常规模式 | 狂暴模式 |
|---|---|---|
| 移动策略 | 接近玩家后保持安全距离 | 直接高速逼近,压到玩家面前 |
| 攻击间隔 | 出招后停顿 1.5 到 2.5 秒 | 停顿缩短到 0.6 到 1.2 秒 |
| 招式选择 | 普通连击、单次重击 | 连击段数增加,重击前摇缩短 |
| 追击策略 | 玩家距离过远时放弃返回 | 不轻易放弃,持续追击时间增加 |
| 反应速度 | 从警觉到出招有延迟 | 感知到玩家后几乎立即进入攻击状态 |
这个拆解过程很重要。如果你不做行为差异表,直接去改行为树参数,最后做出来的“狂暴”往往只是把敌人伤害调高,玩家感受不到行为上的压迫感。
2.2 技术模块归类
从 UE5 工程实现的角度,上面的行为差异落在四个模块里:
- 感知层:AIPerception 负责视觉、听觉,狂暴模式下扩大感知半径、缩短重新感知间隔;
- 决策层:状态机 + 行为树共同决定“当前该做什么”;
- 执行层:CharacterMovementComponent 负责位移,AbilitySystem 或 AnimMontage 负责出招表现;
- 数据层:Blackboard 和属性系统保存敌人当前状态、目标引用、血量、狂暴标记。
一个常见的错误是让行为树承担所有逻辑,包括状态切换。行为树本质上适合“从一堆候选行为里挑一个执行”,不适合做“全局阶段切换”。因为行为树节点一旦被某个分支占用,状态变化很难及时打断当前分支,表现出来就是敌人反应迟钝、切换不果断。正确做法是状态机负责粗粒度切换,行为树在状态内部做细粒度决策。
3. 战斗状态机设计:管住“大节奏”
状态机的任务很单纯:明确敌人当前处于什么战斗阶段,并在合适的时机切到下一个阶段。
3.1 状态定义与转换关系
以狂暴近战敌人为例,建议定义六到七个状态:
Idle → 初始待机 Patrol → 巡逻寻敌 Chase → 追击目标 Combat → 常规战斗 Rage → 狂暴模式 Stagger → 受击硬直 Dead → 死亡状态之间的转换条件用枚举或 Gameplay Tag 表达。下面是一个转换关系示例:
| 当前状态 | 转换条件 | 下一状态 |
|---|---|---|
| Idle / Patrol | AIPerception 发现玩家 | Chase |
| Chase | 进入武器攻击范围 | Combat |
| Combat | 血量低于 30% | Rage |
| Combat | 玩家距离超过放弃追击距离 | Chase |
| Combat | 受到玩家重击触发硬直 | Stagger |
| Rage | 血量回满 / 战斗结束 | Combat 或 Idle |
| Stagger | 硬直动画播放完毕 | Combat 或 Rage |
这张表就是状态机最核心的文档。写代码之前先画清楚,后面所有逻辑都围绕这个表实现,不容易出现“AI 在多个状态之间疯狂抖动”的问题。
3.2 UE5 中状态机怎么落地
UE5 提供多种状态机实现方式:
- Animation Blueprint 状态机:只负责动画切换,不适合做 AI 决策;
- Gameplay Behavior System / GameplayInteractions:新版 UE5 提供的框架,适合大型项目,但学习成本高,且部分接口在小版本之间变动大;
- 自定义 C++ 状态机:可控性最强,团队内部最容易统一规范。
中小型项目强烈建议用自定义 C++ 状态机,不要一上来就上完整框架。你只需要一个枚举、一个当前状态变量、一个 Update 函数:
UENUM(BlueprintType) enum class EMonsterCombatState : uint8 { Idle, Patrol, Chase, Combat, Rage, Stagger, Dead }; UCLASS() class UMonsterStateMachine : public UObject { GENERATED_BODY() public: EMonsterCombatState CurrentState; void ChangeState(EMonsterCombatState NewState) { if (CurrentState == NewState) { return; } ExitState(CurrentState); CurrentState = NewState; EnterState(CurrentState); } void TickState(float DeltaTime) { // 根据不同状态执行对应逻辑 switch (CurrentState) { case EMonsterCombatState::Chase: // 每帧检查距离,决定是否切换 Combat break; } } private: void EnterState(EMonsterCombatState State) {} void ExitState(EMonsterCombatState State) {} };实际项目中不建议把状态逻辑全写死在 OnTick 里,更推荐每个状态用单独的结构体或对象管理,上面代码只是给一个最小可运行的结构做参考。
3.3 状态切换时同步数据到黑板
状态机切换后,要第一时间把结果写入 Blackboard,行为树才能读到。比如:
void UMonsterStateMachine::ChangeState(EMonsterCombatState NewState) { // 写入黑板标记 if (UBlackboardComponent* Blackboard = OwnerController->GetBlackboardComponent()) { Blackboard->SetValueAsEnum(FName("CombatState"), NewState); } }行为树装饰器直接读取CombatState做分支条件。这样状态机和行为树解耦,状态机只负责“我现在处于什么阶段”,行为树只负责“在这个阶段里我下一步具体干什么”。
4. 行为树构建:管住“小决策”
状态机定了大节奏,行为树就是具体到每一帧的动作决策器。
4.1 狂暴敌人行为树总览
一棵战斗 AI 行为树通常长这样:
Root └── Selector(按优先级选择) ├── Sequence: 死亡处理 │ ├── Decorator: CombatState == Dead │ └── Task: 播放死亡动画,禁用碰撞 ├── Sequence: 狂暴攻击 │ ├── Decorator: CombatState == Rage 且进入攻击范围 │ ├── Task: 锁定目标 │ ├── Task: 选择狂暴招式(连招段数 +2) │ └── Task: 播放 Montage 并等待结束 ├── Sequence: 普通攻击 │ ├── Decorator: CombatState == Combat 且进入攻击范围 │ ├── Task: 锁定目标 │ ├── Task: 选择普通招式 │ └── Task: 播放 Montage 并等待结束 ├── Sequence: 接近目标 │ ├── Decorator: CombatState == Chase 或 Combat │ ├── Task: MoveTo 玩家位置 │ └── Task: 攻击距离判定,满足则终止当前分支 └── Sequence: 巡逻 ├── Task: 随机选取巡逻点 └── Task: MoveTo 巡逻点优先级顺序是行为树的灵魂。死亡最高,狂暴攻击其次,普通攻击再次,最后才是接近和巡逻。行为树从上往下判断,前面的分支条件不满足才落到后面的分支。
4.2 UE5 中行为树节点的结构化组织
在实际创建行为树时,建议把节点组织成三层结构:
- 根层:一个总 Selector,下面挂 4 到 5 个大分支;
- 行为层:每个大分支是一个 Sequence 或复合节点;
- 动作层:最下面是具体的 BTTask,比如 MoveTo、PlayMontage、自定义攻击逻辑。
UE5 编辑器里创建流程是:
- 内容浏览器右键 → Artificial Intelligence → Behavior Tree;
- 创建 Blackboard,把敌人用到的关键数据挂上去,例如
TargetActor、CombatState、DistanceToTarget; - 在行为树编辑器里,从 Root 往下拖节点;
- 每个分支开头用 Decorator 绑定 Blackboard 条件;
- 树的最末端挂 BTTask,执行具体动作。
装饰器条件举例:攻击分支要求CombatState == Combat、DistanceToTarget <= 200。这样行为树每个分支都会先判条件,条件成立才执行动作。
4.3 自定义攻击 BTTask
UE5 自带 MoveTo、Wait、RunBehavior 等任务,攻击逻辑必须自定义。最简单的攻击 BTTask 长这样:
UCLASS() class UBTTask_MonsterAttack : public UBTTaskNode { GENERATED_BODY() public: virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override { // 1. 从黑板拿到目标 Actor // 2. 让 AIController 转向目标 // 3. 播放攻击 Montage // 4. 返回 InProgress,等待通知 return EBTNodeResult::InProgress; } virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override { // 检查动画是否播完,播完返回 Succeeded } };攻击任务核心技巧是别在 ExecuteTask 里一次性播完动画就直接返回 Succeeded。因为敌人攻击有前摇、命中、恢复三个阶段,这个过程中行为树不能继续往下跑,否则会出现“边攻击边移动”的滑步问题。正确做法是返回InProgress,等动画通知或定时器结束再返回Succeeded。
5. 行为树与状态机的协同:避免两个系统互相打架
这是战斗 AI 开发里最容易翻车的地方。状态机说“你现在该追击”,行为树却正在播放攻击动画,两边就会冲突。解决方案是划清职权边界。
5.1 谁是主导,谁是执行
推荐设计:状态机主导,行为树执行。
状态机判断“敌人该在哪个大阶段”,行为树判断“这个阶段内具体执行哪个动作”。状态机切换状态时,行为树通过黑板感知变化,自行中断当前任务。比如敌人从Combat切到Rage,行为树里Combat分支的装饰器条件立即失效,树会重新评估,优先进入Rage分支。
为了防止行为树卡在旧状态,State Machine 切换时必须调用一次UBehaviorTreeComponent::RestartTree()或强制重新评估:
void AMyEnemyAIController::OnStateChanged(EMonsterCombatState NewState) { if (UBehaviorTreeComponent* BTComp = Cast<UBehaviorTreeComponent>(GetBrainComponent())) { BTComp->RestartTree(); } }要注意RestartTree不能每帧调用,只应该在状态切换瞬间调用,否则行为树永远处于重启状态,根本跑不到动作节点。
5.2 两者分工案例
以狂暴敌人触发狂暴为例,完整流程是:
- 敌人受击,血量降到 30% 以下;
- 属性系统回调
OnHealthBelowThreshold; - 状态机收到信号,从
Combat切到Rage; - 状态机把
CombatState写入黑板为Rage; - 行为树发现自己当前的
Combat分支 Decorator 失效; - 行为树重新评估,进入
Rage分支; - 狂暴分支里,Blackboard 记录狂暴状态的攻击间隔缩短倍率、攻击段数加成等参数;
- 自定义攻击任务读取这些参数,执行强化后的出招逻辑。
这套流程跑通,敌人的狂暴从“感受上变难打”转化为“机制上确实改变了行为”。
5.3 防抖处理
状态机在 Combat 和 Chase 之间来回切换是很常见的问题。玩家站在离敌人攻击范围临界点附近,AI 会刚切到 Combat 又因为距离判定切回 Chase,表现为敌人左右犹豫。
防抖有三个常用手段:
- 切换冷却:状态机内部记录上一次切换时间,强制 0.5 秒内不允许切回同一状态;
- 滞回区间:进入攻击范围的距离阈值是 220,退出攻击范围的距离阈值设为 280,中间 60 个单位是缓冲带;
- 行为树顶层装饰器判断:在追击分支的装饰器里写“未处于攻击状态”,避免重复触发。
6. 狂暴技能与招式选择
狂暴敌人最有辨识度的部分是攻击招式变化。这里涉及行为树如何“选招”。
6.1 基于权重的招式选择
不建议在行为树里写一堆并列分支做招式选择,那样节点会爆炸。推荐做法是在 Blackboard 或属性组件里维护一个招式权重表,攻击任务按权重随机出招。
USTRUCT(BlueprintType) struct FMonsterAttackEntry { UPROPERTY(EditAnywhere) UAnimMontage* AttackMontage; UPROPERTY(EditAnywhere) float Weight = 1.0f; UPROPERTY(EditAnywhere) bool bRageOnly = false; }; UCLASS() class UMonsterAttackSelector : public UObject { GENERATED_BODY() public: UAnimMontage* SelectMontage(bool bIsRage) { // 1. 过滤 bRageOnly 条目 // 2. 按 Weight 随机 // 3. 返回选中的 Montage return nullptr; } };攻击 BTTask 里调用选择器拿 Montage,然后播放。狂暴模式只需要把普通招式的权重降低、狂暴招式权重调高,选择器同一套代码不需要改。
6.2 利用行为树子树复用招式逻辑
不同敌人如果攻击招式相似,可以把攻击逻辑抽成独立子树,通过RunBehavior任务挂到不同敌人行为树里。这样多个敌人共用同一套“选择招式 → 播放动画 → 等待结束”逻辑,差异只体现在各个怪物自己的招式数据资产和权重表上。
7. 感知、寻路与追击的坑
狂暴敌人在追击环节翻车的概率相当高。这里单独给一段排查优先级。
7.1 AIPerception 配置优先级
先检查 SensesConfig 里视觉范围是不是被调成 0 了,听觉半径是否有数据,感知的AutoBroadcastSenseEvent有没有打开。然后检查感知更新频率,默认PerceptionTickInterval是 0.25 秒更新一次,狂暴模式下可以调短到 0.1 秒,玩家体验会感觉敌人“反应变快”。
7.2 NavMesh 与 MoveTo 失败
MoveTo 节点最常见的问题是找不到路径。顺序排查:
- 玩家站的位置是否在 NavMesh 范围内;
- 敌人的 Agent Radius 和 Agent Height 是否和 NavMesh 的 Agent 配置匹配;
- 目标 Actor 的 Root Component 是否有 Collision;
- 行为树里 MoveTo 的 AcceptableRadius 是否偏大,导致还没贴近目标就判定到达。
7.3 狂暴加速的位移实现
狂暴敌人移速提高,建议直接改CharacterMovementComponent::MaxWalkSpeed,不要靠加一个 permanent 的 root motion 或修改胶囊体大小,容易影响碰撞。状态机切到 Rage 时同步改 MaxWalkSpeed,切回普通模式时改回来。这样 MoveTo 任务自动适配新的移动速度,不用额外处理。
void AMyEnemyCharacter::SetRageMode(bool bEnable) { if (UCharacterMovementComponent* MoveComp = GetCharacterMovement()) { MoveComp->MaxWalkSpeed = bEnable ? RageMoveSpeed : NormalMoveSpeed; } }8. 行为树调试与性能观察
8.1 Behavior Tree Debugger 怎么用
行为树调试器在编辑器里可以直接打开。运行 PIE 后选中敌人 AIController,工具栏中找到 Behavior Tree Debugger 面板,能看到当前行为树执行到哪个节点、每个 Decorator 的判定结果、哪个任务返回了 Success 或 Failed。
调试时重点看三个地方:
- 树当前停在哪个节点。如果停在某个
InProgress攻击任务上,说明动画没正常回调; - Decorator 条件是否如预期。条件变灰说明 Blackboard 值没有按预期写入;
- 任务返回结果。某些任务需要返回
Failed让 Selector 尝试下一个分支,结果返回错了会卡分支。
8.2 AI Debug Draw 与可视化
UE5 自带 AI Debug 工具:
- 选中敌人,按
P键显示感知调试信息,能看到视觉锥体、听觉半径; - Gameplay Debugger 面板中打开 AI 感知条目,能看到目标是否被感知到;
bDrawDebugInGame开启后可以绘制 MoveTo 路径。
建议开发期把感知可视化保持开启,但打包时关闭,否则玩家机器上会看到 AI 的视觉锥体和干扰信息。
8.3 行为树性能观察
行为树本身是低频系统,大部分敌人 AI 的性能瓶颈不在行为树节点,而在:
- 每帧更新的
MoveTo寻路请求; - AIPerception 多角色同时感知导致的 N x N 问题;
- Montage 运动通知触发的碰撞检测;
- Blackboard 中每个 Tick 更新的高频变量。
观察性能用 UE5 的stat命令:
stat AI这条命令能显示 AI Actor、行为树、感知系统、NavMesh 的耗时。多人场景里如果 AI 数量较多,优先把感知 Tick 间隔拉大,其次减少行为树评估频率,最后考虑把 MoveTo 从每帧更新改成 0.2 秒更新一次。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 敌人开局不动 | 行为树未运行,或感知没检测到玩家 | 检查 AIController 的 BrainComponent、感知 SensesConfig | 确认 BT 已挂载;检查感知范围 |
| 敌人追击时反复摇摆 | 状态机 Combat/Chase 切换抖动 | 观察状态机切换日志 | 增加切换冷却或滞回区间 |
| 攻击动作播一半突然移动 | 攻击 BTTask 没保持 InProgress | 打断点看任务返回值 | 攻击任务等待动画结束再 Succeeded |
| 狂暴后速度和攻击没变化 | 状态切换没有写入黑板或没改 Movement 参数 | 查看 Blackboard 当前值 | 状态机切换时同步写黑板并调用 SetRageMode |
| MoveTo 一直失败 | NavMesh 未烘焙或 Agent 参数不匹配 | 打开 P 显示导航,看路径是否生成 | 重新烘焙 NavMesh,调整 Agent Radius |
| 行为树卡死在某个节点 | Decorator 条件永远满足,或任务一直 InProgress | Debugger 看节点状态 | 检查黑板的枚举值是否被重置 |
| 多个敌人同时感知玩家导致集体发呆 | 感知系统更新频率过高,性能来回波动 | stat AI 看耗时 | 调大 PerceptionTickInterval 或限制同屏 AI 数量 |
| 打包后 AI 行为与编辑器不一致 | 调试数据没清理或未烘焙寻路 | 检查打包日志与 Have AI Debug Draw | 关闭调试绘制,重新 Build NavMesh |
10. 工程设计建议与合规边界
战斗 AI 开发不是写完一棵行为树就结束,工程化程度决定后期迭代效率。
10.1 推荐的项目组织方式
- 每个敌人一个 AIController,但共享通用的行为树 Service 和 Task 库;
- Blackboard 名称统一带前缀,比如
CombatState、TargetActor、LastKnownPosition,避免多个敌人复用时字段冲突; - 招式权重表用 DataAsset 或 DataTable 管理,策划可以直接调权重而不用改代码;
- 状态机转换条件写成独立函数,每次状态切换打印日志,方便回放分析;
- 开发期把
LogTemp中 AI 状态切换日志单独用 Category 分离,例如LogEnemyAI,过滤其他日志。
10.2 战斗 AI 开发的性能预算
做大规模战斗场景时,要给每个敌人的 AI 设定预算:
- 感知更新:普通敌人 0.25 秒一次,精英敌人 0.1 秒一次;
- 行为树评估:5 个以内敌人每帧评估,10 个以上敌人每 2 帧评估;
- MoveTo 重算路径间隔:0.3 秒到 0.5 秒,不要每帧请求寻路;
- 同时处于 Combat 状态的敌人建议控制在 6 到 8 个以内,超出部分进入简化 AI 模式(只追击、不出招、不狂暴)。
10.3 版权与内容合规边界
战斗 AI 开发中涉及动画、音效、外观模型时,务必确认素材来源和授权许可。如果项目要发布上线,自制的动画和设计资产也要存档好设计稿,避免后续检查时无法求证。所有本地开发和测试素材,都建议在项目 README 中记录来源和授权状态。面对玩家反馈时,AI 的战斗难度和表现属于正常游戏设计范畴,不需要回避,但在迭代过程中仍要关注战斗表现是否存在过度刺激的不合理设计,适当保留玩家可应对的操作窗口。
11. 总结与下一步
狂暴敌人这个需求,拆开看就是三个步骤:先定义状态和转换条件,把“狂暴”变成状态机里可切换的 Rage 模式;再把每个状态内部的行为用行为树组织起来,优先级明确、条件清晰;最后把参数差异落到 Blackboard 和属性系统上,让狂暴前后真正产生行为层面的变化。
最容易踩的坑有两个:一是状态机和行为树职权重叠,导致 AI 边攻击边移动;二是状态切换条件没有滞回区间,敌人看起来像神经质一样左右摇摆。这两个问题在开发初期就要预防,后面再改成本很高。
下一步建议先搭一个最小原型:一个敌人、一棵树、四个状态(追击、战斗、狂暴、死亡),把最核心的“血量触发狂暴”跑通。之后再慢慢加感知、多目标战斗、战术协同和特殊招式。这个最小原型跑通后,再考虑用更强壮的框架去重构也不迟。如果你正卡在“敌人 AI 总是不按预期出招”这类问题上,按照文中第 8 节和第 9 节的流程排查一遍,大多数问题都出在装饰器条件和任务返回值上。把这套流程做成团队内部的项目模板,后续每个新敌人的开发周期能明显缩短。