敵AIを if 文の羅列で書いていくと、条件が増えるほど手がつけられなくなります。かといって ステートマシン だけでも、状態が増えると遷移が絡まって「状態爆発」に陥りがちです。「プレイヤーを見つけたら追って攻撃、いなければ巡回、体力が減ったら逃げる」——こうした 優先順位つきの複雑な行動 を整理して組めるのが ビヘイビアツリー(Behavior Tree, BT) です。
BTは、行動を 階層的な「タスクの木」 として表現します。この記事では、GDScriptで小さなBTをフルスクラッチ実装し、敵AIに応用するところまでを組み立てます。
Godotには標準のBT機能はありません。 この記事はGDScriptでの自作を通じて「仕組み」を理解する構成です。実務では beehave などのアドオンを使うと、ビジュアルエディタやデバッグ表示が揃っていて大規模でも楽になります。まず仕組みを掴み、必要に応じてアドオンへ、が王道です。
この記事でわかること
- BTと ステートマシンの違い(タスクの木 vs 状態の網)
- BTを構成する 4つのノードタイプ(Sequence / Selector / Decorator / Leaf)
Sequence(AND)とSelector(OR)の 動きと実装- 敵AIツリー の組み立て(発見→戦闘/いなければ巡回)
- Blackboard でノード間のデータを疎結合に共有する
ビヘイビアツリーとは(ステートマシンとの違い)
ステートマシンとBTは、どちらもAIの行動を管理しますが、 構造の考え方が違います 。

- ステートマシン(状態の網) :「巡回」「追跡」「攻撃」などの状態を、遷移でつなぐ。状態が増えると 遷移の数が爆発 して絡まりやすい
- ビヘイビアツリー(タスクの木) :行動を 木構造 で表す。上ほど優先順位が高く、枝を足すだけで拡 張でき、部分ツリーを 使い回せる
「まず攻撃を試し、ダメなら逃げ、それもダメなら巡回」のような 優先順位つきの判断 は、BTだと木の並び順でそのまま表現できます。単純なAIならステートマシンで十分ですが、行動が増えて複雑になったらBTの出番です。
4つのノードタイプ
BTは、たった 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:ノード間で共有するデータ辞書(後述)
SequenceとSelectorを実装する
制御の要となる2つを実装します。動きの違いを押さえるのが肝心です。

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ノードで実処理を書く
Sequence・Selector が「どう判断するか」なら、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は小さく単機能に 保つと、別のツリーでも使い回せます。
実践:敵AIツリーを組む
部品が揃ったので、組み合わせて敵AIを作ります。 プレイヤーがいれば戦闘、いなければ巡回 ——アクションでもRPGでもステルスでも通用する定番の形です。

木構造はこう組みます。ルートを 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の定番パターンです。