FPS 游戏开发里,敌人 AI 的“状态管理”一直是个说不完的话题。之前在项目里做 AI 行为切换时,最头疼的就是状态一多,蓝图的连线就乱成一团。交战中要兼顾巡逻、警戒、追击、攻击、搜敌,如果再叠加换弹、受伤、呼叫队友这类局部行为,传统状态机很快就变得没法维护。后来把 UE5 的 State Tree(状态树)引入纯蓝图流程,再用“嵌套状态树”把宏观状态和微观行为拆开管理,整个 AI 状态转换一下子清晰了许多。这篇文章就把这套通过多状态树嵌套实现状态转换的方法整理出来,从概念到完整案例都会讲到,不管是刚开始学 UE5 蓝图的新手,还是已经有 FPS 项目经验的开发者,都可以按这篇流程走一遍。
1. 多状态树在 FPS 游戏开发中的定位
1.1 传统状态机为什么撑不住复杂 FPS 玩法
很多 FPS 项目最早都是用蓝图里的枚举 + Switch 来管理 AI 状态:一个EAIState枚举,里面填上Patrol、Suspect、Combat、Search,然后在 AI 控制器的 Tick 里不停检查各种条件,再调用SetAIState切换状态。
这种方案在状态只有三四个时非常好用,逻辑直白,排错也方便。但敌人 AI 一旦进入战斗,就会出现下面这些情况:
- 状态枚举越来越多,
Patrol之外还要有Investigate、Evade、Retreat、Cover。 - 每个状态迁移条件都要在 Tick 里重复判断,性能浪费明显。
- 状态之间互相干扰,例如追击中敌人丢失目标,要切回搜索,但搜索又需要回到上次丢失点的坐标,这一串数据流转写起来很啰嗦。
- 想加入“受伤后先蹲下 2 秒再还击”这种过渡行为,就会发现原本的状态机没有地方放“过渡状态”,只能强行加枚举,状态数量继续膨胀。
问题根源并不是枚举状态机本身,而是“平面式状态机”把所有行为放在同一个层级里。宏观的阶段切换和微观的帧级行为全部挤在一起,代码阅读和维护成本必然上升。
1.2 State Tree 与行为树、状态机的区别
UE5 的 State Tree(状态树)是引擎自带的状态管理框架,从 UE 5.1 左右开始逐步成熟。它和传统行为树、状态机最大的区别在于:
- 传统状态机以“状态间的连线”驱动,什么时候进入什么状态写死在逻辑里。
- 状态树以“当前状态 + 节点内任务 + 转换条件”驱动,状态节点可以独立配置进入行为、执行行为、退出行为。
- 状态树支持参数、黑板、事件和子树上嵌套,宏观状态用父树,具体行为用子树。
一句话概括:状态树像“分层的状态机”,父层决定现在处在哪个大阶段,子层决定这个阶段内部具体执行哪些小行为。
这里说一个容易混淆的对比。行为树和状态树表面上很相似,都有节点、都有装饰器,但运行模型完全不同:
| 对比项 | 行为树 Behavior Tree | 状态树 State Tree |
|---|---|---|
| 核心驱动 | 从根节点不断执行 Tick,按优先级挑选子节点 | 当前状态的选中节点持续运行,直到发生转换 |
| 记忆能力 | 本质上没有状态记忆,BT 执行流每次 Tick 重新选择 | 有明确的状态持有概念,叶子任务持续运行 |
| 事件支持 | 通常配合黑板通知 | 原生支持状态树事件,适合状态切换信号 |
| 嵌套方式 | 可以嵌套行为树子树 | 可以直接嵌套子状态树资产 |
如果需求只是“搜索敌人并追击”,行为树完全够用。但如果需求是“多个阶段循环切换,每个阶段内部又有更细的状态”,状态树的优势会更明显。
1.3 为什么用嵌套状态树而不是单棵树
单棵状态树也可以管理全部状态,无非把巡逻、追击、攻击全部平铺成兄弟节点。但 FPS 实战里有两个痛点很难解决:
第一,平铺状态会导致转换条件交叉。例如“追击”状态既受“是否看到玩家”控制,又受“自身血量”控制,还受“子弹是否打空”控制。这些条件全部挂在同一层树上,状态数量稍微增长,编辑器里就会变成一张大网。
第二,局部行为的复用性差。如果多个大状态都需要“移动”,把移动逻辑复制到每个状态里很蠢。嵌套状态树允许把“巡逻”抽成一个独立子树资产,父树里的警戒、战斗、搜索都可以引用它;也可以把子树继续向下拆分。
所以,嵌套状态树的核心价值是用“树的层级”代替“状态的平面铺开”。父树关心“我现在处于什么宏观阶段”,子树关心“这个阶段里要执行什么操作”,状态之间的转换不再是一堆在线之间连接的 Switch 逻辑,而是通过事件、条件和任务完成的有级联关系的转换。
1.4 本文适合什么读者、能学到什么
本文的内容以 UE5 纯蓝图为主,不需要写 C++。读者的能力线大概是这样:
- 会创建 UE5 关卡、会添加 Actor、会使用最常见蓝图节点。
- 看过蓝图枚举和分支,知道什么是 AI Controller、Pawn。
- 想给自己的 FPS 敌人 AI 做一个结构更清晰的状态管理系统。
学完这篇,你将掌握:
- State Tree 基础元素:State、Task、Condition、Transition、Evaluator。
- 如何在纯蓝图流程中创建和配置状态树资产。
- 如何把父状态树和子状态树嵌套起来,实现大状态与小行为的切换。
- 如何在 AI 控制器蓝图中启动状态树。
- 如何使用事件触发状态转换,以及常见排错思路。
2. 环境准备与版本说明
2.1 引擎版本与插件开关
State Tree 在 UE5 中的集成程度因引擎小版本而异。本文的演示思路以 UE 5.x 系列通用功能为基准,具体菜单名称、蓝图节点名可能存在细微差异,建议按你自己安装的引擎版本调整。
简单来说,环境准备分以下几步:
- 安装 Unreal Engine 5.x。
- 创建项目时选择“Blank”或“第一人称游戏”模板都可以,本文用 Blank 加手动 AI Pawn 的方式演示。
- 如果你的项目里找不到 State Tree 相关功能,先打开“编辑 -> 插件”,搜索 StateTree,确认插件处于启用状态。
- 在 Build.cs 或模块设置里,普通蓝图项目不需要额外配置依赖,只要引擎模块可用即可。
注意,State Tree 在不同版本中可能被标记为实验性或正式功能。如果插件列表里显示为“Experimental”,仍然可以用于学习项目,但正式上线前需要仔细评估稳定性。
2.2 项目创建与基础资产
这里为了完整跑通流程,建议先创建一个空地图,准备好以下基础资产:
| 资产类型 | 建议内容 | 用途 |
|---|---|---|
| AI 角色 | 一个 Character 蓝图 | 作为敌人 Pawn |
| AI 控制器 | 一个 AIController 蓝图 | 启动和管理状态树 |
| 玩家角色 | 第一人称角色或简单胶囊体 | 作为敌人感知的目标 |
| 导航网格 | 在场景里放置 NavMeshBoundsVolume | 保证敌人可以寻路 |
| 数据资产 | StateTree 资产 | 承载父树与子树 |
如果不想从第一人称模板开始,可以手动创建 Character 子类并在场景中摆放,AI 控制器的 Auto Possess AI 方式选择“Placed in World 或 Spawned”,确保敌人生成后立刻被控制器接管。
2.3 本文演示的 FPS 敌人 AI 需求
在动手前,先把目标行为讲清楚。我们要实现的 FPS 敌人 AI 分为四个大阶段:
- 巡逻(Patrol):沿路径点巡逻,无战斗目标。
- 警戒(Alert):感知到可疑声音或短暂看到玩家,但还没完全确认。
- 战斗(Combat):确认玩家目标,执行追击、开火、换弹等行为。
- 搜索(Search):战斗中丢失玩家目标,前往最后已知位置搜索,找不到则回到巡逻。
这四个大阶段属于“父状态”。而“巡逻”内部包含“走向路径点”“到达后等待”;“战斗”内部包含“追击”“射击”“换弹”等小状态。这些小状态适合放到嵌套子树里。
嵌套的目标就是:父状态树只负责宏观阶段切换,子树负责具体小行为,事件负责把子树里的结果传回父树。
3. State Tree 核心概念与蓝图化思路
3.1 状态树中的基础元素
在进入编辑器操作之前,先理解几个关键概念。这些词在 Status Tree Editor 里会反复出现:
State(状态)
状态树中的基本节点,一个 State 代表一个持续的逻辑块。State 可以并行运行,也可以只运行激活的子节点。FPS 敌人 AI 中,“Combat”是一个父 State,它的子树里有“Aim”这样的子 State。
Task(任务)
State 内部具体执行的逻辑。例如“移动到目标点”是一个 MoveTo Task,“播放动画”是一个 Play Anim Task。任务可以继承蓝图基类后用蓝图自定义。在纯蓝图项目中,最容易的是使用已有的蓝图任务节点配合流程任务。
Condition(条件)
判断是否满足进入某个 State 或执行某个 Transition 的布尔条件,相当于状态机中的迁移守卫。例如“IsPlayerInLineOfSight”就是条件。
Transition(转换)
从一个 State 切换到另一个 State 的规则。状态树里的 Transition 通常配置在当前 State 节点上,指定“什么条件下转换到哪个 State”。
Evaluator(评估器)
挂在状态树上周期性求值的逻辑,用于更新参数,例如“计算当前到玩家的距离”并写入黑板或状态树参数。
如果之前用过 UE 的 Blackboard,可以把参数理解为 StateTree 的黑板,多个 State 和 Condition 通过参数共享信息。
3.2 嵌套状态树如何解决状态转换难题
嵌套状态树本质上是在父 State 节点中挂载另一个 StateTree 资产。被挂载的树就是“子状态树”。
父树中的某个 State 激活时,子树开始运行;父树中的 State 退出时,子树也会被终止。这样一个宏观状态可以拥有一个内部状态机,比如:
- 父状态
Combat激活时,子树负责在Chase、Shoot、Reload三个小状态间切换。 - 当玩家位置丢失、或者敌人血量归零时,父状态通过事件收到“战斗结束”信号,切换到
Search或Death。
这里的转换链路是双向的:父树控制子树生命期,子树上抛事件让父树发起状态切换。如果只用一张平铺的大状态树,事件和条件会堆在一起;拆成父子嵌套后,子树的内部细节完全封装起来,父组件和子组件可以独立调试。
3.3 蓝图能与状态树交互的几种方式
纯蓝图流程中,与状态树交互的入口主要有这几个:
方式一:AI 控制器持有 StateTree 组件
在 AI 控制器的蓝图里添加一个 StateTree 组件,启动状态树时指定资产,运行时通过组件读取当前状态。
实操中一般是在 AI 控制器 Event BeginPlay 或 On Possess 里调用“Start State Tree”节点。如果你的 AI 控制器里编译不出这个节点,先确认对象类型和版本,节点名以当前引擎为准。
方式二:状态树外部参数传入
通过 StateTreeReference 或相关参数节点,把外部变量传给状态树。例如把敌人 Pawn、玩家 Pawn 写入状态树参数,这样状态树内部任务才能拿到对象引用。
方式三:事件驱动转换
在状态树的 Transition 条件中,可以监听事件。例如“Alert”状态下的子树发出Target_Spotted事件,父树的 Combat 状态监听到该事件后触发转换。
下面先理解整体结构,第 4 节会完整演示。
4. 实战:用嵌套状态树实现 FPS 敌人 AI 状态切换
4.1 设计父级状态与子级状态
先把要拆分的树画在纸上。不要直接打开编辑器,先规划层级:
父状态树 Enemy_MainTree ├── Idle / Delay(初始等待) ├── Patrol(巡逻) │ └── 子树 Patrol_SubTree │ ├── MoveToNextPoint │ ├── WaitAtPoint │ └── CheckAround ├── Alert(警戒) │ └── 子树 Alert_SubTree │ ├── TurnToNoiseLocation │ └── WaitForConfirm ├── Combat(战斗) │ └── 子树 Combat_SubTree │ ├── Chase │ ├── Shoot │ └── Reload └── Search(搜索) └── 子树 Search_SubTree ├── MoveToLastKnownPosition └── ReturnToPatrol父树里的每个大阶段对应一个带子树的 State。子树在自己的资产里管理内部小状态。
这样设计的价值在于:如果想要增加“呼叫同伴”这个行为,只需要在 Combat 父 State 的子树里加一个子状态,完全不影响父树其他部分;如果想要把 Patrol 改成更复杂的战术巡逻,替换 Patrol_SubTree 资产本身就可以。
4.2 创建敌人 AI 控制器蓝图
创建 AI 控制器蓝图时,在内容浏览器里右键 -> 蓝图类 -> 父类选择 AIController,命名为BP_EnemyAIController。
打开蓝图后,添加一个 StateTree 组件,名称建议叫StateTreeComp。在 Event On Possess 里做初始化:
Event On Possess ├── Get Controlled Pawn ├── Cast To BP_EnemyCharacter └── 获取玩家 / 场景引用 └── 调用 StateTreeComp 的 Start StateTree这里不需要写一行 C++,全部是节点连线。关键是在启动前,把下面这些参数写入 StateTree:
| 参数 | 类型 | 说明 |
|---|---|---|
| SelfActor | Object Reference | 敌人 Pawn 自身 |
| PlayerActor | Object Reference | 玩家 Pawn |
| AI Controller | Object Reference | 当前 AIController |
| MoveSpeed | Float | 移动速度,用于移动任务 |
如果你使用的 StateTree 蓝图交互节点名在当前版本中不叫Start StateTree,可以在 AI 控制器的组件面板找到 StateTreeComp,右键生成事件,查找相关函数。
为了让状态树能拿到玩家位置,可以在 AI 控制器 Tick 里每隔一定时间更新参数,或者通过 Evaluator 周期性求值。对性能和清晰度来说,推荐在 Evaluator 节点里统一更新。
4.3 搭建父状态树:警戒 / 战斗 / 搜索
在内容浏览器右键 -> Artificial Intelligence -> State Tree,创建父状态树资产ST_EnemyMainTree。
打开的 State Tree 编辑器中,左侧是状态节点层级,右侧是参数、条件、任务面板。开始前先添加几个参数:
| 参数名 | 类型 | 说明 |
|---|---|---|
| EnemySelf | Object | 敌人 Pawn |
| PlayerRef | Object | 玩家 Pawn |
| bIsPlayerSpotted | Boolean | 是否发现玩家 |
| bIsLostTarget | Boolean | 是否丢失玩家 |
| LastKnownLocation | Vector | 最后看见玩家位置 |
然后开始创建父状态节点,顺序如下:
- 根节点下添加
Initial状态,作为初始等待。 - 添加
Patrol状态,激活时挂载巡逻子树。 - 添加
Alert状态,挂载警戒子树。 - 添加
Combat状态,挂载战斗子树。 - 添加
Search状态,挂载搜索子树。
父状态树的转换规则建议这样做:
Initial->Patrol:延迟 2 秒后自动转换。Patrol->Alert:条件bIsPlayerSpotted == true,转换为警戒。Alert->Combat:收到事件Target_Spotted或条件确认目标。Combat->Search:条件bIsLostTarget == true。Search->Patrol:搜索完成事件Search_Finished。
注意,状态树的 Transition 不能只挂在根或子状态上,需要逐个状态节点配置它们的转换条件。
4.4 搭建嵌套子树:巡逻 / 追击 / 攻击
此时父树只定义了框架,里面还没有细节。接下来创建三个子树资产:
第一个子树:巡逻子树ST_PatrolSub
流程图如下:
State: MoveToNextPoint └── Task: MoveTo / FindNextPatrolPoint State: WaitAtPoint └── Task: Wait 2s State: CheckAround └── Task: 旋转视角 / 更新 bIsPlayerSpotted这里的核心任务是把“寻找下一个巡逻点”和“移动”拆开。实际项目中可以通过GetRandomReachablePointInRadius或自定义巡逻点数组实现,纯蓝图节点已经能完成。
巡逻子树内部转换:
MoveToNextPoint里移动任务完成后,转换到WaitAtPoint。WaitAtPoint等待结束,转换到CheckAround。CheckAround更新完感知,如果玩家未发现,回到MoveToNextPoint;如果发现玩家,则向上发送Target_Spotted事件给父树。
第二个子树:战斗子树ST_CombatSub
战斗子树内部状态:
State: Chase(追击) └── Task: MoveTo Player State: Shoot(射击) └── Task: 朝向玩家 / 发射射线或抛射物 State: Reload(换弹) └── Task: 播放换弹动画 / 等待转换规则:
Chase->Shoot:当距离小于攻击距离,且视线检测成功。Shoot->Reload:当子弹数为 0。Shoot->Chase:玩家距离过远或视线被阻挡。Reload->Shoot:换弹完成后子弹重新填充。
这些内部状态之间的转换跑在子树内部,不会干扰父树。父树只知道“目前处于 Combat 大阶段”。
第三个子树:搜索子树ST_SearchSub
State: MoveToLastKnownLocation └── Task: MoveTo LastKnownLocation State: WaitAndLookAround └── Task: 原地旋转搜索 State: ReturnToPatrol └── Task: 转换到巡逻如果敌人移动到最后已知位置后,仍然没有发现玩家,则向上发送Search_Finished事件,父树收到事件后把大状态从 Search 切回 Patrol。
4.5 通过事件完成父子状态转换
真正实现“多状态树嵌套的状态转换”的关键就是事件。子树无法直接修改父树的当前状态,但可以通过事件告知父树。
实际操作中,在子树的任务蓝图里发送事件。例如在CheckAround状态中判断bIsPlayerSpotted == true后,从任务蓝图中调用“Send StateTree Event”节点,填入事件名Target_Spotted。
然后在父状态树的Alert状态节点上配置 Transition:
| 转换项 | 设置 |
|---|---|
| Trigger | Event |
| Event Tag | Target_Spotted |
| Target State | Combat |
| Conditions | 无,或添加一个确认条件 |
父树收到事件后,发现触发条件匹配,就会执行Alert退出逻辑,然后进入Combat。当Combat被激活时,其挂载的战斗子树自动被激活,战斗内部的 Chase、Shoot、Reload 子状态就可以开始工作。
同样,战斗阶段时如果玩家立刻离开视线,父树判断bIsLostTarget == true,转换到Search,搜索子树开始运行。搜索完成后,子树发送Search_Finished,父树从 Search 切换回 Patrol。
这套机制的最大优势是:每个层级的树只关心自己的转换规则,不需要知道自己被谁调用。子树上抛一个事件即可,父树决定是否响应。
4.6 运行与验证
配置完成后,把BP_EnemyCharacter放到场景中,AI Controller 设置为BP_EnemyAIController。玩家进入关卡,通过移动验证下列行为:
| 操作 | 预期表现 |
|---|---|
| 玩家远离敌人 | 敌人沿巡逻点持续巡逻 |
| 玩家进入视野 | 敌人进入警戒,转身观察 |
| 玩家暴露 | 敌人进入战斗,追击并开火 |
| 玩家躲到掩体后 | 敌人进入搜索,前往最后位置 |
| 搜索无果 | 敌人回到巡逻 |
如果打开状态树调试工具,可以看到当前激活的父状态和子状态,以及事件是否被正确触发。
5. 常见问题与排查思路
5.1 状态树没有启动
现象:AI 控制器已挂载,但状态树完全不运行。
可能原因:
- StateTree 资产没有正确指定。
- Start StateTree 节点没有在 On Possess 中调用。
- AI 控制器没有被正确分配到 Pawn。
排查顺序:
- 在 AI 控制器 BeginPlay 里加 PrintString,确认 Possess 事件触发。
- 确认 StateTreeComp 引用的资产不是 None。
- 确认参数传入逻辑没有在状态树启动前发生异常。
5.2 状态之间无法转换
现象:父状态一直停留在某个状态,迟迟切不到目标状态。
可能原因:
- Transition 的触发类型设置错误,例如监听事件,但事件名不匹配。
- Condition 条件不满足,例如
bIsPlayerSpotted始终为 false。 - 目标状态不可转换,某些状态间缺少双向转换配置。
排查顺序:
- 打开状态树调试面板,查看当前状态。
- 在条件判断位置加断点或 PrintString。
- 检查事件名的大小写和 Tag 是否完全一致。
5.3 嵌套子树触发后父状态卡死
现象:进入 Combat 子树后无法跳出,即使玩家消失也不转换。
可能原因:
- 事件名发送方和接收方不一致。
- 父状态树的 Transition 没有配置从 Combat 出发的规则。
- 子树的生命周期管理出现问题,父状态退出时子树没有正常终止。
解决思路:
- 检查父状态
Combat的 Transition 规则,确认bIsLostTarget == true时能转换。 - 检查子树内是否有正在运行的长任务,例如 Wait 或 Delay 卡住事件发送。
5.4 黑板键 / 参数不生效
现象:状态树内部读取到的参数始终是初始值。
可能原因:
- 参数名拼写不一致。
- 外部传入参数时对象引用为 None。
- Evaluator 更新频率设置得太低。
排查顺序:
- 确认参数名和变量名完全一致,注意大小写。
- 在 AI 控制器传入参数前打印变量值。
- 检查 Evaluator 的 Interval 设置,是否需要实时更新。
5.5 性能问题
现象:敌人多的时候状态树 Tick 开销偏高。
解决思路:
- 减少每帧更新的 Evaluator 数量,改为 0.2 秒到 0.5 秒更新一次。
- 不要把视线检测放在每帧执行的 Task 里,尽量做成低频率任务。
- 对不在玩家附近、或处在非战斗状态的敌人,可以考虑暂停状态树 Tick。
6. 最佳实践与工程建议
6.1 状态粒度设计
嵌套状态树的状态粒度,直接决定后期维护成本。建议遵循“一个状态解决一个问题”的原则。
例如战斗状态内部不要既管位移又管射击又管换弹,而是拆成Chase、Shoot、Reload三个状态。每个状态内部只执行一个核心 Task,转换条件才容易配置。反之,如果拆得过细,例如把“抬起武器”也单独拆一个状态,状态数量会失控。
6.2 嵌套层级控制
父树嵌套子树不是越深越好。推荐最多两层或三层:
- 第一层:宏观阶段,例如战斗、巡逻、搜索。
- 第二层:阶段内子行为,例如追击、开火、换弹。
- 第三层:极少数复杂子行为,例如“破门”、“呼叫支援”这种多步骤流程。
层级超过三层后,事件追踪非常困难,调试面板看不过来,建议重新设计状态划分。
6.3 事件与参数命名规范
状态树项目里最隐蔽的错误就是名字不一致。建议从一开始就建立命名规范:
- 事件名统一使用
目标_动作格式,例如Target_Spotted、Target_Lost、Search_Finished。 - 参数名统一使用类型前缀:
b开头表示布尔,LastKnownLocation这种直接表达含义。 - 状态节点名使用名词短语,避免出现动词短语,例如状态名
Combat,任务名ShootAtTarget。
如果你的项目里事件较多,可以集中维护一张事件表格,放在项目文档中,避免多人协作时各写各的名字。
6.4 调试与回放技巧
UE5 状态树编辑器有内置的调试模式,运行游戏后可以在编辑器中看到当前激活的 State。建议开发时打开“调试器”面板,实时观察状态切换。
实际项目中,最快定位问题的方法是在关键转换点加可视化信息:
- 在 AI Controller Tick 里打印
当前父状态名。 - 在子树任务开始和结束位置打印
进入 XX 状态/发送 XX 事件。 - 在场景中启用 AI 调试绘制,显示视线检测结果。
不需要做成永久功能,可以在开发宏里包一层编译开关,发布时屏蔽。
6.5 纯蓝图项目的工程约束
既然是纯蓝图项目,要注意不要把所有逻辑都塞进状态树的 Evaluator 和 Task 蓝图里。建议维护几个独立蓝图工具类:
| 蓝图 | 职责 |
|---|---|
| BP_Utility_LineOfSight | 封装视线检测、距离判断 |
| BP_Utility_PatrolPoint | 管理巡逻点数组 |
| BP_Utility_WeaponHandler | 管理子弹、换弹、开火 |
| BP_Utility_EventBus | 跨状态广播事件 |
状态树里的 Condition 和 Task 只负责调用这些工具类的方法,不要在状态树内部堆几百行复杂节点。这样即使状态结构调整,底层工具不用重写。
另外,纯蓝图项目也要注意版本管理。StateTree 资产是数据资产,可以单独打包,但注意不要在版本合并时出现二进制冲突。多人在同一个 StateTree 资产上开发时,建议分工明确,一次只让一个人修改一个子树。
7. 总结与下一步
这篇内容围绕 UE5 纯蓝图 FPS 游戏开发中的状态管理问题,重点拆解了多状态树嵌套的状态转换方法。核心收益可以概括为三点:
第一,父树管宏观阶段、子树管微观行为,状态切换不再依赖平铺枚举和成堆的分支节点。
第二,子树通过事件向上反馈结果,父树通过条件与事件完成跨状态转换,逻辑边界清晰,排错时可逐层定位。
第三,纯蓝图可完成整套搭建,不需要 C++ 扩展。代价是状态树资产的结构设计和命名规范必须在项目早期就定下来。
下一步可以继续学习这几个方向:
- 把状态树与 UE5 的 Gameplay Ability System(GAS)结合,把技能和 AI 行为解耦。
- 使用状态树管理玩家角色的操作状态,例如瞄准、换弹、攀爬、交互。
- 尝试用状态树管理多人网络环境下的 AI 同步,处理客户端与服务器状态归属问题。
如果你正在做 FPS 敌人 AI,建议先照着本文的父子树结构搭一个最小 Demo,跑通“巡逻 -> 警戒 -> 战斗 -> 搜索 -> 巡逻”的闭环,再逐步往里面填射击、换弹、掩体等细节。
如果本文对你有帮助,可以收藏备用。下一篇可以继续聊状态树与黑板的具体参数设计,或者纯蓝图下 FPS 武器系统的拆分方式。