Unity BehaviorDesigner(行为树)实现怪物AI
|
随着项目迭代,当怪物增多时,原有的常规的怪物AI逻辑已经不能满足需求了,策划既想要又想要,怎么办呢,要是根据不同怪物单独写AI是个庞大的工作量,也不好维护,有没有更好的解决办法呢? 还真有,那就是行为树Behavior Designer,有人会问为什么不用FSM(有限状态机)? |
先做下对比:Behavior Designer(行为树) vs FSM(有限状态机)
|
FSM |
行为树 |
|
|
结构 |
状态 + 转换箭头(网状) |
树形节点(层级) |
|
决策方式 |
当前状态决定能跳哪些状态 |
每帧从根节点重新评估 |
|
复杂度增长 |
状态越多,转换越爆炸(N状态最多 N x (N-1) 条线) |
加分支就行,互不干扰 |
|
可读性 |
3-5 个状态很清晰,多了就是蜘蛛网 |
状态再多也是清晰的树 |
|
所以我们的目的就是实现怪物的AI逻辑,需要随时中断、复用等等、FSM就不太合适了。 |
一、Behavior Designer 是什么?
Behavior Designer 是 Unity 的行为树可视化编辑插件(Opsive 出品,Asset Store 付费)。用树形结构编排 AI 逻辑,替代传统状态机。
核心优势:可视化拖拽编辑 → 运行时实时调试 → 节点可复用。便于策划进行怪物(AI)行为实现。
官方地址:https://opsive.com/assets/behavior-designer/
官方文档地址:https://opsive.com/support/documentation/behavior-designer/overview/
Unity商店链接:https://assetstore.unity.com/packages/tools/behavior-ai/behavior-designer-behavior-trees-for-everyone-15277?aid=1100lGdc
适用场景
- 复杂游戏 AI 的构建,如 NPC 行为、敌人 AI、角色控制等
- 需要创建多阶段、动态行为的游戏,例如 RPG、策略、射击、冒险类游戏
- 简化 AI 逻辑和加速开发流程,适用于快速迭代和测试 AI 行为的项目
具体的生命周期如下:

|
可以去官网文档 https://opsive.com/support/documentation/behavior-designer/overview/ ,写的更详细、还有视频等。 |
安装后,可通过Tools -> Behavior Designer -> Eidtor 双击打开,行为设计器中有四个部分。从下面的截图中看:
- 第1部分是图区域。这里是你创建行为树的地方。
- 第2部分是属性面板。属性面板是你编辑行为树具体属性、添加新任务、创建新变量或编辑任务参数的地方。
- 第3部分是行为树操作工具栏。你可以使用下拉框选择现有行为树或添加/移除行为树。最后一部分,
- 第4部分,是调试工具栏。你可以在这个面板中开始/停止、步进和暂停Unity。此外,你还会看到树在开始执行之前的错误数量。


行为设计器窗口中的最后一节是操作工具栏。操作工具栏主要用于选择行为树以及添加/移除行为树。以下操作被标记为:
|
标记 |
详情 |
|
1 |
在你已打开的行为树之间来回或前进导航。 |
|
2 |
列出场景或项目内的所有行为树(包括预制件)。 |
|
3 |
列出场景中添加行为树组件的任何GameObject。 |
|
4 |
列出从标签3中选择的与GameObject相关的任何行为树。 |
|
5 |
列出当前行为树引用的任何外部行为树 |
|
6 |
移除当前选择的行为树。 |
|
7 |
为GameObject添加新的行为树。 |
|
8 |
即使你在层级或项目窗口中选择了不同的GameObject,也能保持当前行为树的活跃状态 |
|
9 |
将行为树导出到外部行为树资产。 |
|
10 |
打开“查找”对话框,可以搜索你的行为树。 |
|
11 |
截取当前行为树的截图。 |
|
12 |
显示行为设计者的偏好 |
三、Behavior Tree 的 Task 内容
|
开始前学一下具体的搭建流程,后续在进行规范化操作 |
刚开始我们随便建个GameObject,给它添加Behavior Tree ,并点击Open 进行编辑:

