2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NASA月面ローバーも動かすRust製ビヘイビアツリー「bonsai」で追いかけっこAIを作る

2
Posted at

Rust のビヘイビアツリーライブラリ bonsai(crates.io では bonsai-bt)が、先日 crates.io で累計 10 万ダウンロードを突破しました。作者が r/rust に投稿した達成報告には、こんな採用事例が並んでいます。

  • NASA Lunabotics 2026:ユタ大学チームの月面ローバーの自律制御に採用。模擬月面でレゴリスを掘って運ぶ自律走行を bonsai のビヘイビアツリーが統括し、自律部門で 50 チーム中 3 位
  • Titanfall 2 の Mod サーバー:bp-ort(Northstar 用 NPC プラグイン)で、プレイヤー以外全員が bonsai 駆動のボット
  • Anotherland:MMORPG サーバーの NPC 行動制御

ロボットとゲーム AI の両方で実戦投入されているのが面白いところです。この記事ではbonsaiの基礎から、最後は bonsai を WASM にビルドし、ブラウザ上で 2 体のキャラクターに追いかけっこをさせるデモまで作ります。

デモ: 2体ともbonsaiのBTで動く追いかけっこ

👹(追う側)も 🐰(逃げる側)も、毎フレーム Rust の bonsai-bt が行動決定します

動作確認環境:bonsai-bt 0.13.0 / rustc 1.89 / wasm-pack 0.15 / macOS

ビヘイビアツリーとは

ビヘイビアツリー(Behavior Tree, BT)は、エージェントの行動ルールと実行順序を木構造で表現するデータ構造です。ゲーム NPC の AI として広まり、いまはロボティクス(ROS 界隈の BehaviorTree.CPP が有名)でも標準的に使われています。

各ノードは実行されると Success / Failure / Running のいずれかを返します。毎フレーム(tick)ルートから深さ優先で木をたどり、子の結果を見て親が次にどのノードを実行するか決める——これだけのシンプルな仕組みです。

有限ステートマシン(FSM)と比べたときの利点は、ロジックが育ったときの管理しやすさに尽きます。

FSM ビヘイビアツリー
ノード間の関係 多対多(遷移が絡み合う) 一対多(木構造)
優先度の表現 遷移条件に埋め込む 左から右への並び順そのもの
部品の再利用 状態が文脈に密結合しがち サブツリーごと使い回せる

FSM は状態が 10 個を超えたあたりから遷移の網が手に負えなくなりますが、BT は「左にあるものほど優先」という規約が構造に埋め込まれているので、行動を追加しても見通しが保てます。

bonsai の基本

ツリーは Behavior enum を入れ子にして宣言的に組みます。主なコンビネータ:

Sequence([A, B])        // A が成功したら B へ
Select([A, B])          // A が失敗したら B を試す
If(cond, A, B)          // cond 成功なら A、失敗なら B
Invert(A)               // 成否を反転
While(cond, [A, B])     // cond が Running の間 A, B を繰り返す
WhileAll(cond, [A, B])  // A, B を全部完了するごとに cond をチェック
WhenAll([A, B])         // 並列実行して両方の成功を待つ
WhenAny([A, B])         // 並列実行してどれかの成功を待つ
Race([A, B])            // 並列実行して成否問わず最初の完了を待つ(v0.11 で追加)

WhenAll / WhenAny / Race のような並列セマンティクスを最初から持っているのが bonsai の特徴です。「充電残量の監視と経路走行を並走させ、どちらかが終わったら次へ」のようなロボット的な要求が、スレッドを自分で管理せずツリーの宣言だけで書けます。

もうひとつの部品が Blackboard(黒板)。ツリー内の全ノードが読み書きできる共有メモリで、任意の型をジェネリクスで渡せます。

そして bonsai の設計の肝は、ツリーの構造(宣言)と処理の実装が完全に分離していること。Action の中身は自分で定義した enum のバリアントだけで、実際の処理は tick に渡すクロージャに書きます。

