Behavior Tree と Blackboard¶
一言で言うと¶
- Behavior Tree(BT、行動ツリー): AIの行動選択を「毎フレーム、木の根から評価し直す」ツリーで表す。優先度と条件分岐の構造化に強く、FSMの「遷移の絡まり」を解消する
- Blackboard(黒板): AIが使うデータ(標的、目的地、警戒度)を1つの共有メモ帳に集め、ノード間・システム間で受け渡すパターン。BTとセットで使われることが多い
解決したい問題(FSMの限界)¶
FSMは「今の状態」から書きますが、AIの仕様は大抵優先度で書かれます:「HPが低ければ逃げる。さもなくば敵が見えれば攻撃。さもなくば巡回」。FSMでこれをやると、全状態から全状態への遷移が必要になり(逃げるへは全状態から行ける…)、遷移の網が絡まります。
Behavior Tree の構造¶
[Selector(優先度順に試す。成功したら止まる)]
│
┌────────────────┼──────────────────┐
[Sequence] [Sequence] [Patrol(Action)]
逃げる系 攻撃系 (最後の砦: 必ず成功)
│ │
├[HP<30%?] ├[敵が見える?] ← Condition(判定)
└[逃げる] ├[近づく] ← Action(実行)
└[攻撃]
- Selector(選択): 子を順に試し、最初に成功(または実行中)の子を採用 = 優先度
- Sequence(逐次): 子を順に全部実行。1つ失敗したら失敗 = 手順
- Action/Condition(葉): 実際の行動・判定。戻り値は Success / Failure / Running(実行中: 複数フレームかかる行動の表現)
毎フレーム根から評価し直すのが核心です。巡回中でも、次のフレームでHPが下がれば自動的に「逃げる」へ移る——遷移を書かずに優先度の割り込みが手に入る。
// C++20 — 最小のBT
#include <memory>
#include <vector>
enum class BtStatus { Success, Failure, Running };
class BtNode {
public:
virtual ~BtNode() = default;
virtual BtStatus Tick(class Blackboard& bb) = 0;
};
class Selector : public BtNode {
public:
void Add(std::unique_ptr<BtNode> c) { children_.push_back(std::move(c)); }
BtStatus Tick(Blackboard& bb) override {
for (auto& c : children_) {
BtStatus s = c->Tick(bb);
if (s != BtStatus::Failure) return s; // 成功/Runningの子で確定
}
return BtStatus::Failure;
}
private:
std::vector<std::unique_ptr<BtNode>> children_;
};
class Sequence : public BtNode {
public:
void Add(std::unique_ptr<BtNode> c) { children_.push_back(std::move(c)); }
BtStatus Tick(Blackboard& bb) override {
for (auto& c : children_) {
BtStatus s = c->Tick(bb);
if (s != BtStatus::Success) return s; // 失敗/Runningで中断
}
return BtStatus::Success;
}
private:
std::vector<std::unique_ptr<BtNode>> children_;
};
検証済みサンプル: samples/behavior_tree.cpp(Composite構造そのもの)
Blackboard¶
// C++20 — 型消去を避けた最小形(実務ではキーごとに型付きアクセサを持たせる)
struct Blackboard {
// AIの知識をここに集約。ノードは黒板とだけ話す
float selfHp = 100;
bool canSeePlayer = false;
Vec2 lastSeenPos{};
int targetId = -1;
};
- ノード同士が直接通信しない(黒板経由)ため、ノードの再利用性が上がる(「近づく」ノードは
targetIdさえあれば誰のBTでも動く) - センサー系(視界判定)が黒板に書き、行動系が読む、という分業ができる
- 代償: 黒板は型のないグローバル変数の束になりがち。キーのタイポ・型違い・「誰が書いたか分からない」問題(実務ではキー定義の一元化とデバッグ表示が必須)
FSM vs BT の使い分け¶
| FSM | Behavior Tree | |
|---|---|---|
| 得意 | 排他的な状態と明示的遷移(キャラの動作、画面) | 優先度つき行動選択(AIの意思決定) |
| 割り込み | 遷移を書いた分だけ | 構造から自動(毎フレーム再評価) |
| 状態の記憶 | 自然(今の状態が明確) | 苦手(Runningの管理・「さっき何してた」はBlackboardに置く) |
| 規模の伸び | 遷移が絡まる | ツリーの深さで管理可能 |
実務では併用が普通: 意思決定はBT、実行中の動作(ジャンプ・攻撃モーション)はFSM/アニメステートマシン。
Unity/C# との対応¶
- 標準機能にはBTはない。アセット(Behavior Designer、NodeCanvas)、Unity Behavior(公式パッケージ)、または自作
- ScriptableObjectでノードをアセット化すると、企画がツリーをエディタで組める
Unreal Engine との対応¶
- Behavior Tree + Blackboard は標準搭載でUE AIの中核(BTノードをBPで拡張、BlackboardアセットでキーをGUI定義、デバッガ付き)。UEのAI Perception(視界・聴覚)が黒板へ書き、BTが読む、という上の分業がそのまま製品化されている
- UE5ではStateTreeという代替も登場(FSM+BTのハイブリッド)
使う場面 / 使わない場面¶
- 使う場面: 優先度・条件の組み合わせが多い敵AI・NPC。仕様が「〜なら〜する。さもなくば〜」形式で書かれるもの。企画がAIを調整するワークフロー
- 使わない場面: 行動が2〜3個の単純な敵(FSMかswitchで十分——雑魚全員にBTは過剰)。1フレームも無駄にできない大量AI(毎フレーム根から評価は数百体で重くなる。イベント駆動化・評価間引きが必要)。厳密な状態管理が主で優先度割り込みがない場合(FSM向き)
よくある誤解¶
- 「BTはFSMの上位互換」— 状態の記憶と明示的遷移はFSMの方が得意。問題の形で選ぶ
- 「Blackboardは何でも置いていい場所」— グローバル変数の束と同じ危険を持つ。書き込み責任(どのセンサーがどのキーを書くか)を決めて運用する
関連項目¶
理解度チェック¶
- BTが「遷移を書かずに割り込みを実現する」仕組みは何ですか。
- Runningステータスは何のためにありますか。
- Blackboardの利点と、それがもたらす危険は表裏一体です。説明してください。
演習¶
samples/behavior_tree.cpp に「HPが30%未満なら逃げる(最優先)」の枝を追加し、巡回中にHPを下げると次のTickで逃げ始めることを確認してください。同じ仕様をFSM(samples/fsm_guard.cpp)に足す場合に必要な遷移の本数を数えて比較してください。
前: ステートマシン(FSM/HSM) | カテゴリ目次 | 次: Command Buffer と入力処理