ビヘイビアツリーのプリエンプション


25

私はビヘイビアツリーを回避しようとしています。そのため、いくつかのテストコードを作成しています。私が苦労していることの1つは、優先度の高いものが発生したときに現在実行中のノードをプリエンプトする方法です。

兵士の次の単純な架空の行動ツリーを考えてみましょう。

ここに画像の説明を入力してください

いくつかのティックが過ぎ、近くに敵がいなかったと仮定します。兵士は草の上に立っていたため、実行するためにSit downノードが選択さます。

ここに画像の説明を入力してください

再生するアニメーションがあるため、Sit downアクションの実行に時間がかかりRunning、ステータスとして戻ります。ティックが1つまたは2つ経つと、アニメーションはまだ実行されていますが、敵は近くにいますか?条件ノードのトリガー。次に、攻撃ノードを実行できるように、できるだけ早くシットダウンノードをプリエンプトする必要があります。理想的には、兵士は座っても終わらない-座ったばかりの場合は、代わりにアニメーションの方向を逆にするかもしれません。現実性を高めるために、もし彼がアニメーションの転換点を過ぎた場合、代わりに座ってから立ち直るようにするか、恐らく脅威に反応するために急いでつまずかせることを選択するかもしれません。

このような状況に対処する方法についてのガイダンスを見つけることができませんでした。過去数日間に私が消費したすべての文献とビデオ(そして、それはたくさんありました)は、この問題を回避するようです。私が見つけた最も近いものは、実行中のノードをリセットするというこの概念でしたが、シットのようなノードは「ちょっと、まだ終わっていない!」と言う機会を与えません。

おそらく、基本クラスで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

回答:


6

私はあなたと同じ質問をしていることに気づき、このブログページのコメントセクションで短い会話をしました。そこでは、問題の別の解決策が提供されました。

まず、同時ノードを使用します。同時ノードは、特殊なタイプの複合ノードです。これは、一連の前提条件チェックの後に単一のアクションノードが続きます。アクションノードが「実行中」状態であっても、すべての子ノードを更新します。(現在の実行中の子ノードからの更新を開始する必要があるシーケンスノードとは異なります。)

主なアイデアは、アクションノードの「キャンセル」と「キャンセル」という2つの戻り状態を作成することです。

並行ノードでの前提条件チェックの失敗は、実行中のアクションノードのキャンセルをトリガーするメカニズムです。アクションノードが長時間実行のキャンセルロジックを必要としない場合、すぐに「キャンセル」を返します。それ以外の場合は、「キャンセル」状態に切り替わり、アクションを正しく中断するために必要なすべてのロジックを配置できます。


こんにちは、GDSEへようこそ。その答えをブログからこのブログへ、そして最後にそのブログへのリンクで開くことができれば、素晴らしいことです。リンクはここで完全に答えが出て死ぬ傾向があり、それがより永続的になります。質問には現在8票がありますので、良い答えは素晴らしいでしょう。
カトゥ

動作ツリーを有限状態マシンに戻すものは良い解決策ではないと思います。あなたのアプローチは、各州のすべての終了条件を想像する必要があるように思えます。これが実際にFSMの欠点であるとき!BTには​​ルートから開始するという利点があります。これにより、完全に接続されたFSMが暗黙的に作成され、終了条件を明示的に記述することが回避されます。
v.oddou

5

私はあなたの兵士が心と体(そして他の何でも)に分解されるかもしれないと思います。その後、体は脚と手に分解されます。それから、すべての部分には、独自の動作ツリーと、より高いレベルまたはより低いレベルの部分からのリクエストのためのパブリックインターフェイスが必要です。

そのため、すべてのアクションを細かく管理するのではなく、「ボディ、しばらく座って」または「ボディ、そこに走って」などのインスタントショットメッセージを送信するだけで、ボディがアニメーション、状態遷移、遅延などを管理します。君は。

あるいは、体はこのような行動を自分で管理するかもしれません。注文がない場合、「ここに座ってもいいですか?」さらに興味深いことに、カプセル化のため、疲れや気絶などの機能を簡単にモデリングできます。

部品を交換することもできます-ゾンビの知性で象を作り、人間に翼を付け加えます(彼は気付かないこともあります)、その他何でも。

