Unity-Behavior Designer详解
理论
基本概念
行为树是一个包含逻辑节点和行为节点的树结构,每次需要找出一个行为的时候,会从树的根节点出发,遍历各个节点,找出第一个和当前数据相符合的行为。
如下图,就是一个简单的行为树
当我们要决策一个AI要做什么样的行为的时候, 我们就会自顶向下的,通过一些条件来搜索这颗树,最终确定需要做的行为(叶节点),并且执行它,这就是行为树的基本原理。
Task&Status
有四种不同类型的 task(任务): 包括 action(行为),composite(复合),
conditional(条件),decorator(修饰符)
复合(Composites)
主要有三种:
- Sequence
- Selector
- Parallel
修饰(Decorator)
这个类型的节点只能有一个子节点。它的功能是修改子任务的行为。在上面的例子中,我们没有使用 decorator(修饰符),如果你需要类似于打断操作的话会用得到这个 decorator(修饰符)类型!举个例子:一个收集资源的操作,它可能有一个中断节点,这个节点判断是否被攻击,如果被攻击则中断收集资源操作!decorator(修饰符)的另一个应用场合是重复执行子任务 X 次,或
者执行子任务直到完成
条件(Conditinals)
用来判断某些游戏属性是否合适!
行为(Action)
行为很容易理解,即为具体的动作,他们在某种程度上改变游戏的状态和结果
返回状态(status)
有时候一个 task(任务)需要多帧才能完成。例如,大多数动画不会在一帧开始并结束。此外有 conditional(条件)的任务需要一种方法来告诉他们的父任务条件是否正确,以便让父节点确定子节点的执行顺序。这两个问题都可以使用 status(状态)来解决。一个任务有三种不同状态:运行,成功或者失败
行为树插件
插件下载链接:https://download.csdn.net/download/qq_52324195/85400525
基本介绍
行为树整体的编辑界面
- Behavior Name,行为树名称
- Behavior Description,行为树简介
- Extenral Description,外部行为树
- 关于这一属性,可以通过右上角的Export将行为树导出,创建一个行为树“Prefab”,他就成为了外部行为树,然后将这个外部行为树拖入该属性,就会将外部行为树应用到当前行为树
- Group
- 行为树的分组编号,用来将行为树分组!可以用来方便的查找到特定的行为树
- Start When Enabled
- 如果设置为 true,那么当这个行为树组件 enabled 的时候,这个行为树就会被执行
- Asynchronous Load,异步加载
- Pause When Disabled
- 如果设置为 true,那么当这个行为树组件 disabled 的时候,这个行为树就会被暂停
- Restart When Complete
- 如果设置为 true,那么当这个行为树组件执行结束的时候,这个行为树就会被重新执行
- Reset Values On Restart
- 如果设置为 true,那么当这个行为树组件 reset 的时候,这个行为树就会被重新执行
- Log Task Changes
- 当设置为 true 是,这个行为树下只要 task 流程发生变化就会打印一条 log 日志到控制台中
Variable
全局变量,如图,你可以自己创建规定类型的变量
Inspector
点击任意Task,可以在Inspector界面查看该节点的属性
类似于Unity的编辑界面,出了前三个基本属性外,其他的都是该Task的独有属性,我们可以通过每个属性右边的点按钮,来使用我们的全局变量
Task
在整个任务树的最高层的节点我们称之为 Task(任务)。这些 task 任务拥有类似于 MonoBehavior 那样的接口用于实现和扩展。
基本属性
Task 任务有三个基础的公共属性:name, comment, instant
如下图,你会发现任意Task都有这三种属性
前两个很简单,名称与简介。
不过instant 可能并不好理解,解释如下:
行为树中,当一个 task 任务返回成功或者失败后,行为树会在同一帧中立刻移动到下一个 task 任务。如果,你没有选择 instant 选项,那么在当前 task 任务执行完毕后,都会停留在当前节点中,直到收到了下一个 tick,才会移动到下一个 task 任务!
疑问:什么是tick?在解释这个之前,我们来讲解另外一个知识:BehaviorManager
其实,你会发现,当运行一个行为树的时候,会在场景中自动创建一个名称为 BehaviorManager的 GameObject,并添 BehaviorManage.cs
最初你看到的是下图这样的:
第一行:Update Interval,这很好理解,行为树的更新间隔,他有三种选择
- Every Frame 每帧更新
- Specify Seconds 自定义时间更新
如果你选择了,这个选项。你需要通过脚本来手动调用以下函数来执行所有行为树的更新
BehaviorManager.instance.Tick()
它还有另外一种重载:BehaviorManager.instance.Tick(BehaviorTree); ,很显然,它传入了一个行为树,那么仅仅就是只对该行为树进行更新,而不是全部。
第二行:Task Execution Type 它可以指定这次更新中行为树的执行次数
默认是No Dullicates ,也就是无复制无重复的意思,也就是1次,每次你更新行为树,行为树会执行一次。
如果你指定了5次,那么每次更新就会执行5次
让我们回到之前的话题,到这里我想你已经明白了什么事Tick ,简单来说也就是行为树更新指令
让我们来看看执行顺序流程图:
API
// 当行为树被启用时,OnAwake被调用一次。可以把它看作一个构造函数
public virtual void OnAwake();
// OnStart在执行之前立即被调用。它用于设置需要在上次运行后重新设置的任何变量
public virtual void OnStart();
// OnUpdate运行实际的任务
public virtual TaskStatus OnUpdate();
// 在执行成功或失败后调用OnEnd。
public virtual void OnEnd();
// 当行为暂停并恢复时,调用OnPause
public virtual void OnPause(bool paused);
// 优先级选择需要知道该任务的运行优先级
public virtual float GetPriority();
// OnBehaviorComplete在行为树完成执行后被调用
public virtual void OnBehaviorComplete();
// 检查器调用OnReset来重置公共属性
public propertiespublic virtual void OnReset();
// 允许从任务中调用OnDrawGizmos
public virtual void OnDrawGizmos();
// 保留对拥有此任务的行为的引用
public Behavior Owner;
父任务 Parent Tasks
behavior tree 行为树中的父任务 task 包括:composite(复合),decorator(修饰符)!
以下是他的可扩展API,虽然 Monobehaviour 没有类似的 API,但是并不难去理解这些功能
//一个父任务可以拥有的子任务的最大数量。通常为1或int。MaxValue
public virtual int MaxChildren();
//布尔值,以确定当前任务是否为并行任务
public virtual bool CanRunParallelChildren();
//当前活动子节点的索引
public virtual int CurrentChildIndex();
//布尔值,以确定当前任务是否可以执行
public virtual bool CanExecute();
//为执行状态应用装饰器,输入参数为被修饰节点的状态
public virtual TaskStatus Decorate(TaskStatus status);
//通知parenttask子任务已被执行,其状态为childStatus
public virtual void OnChildExecuted(TaskStatus childStatus);
//通知父任务,其子任务childIndex已被执行,其状态为childStatus
public virtual void OnChildExecuted(int childIndex, TaskStatus childStatus);
//通知任务子进程已经开始运行
public virtual void OnChildStarted();
//通知并行任务,索引为childIndex的子任务已开始运行
public virtual void OnChildStarted(int childIndex);
//一些父任务需要能够覆盖状态,例如并行任务
public virtual TaskStatus OverrideStatus(TaskStatus status);
//如果中断节点被中断,它将覆盖状态。
public virtual TaskStatus OverrideStatus();
//通知复合任务,条件中止已被触发,子索引应重置
public virtual void OnConditionalAbort(int childIndex);
条件节点的终止
一共有四种中断类型的 abort types: None, Self, Lower Priority, and Both.
如上图,所有的条件节点都有一个属性:Abort Type,也就是中止类型
- None
- Self
- 这是一种自包含中断类型。也就是会检测此节点下所有条件判断节点,即便是被执行过的节点,如果判断条件不满足则打断当前执行顺序从新回到判断节点判断,并返回判断结果!
- Lower Priority
- 当运行到后续节点时,本节点的判断条件生效了的话则打断当前执行顺序,返回本节点执行!
- Both
如果你是刚刚接触行为树,我相信,到这里你一定还不明白,接下来,我们结合例子进行讲解
行为树应用简单例子
如图:
这里我们首先说明Selector选择节点,这是系统默认的选择节点,你可以理解为if else,从左向右依次选择,如果能执行就选择该子树执行,如果不能执行则选择下一个子树。注意,我们的用词,子树,也就是说,如果要执行下去,那么左边这个子树的条件节点必须返回Success,否则,那么就会执行下一个子树。
当前的条件节点是两个Int变量的比较,其中一个我们使用了全局变量 A=100,另外一个则赋值初始值10。
其次,左边的Sequence我们赋予了Lower Priority中止
现在,我们执行它看看结果,最初是这样的:
方框呈现绿色,说明该节点正在运行,X则表示返回Failure 或者不能执行,?表示该状态本身返回Success
当我们修改第二个Int值为1000,使条件节点返回Success ,行为树就变成下面这样:
这里有一点需要说明:中止只会发生在节点运行过程中,如果节点已经运行完毕,也就是出现了绿色的勾,此时说明执行完毕,行为树已经结束,中止就无效了。
如果你之前使用的都是状态机,我想你需要转变过来,行为树并非一种状态,它仅仅包含了一系列行为的逻辑模式,一旦它执行完毕,不会向状态机一样执行完就回到Idle,行为树就结束了。如果你想要再次执行它,就需要再次通过脚本来启用他。
当我们的条件节点通过了,此时中止就生效了,他会中止第二个Sequence,然后执行本节点
此外,你可以通过点击节点左上角的X,来禁用该子树
自定义Task
自定义行为节点
这是代码
首先,需要引入命名空间:
using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;
对于行为节点,我们仅仅需要让该类继承Action,然后重写OnUpdate函数即可,你想要实现的功能就写在这个函数中,我这里实现的就是让对象朝向目标移动
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;
public class MyMoveTo : Action
{
public GameObject enemy;
public override TaskStatus OnUpdate()
{
if (GetComponent<Player>())
{
if (Vector3.Distance(transform.position, enemy.transform.position) < 0.1f)
{
return TaskStatus.Success;
}
else
{
transform.position = Vector3.MoveTowards(transform.position,enemy.transform.position,Time.deltaTime);
return TaskStatus.Running;
}
}
else
{
return TaskStatus.Failure;
}
}
}
自定义条件节点
同样引入命名空间,然后继承Conditional
判断敌人是否存活,如果否,那么返回Failure
public class MyConditional : Conditional
{
public GameObject enemy;
public override TaskStatus OnUpdate()
{
if (enemy.GetComponent<EnemyState>().alive)
{
return TaskStatus.Success;
}
else
{
return TaskStatus.Failure;
}
}
}
自定义修饰节点
继承Decorator
修饰节点比较特殊,我这里重写了四个函数
第一个函数是CanExecute() ,这个函数的返回值表明该节点能否通过
第二个函数是OnChildExecuted(TaskStatus childStatus) ,输入参数为孩子节点状态,该函数会在孩子节点执行的时候调用
第三个函数为Decorate(TaskStatus status) ,输入参数仍然是孩子节点状态,该函数的返回值会改变孩子节点的返回值
最后一个函数就没什么可说的了,这里我使用的行为树如下:
这里我仔细讲解一下,行为树调用过程,首先通过Selector尝试调用Sequence,当他调用到MyBreak的时候,由于我们一开始的CanExecute通过条件为
executionStatus == TaskStatus.Inactive || executionStatus == TaskStatus.Running
也就是未激活或者正在运行时都允许通过,此时,通过MyBreak,走到MyConditional,然后判断MyConditional返回的状态,如果返回Success,此时通过OnChildExecuted 检测到孩子节点状态为Success,我们定义的变量executionStatus被赋予Success,这导致CanExecute 返回false ,此时行为树从这条路中退出,能够继续运行,接下来继续执行MyMoveTo,这里有一点要注意,因为我们已经执行了条件判断,因此才可以执行MyMoveTo,我们之所以要在执行完条件判断后将该修饰节点的CanExecute返回false ,是为了能够继续执行下去,如果这个函数一直返回true,这会导致一直执行条件判断,就无法继续执行MyMoveTo了!;如果条件节点返回Failure 那么该Sequence执行失败,Selector选择执行第二个Sequence。
这里要注意一点,中止导致的更新只有条件节点才能触发,并且在触发时只会遵循那时刻的所有节点的变量,如果你尝试更改其他节点使其无法执行,这是没有用的。只有条件节点才能触发此类行为
using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;
public class MyBreak : Decorator
{
private TaskStatus executionStatus = TaskStatus.Inactive;
public override bool CanExecute()
{
Debug.Log(executionStatus == TaskStatus.Inactive || executionStatus == TaskStatus.Running);
return executionStatus == TaskStatus.Inactive || executionStatus == TaskStatus.Running;
}
public override void OnChildExecuted(TaskStatus childStatus)
{
// Update the execution status after a child has finished running.
executionStatus = childStatus;
}
public override TaskStatus Decorate(TaskStatus status)
{
if (GetComponent<Player>() && GetComponent<Player>().ready)
{
return TaskStatus.Success;
}
else
{
return TaskStatus.Failure;
}
}
public override void OnEnd()
{
executionStatus = TaskStatus.Inactive;
}
}
使用脚本创建一个行为树
在某些情况下,你可能想要通过脚本在运行时创建一个行为树,而不是直接使用拖拽或者面板操作去创建!例如:如果你已经导
出了一个外部行为树,并想通过脚本创建它的话,可以如下这么做:
behaviorTree.StartWhenEnabled = false; 使得行为树不要再一开始的时候执行
后续当你想要调用他的时候,使用behaviorTree.EnableBehavior(); 来主动执行行为树
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;
public class BT_Test : MonoBehaviour
{
public bool on;
public BehaviorTree behaviorTree;
public void Start()
{
behaviorTree = GetComponent<BehaviorTree>();
behaviorTree.StartWhenEnabled = false;
}
public void Update()
{
if (on)
{
behaviorTree.EnableBehavior();
on = false;
}
}
}
Event事件
为了说明事件,我们使用下面这个行为树
这里我们使用了一个条件节点:Has Received Event
通过该条件节点的Inspector界面,我们可以看到,它有一个属性和三个变量
我们来看看他的源码的一个函数
很明显,在行为树启动时,他会自动以给定的名称来注册事件,一共有四种注册事件(0个参数到3个参数)
public override void OnStart()
{
// Let the behavior tree know that we are interested in receiving the event specified
if (!registered) {
Owner.RegisterEvent(eventName.Value, ReceivedEvent);
Owner.RegisterEvent<object>(eventName.Value, ReceivedEvent);
Owner.RegisterEvent<object, object>(eventName.Value, ReceivedEvent);
Owner.RegisterEvent<object, object, object>(eventName.Value, ReceivedEvent);
registered = true;
}
}
我们再来看看注册的函数
首先将eventReceived变量赋为true ,然后设置对应的参数,即获得参数
private void ReceivedEvent()
{
eventReceived = true;
}
private void ReceivedEvent(object arg1)
{
ReceivedEvent();
if (storedValue1 != null && !storedValue1.IsNone) {
storedValue1.SetValue(arg1);
}
}
private void ReceivedEvent(object arg1, object arg2)
{
ReceivedEvent();
if (storedValue1 != null && !storedValue1.IsNone) {
storedValue1.SetValue(arg1);
}
if (storedValue2 != null && !storedValue2.IsNone) {
storedValue2.SetValue(arg2);
}
}
通过Update函数,我们可以知道,当注册事件被调用时,eventReceived为true,该条件节点就会返回Success
public override TaskStatus OnUpdate()
{
return eventReceived ? TaskStatus.Success : TaskStatus.Failure;
}
我们通过一个脚本来调用函数,调用对应的事件十分简单,因为我们使用了一个名字来注册事件,想要调用只需要使用behaviorTree.SendEvent() 这个API即可调用,也就是发送,调用了它之后,Has Received Event条件节点就会返回Success,因此条件能够通过,你可以把这个函数当作对应名字事件的触发器,触发即可使得该条件节点返回Success。
另外,再次强调只有在行为树运行的时候才有效,如果你的行为树结束了,也就是出现绿色的勾了,说明行为树已经结束,此时再发送就无效了
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;
public class TaskA : MonoBehaviour
{
public bool send;
public bool on;
public BehaviorTree behaviorTree;
public void Start()
{
behaviorTree = GetComponent<BehaviorTree>();
behaviorTree.StartWhenEnabled = false;
}
public void Update()
{
if (on)
{
behaviorTree.EnableBehavior();
on = false;
}
if (send)
{
behaviorTree.SendEvent<object>("MyEvent",1);
send = false;
}
}
}
行为树运行后,还未收到事件调用信息,此时我们使用Wait防止行为树结束,保持运行
我们发送信息后,条件节点收到信息后,通过执行
Task的引用
我们先创建两个行为节点,并在一个节点中创建第二个节点公共变量,并尝试打印信息
public class TaskF : Action
{
public TaskS referencedTask;
public override void OnAwake()
{
Debug.Log(referencedTask.some);
}
}
public class TaskS : Action
{
public float some;
}
我们来看看Inspector界面
在属性界面,它显示出了一个引用任务的选择按钮,因为对于行为节点我们无法直接赋值,因此采取选择的方式
点击Select,然后点击行为树中对应类型的行为节点,此时就是将选中行为节点赋予给本节点的此变量
通过点击X,可以取消引用
在引用后,我们启动行为树就会正常打印,如果不引用就会报出空指针异常。
变量同步器(Variable Synchronizer)
同步全局变量,箭头表示同步方向,下图中就是将A的值同步给B,点按Add添加同步操作
添加之后,仍然可以改变同步方向,点按箭头按钮即可,启动后才能看到同步效果
Task可用特性
HelpURL : web 连接
[HelpURL("http://www.opsive.com/assets/BehaviorDesigner/documentation.php?id=27")]
public class Parallel : Composite{
}
TaskIcon :任务的图标
[TaskIcon("Assets/Path/To/{SkinColor}Icon.png")]
public class MyTask : Action{
}
TaskCategory:任务的显示位置(在 Task 任务面板中的显示位置)
[TaskCategory("Common")]
public class Seek : Action{
}
[TaskCategory("RTS/Harvester")]
public class HarvestGold : Action{
}
TaskDescription:功能描述的文本内容,显示在编辑器布局区域的左下角
[TaskDescription("The sequence task is similar to an \"and\" operation. ..."]
public class Sequence : Composite{
}
LinkedTask:应用其他的 Task 任务
[LinkedTask]
public TaskGuard[] linkedTaskGuards = **null**;
InheritedField : 继承属性
[InheritedField]
public float moveSpeed;
|