敵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 でノード間のデータを疎結合に共有する
動作確認環境: Godot 4.7
ビヘイビアツリーとは(ステートマシンとの違い)
ステートマシンと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 = 40.0 # 体どうしがぶつかる距離より大きく
func tick(actor: Node, blackboard: Dictionary) -> Status:
var target: Node2D = blackboard.get("target")
if target == null:
return Status.FAILURE # ターゲットがいない=失敗
var dist: float = actor.global_position.distance_to(target.global_position)
if dist <= arrival_distance:
return Status.SUCCESS # 到着=成功
# まだ遠い=移動を続ける
var dir: Vector2 = 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 (巡回) … 戦闘がダメなときの受け皿
木に出てくる残りの3つの Leaf も、小さなスクリプトで書けます。class_name を付けたスクリプトは、ノードの追加ダイアログに BTSelector や BTHasTarget の名前で出てくるので、それを選んで上の木を組みます。
# bt_has_target.gd(条件:ターゲットがいるか)
class_name BTHasTarget
extends BTNode
func tick(_actor: Node, blackboard: Dictionary) -> Status:
return Status.SUCCESS if blackboard.get("target") else Status.FAILURE
# bt_attack.gd(攻撃:ここでは1秒ごとに表示するだけ)
class_name BTAttack
extends BTNode
var last_attack := -100.0
func tick(_actor: Node, _blackboard: Dictionary) -> Status:
var now := Time.get_ticks_msec() / 1000.0
if now - last_attack < 1.0:
return Status.RUNNING # 次の攻撃まで待つ
last_attack = now
print("攻撃!")
return Status.SUCCESS
# bt_patrol.gd(巡回:Blackboardの地点を順に回る)
class_name BTPatrol
extends BTNode
@export var move_speed: float = 60.0
var index := 0
func tick(actor: Node, blackboard: Dictionary) -> Status:
var points: Array = blackboard.get("patrol_points", [])
if points.is_empty():
return Status.FAILURE
var point: Vector2 = points[index]
if actor.global_position.distance_to(point) < 5.0:
index = (index + 1) % points.size() # 着いたら次の地点へ
actor.velocity = actor.global_position.direction_to(point) * move_speed
actor.move_and_slide()
return Status.RUNNING
本体は、毎フレーム「プレイヤーが近いか」を Blackboard に書き 込み、ルートを tick() するだけです(プレイヤーはグループ player に入れておきます)。
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"] = player
blackboard["health"] = 78
blackboard["is_alerted"] = true
# 読み取り(各ノードのtick内で参照)
if blackboard.get("is_alerted"):
# 警戒中の行動へ
pass
ノード同士が 直接つながらず、黒板を介してやり取りする のが肝です。おかげで、MoveToTarget は「誰が target を書いたか」を知らなくても動きます。データの一元管理でデバッグもしやすく、シグナル と並ぶ疎結合の定番手法です。
おまけ:制限と発展
ここまでは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の強みが体感できます。
さらに学ぶため に
- ステートマシンで管理するAIとプレイヤーの状態 ——BTと対になるもう1つのAI設計。併用も可
- ステルスゲームの視界システム ——BTに渡す「プレイヤーを見つけたか」の判定
- NavigationAgent2Dによるパスファインディング ——MoveToTargetの中身(経路移動)
- GDScriptで使うOOPデザインパターン ——class_nameと継承でノードを整理する
- 攻撃判定入門 ——攻撃のLeafに入れる当たり判定(同じ相手に何度も当てない)
- beehave - Godot Behavior Treeアドオン (GitHub) ——実務向けBTアドオン