抢占行为树


25

我试图绕过行为树,所以我要花一些测试代码。我正在努力解决的一件事是,当出现更高优先级的东西时,如何抢占当前正在运行的节点。

考虑一下以下简单的士兵行为树:

在此处输入图片说明

假设经过了一些滴答声并且附近没有敌人,那名士兵正站在草地上,因此选择了“ 坐下”节点以执行该命令:

在此处输入图片说明

现在,“ 坐下”动作需要时间才能执行,因为要播放动画,因此它会返回Running其状态。一两个勾号过去了,动画仍在运行,但是敌人在附近吗?条件节点触发器。现在,我们需要尽快抢占Sit down节点,以便执行Attack节点。理想情况下,士兵甚至都不会坐下来–如果他只是开始坐下,他可能会反转动画方向。为了增加逼真度,如果他已经超过动画中的临界点,我们可以选择让他完成坐下然后再站起来,或者让他跌跌撞撞地对威胁做出反应。

尽我所能,我一直无法找到有关如何处理这种情况的指南。过去几天(而且很多)我消费的所有文献和视频似乎都绕过了这个问题。我能找到的最接近的东西是重置运行中的节点的概念,但这并不能使像Sit down这样的节点有机会说“嘿,我还没有完成!”

我想到也许在我的基类上定义一个Preempt()or Interrupt()方法Node。不同的节点可以按照自己认为合适的方式进行处理,但是在这种情况下,我们将尝试让士兵尽快站起来然后返回Success。我认为这种方法还需要我的基地Node将条件的概念与其他行动分开。这样,引擎只能检查条件,如果条件通过,则可以在开始执行动作之前先抢占任何当前正在执行的节点。如果未建立这种区分,则引擎将需要不加选择地执行节点,因此可能会在抢占正在运行的节点之前触发新的操作。

供参考,以下是我当前的基类。再说一次,这是一个高峰,所以我试图使事情尽可能简单,仅在需要时以及当我理解时才增加复杂性,这就是我现在正在努力的目标。

public enum ExecuteResult
{
    // node needs more time to run on next tick
    Running,

    // node completed successfully
    Succeeded,

    // node failed to complete
    Failed
}

public abstract class Node<TAgent>
{
    public abstract ExecuteResult Execute(TimeSpan elapsed, TAgent agent, Blackboard blackboard);
}

public abstract class DecoratorNode<TAgent> : Node<TAgent>
{
    private readonly Node<TAgent> child;

    protected DecoratorNode(Node<TAgent> child)
    {
        this.child = child;
    }

    protected Node<TAgent> Child
    {
        get { return this.child; }
    }
}

public abstract class CompositeNode<TAgent> : Node<TAgent>
{
    private readonly Node<TAgent>[] children;

    protected CompositeNode(IEnumerable<Node<TAgent>> children)
    {
        this.children = children.ToArray();
    }

    protected Node<TAgent>[] Children
    {
        get { return this.children; }
    }
}

public abstract class ConditionNode<TAgent> : Node<TAgent>
{
    private readonly bool invert;

    protected ConditionNode()
        : this(false)
    {
    }

    protected ConditionNode(bool invert)
    {
        this.invert = invert;
    }

    public sealed override ExecuteResult Execute(TimeSpan elapsed, TAgent agent, Blackboard blackboard)
    {
        var result = this.CheckCondition(agent, blackboard);

        if (this.invert)
        {
            result = !result;
        }

        return result ? ExecuteResult.Succeeded : ExecuteResult.Failed;
    }

    protected abstract bool CheckCondition(TAgent agent, Blackboard blackboard);
}

public abstract class ActionNode<TAgent> : Node<TAgent>
{
}

有谁能指导我朝正确的方向发展?我的想法是否正确,还是像我担心的那样幼稚?


您需要查看以下文档:chrishecker.com/My_liner_notes_for_spore/…在这里,他解释了树的行走方式,而不是像状态机那样,而是在每次跳动时从根目录行走的,这是真正的反应技巧。BT不需要例外或事件。它们本质上是池化系统,并且由于总是从根源流下来,因此可以对所有情况做出反应。抢占是如何工作的,如果检查更高优先级的外部条件,它将流向那里。(Stop()在退出活动节点之前调用一些回调)
v.oddou

Answers:


6

我发现自己在问与您相同的问题,并且在此博客页面的注释部分进行了简短的简短交谈,向我提供了该问题的另一种解决方案。