let status = bt.tick(&e, &mut |args, blackboard| {
    match *args.action {
        EnemyNPC::Shoot => {
            state.perform_action("shoot");
            blackboard.times_shot += 1;   // 黒板に書き込み
            (Success, 0.0)
        }
        EnemyNPC::Rest => {
            if state.fully_rested() {
                return (Success, 0.0);
            }
            state.rest();
            (Running, 0.0)  // Running を返すと次の tick でまたここに来る
        }
        // ...
    }
});

まず公式サンプル simple_npc_ai(アクションポイント制の NPC)を動かしてみると、Rest が Running を返している間は制御がメインループに戻り、tick のたびに回復が 1 ずつ進む——「長い処理を Running で分割する」という BT の感覚がすぐつかめます。

NPC has action points: 3
Performing action: run. Action points: 2
Performing action: shoot. Action points: 1
...
Rested for a while... Action points: 3
NPC died...
NPC shot 2 times during the simulation.

get_graphviz出力について

公式サンプルには bt.get_graphviz() でツリーを Graphviz 出力するコードがありますが、そのままだと 0.13.0 ではコンパイルエラーになります。可視化系 API は visualize feature に移動しているためです。

[dependencies]
bonsai-bt = { version = "0.13.0", features = ["visualize"] }

これで get_graphviz() が実行可能です。

デモ:BT 同士の追いかけっこをブラウザで動かす

ここからが本題です。bonsai-bt は wasm32-unknown-unknown でそのままビルドできるので、Rust で書いた BT をブラウザの three.js シーンに直結できます。冒頭の GIF がその結果です(キャラクターは開発中の自作ゲームの box-person モデルを流用しています。)コード全体は yonaka15/bonsai-chase-demo に置きました。

構成:

  • Rust (WASM):2 体分の BT と世界状態(位置・スタミナ)。毎フレーム tick(dt, 相手の座標) を呼ぶと行動決定+移動積分して状態を返す
  • JS (three.js):返ってきた状態を描画するだけ。行動ロジックは書かない

wasm ビルドで唯一必要だったワークアラウンドは getrandom の問題だけでした。

[dependencies]
bonsai-bt = "0.13.0"
getrandom = { version = "0.2", features = ["js"] }  # wasm32 ビルドに必要
wasm-bindgen = "0.2"

逃げる側のツリー

「ハンターが近い間は逃げる」>「力尽きたら座り込む」>「それ以外は徘徊」という優先度を、そのまま Select の並び順で表現します。

use bonsai_bt::{Action, Select, Sequence, BT};

let tree = Select(vec![
    Sequence(vec![Action(HunterNear), Action(CanFlee), Action(Flee)]).memory(false),
    Sequence(vec![Action(NeedRest), Action(Rest)]).memory(false),
    Action(Wander),
])
.memory(false);

ポイントは .memory(false)(v0.13 で入った memoryless composite)です。通常の Select は Running 中の子を次の tick でも再開しますが、memoryless にすると毎 tick 左から再評価されます。つまり徘徊中にハンターが接近した瞬間、次の tick で Flee が徘徊を横取り(プリエンプト)します。優先度付き反応 AI はこれだけで書けます。

追う側も同じ形のツリーで、Flee が Chase に変わるだけです。両者のパラメータ差——逃げ足 3.6 に対して追い足 1.7、ただし逃げる側はスタミナ切れが早い——から、「元気なうちは絶対捕まらないが、疲れて座り込んだところに追いつかれる」という追いかけっこのドラマが創発します。どちらのキャラにも「追いかけっこをしろ」とはどこにも書いていません。

ハマったところ:memoryless Select はチャタリングする

毎 tick 再評価は強力ですが、条件が境界値で振動すると行動も振動します。

その 1:FLEE↔REST の高速振動。 「スタミナ > 0 なら逃げられる」という条件にしていたら、座り込んで 0.01 回復した瞬間また逃げ出し、即座に力尽きて座り……を毎秒数回繰り返す壊れた動きに。対策はヒステリシスで、「一度力尽きたら 55% 回復するまで逃走不可」というフラグを黒板に持たせました。