1. Behavior Tree的参数如下:
|
具体参数 |
解释 |
|
Behavior Name |
行为树的名称。 |
|
Behavior Description |
描述行为树的作用。备注 |
|
External Behavior |
一个字段,用于指定该行为树启动时应运行的外部行为树。后续我们会动态进行添加该组件和动态加载对应的行为树。 |
|
Group |
行为树的数值组合。可以用来轻松找到行为树。CTF示例项目展示了这一点。类似于唯一ID |
|
Start When Enabled |
如果成立,行为树将在组件启用时开始运行。 |
|
Asynchronous Load |
指定行为树是否应在独立线程中加载。由于 Unity 不允许在工作线程上调用 API,如果你使用共享变量的属性映射,应禁用该选项。 |
|
Pause When Disabled |
如果为真,行为树将在组件被禁用时暂停。如果为真,行为树将结束。 |
|
Restart When Complete |
如果为真,行为树在完成执行后将从头开始。如果为假,行为树将结束。 |
|
Reset Values On Restart |
如果成立,变量和任务公共变量在树重启时会重置为原始值。 |
|
Log Task Changes |
用于调试。如果启用,行为树会在任务状态发生变化(如开始或停止)时输出。 |
|
了解完Behavior Tree后我们继续搭建,我们假设一个怪物最基本的AI逻辑:信息初始化-> 巡逻 -> 追击 -> 攻击 -> 死亡,当我们点击右键AddTask 会发现一堆任务,该用哪个呢? 接下来解释一下,四个主要Task的类型,和对应的具体参数: |