このような分解がなければ、遅かれ早かれ、組み合わせ爆発に遭遇する危険があると思います。

また、http//www.valvesoftware.com/publications/2009/ai_systems_of_l4d_mike_booth.pdf


ありがとう。あなたの答えを3回読んだので、理解できたと思います。今週末、そのPDFを読みます。

1
過去1時間これについて考えてきたので、心と体のために完全に別々のBTを持つか、サブツリーに分解される単一のBTを持つか(ビルド時のスクリプトで特別なデコレータを通して参照される)の違いを理解していないすべてを1つの大きなBTに結び付けます)。これは、同様の抽象化の利点を提供し、複数の個別のBTを調べる必要がないため、実際に特定のエンティティの動作を理解しやすくするように思えます。しかし、私はおそらく素朴です。

@ user13414違いは、ツリーを構築するために特別なスクリプトが必要になるということです。間接アクセスを使用する場合(つまり、ボディノードがどのオブジェクトを脚に表すかをツリーに問い合わせる必要がある場合)は十分であり、追加のブレーンファックも必要ありません。少ないコード、少ないエラー。また、実行時に(簡単に)サブツリーを切り替えることができなくなります。そのような柔軟性が必要でなくても、何も失われません(実行速度を含む)。
雨の影

3

昨夜ベッドに横たわって、私は私の質問に傾いていた複雑さを導入せずにこれをどのように進めることができるかについて、何かひらめきがありました。これには、(名前が正しくない、IMHO)「パラレル」コンポジットの使用が含まれます。私が考えているのは次のとおりです。

ここに画像の説明を入力してください

うまくいけば、それはまだかなり読みやすいです。重要なポイントは次のとおりです。

  • 座っ / 遅延 / スタンドアップシーケンスを並列配列(配列内でA)。ティックごとに、並列シーケンスは敵の近くの条件(反転)もチェックします。敵近くにいる場合、条件は失敗し、並列シーケンス全体も同様に(子シーケンスがSit downDelay、またはStand upの途中であっても)
  • 障害が発生すると、並列シーケンスの上のセレクターBがセレクターCにジャンプして割り込みを処理します。重要なのは、パラレルシーケンスAが正常に完了した場合、セレクタCは実行されないことです。
  • セレクターCは通常どおり立ち上がろうとしますが、兵士が単に立ち上がるにはあまりにも厄介な位置にある場合、つまずきアニメーションをトリガーすることもできます

私は想像していたよりも少し面倒でしたが、これはうまくいくと思います(すぐに私のスパイクで試してみるつもりです)。良いことは、最終的にサブツリーを再利用可能なロジックとしてカプセル化し、複数のポイントからそれらを参照できるようになることです。それはそこでの私の懸念の大部分を軽減するので、これは実行可能な解決策だと思います。

もちろん、私はまだ誰かがこれについて何か考えを持っているかどうか聞いてみたいです。

更新:このアプローチは技術的には機能しますが、私はそれをサックスと決めました。それは、無関係なサブツリーが、ツリーの他の部分で定義された条件を「知る」必要があるためです。サブツリーの参照を共有することでこの痛みを緩和する方法はありますが、ビヘイビアツリーを見たときに期待することとは相反します。実際、非常に単純なスパイクで同じ間違いを2回しました。

そのため、別のルートに進みます。オブジェクトモデル内でのプリエンプションの明示的なサポートと、プリエンプションが発生したときに異なるアクションセットを実行できる特別なコンポジットです。何か問題がある場合は、別の回答を投稿します。


1
サブツリーを本当に再利用したい場合は、いつ中断するかのロジック(ここでは「敵に近い」)は、おそらくサブツリーの一部ではないはずです。代わりに、システムは、より高い優先度の刺激のためにサブツリー(ここではBなど)にそれ自体を中断するように要求できます。 、例えば立っている。例外処理に相当する動作ツリーに少し似ています。
ネイサンリード

1
どの刺激が割り込んでいるかに応じて、複数の割り込みハンドラを組み込むこともできます。たとえば、NPCが座って発砲し始めた場合、NPCが立ち上がらないように(そしてより大きなターゲットを提示したい)、低い位置にとどまり、カバーを奪おうとするかもしれません。
ネイサンリード

