【Godot】ビヘイビアツリーでAIを設計する - GDScriptでの実装と敵AIへの応用

作成: 2026-02-08最終更新: 2026-07-09

Godotでビヘイビアツリー(BT)を使った柔軟なAI設計を解説。ステートマシンとの違い、4つのノードタイプ、Sequence/Selector/Leafの実装、敵AIツリーの組み立て、Blackboardパターンまで、コードと図で学びます。

敵AIを if 文の羅列で書いていくと、条件が増えるほど手がつけられなくなります。かといって ステートマシン だけでも、状態が増えると遷移が絡まって「状態爆発」に陥りがちです。「プレイヤーを見つけたら追って攻撃、いなければ巡回、体力が減ったら逃げる」——こうした 優先順位つきの複雑な行動 を整理して組めるのが ビヘイビアツリー(Behavior Tree, BT) です。

BTは、行動を 階層的な「タスクの木」 として表現します。この記事では、GDScriptで小さなBTをフルスクラッチ実装し、敵AIに応用するところまでを組み立てます。

Godotには標準のBT機能はありません。 この記事はGDScriptでの自作を通じて「仕組み」を理解する構成です。実務では beehave などのアドオンを使うと、ビジュアルエディタやデバッグ表示が揃っていて大規模でも楽になります。まず仕組みを掴み、必要に応じてアドオンへ、が王道です。

上から下へ枝分かれするビヘイビアツリーの階層構造。根ノードから枝、末端の実行ノードへと分岐するAIの判断の木のイメージ

この記事でわかること

  • BTと ステートマシンの違い(タスクの木 vs 状態の網)
  • BTを構成する 4つのノードタイプ(Sequence / Selector / Decorator / Leaf)
  • Sequence(AND)と Selector(OR)の 動きと実装
  • 敵AIツリー の組み立て(発見→戦闘/いなければ巡回)
  • Blackboard でノード間のデータを疎結合に共有する

Sponsored

ビヘイビアツリーとは(ステートマシンとの違い)

ステートマシンとBTは、どちらもAIの行動を管理しますが、 構造の考え方が違います

ステートマシンは状態が遷移矢印で絡み合う網、ビヘイビアツリーは根から末端へ整理された木、という2手法の対比図
  • ステートマシン(状態の網) :「巡回」「追跡」「攻撃」などの状態を、遷移でつなぐ。状態が増えると 遷移の数が爆発 して絡まりやすい
  • ビヘイビアツリー(タスクの木) :行動を 木構造 で表す。上ほど優先順位が高く、枝を足すだけで拡張でき、部分ツリーを 使い回せる

「まず攻撃を試し、ダメなら逃げ、それもダメなら巡回」のような 優先順位つきの判断 は、BTだと木の並び順でそのまま表現できます。単純なAIならステートマシンで十分ですが、行動が増えて複雑になったらBTの出番です。

4つのノードタイプ

BTは、たった 4種類のノード を組み合わせて作ります。

Sequence(順に全部AND)、Selector(成功まで試すOR)、Decorator(結果を加工)、Leaf(実処理)の4つのノードタイプの図
ノード役割結果
Sequence子を順に実行(AND)全部成功で SUCCESS/1つ失敗で FAILURE
Selector子を順に試す(OR)1つ成功で SUCCESS/全部失敗で FAILURE
Decorator子の結果を加工(反転・繰り返しなど)加工後の結果
Leaf実際の処理(移動・攻撃など)SUCCESS / FAILURE / RUNNING

各ノードは tick() で評価され、SUCCESS(成功)・FAILURE(失敗)・RUNNING(実行中)のいずれかを返します。RUNNING は「移動中でまだ終わってない」のような 継続中 を表す、BT独特の状態です。

すべての土台となる基底クラスから作ります(class_nameによる型登録 を使います)。

# bt_node.gd
class_name BTNode
extends Node

enum Status { SUCCESS, FAILURE, RUNNING }