2. 四个主要Task的类型:
|
节点 |
作用 |
|
Composite(复合类) |
控制子节点执行顺序 |
|
Decorator(装饰类) |
修饰子节点(取反、重复等) |
|
Conditional(条件类) |
判断条件,返回成功/失败 |
|
Action(行为类) |
执行具体动作 |
2.1 Composites(复合类)
主要用于控制行为树的走向,也是用的最多最重要的一类,任何一个相对复杂的行为树都包含这类Task节点,但它本身不做任何具体行为,所以它们一般位于父节点或根节点。
|
节点 |
图标 |
详解 |
|
Sequence(序列)节点 |
|
这个节点是一个"和"的关系,也就是它下面的子节点的执行顺序是一个接着一个的,如果其中一个返回false,那么后续的子节点不会被执行,这个序列节点返回false。只有当所有子节点全部完成并返回success的时候,这个Sequence(序列)节点才会返回success。 |
|
Selector(选择)节点 |
|
这个节点是"或"的关系,也就是他下面的子节点的执行顺序是一个或另一个的,只有所有子节点返回false才会返回false。只要有一个子节点返回success,那么这个Selector节点就会返回suiccess,后续的节点不会被执行。 |
|
Parallel(并行)节点 |
|
这个节点类似于Sequence(序列)节点。不同的是,Parallel(并行)节点会在同一时间执行所有子节点而不是一个一个的去执行,如果子节点中有任意一个返回false,则停掉所有子节点并返回false。只有所有子节点全部返回success的时候,才会返回success。 |
|
Parallel Complete(并行结束节点) |
|
子节点同时运行。某个子节点返回成功或失败,则父节点返回成功或失败,并结束所有子节点的运行。 |
|
Parallel Selector(并行选择)节点 |
|
类似于Selector(选择)节点,Priority Selector(并行选择)节点只要有一个子节点返回success,那么它就会返回success。不同点在于,子节点的执行顺序不是从左到右的,而是通过优先级来确定的执行顺序,较高的优先级的子节点会被先执行。 |
|
Random Selector(随机选择)节点 |
|
这个节点的特点是:随机的执行子节点,只要有一个子节点返回成功,它就会返回成功,不再执行后续节点。如果所有子节点都返回false则它也返回false。在这个节点的属性面板中有:seed(随机种子)的设置,自行使用。 |
|
Random Sequence(随机序列)节点 |
|
类似于Sequence(序列)节点,只是他的执行顺序是随机的,只要遇到一个子节点返回false,RandomSequence(随机序列)就返回错误,直到全部子节点都返回success,它才会返回success。在这个节点的属性面板中有:seed(随机种子)的设置,自行使用。 |
|
Selector Evaluator(重复判断选择)节点 |
|
这个节点每帧都会去重新评估子节点的执行状态并选择。它会执行行子节点中优先级最低的子节点,每帧都会这么干。如果当前一个高优先级的节点在运行并且下一顿要执行的子节点优先级I比较低,那么它会打断高优先级的节点,去执行优先级低的子节点。Selector Evaluator重复判断选择)节点会从低到高的去遍历执行所有子节点,直到遇到一个返回success的。如果所有子节点都返回false,那么它就返回false,否则只要有一个返回success,它就会返回success。这个节点模拟了条件打断功能,如果子节点没有条件节点的话。 |
|
Priority Selector(优先选择节点) |
|
子节点按优先级运行,优先级更高有更大的机会先运行,一但有节点返回成功,则父节点返回成功。 |
|
* Utility Selector(效用工具选择节点) |
|
该实用工具选择器任务使用“实用理论算法”来评估子任务。子任务可以重写“获取实用值”方法,并在特定时刻返回实用值。具有最高实用值的任务将被选中,而当前正在运行的任务将被终止。实用工具选择器任务会每帧重新评估其子任务。 |
* Utility Selector (效用工具选择节点) :
和普通 Selector 的区别:
|
Selector |
Utility Selector |
|
|
执行顺序 |
固定从左到右 |
按分数动态排序 |
|
适合 |
明确的优先级(逃跑永远优先于攻击) |
灵活的权衡(根据情况动态决策) |
|
复杂度 |
简单 |
需要设计评分逻辑 |
也就是说,根据不同子物体的得分,来决定走哪个子物体,子节点需要实现评分方法,重写 GetUtility():
public class AttackAction : Action
{
public SharedFloat distanceToPlayer;
public override float GetUtility()
{
// 距离越近,分数越高
return Mathf.Clamp01(1f - distanceToPlayer.Value / 20f) * 100f;
}
public override TaskStatus OnUpdate()
{
// 攻击逻辑
return TaskStatus.Success;
}
}
什么时候用
- 普通 Selector 够用就不要用它
- 当 AI 需要"权衡多个因素动态决策"时才考虑,比如 Boss AI 根据血量、距离、玩家状态综合判断下一步该干嘛
2.2 Decorators(装饰类)
多用于对其下的子Task节点执行额外操作,例如反转结果,重复执行等。
|
装饰器名称 |
Icon |
作用 |
关键参数 |
参数说明 |
使用场景 |
|
Inverter |
|
取反,Success→Failure,Failure→Success |
无 |
- |
把"玩家在范围内"变成"玩家不在范围内",省得再写一个反向 Conditional |
|
Repeater |
|
重复执行子节点 N 次 |
count |
重复次数,-1 为无限循环 |
巡逻点之间来回走、循环播放动画 |
|
repeatForever |
true 时忽略 count 永远重复 |
||||
|
endOnFailure |
true 时子节点返回 Failure 立即停止重复 |
||||
|
Return Failure |
|
无论子节点返回什么,都强制返回 Failure |
无 |
- |
执行一个动作但不想让它影响父节点判断(比如"顺便播个音效",不管成不成功都继续) |
|
Return Success |
|
无论子节点返回什么,都强制返回 Success |
无 |
- |
同上,保证分支不会因子节点失败而中断 |
|
Until Failure |
|
一直重复执行子节点,直到子节点返回 Failure 才返回 Success |
无 |
- |
"一直追击直到追不到"、"持续攻击直到目标死亡" |
|
Until Success |
|
一直重复执行子节点,直到子节点返回 Success 才返回 Success |
无 |
- |
"一直尝试开门直到打开"、"反复检测直到发现玩家" |
|
Conditional Evaluator |
|
每帧重新评估条件子节点,条件失败时立刻中断动作子节点 |
ConditionalTask |
要评估的条件节点 |
"追击玩家,但玩家一离开范围就立刻停下来"——比 Sequence 更灵敏,不用等当前 Action 执行完 |
|
Reevaluate |
是否每帧重新评估条件 |
||||
|
GraphLabel |
是否在编辑器节点上显示条件名称 |
||||
|
Cooldown |
|
子节点执行完后进入冷却,冷却期内直接返回 Failure |
duration |
冷却时间(秒) |
技能冷却:攻击后 3 秒内不能再攻击 |
|
Interrupt |
|
允许低优先级任务被高优先级任务打断 |
无 |
配合 Selector 使用 |
Boss AI 中正在巡逻时突然发现玩家需要立刻切换到追击 |
|
Task Guard |
|
同一时间只允许一个实例执行该子树 |
linkedTaskGuard |
关联的另一个 TaskGuard 节点 |
多个怪物共享同一个行为树时,同时只允许一个执行"拾取物品"动作 |
2.3 Conditionals(条件类)
一般放在Action节点左侧进行约束,只有当条件满足(或不满足)时才继续往下执行,单帧内完成一次判断。更多时候配合复合节点进行打断或任务跳转。
2.3.1 基础条件
|
节点 |
作用 |
参数 |
参数类型 |
参数说明 |
|
RandomProbability |
按概率随机返回成功/失败 |
successProbability |
SharedFloat |
成功概率,0~1,默认 0.5 即 50% |
|
seed |
SharedInt |
随机种子,方便复现调试 |
||
|
useSeed |
SharedBool |
是否启用种子 |
||
|
HasReceivedEvent |
收到指定事件时返回成功 |
eventName |
SharedString |
要监听的事件名 |
|
storedValue1 |
SharedVariable |
可选,存储事件传来的第 1 个参数 |
||
|
storedValue2 |
SharedVariable |
可选,存储第 2 个参数 |
||
|
storedValue3 |
SharedVariable |
可选,存储第 3 个参数 |
||
|
StackedConditional |
把多个条件合并到一个节点 |
conditionals |
Conditional[] |
要组合的条件列表(Inspector 里添加) |
|
comparisonType |
Sequence / Selector |
Sequence = 全部成功才成功(AND),Selector = 一个成功就成功(OR) |
||
|
graphLabel |
bool |
是否在节点上显示条件名称 |
2.3.2 物理碰撞条件(3D)
|
节点 |
作用 |
参数 |
参数类型 |
参数说明 |
|
HasEnteredTrigger |
有物体进入 Trigger 时返回成功 |
tag |
SharedString |
筛选 Tag,留空 = 任意物体 |
|
otherGameObject |
SharedGameObject |
输出:进入的物体引用 |
||
|
HasExitedTrigger |
有物体离开 Trigger 时返回成功 |
tag |
SharedString |
筛选 Tag |
|
otherGameObject |
SharedGameObject |
输出:离开的物体引用 |
||
|
HasEnteredCollision |
发生碰撞时返回成功 |
tag |
SharedString |
筛选 Tag |
|
collidedGameObject |
SharedGameObject |
输出:碰撞的物体引用 |
||
|
HasExitedCollision |
碰撞结束时返回成功 |
tag |
SharedString |
筛选 Tag |
|
collidedGameObject |
SharedGameObject |
输出:碰撞结束的物体引用 |
2.3.3 物理碰撞条件(2D)
|
节点 |
作用 |
参数 |
参数类型 |
参数说明 |
|
HasEnteredTrigger2D |
2D Trigger 进入 |
tag |
SharedString |
筛选 Tag |
|
otherGameObject |
SharedGameObject |
输出:进入的物体引用 |
||
|
HasExitedTrigger2D |
2D Trigger 离开 |
tag |
SharedString |
筛选 Tag |
|
otherGameObject |
SharedGameObject |
输出:离开的物体引用 |
||
|
HasEnteredCollision2D |
2D 碰撞开始 |
tag |
SharedString |
筛选 Tag |
|
collidedGameObject |
SharedGameObject |
输出:碰撞的物体引用 |
||
|
HasExitedCollision2D |
2D 碰撞结束 |
tag |
SharedString |
筛选 Tag |
|
collidedGameObject |
SharedGameObject |
输出:碰撞结束的物体引用 |
2.3.4 反射条件(通用比较,不用写代码)
|
节点 |
作用 |
参数 |
参数类型 |
参数说明 |
|
CompareFieldValue |
比较任意组件的字段值 |
targetGameObject |
SharedGameObject |
目标物体 |
|
componentName |
SharedString |
组件类名,如 "Transform" |
||
|
fieldName |
SharedString |
字段名,如 "tag" |
||
|
compareValue |
SharedVariable |
要比较的值,相等返回成功 |
||
|
ComparePropertyValue |
比较任意组件的属性值 |
targetGameObject |
SharedGameObject |
目标物体 |
|
componentName |
SharedString |
组件类名 |
||
|
propertyName |
SharedString |
属性名,如 "position" |
||
|
compareValue |
SharedVariable |
要比较的值 |
物理类条件(HasEnteredTrigger 等)必须配合 Conditional Abort 或 Parallel 使用才能接收到物理回调,否则只会在节点被执行的那一帧检查一次。
2.3.5 还有一些Unity对应组件的条件参数:
这些比较好理解,不懂的话可以单独查询。
2.4 Actions(行为类):
数量最多,为具体执行行为的Task,一般位于行为树的叶子节点右侧,该类Task可能并非单帧就能完成。没必要每个Action都搞清楚,因为可以很容易的自己扩展Action。后面会具体介绍如何扩展。
3. 复合类Task的优先级和打断:
3.1 优先级规则
行为树的优先级就一句话:越靠左(上)的节点优先级越高。