// tick の冒頭で更新(力尽きた ⇔ 十分回復、の間に不感帯を作る)
if bb.stamina <= 0.0 {
    bb.exhausted = true;
} else if bb.stamina >= 0.55 {
    bb.exhausted = false;
}

その 2:逃走半径の境界振動。 逃げて半径 1.8 の外に出た瞬間 Wander に戻り、ハンターが 1 歩詰めてまた Flee……と、境界線上で行動がパタパタ切り替わる。これも「逃走開始 1.8 / 解除 2.4」と閾値を 2 つに分けて解決しました。

BT 自体の問題ではなく反応型 AI の普遍的な罠ですが、「条件ノードの閾値には不感帯を持たせる」は memoryless composite を使うときの必須テクだと思います。

JS 側は状態を受け取って描くだけ

import init, { Npc, Hunter } from './pkg/bonsai_vrm_demo.js';

await init();
const prey = new Npc(42);
const hunter = new Hunter(7);

let sPrey = { x: 0, z: 0 }, sHunter = { x: -1.8, z: -1.8 };
function frame(now) {
  const dt = Math.min((now - last) / 1000, 0.05);
  sPrey = prey.tick(dt, sHunter.x, sHunter.z);   // Rust の BT が行動決定
  sHunter = hunter.tick(dt, sPrey.x, sPrey.z);
  // → 返ってきた {x, z, heading, action, stamina} を three.js で描画
}

HUD にはツリーをそのまま表示し、アクティブなノードをハイライトしています。BT の「いまどの枝が生きているか」が見えると、デバッグが劇的に楽になります——今回は簡易的ですが、次に紹介する公式のライブ可視化機能もあります。

ライブ可視化と Python バインディング

最近のリリースで大きいのはこの 2 つです。

v0.12.0:ライブ可視化。visualize feature を有効にすると、実行中のツリーをブラウザからリアルタイムに眺められます。どのノードが Running でどこで Failure が起きたかが視覚的に追えるので、BT デバッグの printf との戦いから解放されます。使い方はビルダーを 1 行足すだけです。

let mut bt = BT::new(tree, world).with_telemetry(8910)?; // あとは普通に tick するだけ

実際に今回のデモの「逃げる側」ツリーをネイティブで動かし、http://127.0.0.1:8910/ を開いたのがこの画面です。

bonsai公式ライブ可視化

FLEE 中の瞬間。HunterNear と CanFlee が Success(緑)、Flee までの経路が Running(黄)に光り、休憩系の枝は未訪問のまま——「いまどの枝が生きているか」が一目で分かる

v0.13.0:Python バインディング。pip install bonsai-bt で Python からも同じツリーが使えます。Rust で書いたコアを Python のプロトタイピングから触れるのは、ロボティクス用途だと特に便利そうです。

長時間タスクの扱い

BT の鉄則は「アクションは即座に返す」です。tick がブロックするとツリー全体が止まるので、秒単位かかる処理(経路計画、API 呼び出しなど)はバックグラウンドスレッドに逃がし、channel 越しに状態を受け取って Running を返し続けます。公式の async_drone サンプル も参考にしてみてください。

まとめ

  • bonsai は Rust 製ビヘイビアツリーライブラリ。10 万 DL、NASA Lunabotics 3 位のローバーから Titanfall 2 ボットまで実戦投入例あり
  • ツリー宣言(Behavior enum)と処理実装(tick クロージャ)が分離した設計。並列セマンティクスと Blackboard を標準装備
  • .memory(false) の memoryless composite で優先度プリエンプションが書ける。ただし条件の閾値にはヒステリシスを
  • wasm32 でそのままビルドできるので、ブラウザのデモ・ゲームにも直結できる
  • get_graphviz() など可視化系は visualize feature が必要

参考リンク

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?