第一件事是使用并发节点。并发节点是复合节点的一种特殊类型。它由先决条件检查序列组成,后跟单个动作节点。即使其动作节点处于“运行”状态,它也会更新所有子节点。(不同于必须从当前运行的子节点开始更新的序列节点。)

主要思想是为动作节点创建另外两个返回状态:“取消”和“取消”。

并发节点中前提条件检查失败是一种触发取消其运行操作节点的机制。如果动作节点不需要长时间运行的取消逻辑,则它将立即返回“已取消”。否则,它将切换到“取消”状态,您可以在其中放置所有需要的逻辑以正确中断操作。


您好,欢迎来到GDSE。如果您可以从该博客打开答案,然后到该博客的最后链接,那就太好了。链接会死掉,这里有完整的答案,使其更加持久。问题现在有8票,所以好的答案会很棒。
卡图

我认为将行为树带回到有限状态机的任何方法都不是一个好的解决方案。在我看来,您的方法似乎需要设想每个州的所有退出条件。当这实际上是FSM的缺点时!BT的优势是从根开始,这隐式地创建了一个完全连接的FSM,从而避免了我们显式地编写退出条件。
v.oddou

5

我认为您的士兵可能会分解为身心(以及其他任何东西)。随后,身体可能分解为腿和手。然后,每个部分都需要有自己的行为树以及公共接口,用于上级或下级部分的请求。

因此,您无需发送任何微动操作,而只是发送即时消息,例如“身体,坐下一段时间”或“身体,在那儿奔跑”,而身体将管理动画,状态转换,延迟和其他内容您。

或者,身体可以自己管理这样的行为。如果没有订单,它可能会问“我们可以坐在这里吗?”。更有趣的是,由于封装的原因,您可以轻松地为疲劳或眩晕之类的特征建模。

您甚至可以互换零件-使大象具有僵尸般的智慧,为人类增加翅膀(他甚至不会注意到),或其他。

如果没有这样的分解,我敢打赌您迟早会遇到组合爆炸的危险。

另外:http : //www.valvesoftware.com/publications/2009/ai_systems_of_l4d_mike_booth.pdf


谢谢。阅读您的答案3次后,我想我明白了。我将在本周末阅读该PDF。

1
在过去一个小时中考虑了这一点之后,我不确定我是否了解将身心完全分开的BT与分解成子树的单个BT(通过特殊的装饰器以及生成时脚本进行引用)之间的区别。将所有内容捆绑到一个大BT中)。在我看来,这将提供类似的抽象好处,并且实际上可能使您更容易理解给定实体的行为,因为您不必查看多个单独的BT。但是,我可能很天真。

@ user13414的区别在于,仅使用间接访问时(即,当身体节点必须询问其树中哪个对象代表腿时),就可能需要特殊的脚本来构建树,这也就足够了,并且也不需要任何额外的操心。更少的代码,更少的错误。同样,您将失去在运行时(轻松)切换子树的能力。即使您不需要这种灵活性,也不会损失任何东西(包括执行速度)。
雨中的阴影2013年

3

昨晚躺在床上,我对自己如何处理而又不引入问题的复杂性感到顿悟。它涉及使用(简称IMHO)“平行”复合材料。这就是我的想法:

在此处输入图片说明

希望那还是相当可读的。要点是:

  • 坐下 / 延迟 / 站立”序列是并行序列(A)中的序列。在每个滴答声中,并行序列也正在检查“ 敌人”附近条件(反转)。如果敌人附近,则条件失败,整个并行序列也将失败(立即,即使子序列位于Sit downDelayStand up的中间)
  • 如果失败,并行序列上方的选择器B将跳入选择器C中以处理中断。重要的是,如果并行序列A成功完成,选择器C将不会运行
  • 然后,选择器C尝试正常站立,但如果士兵当前处于过于尴尬的位置以至于无法站立,它也会触发绊倒动画

我认为这会奏效(我会很快尝试一下),尽管比我想象的要凌乱一些。好处是,我最终将能够将子树封装为可重用的逻辑,并从多个角度引用它们。这将减轻我在那里的大部分担忧,因此我认为这是一个可行的解决方案。

当然,我仍然很想听听是否有人对此有任何想法。

更新:尽管这种方法在技术上可行,但我认为它很合适。这是因为不相关的子树需要“知道”在树的其他部分中定义的条件,以便它们可以触发自己的灭亡。尽管共享子树引用可以减轻这种痛苦,但仍然与人们对行为树的期望相反。确实,我一次很简单地犯了两次相同的错误。

