BehaviorTree

7k words

Desc:

行为树是一种用于编写智能行为的图形化编程语言和框架。它是一种树状结构,其中每个节点表示一个行为或一个行为组合,并按照特定的规则组织起来。

行为树的根节点通常表示整个行为树,而叶子节点则表示最基本的行为。每个节点可以包含一个或多个子节点,这些子节点也可以是行为节点或组合节点。行为节点可以表示简单的行为,如移动、攻击、躲避等,而组合节点则可以将多个行为节点组合成更复杂的行为。

行为树的执行是从根节点开始的,然后按照一定的规则遍历每个节点。节点的执行结果可以影响到其父节点和兄弟节点的执行。例如,如果一个行为节点返回失败,则可以触发其父节点中的备选行为,或者如果一个组合节点的一个子节点返回失败,则可以直接返回整个行为树的失败状态。

行为树在游戏开发、机器人控制、智能体控制等领域广泛应用,可以有效地组织和管理复杂的行为逻辑,并提供一种可扩展、易于理解和修改的编程方式


行为树节点

NodeCanvas

叶子节点

叶节点没有子节点,它们是行为树刻度的最终目的地,因此得名。最常见的叶节点将检查条件;游戏状态,或执行动作,改变游戏状态

  • Action Node 动作节点
    表示具体的动作,例如移动、攻击、发射子弹等
  • Condition Node 条件节点
    表示状态检查,例如检查生命值、检查距离等
Action Node 动作节点将执行分配的动作任务。Action 节点将返回 Running,直到 Action Task 完成,此时它将根据分配的 Action Task 返回 Success 或 Failure
常用动作节点
  • SendEvent
    事件派发
  • SetFloat
    设置黑板数据
1
2
3
4
5
6
7
public class ActionNode : BTNode, ITaskAssignable<ActionTask>
{

[SerializeField]
private ActionTask _action;
...
}
1
2
3
4
5
abstract public class ActionTask<T> : ActionTask where T : class
{
sealed public override Type agentType { get { return typeof(T); } }
new public T agent { get { return base.agent as T; } }
}
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
public class DebugLogText : ActionTask<Transform>{
public float labelYOffset = 0;
public float secondsToRun = 1f;
public VerboseMode verboseMode;
public LogMode logMode;
public CompactStatus finishStatus = CompactStatus.Success;
}

protected override void OnExecute() {
if ( verboseMode == VerboseMode.LogAndDisplayLabel || verboseMode == VerboseMode.LogOnly ) {
var label = string.Format("(<b>{0}</b>) {1}", agent.gameObject.name, log.value);
if ( logMode == LogMode.Log ) {
ParadoxNotion.Services.Logger.Log(label, LogTag.EXECUTION, this);
}
if ( logMode == LogMode.Warning ) {
ParadoxNotion.Services.Logger.LogWarning(label, LogTag.EXECUTION, this);
}
if ( logMode == LogMode.Error ) {
ParadoxNotion.Services.Logger.LogError(label, LogTag.EXECUTION, this);
}
}
if ( verboseMode == VerboseMode.LogAndDisplayLabel || verboseMode == VerboseMode.DisplayLabelOnly ) {
if ( secondsToRun > 0 ) {
MonoManager.current.onGUI += OnGUI;
}
}
}

protected override void OnStop() {
if ( verboseMode == VerboseMode.LogAndDisplayLabel || verboseMode == VerboseMode.DisplayLabelOnly ) {
if ( secondsToRun > 0 ) {
MonoManager.current.onGUI -= OnGUI;
}
}
}

protected override void OnUpdate() {
if ( elapsedTime >= secondsToRun ) {
EndAction(finishStatus == CompactStatus.Success ? true : false);
}
}

Condition Node 条件节点将执行条件任务并根据该条件任务返回成功或失败,可以有多个条件任务
常用动作节点
  • CheckEvent
    监听事件
1
2
3
4
5
6
7
public class ConditionNode : BTNode, ITaskAssignable<ConditionTask>
{

[SerializeField]
private ConditionTask _condition;
...
}
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

abstract public class ConditionTask<T> : ConditionTask where T : class
{
sealed public override Type agentType { get { return typeof(T); } }
new public T agent { get { return base.agent as T; } }
}

