赞
踩
行为树(Behavior Trees)是一种许多游戏都很流行的AI 技术(类似人工智能)。halo2(微软的一款射击游戏)是第一款使用行为树的主流游戏,微软发布了halo2游戏中行为树的具体实现之后,行为树在游戏应用中开始流行起来。
行为树是多种AI 技术的组合:
- 分层状态机
- 时间控制
- 计划任务
- 动作执行
它的主要优势是容易理解以及可以使用 可视化编辑器 去编辑行为树。
一个简单的行为树就像下图:
最简单的的行为树是一群任务(Task)的集合。在上面的图中
任务组成
有两个条件任务
- Within Sight – 检查敌人是否在视野范围内
- Enough Bullets – 检查是否有足够的子弹
两个动作任务,这两个条件任务中,如果两个条件任务都满足的话,就开始运行两个动作任务。
- Shoot – 举起武器
- Shoot Animation – 播放射击动画。
行为树的强大之处在于,当一个动作任务运行失败之后,可以继续运行另外一个动作任务,假如还有一个动作任务是逃跑,当开枪失败后执行逃跑。
执行控制
顺序执行任务(Sequence ) – 一个一个的运行他的子任务,一次运行一个任务。首先检查敌人是否在视野中(Within Sight),然后检查自己是否有足够的子弹(Enough Bullets)。
并发执行任务(Parallel) – 一次运行所有任务。如果两个子任务都运行成功就开始运行并发执行任务(Shoot和Shoot Animation)。
在上图中没有出现装饰任务,装饰任务的一个重要用处是进行任务终止。
- 例如一个机器人正在采矿,当机器人发现敌人的时候终止采矿。
- 用于返回子任务的某个值或者继续运行子任务直到完全成功为止。
任务一般有四种不同类型:
- 动作任务(Action) – 通过某种操作改变游戏或者游戏物体状态的任务(例如移动物体,旋转物体)。
- 条件任务(Conditional) – 设定其他任务执行的条件(例如设定一个参数等于2的时候执行其他任务)。
- 复合任务(Composite) – 是一群子任务的父节点。
- 装饰任务(Decorator) – 可以拥有一个子任务,他的功能是改变子任务的值。
行为树还有一个重要的特性,就是任务状态的返回值。很多任务都不是一帧就运行完毕的,比如大多数动画在一帧内都不能运行完毕。另外条件任务需要告诉父节点(比如复合任务)条件是否成立,以便父节点判定是否继续运行子节点。刚才提到的情况都可以使用任务状态来解决。
一个任务具有三种状态:运行中、成功、失败。
在上图例子中,射击动画任务按只要是射击动画在运行,他就会一直处于运行状态。而条件任务中,敌人处于可视范围后,条件任务将在一帧的时间内返回成功或者失败。
行为树组件(行为树创建的时候自动添加的组件,在被附加行为树的GameObject上)在 Behavior Designer 和任务之前之间的扮演了一个连接作用的角色。
通过下面的代码可以开始和终止一个行为树:
public void EnableBehavior(); public void DisableBehavior(bool pause = false);
- 1
- 2
你可以找到任务使用了下面其中的方法:
TaskType FindTask< TaskType >(); List< TaskType > FindTasks< TaskType >(); Task FindTaskWithName(string taskName); List< Task > FindTasksWithName(string taskName);
- 1
- 2
- 3
- 4
行为树当前的执行状态可以使用以下方法获得:
behaviorTree.ExecutionStatus;
- 1
当行为树运行的时候将返回运行状态。当行为树执行完毕将返回成功或者失败,这个取决与任务完成情况。
接下来的事件也可以被描述为:OnBehaviorStart OnBehaviorRestart OnBehaviorEnd
- 1
- 2
- 3
行为树组件有以下属性:
Behavior Name:行为树名称;
Behavior Description:行为树描述;
External Behavior:指定运行的外部行为树;
Group:行为树组,用于更加容易的找到行为树。
Start When Enabled:开始运行游戏时立刻执行;
Pause When Disabled:当游戏暂停时,行为树暂停,该项未打勾时暂停游戏行为树将直接结束;
Restart When Complete:循环执行行为树。
Reset Values On Restart:游戏开始时重设变量值;
Log Task Changes:输出行为树日志;
在某些情况下需要使用代码创建。比如:你已经创建一个外部行为树,你想动态的加载它。你可以使用以下代码来加载:
using UnityEngine; using BehaviorDesigner.Runtime; public class CreateTree : MonoBehaviour { public ExternalBehaviorTree behaviorTree; void Start () { var bt = gameObject.AddComponent<BehaviorTree>(); bt.ExternalBehavior = behaviorTree; bt.StartWhenEnabled = false; } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
说明
公共变量
behaviorTree
引用了一个外部行为树。当新建的行为树加载的时候也会加载外部行为树。设置startWhenEnabled
为flase
是为了阻止外部行为树加载后立刻运行,这样我们可以在任何时候使用bt.enableBehavior()
去手动的运行外部行为树。
行为管理器(Behavior Manager)是在运行行为树插件的时候自动创建的组件,他用于管理场景中的所有行为树。
你可以改变行为树的 更新间隔属性(Update Interval) 。
- “Every Frame” – 行为树将每帧进行更新。
- “Specify Seconds” – 按照指定时间进行更新。
- “Manual” – 手动更新行为树。你可以使用以下方法:
BehaviorManager.instance.Tick();
如果你想不同行为树拥有不同的更新频率,你可以手动的设置他们:
BehaviorManager.instance.Tick(BehaviorTree);
- 1
任务执行类型(Task Execution Type) 允许你指定一个行为树是否应该继续运行任务直到他遇到一个已经运行完毕的任务或者继续运行到一个指定的最大任务运行数量。
例如下面例子:
循环执行任务(Repeater)设置为重复5次。如果任务执行类型设置为No Duplicates,Play Sound任务在一个周期中将只运行一次。如果任务执行类型设置为Count,并设置一个最大值为5,那么他将在一个周期中执行5次。
行为树是一组任务的集合。任务拥有和unity MonoBehaviour 相似的API,所以他很容易的让你创建一个任务。
任务类有如下API:
// OnAwake is called once when the behavior tree is enabled. Think of it as a constructor public virtual void OnAwake(); // OnStart is called immediately before execution. It is used to setup any variables that need to be reset from the previous run public virtual void OnStart(); // OnUpdate runs the actual task public virtual TaskStatus OnUpdate(); // OnEnd is called after execution on a success or failure. public virtual void OnEnd(); // OnPause is called when the behavior is paused and resumed public virtual void OnPause(bool paused); // The priority select will need to know this tasks priority of running public virtual float GetPriority(); // OnBehaviorComplete is called after the behavior tree finishes executing public virtual void OnBehaviorComplete(); // OnReset is called by the inspector to reset the public properties public virtual void OnReset(); // Allow OnDrawGizmos to be called from the tasks public virtual void OnDrawGizmos(); // Keep a reference to the behavior that owns this task public Behavior Owner;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
任务有三个共有的属性:
- 名称
- 注释
- 瞬时状态。
在一个相同的循环周期中,一个任务返回成功或者失败后,任务将立刻运行下一个任务,先前的任务将处于等待状态。这种模式有利于减少行为树性能消耗。
任务运行的流程:
父任务有两类
- 组合任务(Composite)
- 装饰任务(Decorator )。
当父任务API没有合适的Unity’s MonoBehaviour 类的时候,它也可以自己的去定义一个。
// The maximum number of children a parent task can have. Will usually be 1 or int.MaxValue public virtual int MaxChildren(); // Boolean value to determine if the current task is a parallel task public virtual bool CanRunParallelChildren(); // The index of the currently active child public virtual int CurrentChildIndex(); // Boolean value to determine if the current task can execute public virtual bool CanExecute(); // Apply a decorator to the executed status public virtual TaskStatus Decorate(TaskStatus status); // Notifies the parent task that the child has been executed and has a status of childStatus public virtual void OnChildExecuted(TaskStatus childStatus); // Notifies the parent task that the child at index childIndex has been executed and has a status of childStatus public virtual void OnChildExecuted(int childIndex, TaskStatus childStatus); // Notifies the task that the child has started to run public virtual void OnChildStarted(); // Notifies the parallel task that the child at index childIndex has started to run public virtual void OnChildStarted(int childIndex); // Some parent tasks need to be able to override the status, such as parallel tasks public virtual TaskStatus OverrideStatus(TaskStatus status); // The interrupt node will override the status if it has been interrupted. public virtual TaskStatus OverrideStatus(); // Notifies the composite task that an conditional abort has been triggered and the child index should reset public virtual void OnConditionalAbort(int childIndex);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
首先我们将创建条件任务类:是否可视任务(Wihhin Sight)。目前这个任务类没有任何内容,只是完整任务的外壳,该任务需要引入BehaviorDesigner.Runtime.Tasks命名空间。
using UnityEngine; using BehaviorDesigner.Runtime.Tasks; public class WithinSight : Conditional { }
- 1
- 2
- 3
- 4
- 5
下面在该类中创建三个公共变量和一个私有变量:
using UnityEngine; using BehaviorDesigner.Runtime; using BehaviorDesigner.Runtime.Tasks; public class WithinSight : Conditional { //表示物体的可视范围; public float fieldOfViewAngle; //要靠近的目标物体的Tag标签; public string targetTag; //共享变量, 它可以被两个任务共享; public SharedTransform target; //用于缓存所有具有标签的目标的物体位置; private Transform[] possibleTargets; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
重载OnAwake,OnUpdate
public override void OnAwake() { //找到所有有目标标签Tag的游戏物体; var targets = GameObject.FindGameObjectsWithTag(targetTag); possibleTargets = new Transform[targets.Length]; for (int i = 0; i < targets.Length; ++i) { possibleTargets[i] = targets[i].transform; } } public override TaskStatus OnUpdate() { for (int i = 0; i < possibleTargets.Length; ++i) { if (withinSight(possibleTargets[i], fieldOfViewAngle)) { target.Value = possibleTargets[i]; return TaskStatus.Success; } } return TaskStatus.Failure; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
该任务类将一直检测是否有物体处于可视范围内,一旦有目标处于可视范围,它将设置目标的值,同时返回成功Success,设置目标的值以便移动物体类知道该目标的方向。如果没有目标处于可视范围将返回失败Failure。
public bool withinSight(Transform targetTransform, float fieldOfViewAngle) { Vector3 direction = targetTransform.position - transform.position; return Vector3.Angle(direction, transform.forward) < fieldOfViewAngle; }
- 1
- 2
- 3
- 4
- 5
该方法首先获得当前物体位置和目标物体位置的矢量差direction ,然后会计算direction与当前方向间的角度,如果该角度小于可视角度fieldOfViewAngle,于是判断该物体处于可视范围内。
需要注意的是,不同于MonoBehaviour对象,所有的行为树任务已经缓存了所有组件,所以不需要对transform组件进行预缓存了。
完整的任务类如下:
using UnityEngine; using BehaviorDesigner.Runtime; using BehaviorDesigner.Runtime.Tasks; public class WithinSight : Conditional { // How wide of an angle the object can see public float fieldOfViewAngle; // The tag of the targets public string targetTag; // Set the target variable when a target has been found so the subsequent tasks know which object is the target public SharedTransform target; // A cache of all of the possible targets private Transform[] possibleTargets; public override void OnAwake() { // Cache all of the transforms that have a tag of targetTag var targets = GameObject.FindGameObjectsWithTag(targetTag); possibleTargets = new Transform[targets.Length]; for (int i = 0; i < targets.Length; ++i) { possibleTargets[i] = targets[i].transform; } } public override TaskStatus OnUpdate() { // Return success if a target is within sight for (int i = 0; i < possibleTargets.Length; ++i) { if (withinSight(possibleTargets[i], fieldOfViewAngle)) { // Set the target so other tasks will know which transform is within sight target.Value = possibleTargets[i]; return TaskStatus.Success; } } return TaskStatus.Failure; } // Returns true if targetTransform is within sight of current transform public bool withinSight(Transform targetTransform, float fieldOfViewAngle) { Vector3 direction = targetTransform.position - transform.position; // An object is within sight if the angle is less than field of view return Vector3.Angle(direction, transform.forward) < fieldOfViewAngle; } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
条件任务类将要完成的是让主角靠近目标物体,该类会改变主角游戏状态,让主角产生位移。
这个类需要两个变量:运动速度,目标物体
using UnityEngine; using BehaviorDesigner.Runtime; using BehaviorDesigner.Runtime.Tasks; public class MoveTowards : Action { public float speed = 0; public SharedTransform target; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
目标物体变量是一个共享变量(在行为树插件中设置的公共变量,可以被所有任务共享),它也被WithinSight 的条件任务引用。接下来的步骤是重写OnUpdate方法:
public override TaskStatus OnUpdate() { if (Vector3.SqrMagnitude(transform.position - target.Value.position) < 0.1f) { return TaskStatus.Success; } transform.position = Vector3.MoveTowards(transform.position, target.Value.position, speed * Time.deltaTime); return TaskStatus.Running; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
当OnUpdate方法运行的时候,它会监测主角是否抵达了目标处。如果主角抵达目标处返回状态success。如果主角没有抵达目标,主角将按照指定的速度向目标靠近,并且返回状态running。
完整的类如下:
using UnityEngine; using BehaviorDesigner.Runtime; using BehaviorDesigner.Runtime.Tasks; public class MoveTowards : Action { // The speed of the object public float speed = 0; // The transform that the object is moving towards public SharedTransform target; public override TaskStatus OnUpdate() { // Return a task status of success once we've reached the target if (Vector3.SqrMagnitude(transform.position - target.Value.position) < 0.1f) { return TaskStatus.Success; } // We haven't reached the target yet so keep moving towards it transform.position = Vector3.MoveTowards(transform.position, target.Value.position, speed * Time.deltaTime); return TaskStatus.Running; } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
至此两个任务类已经写完,使用顺序执行任务将两个任务连接起来,在inspector面板设置他们的变量。共享变量需要在Variables面板提前设置
当行为树运行的时候你可以看见任务节点在绿色和灰色之间改变,绿色表示任务正在运行,灰色表示任务停止运行。任务运行完毕后有一个勾或者叉的图标显示在任务节点上 。任务运行成功显示勾√,任务失败显示叉×。当任务处于运行中(绿色),你可以该检视面板(行为树的检视面板)盖面变量的值,它会立刻体现在游戏中。
右键单击任务将出现菜单栏,菜单栏中选择设置断点(breakpoint),行为树在遇到被设置断点的任务后将暂停运行。
上图中,选择一个任务节点,在检视面板(Inspector)中点击变量名称右边的放大镜图标,可以让变量显示在图表窗口(绘制图表的地方)中任务节点的右侧,这样即使你没有点开检视面板,也可以看见该任务节点的变量。下图中,Flee from Transform就是通过点击放大镜出现在任务节点右侧的:
有时候你只想测试行为树中的某个任务,这个时候你可以关闭任务的某些任务。在任务节点的上方有个橙色的×图标,点击×图表可以使该任务节点失效,失效的任务节点会永远返回成功success,并且显示成暗灰色的。
另外一种测试方法是输出日志。在行为树组件上选择“Log Task Changes”,当行为树运行的时候,将出现类似以下列表的日志:
GameObject - Behavior: Push task Sequence (index 0) at stack index 0 GameObject - Behavior: Push task Wait (index 1) at stack index 0 GameObject - Behavior: Pop task Wait (index 1) at stack index 0 with status Success GameObject - Behavior: Push task Wait (index 2) at stack index 0 GameObject - Behavior: Pop task Wait (index 2) at stack index 0 with status Success GameObject - Behavior: Pop task Sequence (index 0) at stack index 0 with status Success Disabling GameObject – Behavior
- 1
- 2
- 3
- 4
- 5
- 6
- 7
日志的组成为以下格式
{behavior name}: {task change} {task type} (index {task index}) at stack index {stack index} {optional status}
- 1
{game object name} :行为树附加的游戏物体名称;
{behavior name} :行为树的名称;
{task change} :任务节点的最新状态;
{task type} :任务节点的class类型;
{task index} :任务节点的顺序,左边先执行,右边后执行,按照这种顺序来运行;
{stack index} :任务被加入栈中的顺序,如果使用parallel节点,就是并发执行节点,就会使用多个栈;
{optional status} :特殊任务状态改变的额外信息,大多数任务会输出任务状态信息。
行为树其中一个优秀的特性是:低耦合,这样每个任务都不需要依赖其他任务去运行。但是低耦合的缺点就是任务节点之间很难进行信息传递,例如,有一个条件任务判断目标对象是否处于可视区域,如果目标处于可视区域就运行动作任务,让主角靠近目标对象,条件任务和动作任务这个时候需要共享一个变量:目标对象。在传统的行为树中是编写一个黑板模块来解决这个问题,在行为树设计师插件中有更加简单的方法来解决,就是在行为树设计师插件提供的变量面板中直接编写变量。
刚才的例子中我们提到两个任务:其中一个条件任务判断目标对象是否处于可视范围,另外一个动作任务让主角靠近目标对象,请看下图:
在条件任务和动作任务中申明一个共享变量是:
public SharedTransform target;
在变量面板(Variables)中创建一个全局变量(Global Variables),变量名Target,类型transform:
拖动目标对象到None上。
切换到inspector面板,把全局变量分配给target(刚才建立的共享变量)。
这样两个任务现在可以这样共享信息:在属性中选择刚才建立的全局变量。这样操作后,target的值将返回目标对象的位置,当条件任务运行的时候将记录目标对象的位置信息,当动作任务运行的时候将读取之前设置的位置信息。
在下图的inspector面板中,Position右边靠近×的一个三角形符号,是一个变量映射按钮:
变量映射允许你共享相同类型的属性,你可以通过这个快速的获得unity中gameobject的组件属性。例如,在unity中有一个gameobject名字是Agent,我们想要获得它的transform的postion属性,点击三角形,找到相应的属性就可以了,如上图所示。这样你设置了agent的位置属性就不需要增加额外的任务节点了。
支持局部和全局变量,全局变量和局部变量唯一不同就是全局变量是所有任务节点共享的。两种变量都可以被非任务节点的类引用。
共享变量类型
SharedBool SharedColor SharedFloat SharedGameObject SharedGameObjectList SharedInt SharedMaterial SharedObject SharedObjectList SharedQuaternion SharedRect SharedString SharedTransform SharedTransformList SharedVector2 SharedVector3 SharedVector4
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
全局变量是行为树中所有任务都可以使用的变量。设置一个全局变量,可以使用以下路径:
Window->Behavior Designer->Global Variables
或者可以在插件左侧Variables面板中设置。
当添加一个全局变量以后,系统会生成一个相应的资源文件,这个资源文件储存了所有的全局变量值。这个资源文件默认生成在:/Behavior Designer/Resources/BehaviorDesignerGlobalVariables.asset,你可以移动这个资源,但是必须在Resources目录中。
可以使用相同的方法去分配全局变量和局部变量:在inspector面板中,全局变量都在集中在Globals选项中,如图:
如果你希望使用预制里面没有的共享变量(全局变量也是共享变量的一种),你可以自己定义一个。创建自定义共享变量需要继承SharedVariable类,然后实现以下方法:(关键词需要被替换成你需要的类型)
[System.Serializable] public class SharedOBJECT_TYPE : SharedVariable < OBJECT_TYPE > { public static implicit operator SharedOBJECT_TYPE(OBJECT_TYPE value) { return new SharedOBJECT_TYPE { Value = value }; } }
- 1
- 2
- 3
- 4
- 5
上面这个类最重要的是“Value”属性。如果共享变量创建错误,variable面板会出现错误提示。共享变量可以包含任何对象类型,包括:arrays,lists,单个变量,自定义类等。
下面一个例子将把一个自定义类作为共享变量:
[System.Serializable] //定义一个类 public class CustomClass { public int myInt; public Object myObject; } //把自定义类作为共享变量 [System.Serializable] public class SharedCustomClass : SharedVariable < CustomClass > { public static implicit operator SharedCustomClass(CustomClass value) { return new SharedCustomClass { Value = value }; } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
在行为树设计师插件的inspector面板中设置的变量,可以被非任务节点的类正常的引用。局部变量(单个任务节点的变量)可以在继承MonoBehaviour的类中使用以下方法引用:
behaviorTree.GetVariable("MyVariable"); behaviorTree.SetVariable("MyVariable", value); behaviorTree.SetVariableValue("MyVariableName", value);
- 1
- 2
- 3
在设置变量的时候,如果你想要变量引用成功,需要提前设置好变量的名称。下面的代码片段是非任务节点类改变任务节点中变量的值的例子:
using UnityEngine; using BehaviorDesigner.Runtime; public class AccessVariable : MonoBehaviour { public BehaviorTree behaviorTree; public void Start() { var myIntVariable = (SharedInt)behaviorTree.GetVariable("MyVariable"); myIntVariable.Value = 42; } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
上面的代码中引用了一个名叫“MyVariable”的变量,这个变量是在行为树设计师插件的面板中设置的,在代码中你可以获得和设置变量的值。
如何获取全局变量:
GlobalVariables.Instance.GetVariable("MyVariable"); GlobalVariables.Instance.SetVariable("MyVariable", value);
- 1
- 2
条件终止(Conditional Aborts)允许你的行为树可以动态的响应改变,而不要大量杂乱的使用Interrupt/Perform去中断任务,这个功能非常类似Unreal Engine 4引擎里面的观察者终止功能。其他大多数行为树插件的实现方式是重新运行整个行为树,而条件终止则采用了更加优化的方式来实现,不需要重新运行整个行为树,例如下面的行为树:
当上图的行为树运行的时候,如果左边的Conditonal任务满足条件则返回成功,那么将会立刻运行右边的Wait任务。Wait任务设置为等待10000秒(目的是让他一直运行)。当Wait任务在运行过程中,Conditonal任务突然不满足条件了,这个时候会返回失败,假如Sequence父任务是一个条件终止任务(备注:上图Sequence是普通任务,需要设置才是条件终止任务),那么它就会立刻终止Wait任务继续运行,然后按照行为树的运行设置去运行Sequence以外的其他任务。
所有的复合任务类型都可以设置条件终止,如图Sequence任务中,Abort Type就是设置条件终止:
一共有4种条件终止类型:
- 不终止(None)
- 自我终止(Self)
- 低级别终止(Lower Priority)
- 双向终止(Both)
类型 | 图示 | 功能 |
---|---|---|
None 不终止 | 不进行条件终止 | |
Self 自我终止 | 可以终止同一个父节点下的Action类型的任务 | |
Lower Priority 低级别终止 | 设置成低级别终止的任务,具有比其他任务更高级的级别,当该任务条件发生改变时,可以终止其他任务 | |
Both 双向终止 | self和lower priority一起使用 |
下面的例子使用了Lower Priority低级别终止:
在上图例子中,左边Sequnce(顺序执行任务)设置了低级别终止。刚开始的时候左边分支的条件不满足,返回失败,于是Selector(选择执行任务)选择右边分支运行,过一段时间后,当左边分支的条件满足时候,左边分支因为有低级别雕件终止,所以终止了右边分支的任务,从而执行左边分支的action任务。
在条件终止运行的时候,任务节点上会出现一个重复运行的ICON标志,表明行为树在重新判断条件,如图:
例如,有一个任务分支有两个任务,一个是能够看见敌人(can see object),另一个是可以听见敌人(can hear object)。任何一个任务满足条件的时候,都会运行Action任务。如下图,Selector任务具有条件终止,Sequence也具有条件终止,他们进行了嵌套。这样,看见敌人和听见敌人的任务就可以持续的监听,当满足条件时候终止其他低级别的任务,转而运行本分支任务。
上图示例中selector任务必须附加条件终止,不然就不能进行持续的监听。
事件通过send event(发送事件,预制的动作任务)和has received event(接收事件,预制的条件任务)来进行信号传递。send event用于发送事件信号,has received event用于接收信号,成功接收后返回SUCCESS。可以为所有任务指定任务的名称。
另外可以通过代码向行为树发送事件,BehaviorTree.SendEvent方法允许你发送事件给你指定的行为树,例如:
var behaviorTree = GetComponent< BehaviorTree >(); behaviorTree.SendEvent< object >("MyEvent", Vector3.zero);
- 1
- 2
在上面的代码中,”MyEvent”是事件名称,Vector3.zero是事件参数,他们讲发送给行为树组件。如果行为树组件中有一个Has Received Event的条件任务,就可以接收事件并做出响应的反应了。
你可能需要运行的行为树来至多个对象,例如,这里有一个行为树用于巡查一个房间,你不必为每一个单元创建一个行为树,只需要使用外部行为树就可以了。外部行为树使用Behavior Tree Reference任务来引用。当父行为树运行的时候,它会加载所有外部行为树,把外部行为树当作父行为树的一部分来运行。外部行为树中的共享变量如果与父行为树具有相同的名字和类型,将被父行为树的值覆盖。例如,一个父行为树有一个整数类型共享变量,名字为“MyInt”,值为整数7,外部行为树也有一个名字叫“MyInt”的整数类型共享变量,但是值为0,当行为树运行的时候,外部行为树中“MyInt”的值会是整数7(父行为树覆盖了外部行为树的值)。
在某些情况下,新的task任务需要获取其他任务的内容。例如,TaskA可能想要获得TaskB的SomeFloat值,于是TaskA就需要去引用TaskB,在这个例子中
TaskA的代码是:
using UnityEngine; using BehaviorDesigner.Runtime.Tasks; public class TaskA : Action { public TaskB referencedTask; public void OnAwake() { Debug.Log(referencedTask.SomeFloat); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
TaskB的代码是:
using UnityEngine; using BehaviorDesigner.Runtime.Tasks; public class TaskB : Action { public float SomeFloat; }
- 1
- 2
- 3
- 4
- 5
- 6
把TaskA和TaskB加入到行为树中(面板选择或者右键点击):
选择TaskA,在Inspector面板中可以看见Refernced Task变量,点击“select”选择TaskB,这样就建立引用。这个时候TaskB会出现一个链接的图标:
现在我们运行TaskA的时候,就会输出TaskB的SomeFloat的值。如果要清楚引用,可以点击被引用任务右上角的“×”,另外点击“i”可以变成高亮橙色。
还可以使用数组来引用任务:
public class TaskA : Action { public TaskB[] referencedTasks; }
- 1
- 2
- 3
- 4
共享变量(全局变量)可以非常好的在行为树和任务之间共享数据,但是有的时候你需要共享的数据不在行为树组件上,例如,你有一个管理GUI的组件,这个GUI组件有一个GUI元素用于显示行为树附加的对象是否处于活动期,这样用一个布尔值来表示他是否处于活动期:
public bool isAlive { get; set; }
使用变量同步(Variable Synchronizer)组件,可以自动的保持GUI元素与行为树数据一致。
建立变量同步,首先保证你正确创建了共享变量,在这个例子中我们将建立三个共享变量:
然后,添加变量同步组件(路径:Behavior Designer/Variable Synchronizer)到游戏对象上:
然后,添加你想要保持同步的共享变量,在本例中我们将要添加IS ALIVE到组件中:
GameObject:指定你想要同步数据的行为树附加的游戏对象;
Shared Variable:选择需要同步的共享变量;
Direction:指定一个方向,意思就是箭头朝左表示将要设置共享变量值,剪头朝右表示将要获取共享变量的值;
Type:指定同步的类型,目前支持的同步类型有:Behavior Designer, Property属性, Animator, and PlayMaker.;
下面的内容取决与你第4部选择的内容,本例中选择了Property属性,下面的内容包含了你想要同步的内容,需要同步的对象GUI,组件GUI controller,属性isAlive;
点击添加。
添加之后,共享变量isAlive就会按照Update Interval设置的频率去执行同步了。下面的截图显示了其他的变量设置:
Is Alive:获取该变量值;
Speed:获取该变量值;
Target:设置对象;
HelpURL - 指向网站的网址。
当你打开任务检视面板你会看见右上角一个文本图标,点击这个图标可以出现一个帮助页面,这个帮助页面链接是通过下面的代码实现的:
[HelpURL("http://www.opsive.com/assets/BehaviorDesigner/documentation.php?id=27")] public class Parallel : Composite { }
- 1
- 2
- 3
- 4
TaskIcon - 添加任务图标。
[TaskIcon("Assets/Path/To/{SkinColor}Icon.png")] public class MyTask : Action{}
- 1
- 2
任务图标可以帮助你清楚这个任务在做什么,图标的路径再工程根目录上。上面代码的关键词“SkinColor”可以替换成unity的皮肤颜色,“Light”或则“Dark”。
TaskCategory - 任务归类
[TaskCategory("Common")] public class Seek : Action{}
- 1
- 2
这样这个任务就可以被归类到Common的类别中了:
还可以使用斜线对任务归类进行嵌套:
[TaskCategory("RTS/Harvester")] public class HarvestGold : Action{}
- 1
- 2
TaskDescription - 在图形区域显示类里面写入的描述内容
[TaskDescription("The sequence task is similar to an \"and\" operation. ..."] public class Sequence : Composite{}
- 1
- 2
在图像区域这个描述将显示在底部:
LinkedTasks - 共享任务
[LinkedTask] public TaskGuard[] linkedTaskGuards = null;
- 1
- 2
变量可以很好的使得任务之间共享数据,但是我们这里却没有共享任务的东西,但是可以使用LinkedTasks属性共享任务。例如,可以看看警卫任务(一个默认的任务类型),当你引用一个带有警卫任务的任务的时候,同样会引用他的警卫任务。LinkedTasks不是必须的,他主要是确认值是同步的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。