因此,我将走另一条路:在对象模型中显式支持先占,以及特殊的组合,允许在发生先占时执行不同的操作集。当我有工作时,我将发布一个单独的答案。


1
如果您真的想重用子树,那么何时中断(此处为“敌人附近”)的逻辑应该不属于子树。取而代之的是,由于更高优先级的刺激,系统可能会要求任何子树(例如B)中断自身,然后系统会跳转到一个特殊标记的中断节点(C此处),该节点将使字符恢复到某种标准状态。 ,例如站立。有点像行为树等同于异常处理。
内森·里德

1
您甚至可以结合多个中断处理程序,具体取决于要中断的刺激。例如,如果NPC正在坐着并开始开火,您可能不希望他站起来(提出一个更大的目标),而是呆在低处争抢掩护。
内森·里德

@Nathan:有趣的是您提到“异常处理”。昨晚我想到的第一种可能的方法是采用Preempt复合概念,它有两个子代:一个用于正常执行,另一个用于抢先执行。如果正常孩子通过或失败,则结果会传播出去。只有在发生抢占的情况下,抢占的子代才会运行。所有节点都有一个Preempt()方法,该方法将在树中滴流。但是,真正要“处理”的唯一一件事就是抢占式组合,它将立即切换到其抢占的子节点。
我-

然后,我想到了我在上面概述的并行方法,它看起来更优雅,因为它不需要整个API的额外负担。就封装子树而言,我认为无论哪里出现复杂性,这都是一个可能的替代点。甚至可能是您经常检查多个条件的地方。在这种情况下,替换的根将是具有多个条件作为其子代的序列复合。
我-

我认为子树在执行之前知道它们需要“命中”的条件是完全合适的,因为它使它们自包含,并且非常显式或隐式。如果这是一个更大的问题,则不要将条件保留在子树中,而应将其保留在子树中。
西文(Seivan)'18年

2

这是我目前确定的解决方案...

  • 我的基Node类有一个Interrupt默认情况下不执行任何操作的方法
  • 条件是“一流”构造,因为它们需要返回bool(因此暗示它们执行速度很快,并且永远不需要多个更新)
  • Node 将条件集合单独暴露给其子节点集合
  • Node.Execute首先执行所有条件,如果任何条件失败,则立即失败。如果条件成功(或没有条件),它将进行调用,ExecuteCore以便子类可以完成其实际工作。有一个参数允许跳过条件,原因如下
  • Node还允许通过一种CheckConditions方法独立执行条件。当然,Node.Execute实际上只是CheckConditions在需要验证条件时调用
  • Selector现在CheckConditions,我的复合材料会调用它考虑执行的每个子对象。如果条件失败,它将直接移到下一个孩子。如果它们通过,它将检查是否已经有一个执行子级。如果是这样,它将调用Interrupt,然后失败。这就是它目前可以做的所有事情,希望当前正在运行的节点能够响应该中断请求,它可以通过...
  • 我添加了一个Interruptible节点,它是一种特殊的装饰器,因为它的装饰子级具有规则的逻辑流,然后有一个单独的节点用于中断。只要不中断,它将执行其常规子项以完成或失败。如果被中断,它将立即切换到执行其中断处理子节点,该子节点可能是所需的复杂子树

最终结果是这样的,取自我的峰值:

在此处输入图片说明

上面是蜜蜂的行为树,它收集花蜜并将其返回到蜂巢。当它没有花蜜并且不在有花的花朵附近时,它会游走:

在此处输入图片说明

如果该节点不可中断,它将永远不会失败,因此蜜蜂将永远徘徊。但是,由于父节点是选择器,并且具有更高优先级的子节点,因此将不断检查其执行资格。如果它们的条件通过,则选择器将引发中断,并且上面的子树会立即切换到“中断”路径,该路径会通过失败而尽快获得保全。当然,它可以先执行其他一些操作,但是我的峰值除了保释外没有任何其他事情。

不过,将其与我的问题联系起来,您可以想象“中断”路径可能会试图扭转坐下的动画,如果失败,士兵就会跌跌撞撞。所有这些将阻止过渡到更高优先级的状态,而这正是目标。

