当前位置:   article > 正文

Unity-Behavior Designer详解_unity behavior designer

unity behavior designer

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 自定义时间更新

在这里插入图片描述

  • Manual 手动更新

如果你选择了,这个选项。你需要通过脚本来手动调用以下函数来执行所有行为树的更新

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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

父任务 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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

条件节点的终止

一共有四种中断类型的 abort types: None, Self, Lower Priority, and Both.

在这里插入图片描述

如上图,所有的条件节点都有一个属性:Abort Type,也就是中止类型

  • None
    • 无中止
  • Self
    • 这是一种自包含中断类型。也就是会检测此节点下所有条件判断节点,即便是被执行过的节点,如果判断条件不满足则打断当前执行顺序从新回到判断节点判断,并返回判断结果!
  • Lower Priority
    • 当运行到后续节点时,本节点的判断条件生效了的话则打断当前执行顺序,返回本节点执行!
  • Both
    • 同时包含Self与LowerPriority

如果你是刚刚接触行为树,我相信,到这里你一定还不明白,接下来,我们结合例子进行讲解

行为树应用简单例子

如图:

在这里插入图片描述

这里我们首先说明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;
  • 1
  • 2

对于行为节点,我们仅仅需要让该类继承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;
        }
    }
}
  • 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
自定义条件节点

同样引入命名空间,然后继承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;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
自定义修饰节点

继承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;
    }
}
  • 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

使用脚本创建一个行为树

在某些情况下,你可能想要通过脚本在运行时创建一个行为树,而不是直接使用拖拽或者面板操作去创建!例如:如果你已经导

出了一个外部行为树,并想通过脚本创建它的话,可以如下这么做:

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;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

Event事件

为了说明事件,我们使用下面这个行为树

在这里插入图片描述

这里我们使用了一个条件节点:Has Received Event

通过该条件节点的Inspector界面,我们可以看到,它有一个属性和三个变量

  • Event 事件名称
  • 参数

我们来看看他的源码的一个函数

很明显,在行为树启动时,他会自动以给定的名称来注册事件,一共有四种注册事件(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;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

我们再来看看注册的函数

首先将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);
  }
}
  • 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

通过Update函数,我们可以知道,当注册事件被调用时,eventReceived为true,该条件节点就会返回Success

public override TaskStatus OnUpdate()
{
  return eventReceived ? TaskStatus.Success : TaskStatus.Failure;
}
  • 1
  • 2
  • 3
  • 4

我们通过一个脚本来调用函数,调用对应的事件十分简单,因为我们使用了一个名字来注册事件,想要调用只需要使用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;
        }
    }
}
  • 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

行为树运行后,还未收到事件调用信息,此时我们使用Wait防止行为树结束,保持运行

在这里插入图片描述

我们发送信息后,条件节点收到信息后,通过执行

在这里插入图片描述

Task的引用

我们先创建两个行为节点,并在一个节点中创建第二个节点公共变量,并尝试打印信息

public class TaskF : Action
{
    public TaskS referencedTask;
    public override void OnAwake()
    {
        Debug.Log(referencedTask.some);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
public class TaskS : Action
{
    public float some;
}
  • 1
  • 2
  • 3
  • 4

我们来看看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{
}
  • 1
  • 2
  • 3

TaskIcon :任务的图标

[TaskIcon("Assets/Path/To/{SkinColor}Icon.png")]
public class MyTask : Action{
}
  • 1
  • 2
  • 3

TaskCategory:任务的显示位置(在 Task 任务面板中的显示位置)

[TaskCategory("Common")]
public class Seek : Action{ 
}

[TaskCategory("RTS/Harvester")]
public class HarvestGold : Action{
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

TaskDescription:功能描述的文本内容,显示在编辑器布局区域的左下角

[TaskDescription("The sequence task is similar to an \"and\" operation. ..."]
public class Sequence : Composite{
}
  • 1
  • 2
  • 3

LinkedTask:应用其他的 Task 任务

[LinkedTask]
public TaskGuard[] linkedTaskGuards = **null**;
  • 1
  • 2

InheritedField : 继承属性

[InheritedField]
public float moveSpeed;
  • 1
  • 2
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/110517
推荐阅读
相关标签
  

闽ICP备14008679号