Selector 从左到右依次尝试,成功就停。所以把最重要的行为放左边。
3.2 打断机制:Abort Type
这一点非常重要,是复合类Task的唯一特殊属性:分为四种——不打断,可打断自身,可打断低于该Task优先级的其他Task,既可以打断自身也可以打断低于其优先级的。
- None: 不会产生中断
- Self: 当前组合任务还没运行完,所有子任务的OnUpdate会继续运行,检测是否发生中断
- Lower Priority:行为树可以从更重要的任务到最不重要的任务进行组织。如果更重要的条件任务更改状态,则可以发出中止,以阻止优先级较低的任务运行。
- Both:Self和Lower Priority的结合

4. Variables 变量
|
信息初始化,为什么要初始化?因为我们已经定好怪物的基本逻辑 巡逻-追击等等,这里需要配置一下 巡逻范围、追击距离等等,两个办法,第一:我们可以走配置,便于策划维护。第二:我们直接用公共变量去承接,直接在面板上进行修改。 我们先用Behavior Tre本地变量来实现,这就涉及到Behavior Tre的Variables 变量系统,很好理解,跟我们代码中的变量是一个意思: |

|
类型 |
值类型 |
常见用途 |
|
SharedBool |
bool |
开关标记(是否激活、是否死亡) |
|
SharedInt |
int |
血量、计数器、索引 |
|
SharedUInt |
uint |
无符号整数 |
|
SharedFloat |
float |
速度、距离、冷却时间、半径 |
|
SharedString |
string |
名称、事件名、Tag |
|
SharedVector2 |
Vector2 |
2D 坐标 |
|
SharedVector2Int |
Vector2Int |
2D 网格坐标 |
|
SharedVector3 |
Vector3 |
位置、方向 |
|
SharedVector3Int |
Vector3Int |
3D 网格坐标 |
|
SharedVector4 |
Vector4 |
四维向量 |
|
SharedQuaternion |
Quaternion |
旋转 |
|
SharedRect |
Rect |
矩形区域 |
|
SharedColor |
Color |
颜色 |
|
SharedGameObject |
GameObject |
引用目标物体(玩家、敌人、道具) |
|
SharedTransform |
Transform |
引用 Transform 组件 |
|
SharedCollider |
Collider |
引用碰撞体 |
|
SharedMaterial |
Material |
引用材质 |
|
SharedBehaviour |
Behaviour |
引用任意组件 |
|
SharedObject |
Object |
引用任意 Unity 对象 |
|
SharedAnimationCurve |
AnimationCurve |
动画曲线 |
|
SharedLayerMask |
LayerMask |
层级掩码 |
|
SharedHumanBodyBones |
HumanBodyBones |
人形骨骼枚举 |
|
SharedGameObjectList |
List<GameObject> |
多个物体引用 |
|
SharedTransformList |
List<Transform> |
多个 Transform |
|
SharedObjectList |
List<Object> |
多个对象 |
3 种变量作用域
|
作用域 |
谁能访问 |
在哪设置 |
用途 |
|
Local(局部变量) |
当前行为树内的所有节点 |
行为树编辑器 → Variables 面板 |
这棵树自己用的数据(速度、目标点) |
|
Global(全局变量) |
所有行为树都能访问 |
Tools → Behavior Designer → Global Variables |
全局共享数据(玩家引用、全局状态) |
|
Dynamic(动态变量) |
代码运行时创建 |
代码中 SetVariable |
运行时才确定的临时数据 |
|
我们了解完Behavior Tree的一些必要元素后,再来看我们要实现AI逻辑:信息初始化-> 巡逻 ->追击-> 攻击 -> 死亡。根据上述元素后,我开始尝试搭建: |
4.1.1 信息初始化
根据我们定好的参数,我们需要巡逻、追击的半径和速度,同时建一个怪物状态的枚举便于切换,我们就在局部创建这个几个参数,名字、类型输入完后,点击add进行添加:

