概要
動作確認環境: Godot 4.3+
複雑な敵AIやNPCの行動を設計する際、if文の羅列やステートマシンだけでは管理が難しくなります。ビヘイビアツリー(Behavior Tree, BT)は、タスクの優先順位や条件判断を階層的に構造化することで、再利用可能で拡張しやすいAI設計を実現します。
tips: Godotにはビヘイビアツリーの標準機能がありません。本記事ではGDScriptでフルスクラッチ実装する方法を解説しますが、実務ではbeehaveなどのアドオンを利用する選択肢もあります。アドオンはビジュアルエディタやデバッグツールが充実しており、大規模プロジェクトでの生産性が高くなります。
この記事では、GDScriptで基本的なビヘイビアツリーを実装し、敵AIに適用する方法を解説します。
ビヘイビアツリーの基 本構造
まずはビヘイビアツリーを構成する4つのノードタイプを押さえましょう。たった4種類の部品を組み合わせるだけで、「敵を発見したら近づいて攻撃、いなければパトロール」といった複雑な行動を階層的に表現できます。
| ノードタイプ | 役割 | 戻り値 |
|---|---|---|
| Sequence | 子ノードを順番に実行し、すべて成功したらSUCCESS | 1つでも失敗したらFAILURE |
| Selector | 子ノードを順番に試し、1つでも成功したらSUCCESS | すべて失敗したらFAILURE |
| Decorator | 子ノードの結果を加工(反転、繰り返しなど) | 加工後の結果 |
| Leaf | 実際の処理を行う末端ノード(移動、攻撃など) | SUCCESS/FAILURE/RUNNING |
各ノードはtick()メソッドで評価され、SUCCESS、FAILURE、RUNNINGのいずれかを返します。
基底ノードクラスの実装
ノードタイプの全体像を把握したところで、実装に入りましょう。まずはすべてのノードの基底クラスを定義します。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: ノード間で共有するデータ辞書tick(): 毎フレーム呼ばれる評価メソッド
Sequenceノードの実装
基底クラスができたら、次は制御ノードを実装していきます。Sequenceはすべての子ノードが成功するまで順番に実行するノードです。AND論理に相当し、「一連の行動をすべて完了させる」という動作を表現します。
# bt_sequence.gd
class_name BTSequence
extends BTNode
func tick(actor: Node, blackboard: Dictionary) -> Status:
for child in get_children():
var status = child.tick(actor, blackboard)
if status == Status.FAILURE:
return Status.FAILURE # 1つでも失敗したら中断
elif status == Status.RUNNING:
return Status.RUNNING # 実行中は待機
return Status.SUCCESS # すべて成功
使用例: 「敵を見つける → 近づく → 攻撃する」のような一連の行動。
Selectorノードの実装
成功する子ノードを探して順番に試します(優先順位制御)。OR論理に相当し、「複数の選択肢から最初に成功した行動を採用する」という動作を表現します。
# bt_selector.gd
class_name BTSelector
extends BTNode
func tick(actor: Node, blackboard: Dictionary) -> Status:
for child in get_children():
var status = child.tick(actor, blackboard)
if status == Status.SUCCESS:
return Status.SUCCESS # 1つでも成功したら終了
elif status == Status.RUNNING:
return Status.RUNNING # 実行中は待機
return Status.FAILURE # すべて失敗
使用例: 「攻撃する or 逃げる or パトロールする」のような優先順位付き行動選択。たとえばアクションRPGでは、「体力が低ければ回復アイテムを使う → それがなければ逃げる → 逃げ場がなければ戦う」という優先順位をSelectorで表現できます。
Leafノードの実装
SequenceとSelectorが「どう判断するか」を決める制御ノードだったのに対し、Leafノードは「実際に何をするか」を担当します。移動、攻撃、待機といった具体的なゲーム処理を実装する場所で、ツリーの葉(末端)に配置されます。
以下は、ターゲットに向かって移動する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 = blackboard.get("target")
if not target:
return Status.FAILURE # ターゲットがいない
var distance = actor.global_position.distance_to(target.global_position)
if distance <= arrival_distance:
return Status.SUCCESS # 到着
# 移動処理
var direction = (target.global_position - actor.global_position).normalized()
actor.velocity = direction * move_speed
actor.move_and_slide()
return Status.RUNNING # 移動中
実践: 敵AIの実装例
ここまでで、必要な部品が一通り揃いました。それでは実際にこれらを組み 合わせて、敵AIを作ってみましょう。プレイヤーを発見したら戦闘モードに入り、いなければパトロールを続ける――よくあるアクションゲームの敵キャラを実装します。
# enemy.gd
extends CharacterBody2D
@onready var bt_root = $BehaviorTree
var blackboard = {}
func _ready():
# Blackboardの初期化
blackboard["target"] = null
blackboard["patrol_points"] = [Vector2(100, 100), Vector2(300, 100)]
blackboard["current_patrol_index"] = 0
func _process(delta):
# プレイヤー検出(簡易実装)
var player = get_tree().get_first_node_in_group("player")
if player and global_position.distance_to(player.global_position) < 200:
blackboard["target"] = player
else:
blackboard["target"] = null
# ツリーの実行
bt_root.tick(self, blackboard)
シーンツリー構成例:
Enemy (CharacterBody2D)
└─ BehaviorTree (BTSelector)
├─ CombatSequence (BTSequence)
│ ├─ HasTarget (BTCondition)
│ ├─ MoveToTarget (BTMoveToTarget)
│ └─ Attack (BTAttack)
└─ Patrol (BTPatrol)
動作: プレイヤーが近ければ戦闘行動、いなければパトロールを実行。
Blackboardパターン
実践例の中でblackboardという辞書が登場しましたが、これはビヘイビアツリーの定番パターンです。Blackboardを使うことで、各ノードが直接他のノードに依存することなく、必要なデータを読み書きできます。
以下の ように、AIが参照する情報をBlackboardに格納して管理します。
# 使用例
blackboard["target"] = player
blackboard["health"] = 100
blackboard["is_alerted"] = true
# ノード内で参照
if blackboard.get("is_alerted"):
# 警戒行動
メリット:
- ノード間の疎結合化
- データの一元管理
- デバッグが容易
制限事項と発展
ここまでの実装はシンプルなBTの基本形です。実際のゲームに組み込む前に、以下の課題を把握しておきましょう。
- RUNNING状態の再開位置: 本実装ではRUNNINGを返したノードの位置を記憶していないため、毎フレーム全ツリーをルートから再評価します。大きなツリーでは、前回RUNNINGだったノードのインデックスを保存して再開位置を管理する最適化が必要です
- Decoratorノード: 結果の反転(Inverter)、繰り返し(Repeater)、タイマー制限などのDecoratorを追加すると表現力が向上します
- アドオンの活用: beehave はビジュアルエディタ、デバッグオーバーレイ、豊富なノードタイプを提供しており、大規模プロジェクトでの採用を推奨します
まとめ
- ビヘイビアツリーは階層的なタスク制御でAIを設計する手法(Godot標準機能ではなくスクラッチ実装またはアドオンが必要)
- Sequenceはすべての子ノードを順番に実行(AND論理)
- Selectorは成功する子ノードを探す(OR論理)
- Leafノード に実際の処理(移動、攻撃など)を実装
- Blackboardでノード間のデータ共有を効率化
- 単一責任の原則でLeafノードを小さく保つと再利用性が高まる
- RUNNING状態の再開位置管理は実務上の重要な最適化ポイント