//当监听到对应的事件时,保存值
public class CheckEvent<T> : ConditionTask<GraphOwner>
{

[RequiredField]
public BBParameter<string> eventName;
[BlackboardOnly]
public BBParameter<T> saveEventValue;

...

void OnCustomEvent(string eventName, IEventData data) {
if ( eventName.Equals(this.eventName.value, System.StringComparison.OrdinalIgnoreCase) ) {
if ( data is EventData<T> ) { //avoid boxing if able
saveEventValue.value = ( (EventData<T>)data ).value;
} else if ( data.valueBoxed is T ) {
saveEventValue.value = (T)data.valueBoxed;
}

YieldReturn(true);
}
}
}

组合节点

复合节点的存在主要是为了确定行为的流程,例如控制将要执行哪些叶节点或以什么顺序执行。因此,复合节点可能有任意数量的子节点,它们将“询问”如何进行。Composite 节点中最基本的是Sequencer,它按顺序执行其所有子节点,直到一个Fails,以及Selector,它按顺序执行其所有子节点,直到一个Succeed

  • Selector Node 选择节点
    从它的子节点中选择第一个成功的节点执行
  • Sequence Node 顺序节点
    从它的子节点中按顺序执行所有节点
  • Parallel Node 并行节点
    同时执行它的所有子节点,直到达到某个条件
  • Probability Selector
    将根据其被选中的机会来选择并执行子节点。如果选定的孩子返回成功,则概率选择器也将返回成功。但是,如果它返回失败,则会选择一个新的孩子(类似于普通的选择器)。如果所有孩子都返回失败,则概率选择器将返回失败,或者如果引入“失败机会”,则概率选择器可能会立即返回失败
  • Priority Selector
    执行具有最高效用权重的孩子,如果另一个孩子已经在运行,则中断该孩子。如果选定的孩子失败,优先级选择器将移动到下一个效用权重最高的孩子,直到一个成功(类似于普通选择器)
  • Flip Selector
    翻转选择器的工作方式与普通选择器类似,但一旦子节点返回Success,它就会移动到末尾(右侧)。因此,以前失败的子项总是首先执行,最近成功的子项是最后执行
  • Switch
    Switch 组合可以切换 Enum 或 Integer 值。根据当前枚举或整数值是什么,它将执行相应的子节点并返回其状态。如果另一个子节点已经在运行,除非启用动态选项,否则它不会被中断。连接将读取每个孩子的整数或枚举值。
    该节点对于创建类似状态的行为并在它们之间切换非常有用
  • Step Sequencer
    与执行其所有子级直到一个失败的普通音序器相比,步进音序器在每次步进音序器执行时一个接一个地执行其子级。无论成功或失败,都返回已执行的子状态

装饰节点

装饰器节点在每个节点的功能上都是特殊的,但一般来说,它们的存在是为了以某种方式改变或附加到它们可以拥有的唯一一个子节点的功能。常见的装饰器包括多次循环子节点、限制对子节点的访问或中断子节点的执行等等

  • ConditionalEvaluator
    只有当分配的条件为真时,条件才会执行其子节点,然后返回子节点返回的任何内容。如果条件为假且子节点尚未运行,它将返回 Failure ,除非启用动态选项,在这种情况下,每个滴答重新评估条件,如果失败,子节点将被中断
  • Interruptor
    中断器被分配了一个条件任务。如果条件为真或变为真,子节点如果Running就会被中断,Interruptor会返回Failure。否则,中断器将返回子节点返回的任何内容
  • Inverter
    会将其子节点返回的 Success 和 Failure 重新映射到相反的位置
  • Repeater
    将重复其子节点Number of Times,或者直到它返回特定状态,或者Forever
  • Filters
    以特定的次数 或每隔特定的时间量(如Cooldown)过滤其子节点的访问。默认情况下,如果此节点被过滤,则此节点将被视为关于其父节点的可选节点。取消选中此选项将返回失败
  • Iterator
    迭代器将迭代从黑板上获取的列表。在每次迭代中,当前迭代的元素可以存储在黑板变量中,子节点将执行。可以将迭代器设置为具有以下策略之一:
    第一次成功。一旦子返回 Success,将跳出迭代并返回 Success。
    第一次失败。一旦孩子返回失败,就会中断迭代并返回失败。
    没有任何。将始终迭代整个列表。在这种情况下,迭代器将在最后一次迭代时返回其子状态
  • Timeout
    如果 Running 的时间超过以秒为单位指定的时间,则 Timeout 装饰器将中断子节点。否则,它将返回子节点返回的任何内容
  • Wait Until
    Wait Until 将返回 Running,直到分配的条件任务变为真。如果孩子被勾选后条件变为假,则不会中断它。仅当子项尚未运行时才检查条件。
  • Monitor
    将监视从孩子返回的状态,并根据该状态执行分配的操作任务。返回的最终状态可以是原始装饰子状态,也可以是新的动作任务状态
  • Optional Decorator
    执行装饰后的子项并将 Status.Optional 返回给父项,从而使子项在其成功或失败方面成为可选的
  • Guard
    如果另一个具有相同指定令牌的 Guard 已经在保护(运行)该令牌,​​则保护装饰的孩子免于运行。对于为同一代理运行的所有行为树,保护是全局的。
    当受保护时,它可以设置为返回失败或运行
  • Override Agent
    将从此时开始为行为树的其余部分设置另一个 Agent。这意味着这个装饰器下面的每个节点现在都将被勾选为新代理,并且每个任务“Self”参数都将使用该新代理。您也可以稍后使用另一个覆盖代理并选择“恢复为原始”选项恢复为原始代理

