名称 | MIRS2403 Behavior Treeを用いた全体制御 |
---|---|
番号 | MIRS2403-REPT-0001 |
版数 | 最終更新日 | 作成 | 承認 | 改訂記事 |
---|---|---|---|---|
A01 | 2025.2.12 | 中村 介 | 第1版 |
MIRS2403 華蟻projectではBehaviorTreeを用いての全体制御を実装した。その詳細について記述する。
BehaviorTreeとはロボットやコンピュータゲーム内の仮想エンティティなどの自律エージェント内の様々なタスク間の切り替えを構造化する方法である。
さらに簡単に言えばフローチャートの書き方を変えたものである。
最も頻繁に使用されているはゲームの敵AIなどであり、これらを現実のロボットの制御に利用するために輸入された物を利用している。
利点としては「階層構造を持っており、複雑な動作の実装が容易」「グラフィカルな表現ができる」などがある。モジュール性に優れているため、プログラムをモジュールとして扱うROS2との相性が良い(概念的には)
名前が長いため今後BehaviorTreeはBTと記述する。
シンプルな構造のBTを以下に示す。これは冷蔵庫をあけ、ビールを取り出し、冷蔵庫を閉める動作を記述している。
BehaviorTreeでは各タスクを「ノード」と呼んでいる。このノードとROS2のノードはまったく別のものであるため、調べる際は注意する必要がある。
また、上部のノードを親、その下のノードを子と呼ぶ。上図の場合OpenDoorなどが子であり、Sequenseがその親となる。
BTが起動すると定期的に「ティック」と呼ばれる信号がスタート位置(Root)から送られ、伝わっていく。
ティック信号を受信したノードは以下のいずれかのコールバックを返す
末端のノードは「LeafNodes」と呼ばれ、実際の動作コマンドにあたる。それ以外は流れを決定するためのノードである。
ノードには以下の種類がある
ノードの種類 | 子供の数 | 説明 |
---|---|---|
ControlNode | 1~N | 子供の動作を管理するノード。子供のコールバックに応じて動作を変えることが多い |
DecoratorNode | 1 | 子供の結果を変更したり、繰り返し動作を指せたりするノード |
ConditionNode | 0 | 条件分岐など |
ActionNode | 0 | 実際の動作を行うノード |
最低限の理解を支えるために代表的なものを紹介する
最も基本的で頻繁に使用されるControlNode。子要素の動作を順番付け、常に左から実行する。
動作要件
頻繁に使用されるControlNodeその2。うまくいく動作が見つかるまで様々な戦略を試すことが目的
動作要件
これらがわかれば大体の動作の流れはわかるはず
BTを作成していると当然ノードをまたいで変数を使いたくなる時がある。そんな時に利用するのがBlackBoardとPortである。
BT内で共有される変数を格納する場所。グローバル変数だと思えばいい。
BlackBoardの変数をノード間で受け渡すための手段。関数の引数だと思えばいい。
BehaviorTreeCPPはこれまでに話した内容をC++で記述し、利用できるようにしたライブラリである。今回はこれを利用している。
ライブラリ自体はC++で記述されているが、BehaviorTreeの構造はXML形式で記述されている。
直接XMLを書くことも可能だが、Grootというアプリを使うことでGUIを用いたBTの作成が出来る。ただしノード単体の動作についてはC++で作成する必要がある。
BehaviorTreeCPPにはいくつかバージョンがあるが、今回はv3を利用している。Grootは無印。
BehaviorTreeCPPおよびGrootのインストールは以下のサイトを参考にして行った。
https://zenn.dev/tasada038/articles/b7d193b567b94a
以下は個人的な思想です。この思想を基にプログラムを構成しているため華蟻のプログラムの理解のための参考としてお使いください。
BehaviorTreeはあくまでもC++のライブラリであり、全体動作は一つのmainファイルが実行されているだけという理解が重要。ROS2と接続するというのは単純にこのライブラリと実行プログラムをROS2から起動できるようにしているだけ。
直接BehaviorTreeの内部で各機能ごとにノードを生成し、実行することもできるがこれは個人的にお勧めできない。
なぜならROS2のノードは同じ名前で複数定義出来ないため、ノードを新たに生成すると同じ動作を繰り返させたときにエラーが起こるからである。複数回動作をさせないことで解決もできるが、複数回同じ動作をさせないことはBehaviorTreeの利点を消すことになる。
そこでBehaviorTree全体で一つのノードを共有し、このノードに対してsubscriberやpublisher等の機能を持たせていく構成が望ましい。
また、BehaviorTreeはmain関数内でtickを回しているため、よくあるspinでROS2ノードの動作を回す記述と相性が悪い。
(出来るだけtickのループを留めたくないが、ROS2のspinを枝の末端で回すと根本にあるtickのループが止まってしまう。)
そこで華蟻ではBehaviorTreeのmainループ内にtickに加えてROS2のrclcpp.spin_once()を追加することで同時に二つのループを実行できるようにしている。
Navigation2もBehaviorTreeを採用しており、Nav2内部のBehaviorTreeに追記していく形で全体動作を記述することもできる。
しかし個人的にはNav2はあくまでも自動走行単体のためのアプリケーションとして利用したく、全体制御役は別で用意するべきであると考えている。(上司と部下は明確に分かれているべきという思想)
よって別で全体制御のBehaviorTreeをたて、その中でNav2のノードをインポートして利用できるようにしている。
ノードの実装例を以下に示す。これはパブリッシュを指定回数行うActionノードである。ヘッダーファイルとして記述してmainにインポートして利用する。
簡単に構成を説明すると
PublishTest(const std::string& name, const NodeConfiguration& config):SyncActionNode(name, config){}
でノードの定義と初回起動時の動作内容を記述。
static PortsList providedPorts(){}
でポートの設定。
NodeStatus tick() override{}
でティック時の動作を記述している。
mainの実装例を以下に示す。
内容は単純で、作成したノードをインポートしてからmain関数内で登録し、tickRootとspin_onceをループ実行させているだけ。
ノードの登録方法についてはregisterNodeType
を利用した方法とregisterBuilder
を利用した方法がある。
Builderを利用することでノードの引数にROS2の共有ノードのアドレスを渡せる。これを公式は推奨しているが、NodeBuilderを定義する必要があるため記述が長く面倒くさい。
そのため初めの知識がないときに実装したノードや急いで実装したノードは利用していない。
華蟻のBehaviorTreeは以下のようになっている。これは一台だけ椅子を運ぶ動作を行う。