这次我们来看一个动作游戏项目里几乎绕不开的实战主题:虚幻引擎5战斗AI开发。具体需求很典型:一个敌人,平时会索敌,锁定玩家后追击,近身后开始攻击,血量降到一定阈值就进入狂暴状态——冲刺更快、攻击频率更高、招式从单段变成连击。
这类敌人AI在很多ARPG和第三人称动作项目里是标配,但落到UE5里做的时候,很容易翻车。最常见的错误是:行为树里塞满动画播放逻辑,动画状态机里挂一堆判断分支,最后节点缠成一团,敌人要么发呆,要么攻击动作和伤害判定对不上,要么狂暴后只会加速平A,招式完全没有变化。
这篇文章会把任务拆成三层来写:行为树管决策,状态机管执行,狂暴逻辑用黑板键和动画通知联动。我会围绕“狂暴敌人逻辑”给出行为树节点设计方案、动画状态机状态划分、血量阈值触发狂暴的实现思路,以及一套编辑器内可复用的调试和排查流程。
适合读者:正在用UE5做动作类游戏,想把敌人AI从“傻站、只会平A”提升到“会冲刺、会连击、会狂暴”的开发者。看完你应该能判断出自己的项目里哪些逻辑放错位置了,以及怎么重构。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 核心机制 | 行为树负责AI决策,状态机负责动作执行,通过黑板键和动画通知联动 |
| 敌人类型 | 近战型狂暴敌人:索敌、追击、冲刺、三段攻击、硬直、狂暴化 |
| 主要工具 | Behavior Tree(行为树)、Blackboard(黑板)、AIPerception(AI感知)、NavMesh、动画蓝图状态机 |
| 引擎版本 | UE5,建议在 5.1 及以上版本内测试,具体以本机项目版本为准 |
| 前置要求 | 熟悉蓝图基础,理解 Selector、Sequence、Task 的基本用法 |
| 调试工具 | UE5 编辑器内置 Behavior Tree Debugger、AI 可视化日志 |
| 适用场景 | 第三人称动作、ARPG、竞技场敌人、Boss 战第一阶段 |
需要说明:下面给出的是逻辑设计模板,不是某个现成插件的操作手册。不同UE5版本中节点名称和细节可能有差异,落地时以你所在版本为准。没有写死显存、帧率等性能参数,因为AI逻辑的消耗主要取决于感知频率、行为树运行周期和动画混合数量,不依赖特定显卡型号。
2. 适用场景与使用边界
2.1 适合谁在用
这套“行为树+状态机”组合适合以下几类开发者:
- 刚接触UE5 AI系统,想弄清楚行为树和状态机各自该管什么的新手。
- 已经有敌人原型,但索敌和攻击逻辑写在一个巨大的Tick事件里,想重构的开发者。
- 需要做多个不同类型的敌人,想抽出一套可复用AI框架的项目组。
- 准备做Boss战,需要“狂暴阶段”这类二阶段机制的动作游戏团队。
2.2 解决的问题
这套设计解决的是三个具体痛点:
第一,决策和表现解耦。行为树不直接播放动画,只输出“下一步要做什么”的意图;状态机根据意图播放对应动画。这样改AI策略不会动动画资源,改动画表现不会影响决策逻辑。
第二,连击和硬直可控。攻击状态用子状态机管理,每个动作什么时候能取消、什么时候能接下一段,由动画通知配合状态切换来控制,不再依赖一堆If分支。
第三,狂暴逻辑可扩展。血量阈值、狂暴后的攻击频率、冲刺速度、招式变化,全部做成黑板键和角色变量。以后想做“二阶段丢远程技能”,只需要加状态和条件,不需要推翻整个流程。
2.3 不适合什么场景
- 如果敌人只需要“看见玩家然后走过去”,行为树+状态机的组合确实偏重,直接用蓝图Tick加接口调用更快。
- 如果项目是射击类AI,需要频繁掩护、侧翼包抄、团队协作,这套近战向的状态划分需要扩展,不能直接套用。
- 如果团队没有程序,纯靠蓝图搭行为树,后期节点会非常多,维护成本高。建议至少掌握基础C++或Blueprint Function封装能力。
2.4 版权与内容合规提醒
训练AI、做人脸相关功能时授权问题很敏感,但游戏敌人AI本身没有这类风险。不过有几点仍然要注意:
- 敌人模型、动作动画、特效素材,确认项目有合法授权。
- 涉及真实人物肖像、声音素材的任何功能,必须单独确认授权范围。
- 狂暴化涉及出血、断肢等表现时,注意按发行平台要求做分级处理和显示开关。
- 本地测试环境尽量用内网或本机端口,避免调试服务暴露到公网。
3. 环境准备与前置条件
3.1 基础项目配置
新建或打开一个UE5项目,推荐使用第三人称模板,因为内置了角色移动组件和基础动画蓝图,可以省掉最早期的搭建工作。
需要确认的工程设置项:
- 项目设置 -> 引擎 -> 导航系统,勾选启用导航网格。
- 项目设置 -> 引擎 -> AI系统,确认AIController类指向你即将创建的AI控制器类。
- 项目设置 -> 引擎 -> 碰撞,检查敌人的碰撞通道是否与WorldStatic、Pawn正确响应。
这些设置决定了敌人能不能找到路、能不能被AIController驱动。
3.2 导航网格启用
敌人追击需要NavMesh。在关卡中放置一个NavMeshBoundsVolume,把范围调整到战斗场地大小,按P键预览绿色区域。
如果战斗场景是多层结构或包含动态门,需要用RecastNavMesh的配置来匹配关卡几何体。测试阶段用一块平整地面即可,不用刻意做复杂地形,避免把NavMesh调参和AI逻辑混在一起排查。
3.3 敌人控制器的创建
在内容浏览器中创建Blueprint Class,父类选择AIController。这个类后面用来运行行为树、感知玩家、存储当前目标。
创建完成后,在敌人角色的蓝图细节面板中,将AIController Class指定为刚才创建的类。这样敌人被生成出来时,引擎会自动挂上控制器。
4. 核心概念:行为树和状态机怎么分工
这一步是全部设计的地基,建议先理解再动手。
4.1 行为树是“决策层”
行为树回答的问题是:“当前这个时刻,我应该把AI资源花在哪件事上?”
它的运行方式是按一定频率从上到下扫描节点,挑选一个可以执行的分支。常用节点含义:
| 节点类型 | 作用 | 类比 |
|---|---|---|
| Selector | 从左到右尝试子节点,遇到能运行的就不再看后面的 | 一组备选方案,选第一个能用的 |
| Sequence | 从左到右依次执行子节点,遇到失败就中断 | 必须全部做完才算成功 |
| Task | 执行具体动作,返回成功或失败 | 行为树的叶子逻辑 |
| Decorator | 挂在节点上,控制子节点是否允许运行 | 前置条件开关 |
| Service | 在分支运行时周期执行,更新数据 | 后台刷新黑板键 |
在狂暴敌人AI里,行为树只输出目标:追目标、攻击目标、冲刺到目标身边、进入狂暴追击。它不关心攻击动画的名字,不关心哪一帧造成伤害,不关心动画蒙太奇怎么播放。
4.2 状态机是“执行层”
状态机回答的问题是:“当我决定做某件事之后,角色身体应该播放什么动作?”
动画状态机负责:
- Idle(待机)
- Locomotion(移动)
- Attack 三段连击
- Stagger(被击中硬直)
- EnrageStart(狂暴启动)
- EnrageAttack(狂暴攻击)
- Death(死亡)
行为树下达“攻击”意图后,状态机从Locomotion切到Attack,播放攻击动画。Attack动画播到某个通知帧时,通知蓝图开启伤害检测、触发下一段连击。
如果用行为树直接播放动画蒙太奇,会出现一个经典问题:行为树的决策周期比如0.2秒扫描一次,但攻击动画要1.2秒播完。行为树以为攻击已经“完成”,下一轮扫描可能又触发一次攻击,动画就被打断重播。状态机天然处理了“当前动画未播完之前不允许切走”的规则。
4.3 狂暴逻辑的联动设计
狂暴逻辑横跨三层,但每层只做自己负责的部分:
| 层级 | 狂暴逻辑的职责 |
|---|---|
| Gameplay | 角色蓝图监听血量变化,血量低于阈值时置 bIsEnraged = true |
| 行为树 | 读取 bIsEnraged,进入狂暴分支,刷新追击目标、调整冲刺距离 |
| 状态机 | 读取 bIsEnraged,切换狂暴待机/狂暴攻击状态,播放动画 |
这样做的好处是:换一个敌人模型,不需要改AI逻辑;换一套狂暴招式,不需要改行为树结构。
5. 狂暴敌人行为树构建
5.1 黑板键设计
黑板是行为树共享数据的存储区域。狂暴敌人AI至少需要这些键:
| 黑板键 | 类型 | 用途 |
|---|---|---|
| TargetActor | Object | 当前索敌目标 |
| bHasTarget | Bool | 是否已经锁定目标 |
| DistanceToTarget | Float | 与目标的距离,由Service刷新 |
| bIsEnraged | Bool | 是否进入狂暴状态 |
| bInAttackRange | Bool | 是否进入攻击范围 |
| AttackIndex | Int | 用于配合动画状态机选择连击段落 |
创建这些键之后,选择对应类型并给默认值。TargetActor默认空,bHasTarget默认false,bIsEnraged默认false。
5.2 行为树顶部分支
根部建议用一棵主Selector,把三个大阶段并列:
Root ├── Selector (MainLogic) │ ├── Sequence 狂暴分支,Decorator: bIsEnraged == true │ │ ├── Task 狂暴追击 │ │ ├── Task 狂暴冲刺 │ │ └── Task 狂暴连击 │ ├── Sequence 战斗分支,Decorator: bHasTarget == true │ │ ├── Task 索敌确认 │ │ ├── Task 追击 │ │ └── Task 近战攻击 │ └── Sequence 巡逻分支,Decorator: bHasTarget == false │ ├── Task 随机巡逻 │ └── Service 更新感知主Selector确保每轮决策只会走其中一个分支。狂暴分支在最前面,说明狂暴状态有最高优先级。
5.3 索敌行为
索敌通常不放在行为树的Task里用循环等节点,而是在AIController中启动AIPerception组件,用感知回调更新黑板键。
AIPerception的配置要点:
- DominantSense 设为 AI Sight Sense。
- AI Sight Sense 的视线范围按战斗场地调整,比如 1500 单位。
- 自动感知范围 600 单位,用于近距离听到玩家的声音。
感知到玩家后,在蓝图或C++中调用行为树黑板接口,写入TargetActor和bHasTarget。行为树因此获得索敌结果,而不是自己原地等感知结果。
5.4 追击与冲刺逻辑
战斗分支中,首先读取DistanceToTarget。如果距离大于攻击范围,走追击,使用MoveTo节点移动。
这里通过一种更直接的“狂暴冲刺”实现思路:
- 在狂暴分支中增加冲刺Task。
- Task内部读取当前与目标的距离。
- 如果距离大于一个阈值,比如500单位,使用AIController的MoveTo接口并设置Acceptable Radius、Speed比例。
- 冲刺期间同时通知角色蓝图改变移动速度倍率,配合动画状态机里的sprint动画。
设计建议:不要用游戏内真实时间来判断“冲刺是否结束”。应该在Task执行期间持续检查距离,一旦DistanceToTarget降到攻击范围内,Task返回成功,行为树自动进入攻击序列。
5.5 近战攻击循环
近战攻击行为树分支可以使用一个Sequence:
Sequence 攻击分支 ├── Decorator: bInAttackRange == true ├── Task 播放攻击动作(通知角色蓝图选择连击段) ├── Task 攻击后摇等待 └── Task 恢复判断Task“播放攻击动作”的做法:行为树通过黑板键AttachIndex告诉角色蓝图“我要打第几段”,角色蓝图响应后播放对应动画蒙太奇,返回成功。
Task“攻击后摇等待”的常见实现:在AIController里设置一个CooldownTime,比如1.2秒。Task等待CooldownTime结束后返回成功。
这样做的好处是:行为树不会在攻击动画播完前反复触发攻击,同时攻击节奏由Gameplay数值控制,不在行为树里写死。
5.6 狂暴化分支
狂暴化分支由角色蓝图的血量监听触发。C++或蓝图中,在TakeDamage回调里判断:
// 示意代码,函数名和参数以项目实际为准 void ABerserkerEnemy::HandleDamage(float DamageAmount) { CurrentHealth -= DamageAmount; if (CurrentHealth <= MaxHealth * 0.3f && !bIsEnraged) { bIsEnraged = true; // 同步给行为树黑板 if (UBlackboardComponent* Blackboard = GetAIController()->GetBlackboardComponent()) { Blackboard->SetValueAsBool(TEXT("bIsEnraged"), true); } // 通知动画状态机进入狂暴 OnEnrageStart.Broadcast(); } }行为树读取到bIsEnraged=true之后,下一轮决策会优先进入狂暴分支。注意:监听血量阈值的位置应该在Gameplay层(角色蓝图),不要放在行为树里用PeridicDecorator轮询,因为血量变化是事件驱动的,不是周期扫描的问题。
6. 战斗状态机构建
6.1 状态列表
动画蓝图中的状态机建议按下面划分状态:
| 状态名 | 触发条件 | 关联动画 |
|---|---|---|
| Idle | 移动速度接近0 | 待机动画 |
| Locomotion | 移动速度大于0 | 跑步动画 |
| Attack | 攻击意图 + 动画通知 | 三段攻击动画 |
| Stagger | 收到被击中通知 | 硬直动画 |
| EnrageStart | bIsEnraged变为true | 狂暴启动动画 |
| EnrageLocomotion | 狂暴状态下移动 | 狂暴跑步动画 |
| EnrageAttack | 狂暴状态下攻击 | 狂暴攻击动画 |
| Death | 血量归零 | 死亡动画 |
6.2 动画状态机基础
动画蓝图中创建AnimGraph,加一个状态机,起名MainStateMachine。
State的转换条件大部分用速度变量来判定:
- 从Idle到Locomotion:当
Speed > 10。 - 从Locomotion到Idle:当
Speed < 10。 - 从Locomotion到Attack:在行为树Task里调用角色蓝图接口,蓝图接口设置
bIsAttacking = true。
这样把“动画播放”的开关和“AI决策”隔离,动画蓝图只关心动作表现,不关心为什么敌人要攻击。
6.3 攻击连击状态
攻击状态建议再套一层子状态机,或者用三段攻击序列配合动画通知实现。
推荐做法:在攻击状态内部使用三个连续动画节点,每个动画通过AnimNotify_AttackJudge通知开启伤害判定,通过AnimNotify_ResetCombo通知切换下一段。
连击段的选择逻辑放在角色蓝图中:
// 示意代码:根据行为树传入的AttackIndex决定播放哪段 void ABerserkerEnemy::PlayAttackAction() { int32 Index = AttackIndex % 3; switch (Index) { case 0: PlayAnimMontage(AttackMontageA); break; case 1: PlayAnimMontage(AttackMontageB); break; case 2: PlayAnimMontage(AttackMontageC); break; } }行为树通过黑板键设置AttackIndex,角色蓝图响应并按段播放。这套结构以后加第四段攻击,只需要在角色蓝图中加一个case,不动行为树。
6.4 状态切换触发方式
状态切换有三种常见方式:
- 变量驱动:速度、bIsEnraged、bIsAttacking等布尔和浮点变量,在动画蓝图更新中判断。
- 动画通知驱动:攻击动画的某个通知帧调用AnimInstance中的接口,强制切换状态。
- 事件驱动:角色蓝图中的HitReact事件触发Stagger状态,不依赖行为树。
Stagger状态的进入,我建议放在Gameplay层。角色受伤时,无论行为树当前在哪个节点,都应该被硬直打断。如果在行为树里做Stagger,会导致敌人被打断后行为树还在等待MoveTo完成,行动变得卡顿。
7. 把AI装备到敌人角色上
7.1 AIController运行行为树
在敌人AIController类中,重写Possess事件,加载并运行行为树:
void ABerserkerAIController::OnPossess(APawn* InPawn) { Super::OnPossess(InPawn); if (UBehaviorTree* BTAsset = LoadObject<UBehaviorTree>(nullptr, TEXT("/Game/AI/BT_BerserkerEnemy.BT_BerserkerEnemy"))) { RunBehaviorTree(BTAsset); } }注意路径/Game/AI/BT_BerserkerEnemy.BT_BerserkerEnemy需要替换成项目实际资源路径。
如果希望行为树在游戏过程中重启,也可以提供公开接口,玩家重生或敌人重新激活时重新运行。
7.2 角色蓝图暴露关键变量
敌人角色蓝图需要暴露给动画蓝图和行为树使用的核心变量:
CurrentHealth当前血量MaxHealth最大血量bIsEnraged是否狂暴bIsAttacking是否正在攻击AttackIndex当前连击索引bIsDead是否死亡
这些变量在动画蓝图和角色蓝图间通过类引用或接口传递。行为树黑板键和角色变量要保持命名一致,避免调试时出现“变量改名漏改引用”的问题。
7.3 测试运行流程
在关卡中放置敌人角色和玩家角色,运行Play。
验证步骤:
- 玩家站在敌人视野范围内,确认敌人从Idle进入追击或索敌。
- 敌人移动到玩家身边,确认进入攻击状态。
- 玩家持续按攻击键,将敌人血量打到30%以下,确认敌人播放狂暴启动动画。
- 狂暴后确认敌人移动速度和攻击频率明显提升。
- 击杀敌人,确认进入Death状态。
如果敌人站在原地不动,先检查AIController是否被正确附加,再检查行为树是否在运行,最后检查黑板键是否写入。
8. 调试与性能观察
8.1 行为树调试器
UE5编辑器左上角菜单打开Window -> Behavior Tree Debugger。
调试器可以做到:
- 查看行为树当前执行到哪个节点。
- 查看节点上一次返回成功还是失败。
- 查看Service上一次刷新的黑板键值。
- 右键节点添加断点,运行到断点时暂停。
这是排查“AI为什么不追人”最直接的手段。如果Debugger显示节点已经走到追击Task,说明行为树逻辑没问题,问题在移动组件或NavMesh;如果断点卡在Decorator上,说明黑板条件不满足。
8.2 AI可视化日志
在AIController的蓝图中调用PrintToScreen输出日志,或者使用Gameplay Debugger。
Gameplay Debugger的开启方式:运行游戏后用键盘反引号键打开控制台,输入EnableGDT。
Gameplay Debugger里可以看到AI感知的视觉范围、行为树当前节点、NavMesh路径的走向。配合LogTemp打印血量、距离等关键值,能快速定位是感知问题、决策问题还是移动问题。
8.3 性能观察方式
战斗AI的性能消耗点主要是三个:
- 行为树扫描频率:在行为树节点上右键,有一个
Interval参数控制运行周期。建议设置为0.1到0.2秒,不需要每帧扫描。 - AI感知更新:AIPerception组件也有自定义更新频率。如果敌人数量多,感知半径越大开销越高。
- 状态机状态切换:动画蓝图每帧都要做状态判断。如果状态数量很多且每个节点都有复杂混合逻辑,动画蓝图的CPU开销会上升。
在项目中使用stat GameplayTags或stat AI查看AI相关统计。如果场景里有20个敌人,优先检查行为树周期和感知半径,这两项通常是最容易过度的配置。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 敌人完全不动,不索敌 | AIController未附加或行为树未运行 | 检查敌人类中AIController Class设置;运行时用Behavior Tree Debugger查看 | 重新指定AIController类,确保Possess后运行行为树 |
| 敌人不移动但行为树已走到追击 | NavMesh未生成或路径不可达 | 按P键查看NavMesh覆盖范围 | 调整NavMeshBoundsVolume或重建网格 |
| 攻击动画不播放 | 行为树期待攻击,但状态机没有收到变量 | 检查角色蓝图bIsAttacking是否被正确置位 | 在角色蓝图接口中设置bIsAttacking,并确认动画蓝图引用到值 |
| 狂暴后攻击频率没变化 | 行为树没有走狂暴分支,或动漫状态机没切狂暴状态 | 查看黑板bIsEnraged是否为true,Debugger确认执行路径 | 确认TakeDamage事件中正确写入bIsEnraged,状态机条件使用相同变量 |
| 敌人攻击但玩家没掉血 | 攻击判定没在正确的动画通知帧开启 | 检查AnimNotify_AttackJudge是否绑定到攻击动画的命中帧 | 将通知帧移到攻击动画实际打出的时间点 |
| 敌人被打断后卡在原地 | Stagger状态切换和AI决策冲突 | 查看状态机是否还在等待某个状态退出 | 将硬直处理移出行为树,直接由角色蓝图事件驱动 |
| 多个敌人AI消耗过高 | 行为树扫描频率过高或感知范围过大 | 用stat AI查看单帧耗时 | 调大Interval,缩小感知半径,按距离分帧更新 |
| 行为树Debugger看不到节点 | 行为树资源未运行 | 检查RunBehaviorTree是否被调用 | 在AIController的BeginPlay或OnPossess中运行行为树 |
10. 最佳实践与使用建议
10.1 逻辑与动画解耦
永远不要在行为树的Task里直接写PlayMontage就完事。正确做法是Task里调用角色蓝图接口,角色蓝图决定播放哪段动画,动画蓝图决定怎么混合,回调通知决定什么时候结束。
这套解耦带来的好处是:做动画替换时不会碰到AI逻辑,做AI逻辑调整时不会逼着美术重刷动画。
10.2 AI资产分包管理
建议在内容浏览器中建立固定目录:
Game/ ├── AI/ │ ├── Controllers/ │ ├── BehaviorTrees/ │ ├── Blackboards/ │ ├── Tasks/ │ ├── Services/ │ └── Decorators/ ├── Characters/ │ ├── Berserker/ │ └── Animations/目录清晰比“先跑通再说”更重要。行为树、黑板、Task各自独立文件,后续复用和调试都能直接定位。
10.3 工程化提示
- 第一次搭建先用小地图、单敌人、单状态跑通,不要一次做十个阶段。
- 关键变量统一命名,特别是黑板键和角色蓝图变量。
- 行为树每个重要分支挂一个Service去刷新黑板数据,避免黑板上一次的数据残留。
- 测试环境和正式环境分开,NavMesh、AI感知、行为树参数不要只有一套写死配置。
- 多人模式下,AI决策全部走服务器端,客户端只表现动画结果,避免每个客户端各自跑行为树导致不同步。
11. 总结与下一步
这个项目最值得掌握的不是某个节点的用法,而是“决策层和执行层分开”的设计思路。行为树负责什么时候追、什么时候打、什么时候狂暴;状态机负责怎么移动、怎么出招、怎么反馈受伤;狂暴逻辑作为一条贯通两者的数据流,从血量变化触发,经由黑板键驱动行为树分支,再通过状态机表现为攻击节奏的变化。
最先应该验证的功能是:敌人索敌成功后能不能稳定追击,近身之后会不会触发攻击。这两个功能跑通之后,再去做狂暴化和连击段。最容易踩的坑是行为树和动画状态机在任务边界上没有明确分工,导致攻击动作重复触发、硬直表现迟钝。
后续可以继续扩展的方向:给狂暴状态增加远程技能阶段、用EnvironmentQuery实现绕后和包抄、把连击段和伤害判定做成数据驱动的资产配置。这套“行为树决策+状态机执行+黑板键联动”的框架,在后面做Boss战、换不同敌人类型时都可以复用。