那么问题来了,我们怪物的状态变化,都是跟随主角来计算的,比如,距离主角10米开始追击等等。
怎么样获取主角的位置信息呢,获取后怎么共享给所有怪物?,这个时候就需由全局变量了,类似于单例一样。
双击打开 Global Variables ,输入Name : playerObj ,Type :GameObject, 点击Add进行添加。

追击、训练的速度和半径我们直接进行参数面板填写就行,但主角的对象信息我们则需要再初始运行的时候,进行赋值。
就相当于我们Unity脚本的 Start()方法 中进行初始化,在Behavior Tree 我们需要再开始时进行主角查找。
我们选择 Sequence 做我们的复合节点控制,保证该节点下的子节点从左到右依次进行,这样我们可以再最左边进行主角查找并赋值给全局变量:

我们采用Find With Tag 进行全局查找主角,因为是项目后期加入的行为树,之前的逻辑保证不动的情况下,才用这个,也可以专门封装脚本进行查找,其中 我们选中 Find With Tag 后,在对应的检测面板进行 tag 填写和Store Value 找到之后赋值给我们的全局变量playerObj。
Store Value :这个节点的输出口,把计算/获取到的结果写入你指定的共享变量。
4.1.2 循环遍历
找到主角的位置信息了,接下来就可以进行实时计算位置信息了,我们就需要Repeater 装饰节点来实现循环效果,类似于 Update() 方法,这样我们可以开始进行 各个模块的搭建了,
为什么用Selector 来做我们的父节点,因为我们的怪物生成的时候默认状态的是巡逻,如果主角距离我比较远我就继续进行巡逻状态设置 下图区域 1 ,这个时候直接返回True,右边区域 2 部分就不会进行了,除非主角距离我很近了,这个时候1 就不满足了,区域2就会执行,就达成巡逻到追击的状态切换,然后追击主角当追击到攻击半径就切换为攻击状态。
这里只是状态的切换,具体状态逻辑还在后面