@ネイサン:あなたが「例外処理」について言及する面白い。昨夜考えた最初の可能なアプローチは、プリエンプトコンポジットのアイデアでした。これには、通常実行用とプリエンプト実行用の2つの子があります。通常の子が合格または不合格の場合、その結果は伝播します。先取りの子は、先取りが発生した場合にのみ実行されます。すべてのノードにはPreempt()メソッドがあり、ツリー内をトリクルします。ただし、これを実際に「処理」する唯一のものは、プリエンプトコンポジットであり、即座にそのプリエンプト子ノードに切り替わります。

次に、私が上で概説した並列アプローチを考えましたが、API全体で余分な処理を必要としないため、よりエレガントに見えました。サブツリーのカプセル化に関するあなたのポイントまで、複雑さが発生する場合はいつでも、それが可能な代替ポイントになると思います。それは、頻繁に一緒にチェックされるいくつかの条件がある場所でさえありえます。その場合、置換のルートは、複数の条件を子とするシーケンス複合になります。

実行前に「ヒット」する必要がある条件を知っているサブツリーは、自己完結型であり、非常に明示的であるか暗黙的であるかにより、完全に適切であると思います。それがより大きな懸念である場合、サブツリー内ではなく、その「呼び出しサイト」で条件を維持しないでください。
セイバン

2

ここに私が解決した解決策があります...

  • 基本Nodeクラスには、Interruptデフォルトでは何もしないメソッドがあります
  • 条件は「ファーストクラス」の構造であり、戻る必要があるという点でbool(したがって、実行が高速であり、複数の更新を必要としないことを意味します)
  • Node 条件のコレクションを子ノードのコレクションに個別に公開します
  • Node.Execute最初にすべての条件を実行し、いずれかの条件が失敗するとすぐに失敗します。条件が成功した場合(または条件がない場合)はExecuteCore、サブクラスが実際の作業を行えるように呼び出します。以下に示す理由により、条件をスキップできるパラメーターがあります
  • Nodeまた、CheckConditionsメソッドを介して条件を分離して実行することもできます。もちろん、条件を検証する必要Node.ExecuteがあるCheckConditionsときに実際に呼び出すだけです
  • 私のSelector複合体は、今呼び出し、CheckConditionsそれが実行するために考えて、それぞれの子のために。条件が満たされない場合、次の子に向かってまっすぐ移動します。合格した場合、実行中の子がすでに存在するかどうかをチェックします。その場合、呼び出しInterruptてから失敗します。現在実行中のノードが割り込み要求に応答することを期待して、この時点でできることはそれだけです。
  • Interruptibleノードを追加しました。これは、装飾された子として通常のロジックフローを持っているため、一種の特別なデコレータです。次に、割り込み用の別のノードがあります。中断されない限り、通常の子を完了または失敗するまで実行します。中断された場合、すぐにその子ノードを処理する中断に切り替わります。これは、必要に応じて複雑なサブツリーになる可能性があります

最終結果は、私のスパイクから取られたこのようなものです:

ここに画像の説明を入力してください

上記は蜂の行動ツリーで、蜜を収集して巣に戻します。蜜がなく、花のある花の近くにないとき、さまよう。

ここに画像の説明を入力してください

このノードが中断可能でない場合、失敗することはないため、ハチは永久にさまよいます。ただし、親ノードはセレクタであり、優先度の高い子があるため、実行の適格性は常にチェックされています。条件が満たされると、セレクターは割り込みを発生させ、上のサブツリーはすぐに「割り込み済み」パスに切り替わります。もちろん、最初に他のアクションを実行することもできますが、私のスパイクには保釈以外に何もすることはありません。

しかし、これを私の質問に結び付けるために、「中断された」パスが座っているアニメーションを反転させ、失敗すると兵士をつまずかせることを想像できます。これはすべて、より高い優先度の状態への移行を遅らせることになり、まさにそれが目標でした。

このアプローチ、特に上で概説した中核部分には満足していると思いますが、正直なところ、条件とアクションの特定の実装の急増、およびビヘイビアツリーをアニメーションシステムに結び付けることについてさらに疑問を投げかけています。これらの質問を明確に表現できるかどうかはまだわからないので、考え続けてください。


1