# 子クラスでオーバーライドする評価メソッド
func tick(actor: Node, blackboard: Dictionary) -> Status:
    return Status.FAILURE
  • actor :このツリーを動かす本体(敵キャラなど)
  • blackboard :ノード間で共有するデータ辞書(後述)
Sponsored

SequenceとSelectorを実装する

制御の要となる2つを実装します。動きの違いを押さえるのが肝心です。

SequenceはANDで1つでも失敗したら中断し失敗、SelectorはORで1つ成功したら採用し成功、という実行の違いの図

Sequence(AND) は、子を順に実行し、 1つでも失敗したらそこで中断 します。「敵を見つける → 近づく → 攻撃」のような、 全部を順にやり遂げる 一連の行動に使います。

# bt_sequence.gd
class_name BTSequence
extends BTNode

func tick(actor: Node, blackboard: Dictionary) -> Status:
    for child in get_children():
        var status: Status = child.tick(actor, blackboard)
        if status == Status.FAILURE:
            return Status.FAILURE   # 1つでも失敗したら即中断
        if status == Status.RUNNING:
            return Status.RUNNING   # 実行中なら今フレームはここまで
    return Status.SUCCESS           # 全部成功

Selector(OR) は、子を順に試し、 1つ成功したらそこで採用 します。「回復する or 逃げる or 戦う」のような、 優先順位つきの選択 に使います。

# bt_selector.gd
class_name BTSelector
extends BTNode

func tick(actor: Node, blackboard: Dictionary) -> Status:
    for child in get_children():
        var status: Status = child.tick(actor, blackboard)
        if status == Status.SUCCESS:
            return Status.SUCCESS   # 1つ成功したら採用して終了
        if status == Status.RUNNING:
            return Status.RUNNING
    return Status.FAILURE           # 全部失敗

2つはちょうど鏡写しです。 Sequenceは「失敗で止まる」、Selectorは「成功で止まる」 ——ここさえ掴めば、あとは組み合わせるだけです。

Leafノードで実処理を書く

SequenceSelector が「どう判断するか」なら、Leaf「実際に何をするか」 です。移動・攻撃・待機など、具体的なゲーム処理を書きます。ターゲットへ移動する例を見てみましょう。

# bt_move_to_target.gd
class_name BTMoveToTarget
extends BTNode

@export var move_speed: float = 100.0
@export var arrival_distance: float = 10.0

func tick(actor: Node, blackboard: Dictionary) -> Status:
    var target: Node2D = blackboard.get("target")
    if target == null:
        return Status.FAILURE              # ターゲットがいない=失敗
    var dist := actor.global_position.distance_to(target.global_position)
    if dist <= arrival_distance:
        return Status.SUCCESS              # 到着=成功
    # まだ遠い=移動を続ける
    var dir := actor.global_position.direction_to(target.global_position)
    actor.velocity = dir * move_speed
    actor.move_and_slide()
    return Status.RUNNING                  # 移動中=継続

到着したら SUCCESS、まだ遠ければ RUNNING を返して次フレームも続ける——RUNNING があるおかげで、「移動が終わってから次のタスクへ」が自然に書けます。実際の経路移動は NavigationAgent2D に任せると、障害物も避けられます。 Leafは小さく単機能に 保つと、別のツリーでも使い回せます。

Sponsored

実践:敵AIツリーを組む

部品が揃ったので、組み合わせて敵AIを作ります。 プレイヤーがいれば戦闘、いなければ巡回 ——アクションでもRPGでもステルスでも通用する定番の形です。

Selectorを根に、戦闘Sequence(HasTarget→MoveToTarget→Attack)とPatrolが並ぶ敵AIのビヘイビアツリー実例図

木構造はこう組みます。ルートを Selector にすると、「まず戦闘を試し、ダメなら巡回」という優先順位が自然に表現できます。

Enemy (CharacterBody2D)
└─ BehaviorTree (BTSelector)          … まず上(戦闘)を試す
   ├─ CombatSequence (BTSequence)     … 戦闘は「全部そろって」成立
   │  ├─ HasTarget   (条件: ターゲットがいる?)
   │  ├─ MoveToTarget(近づく)
   │  └─ Attack      (攻撃)
   └─ Patrol         (巡回)            … 戦闘がダメなときの受け皿