4.1.3 Task扩展
上面你会发现 BT Distance For Player 这个Task,我怎么没见过,因为用Unity自带的Vector3的distance 很笨重要实时对坐标进行更新;

这就需要我们进行Task扩展:
因为我们要的是判断主角距离,直接返回的是bool值,所以要扩展Conditional条件类型。只需新建脚本继承Conditional即可:
public class BT_DistanceForPlayer : Conditional
{
private SharedGameObject player;
// 目标距离
[BehaviorDesigner.Runtime.Tasks.Tooltip("是否大于等于目标距离")]
public SharedFloat targetDistance;
public bool isMoreThen = false;
public override TaskStatus OnUpdate()
{
if (player == null)
{
// 获取我们已经初始化的全局主角信息
player = GlobalVariables.Instance.GetVariable("playerObj") as SharedGameObject;
}
if (player.Value == null)
return TaskStatus.Failure;
float dis = Vector3.Distance(player.Value.transform.position, transform.position);
if (isMoreThen)
{
return dis >= targetDistance.Value ? TaskStatus.Success : TaskStatus.Failure;
}
else
{
return dis < targetDistance.Value ? TaskStatus.Success : TaskStatus.Failure;
}
}
public override void OnReset()
{
targetDistance.Value = 0;
isMoreThen = false;
}
}
扩展很简单,我们只需要实现 你给我一个距离,我就返回你主角距离你是否超过这个距离即可。
同时,我们也需要写一个能设置怪物状态的脚本,不然怪物状态就没法比实时切换了,这个时候我们需要扩展一个Action,用来干活的,它只做一件事,就是切换状态,具体怎么切换就要根据自己设置的条件了:
public class BT_SetMonsterState : Action
{
public SharedMonsterState curState;
public SharedMonsterState tragetState;
public override TaskStatus OnUpdate()
{
curState.Value = tragetState.Value;
return TaskStatus.Success;
}
}
扩展是不是很简单,我们想要什么直接扩展对应类型即可,加上我们项目本身就有一些模块封装。这样便能跟我们项目的框架进行信息互通了。比如:检查主角血量、检测怪物当前状态等等。
我们学会扩展后,为了完善逻辑我们还要考虑万一主角死了呢,这个时候利用 BT_CheckPlayerHP 模块进行判断,主角血量小于0我们就切回巡逻。
public class BT_CheckPlayerHP : Action
{
public SharedGameObject player;
private LuaBehaviour luaBehaviour;
public override void OnStart()
{
if (luaBehaviour == null )
{
// 因为我们逻辑都在lua 脚本里,这里做了桥接来获取主角血量。
luaBehaviour = player.Value.GetComponent<LuaBehaviour>();
}
}
[CSharpCallLua]
public bool CheckHp()
{
int hp = luaBehaviour.table.Get<int>("hp");
return hp > 0;
}
public override TaskStatus OnUpdate()
{
return CheckHp() ? TaskStatus.Success : TaskStatus.Failure;
}
}