「When」デコレータを発明して、同じ問題を修正しました。条件と2つの子動作( "then"および "otherwise")があります。「いつ」が実行されると、条件をチェックし、その結果に応じて、then / otherwise childを実行します。条件の結果が変わると、実行中の子がリセットされ、他のブランチに対応する子が開始されます。子が実行を終了すると、「いつ」全体が実行を終了します。

重要な点は、シーケンスの開始時にのみ条件がチェックされるこの質問の最初のBTとは異なり、私の「いつ」は実行中に条件をチェックし続けるということです。したがって、ビヘイビアツリーの上部は次のように置き換えられます。

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

より高度な「When」の使用法については、指定された時間または無期限に(親の動作によってリセットされるまで)何もしない「待機」アクションを導入することもできます。また、 "When"のブランチが1つだけ必要な場合、他のブランチには "Success"または "Fail"アクションのいずれかを含めることができ、それぞれがすぐに成功および失敗します。


このアプローチは、BTの最初の発明者が念頭に置いていたものに近いと思います。より動的なフローを使用するため、BTの「実行中」状態は非常に危険な状態であり、まれにしか使用されません。いつでもルートに戻る可能性を常に念頭に置いて、BTを設計する必要があります。
v.oddou

0

私は遅れていますが、これが役立つことを願っています。主に、私もこれを理解しようとしているので、自分で何かを見逃していないことを確認したいからです。私は主からこのアイデアを借りてきましたUnrealが、それにすることなくDecorator、ベースのプロパティNodeまたは強く結びついてBlackboard、それはより一般的なのです。

これはと呼ばれる新しいノードタイプご紹介しますGuardの組み合わせのようなものDecorator、とCompositeして持っているcondition() -> Resultと一緒に署名をupdate() -> Result

またはがGuard返されたときにキャンセルが発生する方法を示す3つのモードがあり、実際のキャンセルは呼び出し元によって異なります。したがって、呼び出しの場合:SuccessFailedSelectorGuard

  1. キャンセル.self -> Guard実行中で条件が次の場合にのみ(および実行中の子)をキャンセルしますFailed
  2. キャンセル.lower- >のみが実行されている場合は優先順位の低いノードをキャンセルした状態でしたSuccessRunning
  3. キャンセル.both - >両方.self.lower条件に応じてノードを実行しています。実行中の場合は自己をキャンセルし、条件がの場合falseCompositeルール(Selectorこの場合)に基づいて優先度が低いと見なされる場合、実行中のノードに条件を付けるか、キャンセルしますSuccess。つまり、基本的に両方の概念を組み合わせたものです。

同様Decoratorに、Compositeそれは単一の子だけを取ります。

けれどもGuard、単一の子供を取るだけで、あなたは多くのように巣ができSequencesSelectorsまたは他のタイプをNodes含む他、あなたが望むようGuardsDecorators

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

上記のシナリオでは、Selector1更新するたびに、常にその子に関連付けられたガードに対して条件チェックを実行します。上記の場合、Sequence1は保護されておりSelector1runningタスクを続行する前にチェックする必要があります。

チェック中に戻るとすぐに、Selector2またはSequence1実行されているときはいつでも、割り込み/キャンセルを発行し、通常どおり続行します。EnemyNear?successGuards condition()Selector1running node

言い換えれば、いくつかの条件に基づいて「アイドル」または「攻撃」ブランチのいずれかに反応することができます。 Parallel

これにより、同じNode環境での実行に対して優先度の高いシングルを保護することもできますNodesComposite

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

場合はHumATune、長時間実行されNodeSelector2それがためではなかった場合は、必ず最初の1つをチェックしますGuard。したがって、NPCが草のパッチにテレポートされた場合、次回のSelector2実行時に、実行するためにチェックしGuardてキャンセルHumATuneしますIdle

芝生のパッチからテレポートされると、実行中のノード(Idle)をキャンセルし、HumATune

このように、意思決定Guardは、Guardそれ自体ではなく呼び出し元に依存しています。誰とみなされるかのルールはlower priority、呼び出し元に残ります。両方の例で、それがSelectorを構成するものを定義するのは誰lower priorityです。

あなたがComposite呼ばれたなら、あなたはRandom Selectorその特定の実装の中で規則を定義するでしょうComposite

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.