子树节点

表示嵌套其他行为树,子图是对其他整个图的引用,用于组织、可重用性和行为模块化

  • SubTree
    子树是一个完整的其他行为树。子树节点将返回分配的行为树的根节点(“开始”)返回的任何内容。
    代理和游戏对象 Blackboard 将向下传递到 SubTree,因此根 BehaviourTreeOwner 上的所有游戏对象 blackboard 变量也将可用于 SubTree。请记住,您还可以使用变量映射功能将局部子图变量映射到父图变量!
  • SubFSM
    可以为 SubFSM 分配一个完整的 FSM。执行时,FSM 将启动。只要 SubFSM 正在运行,SubFSM 节点就会返回 Running。您可以为成功指定 FSM 的一种状态,为失败指定另一种状态。一旦 SubFSM 进入这些状态中的任何一个,它将停止并且该节点将相应地返回成功或失败。否则,当 SubFSM 以某种方式完成时,它将返回 Success。
    代理和游戏对象 Blackboard 将被传递给 SubFSM,因此根 BehaviourTreeowner 的所有游戏对象 blackboard 变量也将对 SubFSM 可用。请记住,您还可以使用变量映射功能将局部子图变量映射到父图变量!
  • SubDialogue
    可以为 SubDialogue 分配整个对话树。执行时,对话将开始。只要 SubDialogue 正在运行,SubDialogue 节点就会返回 Running。您可以使用对话中的“完成”对话树节点将成功或失败返回到行为树。请记住,如果没有留下任何节点供其执行,对话树默认以成功完成。
    代理和游戏对象 Blackboard 将被传递到 SubDialogue,因此根 BehaviourTreeowner 的所有游戏对象 blackboard 变量也将可用于 SubDialogue。请记住,您还可以使用变量映射功能将局部子图变量映射到父图变量!

状态

如前所述,每个节点都返回一个 Status。以下是节点可以返回/处于的可能状态及其用途的参考

  • Resting,是一个标记节点为“就绪”的状态,它是树重置时每次遍历后节点重置到的状态。您通常不应返回 Status.Resting
  • Success,表示节点成功完成其任务
  • Failure,表示节点在其任务中失败
  • Running,表示节点仍在运行,最终状态尚未确定
  • Error,是通常在执行错误时返回的状态(例如未设置所需的引用)。Status.Error 未被父节点处理(既不是成功,也不是失败),这意味着父节点将继续执行,就好像返回 Status.Error 的节点根本不存在一样
  • Optional,与上面的类似,它也是未处理的,意味着父节点将继续执行,就好像返回 Status.Optional 的节点从未存在过一样。当然,与 Status.Error 的不同之处在于 Status.Optional 是故意可选的

节点图

事件监听和派发
Image text


行为树存在重复子树逻辑要如何解决

重复子树是指在同一个行为树中,多次出现相同的子树结构。这种情况可能会导致代码重复,难以维护和修改。

为了解决重复子树的问题,可以将这些子树提取出来,形成一个独立的子行为树。然后,在需要使用该子树的地方,可以使用一个 Subtree 节点来引用该子树。

例如,假设我们有一个包含多个行为树的主行为树,并且这些子行为树中存在重复的子树结构。我们可以把这些子树提取出来,形成独立的子行为树,并在需要的地方使用 Subtree 节点引用这些子行为树。这样做可以避免代码的重复,提高代码的复用性和可维护性。

除了使用 Subtree 节点外,还可以使用其他技术来避免重复子树。例如,可以使用状态机来代替部分行为树结构,或者使用基于规则的系统来处理一些简单的行为逻辑。这些技术都可以帮助我们更好地组织和管理行为逻辑,提高代码的可读性、可维护性和可扩展性