这个时候我们已经添加了两个怪物状态切换的模块,具体的状态实现主逻辑还没添加呢,这时候发现个问题,这两个转模块和我们的状态实现模块 需要并行。就是各玩各的,这个时候我们就用到Parallel(并行)节点,这样就实现当状态变化后,我们会实时进行实际的状态切换:

4.1.4 模块划分
上面只是介绍了怪物状态的切换,接下来我们简单介绍一下怪物状态的具体实现:
- 巡逻状态实现:
基本逻辑就是 检查是否是巡逻状态 -> 开启寻路 -> 定点巡逻(根据速度和半径,进行随机移动)-> 播放walk动画

- 追击状态实现:
基本逻辑 检查是否是追击状态 -> 开启寻路-> 寻路到主角位置-> 播放walk动画

- 攻击逻辑实现:
基本逻辑 检查是否是攻击状态 -> 初始化技能信息-> 关闭寻路(停止移动)-> 停止walk动画 -> 播放attack动画 -> 技能释放 -> 开始CD倒计时

上面基本上都是我自己实现的task,因为这样可以很好的跟项目结合,大家可以自由发挥,怎么方便怎么搞。
接下来就是调试,纠错了。等基本逻辑跑通后我们就可以保存Behavior Tree,点击右上角面板的Export保存相应位置即可。
- 动态加载:
当我们创造怪物时,就可以动态添加Behavior Tree.cs 同时对ExternalBehavior 进行赋值。这样可以实现多个怪物共享同一套 AI 逻辑。
AssetManager.LoadBehaviorTreeAsync(conf.BehaviorTreePath, function(tree, hd)
self.behaviorTreeHd = hd
self.behaviorTree.ExternalBehavior = tree;
end)
到这里,一套简单的怪物的AI系统算是搭建完成,接下来就是根据策划需求扩展一些必要的task,来丰富我们task库。
4.2 使用心得
|
经验 |
说明 |
|
先设计状态,再拆节点 |
先画清楚怪物有哪些状态、状态之间怎么转换,再去拼行为树,避免边想边搭 |
|
善用 Abort Type |
不加打断的行为树跟状态机没区别,Lower Priority + Self(Both)才是行为树的精髓 |
|
Task 扩展优先于拼凑内置节点 |
内置 Action 覆盖不了业务需求时,写一个自定义 Task 远比用多个内置节点拼凑更清晰 |
|
全局变量尽量少用 |
只放真正全局的数据(如玩家引用),怪物自身的数据用局部变量,避免命名冲突 |
|
模块化分层 |
把初始化、状态切换、状态执行分成独立的子树,用 Parallel 并行运行,便于维护和复用 |
4.3 适用边界
|
场景 |
推荐方案 |
|
状态少(3-5个)、转换固定 |
FSM 更简单直接 |
|
状态多、行为复杂、需策划调整 |
Behavior Designer |
|
逻辑极简的小怪(走直线、不追击) |
纯代码/Lua 脚本即可 |
|
需要多个怪物共享同一套 AI 逻辑 |
ExternalBehaviorTree 导出为 .asset 复用 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐







所有评论(0)