本体は、毎フレーム「プレイヤーが近いか」を Blackboard に書き込み、ルートを tick() するだけです。

extends CharacterBody2D

@onready var bt_root: BTNode = $BehaviorTree
var blackboard := {}

func _ready() -> void:
    blackboard["patrol_points"] = [Vector2(100, 100), Vector2(300, 100)]

func _physics_process(_delta: float) -> void:
    # プレイヤー検出結果をBlackboardに反映(検出は別システムに委ねてもよい)
    var player := get_tree().get_first_node_in_group("player")
    var near := player and global_position.distance_to(player.global_position) < 200
    blackboard["target"] = player if near else null
    # ツリーを評価(この1行で全行動が動く)
    bt_root.tick(self, blackboard)

ポイントは2つです。

  • 優先順位はツリーの並び順で決まるSelector の子は上から試されるので、「戦闘」を「巡回」より上に置けば、プレイヤーがいる限り戦闘が優先されます。行動の優先度を変えたければ、 枝を並べ替えるだけ です。
  • 判断(BT)と検出・移動は分ける :「プレイヤーが見えるか」は ステルスの視界システム、「どう近づくか」は NavigationAgent2D に任せ、BTは 「どう振る舞うか」の判断 に集中させると、各パーツが小さく保てます。

Blackboardでデータを共有する

実践例に出てきた blackboard は、BTの定番パターンです。 ノード間で共有するデータを1つの辞書に集約 し、各ノードはそこを読み書きします。

中央のBlackboard(target/health/is_alerted)を、複数のノードが読み書きの矢印でやり取りする疎結合の図
# 書き込み(検出システムや本体が更新)
blackboard["target"] = player
blackboard["health"] = 78
blackboard["is_alerted"] = true

# 読み取り(各ノードのtick内で参照)
if blackboard.get("is_alerted"):
    # 警戒中の行動へ

ノード同士が 直接つながらず、黒板を介してやり取りする のが肝です。おかげで、MoveToTarget は「誰が target を書いたか」を知らなくても動きます。データの一元管理でデバッグもしやすく、シグナル と並ぶ疎結合の定番手法です。

Sponsored

おまけ:制限と発展

ここまではBTの基本形です。実戦投入の前に、次を知っておくと安心です。

  • RUNNINGの再開位置 :この実装は毎フレーム ルートから全評価 します。小さいツリーなら問題ありませんが、大きくなったら「前回RUNNINGだった位置」を覚えて、そこから再開する最適化を入れます。
  • Decoratorで表現力アップ :結果を反転する Inverter、繰り返す Repeater、時間制限をかける Timeout などのDecoratorを足すと、「◯秒だけ試す」「失敗を成功に読み替える」といった小回りが利きます。
  • アドオンという選択 :大規模なら beehave が有力です。ツリーを ビジュアルに組める ので、木が大きくなっても見通せます。まず自作で仕組みを理解しておくと、アドオンの挙動もすんなり掴めます。
  • 状態管理と併用 :BTとステートマシンは対立しません。大きな状態は ステートマシン、各状態の中の細かい行動はBT、と 組み合わせる のも定番です。

まとめ

  • BT は行動を「タスクの木」で表す設計手法。優先順位つきの複雑AIに強い(Godot標準機能はなく、自作かアドオン)
  • Sequence(AND) は1つ失敗で中断、Selector(OR) は1つ成功で採用——鏡写しの関係
  • Leaf に実処理を書き、RUNNING で「継続中」を表す。小さく単機能に保つと使い回せる
  • 敵AIツリー はルート Selector で「戦闘 → 巡回」の優先順位を表現
  • Blackboard でノード間のデータを疎結合に共有する

まずは Selector の下に「戦闘 Sequence」と「巡回 Leaf」を並べた小さなツリーから始めてみてください。木に枝を足すだけで行動が増えていく感覚が掴めたら、BTの強みが体感できます。

さらに学ぶために