资讯动态

UE5狂暴敌人AI完整实战:状态机+行为树

发布时间:2026/9/30 9:56:01 来源:尧图企业网站定制
最近在做 UE5 战斗 AI 时“狂暴敌人”这个需求让我折腾了好一阵子。表面上看只是几个状态来回切换但真正把行为树搭起来之后才发现状态切不过去、分支中断、黑板数据不同步的问题一个接一个。后来我把“战斗状态机”和“行为树”两者的职责边界重新梳理了一遍整个战斗 AI 才稳定下来。这篇文章就是一次完整复盘从底层概念到实战构建都会讲到包含可直接参考的 C 类示例和蓝图行为树搭建思路适合正在做 UE5 战斗系统的开发者也适合刚接触行为树的同学。1. 背景与核心概念1.1 什么是“狂暴敌人”AI“狂暴敌人”是战斗类游戏中很常见的敌人设计通常表现为当生命值低于某个阈值或者受到一定次数攻击后敌人会进入一种更激进的状态移动速度上升、攻击频率变快、伤害变高甚至解锁新的技能。从 AI 实现的角度看它不是一个单纯的行为树分支而是整个 AI 行为框架需要支持“运行时状态变化”。也就是说AI 不能只靠一两个条件判断写死它需要一套机制来感知自身状态、环境状态并在不同状态下切换不同的行为策略。用 UE5 来实现一般有三种思路纯蓝图状态机用枚举变量 分支判断实现。纯行为树把状态用黑板键表示行为树里大量使用装饰器做条件分流。状态机 行为树状态机负责状态切换行为树负责当前状态下的行为执行。第三种是项目实战中性价比最高的方案也是本文重点。1.2 行为树是什么行为树是一种树状结构的决策系统阅读和执行顺序很直观。树的根节点一般是Root往下挂Selector、Sequence这样的组合节点最终落到Task节点上。行为树的执行逻辑可以简单理解为Selector从上往下找第一个能执行的分支。Sequence从上往下顺序执行所有子节点任意一个失败则整条序列失败。Task真正执行动作例如移动、攻击、等待。Decorator装饰器附加在节点上控制该分支是否允许执行。Service服务在节点执行期间不断更新外部数据例如刷新玩家位置。行为树在 UE5 中的最大优点是“可视化、可配置”美术和策划也能看懂一部分逻辑方便调参。1.3 状态机是什么状态机全称有限状态机FSM核心思想是“在任一时刻对象只能处于有限个状态中的一个并且只能从当前状态切换到允许的相邻状态”。战斗 AI 中常见的状态有待机 Idle巡逻 Patrol追踪 Chase攻击 Attack狂暴 Berserk状态机擅长处理“状态切换条件”但一旦状态多起来嵌套的 if-else 会非常难维护。所以需要行为树来承接“每个状态下具体怎么做”的执行逻辑。1.4 为什么要状态机 行为树结合状态机解决“何时切换状态”的问题行为树解决“当前状态下做什么”的问题。如果只用状态机行为逻辑全写在状态内部状态一多代码很容易膨胀而且每个状态内部的分支条件会让调试变得困难。如果只用行为树虽然也可以用黑板键模拟状态但状态切换的边界不够清晰尤其是遇到“狂暴不可逆”“受伤后强制追击”这类强约束条件时行为树分支会变得臃肿。把两者结合后职责非常明确EnemyCharacter 维护当前状态并处理状态切换逻辑。AI Controller 负责运行行为树。行为树根据黑板上的状态值决定执行哪条分支。狂暴状态通过修改黑板键和数值属性让行为树和动画系统同步感知。2. 环境准备与版本说明2.1 UE5 版本与模块准备本文示例基于 UE5.1 之后的通用特性编写UE5.0 到 UE5.4 的大部分 API 和编辑器操作基本一致如果你的项目版本不同注意路径和节点名称可能出现细微差异。需要确保项目启用了以下模块AIModule行为树、黑板、AI Controller 的基础模块。NavigationSystemAI 移动寻路必须的导航系统。GameplayTasks行为树任务节点的底层依赖通常在 Build.cs 中一并加上。打开项目的 Build.cs 文件例如// Source/BerserkDemo/BerserkDemo.Build.cs public class BerserkDemo : ModuleRules { public BerserkDemo(ReadOnlyTargetRules Target) : base(Target) { PCHUsage PCHUsageMode.UseExplicitOrSharedPCHs; PublicDependencyModuleNames.AddRange(new string[] { Core, CoreUObject, Engine, InputCore, AIModule, GameplayTasks, NavigationSystem }); } }如果是蓝图项目不需要手动改 Build.cs但依然要在项目设置中保证默认的 AI 模块和导航网格相关功能可用。2.2 编辑器准备在 UE5 编辑器中至少需要准备一个第三人称模板或空白项目方便放置玩家角色和敌人。Content 目录下建好 AI 相关文件夹。场景中放置 NavMeshBounds 或 RecastNavMesh并按P键显示导航网格范围。建议目录结构如下Content/ ├── AI/ │ ├── Blackboards/ │ ├── BehaviorTrees/ │ ├── Controllers/ │ └── Tasks/ ├── Characters/ │ ├── BerserkEnemy/ ├── Level/ └── ...3. 行为树核心机制拆解3.1 Blackboard 黑板AI 的公共记忆黑板是行为树与外界通信的数据池。行为树节点可以从黑板读取数据也可以向黑板写入数据。强烈建议在这个阶段把黑板键类型定义清楚不要混用黑板键类型作用TargetActorObject保存玩家/目标的 Actor 引用CurrentStateEnum当前 AI 状态对应 EEnemyStatebIsBerserkBool是否处于狂暴状态DistanceToTargetFloat与目标距离供装饰器条件判断在 C 中定义枚举时建议使用UENUM(BlueprintType)这样蓝图和黑板编辑器都能直接使用该枚举类型。3.2 行为树节点类型行为树中的节点通常分为四类组合节点 Composite任务节点 Task装饰器 Decorator服务节点 Service在 UE5 编辑器中右键行为树空白处即可创建节点。一个典型的狂暴敌人行为树结构如下Root └── Selector ├── [Decorator: bIsBerserk True] Sequence │ ├── Service: UpdateTarget │ ├── Task: MoveTo TargetActor │ └── Task: BerserkAttack ├── [Decorator: DistanceToTarget AttackRange] Sequence │ ├── Task: MoveTo TargetActor │ └── Task: NormalAttack ├── [Decorator: TargetActor ! None] Task │ └── MoveTo TargetActor └── Task └── Wait / Patrol3.3 Decorator 与状态切换的关键Observer Aborts行为树里最常见的坑是状态已经变了但行为树没有切换分支。原因通常是 Decorator 没有设置好监听机制。UE5 的 Blackboard Decorator 需要在细节面板中配置Observer Aborts可选值包括None不主动中止只能等当前分支自然结束。Self只中止自身节点。Lower Priority中止优先级更低的分支。Both同时中止自身和低优先级分支。在狂暴敌人 AI 中一般建议设置为Both这样当bIsBerserk从 false 变为 true 时行为树能立刻中止当前分支重新进入狂暴分支。4. 狂暴敌人整体战斗状态机设计4.1 状态定义先用枚举定义敌人状态UENUM(BlueprintType) enum class EEnemyState : uint8 { Idle UMETA(DisplayName Idle), Chase UMETA(DisplayName Chase), Attack UMETA(DisplayName Attack), Berserk UMETA(DisplayName Berserk), Dead UMETA(DisplayName Dead) };不建议把 Patrol 放到状态枚举里巡逻可以视为 Idle 状态下的一种行为这样可以减少状态切换复杂度。4.2 状态切换条件狂暴敌人 AI 的状态切换条件可以整理成下表状态触发条件行为表现退出条件IdleAI 出生后默认状态待机、缓慢巡逻感知到玩家或受伤Chase发现玩家或丢失攻击距离向目标移动玩家死亡或拉到攻击距离Attack距离小于攻击范围普通攻击循环玩家拉远距离Berserk生命值比例低于阈值加速、高频率攻击、释放狂暴技能不可逆直到死亡狂暴状态一般是不可逆状态也就是一旦生命值低于阈值不会再恢复到普通状态。这个设计符合大多数战斗中“狂怒”的行为语义。4.3 狂暴状态的参数变化普通状态下移动速度420攻击间隔2.0 秒单次攻击伤害20狂暴状态下移动速度600攻击间隔1.0 秒单次攻击伤害30身体缩放1.15 倍左右并触发特效材质变化这里的数值只是示例要根据你的游戏平衡去调整。但注意狂暴参数最好不要写死在行为树节点中而是通过配置数据表或者暴露给蓝图调整。5. 完整实战构建一个狂暴敌人 AI5.1 创建项目结构我以一个名为BerserkDemo的项目为例实际操作时替换成你的项目模块名。项目结构如下BerserkDemo/ ├── Source/ │ └── BerserkDemo/ │ ├── BerserkDemo.Build.cs │ ├── BerserkDemoGameMode.h │ ├── BerserkEnemy.h / .cpp │ ├── BerserkAIController.h / .cpp │ └── BTTask_BerserkAttack.h / .cpp └── Content/ ├── AI/ │ ├── Blackboards/ │ └── BehaviorTrees/ └── Characters/5.2 创建敌人角色类在 C 中敌人角色类继承自ACharacter需要包含状态枚举、血量、狂暴阈值、伤害回调等。// Source/BerserkDemo/BerserkEnemy.h #pragma once #include CoreMinimal.h #include GameFramework/Character.h #include BerserkEnemy.generated.h UENUM(BlueprintType) enum class EEnemyState : uint8 { Idle UMETA(DisplayName Idle), Chase UMETA(DisplayName Chase), Attack UMETA(DisplayName Attack), Berserk UMETA(DisplayName Berserk), Dead UMETA(DisplayName Dead) }; UCLASS() class BERSERKDEMO_API ABerserkEnemy : public ACharacter { GENERATED_BODY() public: ABerserkEnemy(); virtual float TakeDamage( float DamageAmount, struct FDamageEvent const DamageEvent, AController* EventInstigator, AActor* DamageCauser) override; UPROPERTY(BlueprintReadOnly, Category AI|State) EEnemyState CurrentState; UPROPERTY(BlueprintReadOnly, Category AI|State) bool bIsBerserk; void PerformBerserkAttack(); protected: virtual void BeginPlay() override; UPROPERTY(EditDefaultsOnly, Category AI|Config) float MaxHealth; UPROPERTY(VisibleInstanceOnly, Category AI|Config) float CurrentHealth; UPROPERTY(EditDefaultsOnly, Category AI|Config) float BerserkHealthPercent; void CheckAndEnterBerserk(); };对应的 CPP 文件// Source/BerserkDemo/BerserkEnemy.cpp #include BerserkEnemy.h #include AIController.h #include BehaviorTree/BlackboardComponent.h #include GameFramework/CharacterMovementComponent.h ABerserkEnemy::ABerserkEnemy() { MaxHealth 100.f; CurrentHealth MaxHealth; BerserkHealthPercent 0.3f; bIsBerserk false; CurrentState EEnemyState::Idle; } void ABerserkEnemy::BeginPlay() { Super::BeginPlay(); CurrentHealth MaxHealth; } float ABerserkEnemy::TakeDamage(float DamageAmount, struct FDamageEvent const DamageEvent, AController* EventInstigator, AActor* DamageCauser) { float ActualDamage Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser); CurrentHealth FMath::Max(0.f, CurrentHealth - ActualDamage); CheckAndEnterBerserk(); return ActualDamage; } void ABerserkEnemy::CheckAndEnterBerserk() { if (bIsBerserk || CurrentHealth 0.f) { return; } if (CurrentHealth / MaxHealth BerserkHealthPercent) { bIsBerserk true; CurrentState EEnemyState::Berserk; UCharacterMovementComponent* MoveComp GetCharacterMovement(); if (MoveComp) { MoveComp-MaxWalkSpeed 600.f; } AController* Controller GetController(); AAIController* AIController CastAAIController(Controller); if (AIController) { UBlackboardComponent* Blackboard AIController-GetBlackboardComponent(); if (Blackboard) { Blackboard-SetValueAsEnum(CurrentState, (uint8)CurrentState); Blackboard-SetValueAsBool(bIsBerserk, true); } } } } void ABerserkEnemy::PerformBerserkAttack() { // 在这里接入你的攻击系统例如播放动画、发射碰撞体、结算伤害。 // 项目中如果用 Gameplay Ability System建议通过 GAS 触发。 UE_LOG(LogTemp, Log, TEXT([BerserkEnemy] Perform Berserk Attack!)); }说明CheckAndEnterBerserk中的核心思路是“敌人自己判断狂暴条件并同步更新黑板上键值”。这样行为树不用轮询血量只要监听bIsBerserk变化即可。5.3 创建 AI Controller 类AI Controller 负责控制敌人 Pawn并在 Possess 时运行行为树。// Source/BerserkDemo/BerserkAIController.h #pragma once #include CoreMinimal.h #include AIController.h #include BerserkAIController.generated.h class UBehaviorTree; UCLASS() class BERSERKDEMO_API ABerserkAIController : public AAIController { GENERATED_BODY() public: ABerserkAIController(); protected: virtual void OnPossess(APawn* InPawn) override; UPROPERTY(EditDefaultsOnly, Category AI) TObjectPtrUBehaviorTree BehaviorTreeAsset; };// Source/BerserkDemo/BerserkAIController.cpp #include BerserkAIController.h #include BehaviorTree/BehaviorTree.h void ABerserkAIController::OnPossess(APawn* InPawn) { Super::OnPossess(InPawn); if (BehaviorTreeAsset) { RunBehaviorTree(BehaviorTreeAsset); } }5.4 配置黑板键在 Content 中创建 Blackboard 资产命名为BB_BerserkEnemy然后添加以下键TargetActorObject 类型CurrentStateEnum 类型枚举选择EEnemyStatebIsBerserkBool 类型如果后续行为树需要根据距离切换攻击分支建议再加一个 Float 键DistanceToTarget由 Service 节点更新。5.5 构建行为树创建 Behavior Tree 资产命名为BT_BerserkEnemy并关联BB_BerserkEnemy。行为树根节点下方挂一个Selector根架构如下Root └── Selector ├── Berserk Branch ├── Attack Branch ├── Chase Branch └── Idle Branch每个分支结构为Berserk BranchDecoratorBlackboard 判断bIsBerserk trueSequenceService更新目标与距离TaskMoveTo TargetActorTask自定义BTTask_BerserkAttackAttack BranchDecoratorBlackboard 判断DistanceToTarget AttackRangeSequenceTaskMoveTo TargetActorTask普通攻击Chase BranchDecoratorBlackboard 判断TargetActor ! NoneTaskMoveTo TargetActorIdle BranchTaskWait也可以挂巡逻任务关键一步Berserk Branch 的 Decorator 必须在细节面板中设置Observer Aborts Both否则bIsBerserk变化时行为树可能不会立刻切换。5.6 自定义攻击任务节点创建一个UBTTaskNode子类命名为BTTask_BerserkAttack。// Source/BerserkDemo/BTTask_BerserkAttack.h #pragma once #include CoreMinimal.h #include BehaviorTree/BTTaskNode.h #include BTTask_BerserkAttack.generated.h UCLASS() class BERSERKDEMO_API UBTTask_BerserkAttack : public UBTTaskNode { GENERATED_BODY() public: UBTTask_BerserkAttack(); protected: virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent OwnerComp, uint8* NodeMemory) override; };// Source/BerserkDemo/BTTask_BerserkAttack.cpp #include BTTask_BerserkAttack.h #include AIController.h #include BerserkEnemy.h UBTTask_BerserkAttack::UBTTask_BerserkAttack() { NodeName TEXT(Berserk Attack); } EBTNodeResult::Type UBTTask_BerserkAttack::ExecuteTask(UBehaviorTreeComponent OwnerComp, uint8* NodeMemory) { AAIController* AIController OwnerComp.GetAIOwner(); if (!AIController) { return EBTNodeResult::Failed; } ABerserkEnemy* Enemy CastABerserkEnemy(AIController-GetPawn()); if (!Enemy) { return EBTNodeResult::Failed; } Enemy-PerformBerserkAttack(); return EBTNodeResult::Succeeded; }5.7 运行验证在编辑器中进行以下操作将敌人蓝图或 C 类的AIControllerClass设置为BerserkAIController。将BerserkAIController的BehaviorTreeAsset设置为BT_BerserkEnemy。在场景中放置NavMeshBoundsVolume并按P键查看导航网格是否覆盖地面。运行游戏控制玩家角色靠近敌人。用武器或控制台命令对敌人造成伤害观察敌人状态是否切换。验证要点正常状态下敌人会追向玩家并攻击。当生命值降到 30% 以下时移动速度明显提高攻击动画频率变快。行为树调试窗口中应从普通分支跳到 Berserk 分支。6. 常见问题与排查思路问题现象常见原因解决思路敌人站在原地不动场景没有导航网格或 NavMesh 没有覆盖到敌人位置检查P键下的绿色导航范围调整 NavMeshBoundsVolume行为树完全不执行AI Controller 未设置或 OnPossess 中没有调用 RunBehaviorTree确认 Pawn 的 AIControllerClass 已配置并检查 BehaviorTreeAsset 是否有效黑板键报错设置值与黑板键类型不匹配检查SetValueAsEnum、SetValueAsBool与黑板中键的类型是否一致状态切到狂暴后行为树没有反应Decorator 没有监听黑板键变化将对应 Decorator 的 Observer Aborts 设置为 Both敌人追踪时走走停停目标类型不对或 MoveTo 每次都在拿旧位置使用 Service 高频更新 TargetActor 黑板键建议频率设置为 0.2 - 0.5 秒狂暴后速度没有生效CharacterMovement 的 MaxWalkSpeed 被动画或 Ability 覆盖检查是否有其他系统在覆盖移动速度并用日志打印实际速度Task 返回 Failed 后整条分支卡死Sequence 中某个 Task 快速失败又没有 Fallback 分支给高风险 Task 加 Decorator 条件或对失败分类区分可重试和不可重试的失败7. 最佳实践与工程建议7.1 职责边界不要混状态机负责“状态切换”行为树负责“状态内执行”。不要在行为树里写太多状态切换逻辑也不要让 Character 类去直接驱动行为树节点。正确的数据流是受伤事件 - Character 判断状态 - 更新黑板 - 行为树感知变化 - 切换分支7.2 使用 Service 更新高频数据玩家位置、目标距离这类数据不适合在 Task 中一次性获取因为 AI 需要持续更新。建议使用 Service 节点在行为树分支执行期间不断刷新黑板键。比如// 自定义 BTS_UpdateTarget 的 TickNode 示意 void UBTS_UpdateTarget::TickNode(UBehaviorTreeComponent OwnerComp, uint8* NodeMemory, float DeltaSeconds) { Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds); AAIController* AIController OwnerComp.GetAIOwner(); if (!AIController) { return; } APawn* TargetPawn nullptr; // 通过 AI 感知组件或简单距离探测获取玩家 // 将目标写入 TargetActorKey UBlackboardComponent* Blackboard OwnerComp.GetBlackboardComponent(); if (Blackboard) { Blackboard-SetValueAsObject(TargetKey.SelectedKeyName, TargetPawn); if (TargetPawn) { const float Distance FVector::Distance(GetOwner()-GetActorLocation(), TargetPawn-GetActorLocation()); Blackboard-SetValueAsFloat(DistanceToTargetKey.SelectedKeyName, Distance); } } }7.3 狂暴参数集中配置不要把一个敌人的血量、速度、狂暴阈值散落到行为树节点中。建议使用DataTable、PrimaryDataAsset或UDeveloperSettings管理方便批量调整和做变种敌人。7.4 降低轮询开销大量 AI 如果用 Service 每帧计算距离性能会明显下降。实际项目中可以通过 AI 感知组件AIPerception来触发视野感知和伤害感知状态切换由事件驱动而不是每帧轮询。7.5 调试建议推荐使用 UE5 行为树调试功能运行游戏后选中 AI Controller可以查看当前行为树停留在哪个节点。常用手段在 Task 和 Service 中打印关键状态日志。在 Character 的TakeDamage中断言黑板上键值是否正确。用可视化辅助把当前CurrentState绘制在屏幕上。8. 总结与下一步学习路线这一段内容并没有太多需要往深扩展的理论重点是你在动手前先把状态机与行为树的边界想清楚。本文核心要点可以浓缩成三点状态机负责定义“狂暴不可逆”这类强状态规则。行为树负责当前状态下的攻击、追踪、待机行为。黑板是状态机和行为树之间的通信桥梁Decorator 的 Observer Aborts 是状态切换能否立即生效的关键。如果再往下学建议按这个顺序进阶AI 感知使用AIPerception组件替代手动轮询玩家位置。EQS 环境查询解决“狂暴敌人优先选择哪个目标”或“跳到哪个位置”等更复杂的决策问题。GAS 技能系统把攻击、伤害、减速等效果交给 Gameplay Ability 管理。MassAI如果你的游戏有大量 AI 单位可以考虑 UE5 的 Mass 框架优化性能。建议先做一个最简单的“巡逻敌人”再逐步加入状态机和狂暴分支。等这套结构跑通之后你会发现后续加入飞行敌人、远程敌人、Boss 技能都只是在这个骨架上不断填充分支节点而已。如果这篇文章对你有帮助可以收藏备用等真正开始调 AI 时再对照排查。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