赞
踩
行为树是一个包含逻辑节点和行为节点的树结构,每次需要找出一个行为的时候,会从树的根节点出发,遍历各个节点,找出第一个和当前数据相符合的行为。
如下图,就是一个简单的行为树
当我们要决策一个AI要做什么样的行为的时候, 我们就会自顶向下的,通过一些条件来搜索这颗树,最终确定需要做的行为(叶节点),并且执行它,这就是行为树的基本原理。
有四种不同类型的 task(任务): 包括 action(行为),composite(复合),
conditional(条件),decorator(修饰符)
主要有三种:
这个类型的节点只能有一个子节点。它的功能是修改子任务的行为。在上面的例子中,我们没有使用 decorator(修饰符),如果你需要类似于打断操作的话会用得到这个 decorator(修饰符)类型!举个例子:一个收集资源的操作,它可能有一个中断节点,这个节点判断是否被攻击,如果被攻击则中断收集资源操作!decorator(修饰符)的另一个应用场合是重复执行子任务 X 次,或
者执行子任务直到完成
用来判断某些游戏属性是否合适!
行为很容易理解,即为具体的动作,他们在某种程度上改变游戏的状态和结果
有时候一个 task(任务)需要多帧才能完成。例如,大多数动画不会在一帧开始并结束。此外有 conditional(条件)的任务需要一种方法来告诉他们的父任务条件是否正确,以便让父节点确定子节点的执行顺序。这两个问题都可以使用 status(状态)来解决。一个任务有三种不同状态:运行,成功或者失败
插件下载链接:https://download.csdn.net/download/qq_52324195/85400525
行为树整体的编辑界面
全局变量,如图,你可以自己创建规定类型的变量
点击任意Task,可以在Inspector界面查看该节点的属性
类似于Unity的编辑界面,出了前三个基本属性外,其他的都是该Task的独有属性,我们可以通过每个属性右边的点按钮,来使用我们的全局变量
在整个任务树的最高层的节点我们称之为 Task(任务)。这些 task 任务拥有类似于 MonoBehavior 那样的接口用于实现和扩展。
Task 任务有三个基础的公共属性:name, comment, instant
如下图,你会发现任意Task都有这三种属性
前两个很简单,名称与简介。
不过instant
可能并不好理解,解释如下:
行为树中,当一个 task 任务返回成功或者失败后,行为树会在同一帧中立刻移动到下一个 task 任务。如果,你没有选择 instant 选项,那么在当前 task 任务执行完毕后,都会停留在当前节点中,直到收到了下一个 tick,才会移动到下一个 task 任务!
疑问:什么是tick?在解释这个之前,我们来讲解另外一个知识:BehaviorManager
其实,你会发现,当运行一个行为树的时候,会在场景中自动创建一个名称为 BehaviorManager的 GameObject,并添 BehaviorManage.cs
最初你看到的是下图这样的:
第一行:Update Interval,这很好理解,行为树的更新间隔,他有三种选择
如果你选择了,这个选项。你需要通过脚本来手动调用以下函数来执行所有行为树的更新
BehaviorManager.instance.Tick()
它还有另外一种重载:BehaviorManager.instance.Tick(BehaviorTree);
,很显然,它传入了一个行为树,那么仅仅就是只对该行为树进行更新,而不是全部。
第二行:Task Execution Type 它可以指定这次更新中行为树的执行次数
默认是No Dullicates
,也就是无复制无重复的意思,也就是1次,每次你更新行为树,行为树会执行一次。
如果你指定了5次,那么每次更新就会执行5次
让我们回到之前的话题,到这里我想你已经明白了什么事Tick
,简单来说也就是行为树更新指令
让我们来看看执行顺序流程图:
// 当行为树被启用时,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;
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,也就是中止类型
如果你是刚刚接触行为树,我相信,到这里你一定还不明白,接下来,我们结合例子进行讲解
如图:
这里我们首先说明Selector选择节点,这是系统默认的选择节点,你可以理解为if else,从左向右依次选择,如果能执行就选择该子树执行,如果不能执行则选择下一个子树。注意,我们的用词,子树,也就是说,如果要执行下去,那么左边这个子树的条件节点必须返回Success,否则,那么就会执行下一个子树。
当前的条件节点是两个Int变量的比较,其中一个我们使用了全局变量 A=100,另外一个则赋值初始值10。
其次,左边的Sequence我们赋予了Lower Priority中止
现在,我们执行它看看结果,最初是这样的:
方框呈现绿色,说明该节点正在运行,X则表示返回Failure
或者不能执行,✔表示该状态本身返回Success
当我们修改第二个Int值为1000,使条件节点返回Success
,行为树就变成下面这样:
这里有一点需要说明:中止只会发生在节点运行过程中,如果节点已经运行完毕,也就是出现了绿色的勾,此时说明执行完毕,行为树已经结束,中止就无效了。
如果你之前使用的都是状态机,我想你需要转变过来,行为树并非一种状态,它仅仅包含了一系列行为的逻辑模式,一旦它执行完毕,不会向状态机一样执行完就回到Idle,行为树就结束了。如果你想要再次执行它,就需要再次通过脚本来启用他。
当我们的条件节点通过了,此时中止就生效了,他会中止第二个Sequence,然后执行本节点
此外,你可以通过点击节点左上角的X,来禁用该子树
这是代码
首先,需要引入命名空间:
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; } } }
为了说明事件,我们使用下面这个行为树
这里我们使用了一个条件节点: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防止行为树结束,保持运行
我们发送信息后,条件节点收到信息后,通过执行
我们先创建两个行为节点,并在一个节点中创建第二个节点公共变量,并尝试打印信息
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,可以取消引用
在引用后,我们启动行为树就会正常打印,如果不引用就会报出空指针异常。
同步全局变量,箭头表示同步方向,下图中就是将A的值同步给B,点按Add添加同步操作
添加之后,仍然可以改变同步方向,点按箭头按钮即可,启动后才能看到同步效果
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;
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。