认为我对这种方法感到满意-特别是我上面概述的核心内容-但老实说,它提出了有关条件和动作的特定实现的泛滥以及将行为树与动画系统联系在一起的进一步问题。我什至不确定我是否可以表达这些问题,所以我会继续思考/说话。


1

我通过发明“ When”装饰器解决了同一问题。它有一个条件和两个子行为(“ then”和“ otherwise”)。当执行“ When”时,它将检查条件,并根据其结果运行then /否则运行child。如果条件结果发生变化,则重置运行中的子级,并启动与其他分支相对应的子级。如果孩子完成执行,则整个“何时”完成执行。

关键是与本问题中的初始BT不同,条件是仅在序列开始时才检查条件,而我的“ When”在运行时会一直检查条件。因此,行为树的顶部被替换为:

When[EnemyNear]
  Then
    AttackSequence
  Otherwise
    When[StandingOnGrass]
      Then
        IdleSequence
      Otherwise
        Hum a tune

对于更高级的“当”用法,人们还想引入“等待”操作,该操作在指定的时间段内或无限期地不执行任何操作(直到由父行为重置)。同样,如果只需要一个“ When”分支,则另一个分支可以包含“ Success”或“ Fail”操作,则相应的操作将成功并立即失败。


我认为这种方法与BT最初的发明者所想的更接近。它使用了更动态的流程,这就是为什么BT中的“运行”状态是非常危险的状态,因此很少使用。我们应该始终牢记设计BT的可能性,因为它随时可能重新出现。
v.oddou 2015年

0

虽然我迟到了,但希望能对您有所帮助。主要是因为我想确保自己也没有自己错过任何东西,因为我也一直在努力解决这一问题。我主要是从借用这个想法的Unreal,但没有将其Decorator设为基于的属性Node或与紧密相关的属性Blackboard,而是更通用的。

这将引入一个称为的新节点类型Guard,它类似于的组合Decorator,并且Compositecondition() -> Result旁边有一个签名。update() -> Result

它有三种模式,以指示何时取消应该是如何发生的Guard退货Success或者Failed,在actualy取消取决于调用者。因此,Selector致电Guard

  1. 取消.self ->仅在Guard(及其正在运行的子项)正在运行且条件为Failed
  2. 取消.lower->仅在低优先级节点正在运行且条件为Success或时才取消它们Running
  3. 取消.both ->两者,.self.lower取决于条件和运行的节点。如果要取消自身的运行,则要取消它;如果条件为false,则根据Composite规则(Selector在我们的情况下)将它们视为优先级较低的节点,则可以限制或取消正在运行的节点Success。换句话说,这基本上是两个概念的结合。

喜欢Decorator和不同Composite,它只需要一个孩子。

虽然Guard只需要一个孩子,你可以嵌套尽可能多的SequencesSelectors或其他类型的Nodes,只要你想,包括其他GuardsDecorators

Selector1 Guard.both[Sequence[EnemyNear?]] Sequence1 MoveToEnemy Attack Selector2 Sequence2 StandingOnGrass? Idle HumATune

在上述情况下,无论何时进行Selector1更新,它将始终对与其子级关联的防护运行状态检查。在上述情况下,处于“受Sequence1保护”状态,需要先进行检查,然后再Selector1继续执行running任务。

只要在检查过程中返回Selector2Sequence1运行,就会向发出中断/取消通知,然后照常继续。EnemyNear?successGuards condition()Selector1running node

换句话说,我们可以根据一些条件对“ idle”或“ attack”分支做出反应,从而使行为的反应性远比我们决定的情况高 Parallel

这也使您可以保护Node具有较高优先级的单身人士,避免Nodes在同一个人中跑步Composite

Selector1 Guard.both[Sequence[EnemyNear?]] Sequence1 MoveToEnemy Attack Selector2 Guard.both[StandingOnGrass?] Idle HumATune

如果HumATune是长期运行的话NodeSelector2总是会首先检查一次是否不是Guard。因此,如果npc被传送到草丛上,则下次Selector2运行时,它将检查Guard和取消HumATune以运行Idle

如果它被草丛中传送出去,它将取消正在运行的节点(Idle)并移至HumATune

如您在此处看到的那样,决策制定取决于调用方,Guard而不是其Guard本身。谁被认为是谁的规则lower priority仍由呼叫者保留。在两个示例中,都是由Selector谁来定义构成的lower priority

如果您有一个Composite被叫Random Selector,那么您将可以在该特定实现的内部定义规则Composite

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.