ビヘイビアツリーの基本概念
ビヘイビアツリー(Behavior Tree)は、AIの意思決定を木構造で表現する手法で、ゲーム開発やロボット制御の分野で広く使われている。複雑な行動ロジックを小さなノードに分割し、それらを組み合わせることで、拡張性と再利用性に優れたAIシステムを実現する。
各ノードは評価時に「成功」「失敗」「実行中」のいずれかのステータスを返す。ツリーはルートから深さ優先で評価され、途中で「実行中」が返るとそのフレームの処理を中断し、次のフレームで継続できる。
ノードの種類
- コンポジットノード(Composite): 子ノードを順序付きで評価する。代表的なものにシーケンス(すべて成功すれば成功)やセレクター(いずれかが成功すれば成功)がある。
- デコレータノード(Decorator): 子ノードを一つだけ持ち、その結果を加工したり実行条件を変えたりする。
- リーフノード(Leaf): 条件評価を行うConditionノードと、実際の行動を実行するActionノードに分かれる。
TypeScriptによるビヘイビアツリーの実装
以下では、最小限のビヘイビアツリーライブラリをTypeScriptで実装する。状態や基本クラスから定義し、最終的にキャラクターAIを構築する。
// ノードの実行結果を表す列挙型
enum BTStatus {
Running,
Success,
Failure
}
// 全ノードの基底クラス
abstract class BTNode {
constructor(public readonly name: string) {}
abstract evaluate(context: any): BTStatus;
}
// 子ノードを持つコンポジットの基底クラス
abstract class BTComposite extends BTNode {
protected children: BTNode[] = [];
addChild(child: BTNode): void {
this.children.push(child);
}
}
// シーケンスノード:子を順に実行し、一つでも失敗すれば即座にFailure
class BTSequence extends BTComposite {
evaluate(context: any): BTStatus {
for (const child of this.children) {
const status = child.evaluate(context);
if (status === BTStatus.Failure || status === BTStatus.Running) {
return status;
}
}
return BTStatus.Success;
}
}
// セレクターノード:子を順に実行し、一つでも成功すれば即座にSuccess
class BTSelector extends BTComposite {
evaluate(context: any): BTStatus {
for (const child of this.children) {
const status = child.evaluate(context);
if (status === BTStatus.Success || status === BTStatus.Running) {
return status;
}
}
return BTStatus.Failure;
}
}
// 条件ノード:指定された述語をもとに成功/失敗を返す
class BTCondition extends BTNode {
private predicate: (context: any) => boolean;
constructor(name: string, predicate: (context: any) => boolean) {
super(name);
this.predicate = predicate;
}
evaluate(context: any): BTStatus {
return this.predicate(context) ? BTStatus.Success : BTStatus.Failure;
}
}
// 行動ノードの抽象クラス
abstract class BTAction extends BTNode {
constructor(name: string) {
super(name);
}
}
キャラクターAIの構築例
上記のノードを用いて、ゲーム内キャラクターの簡単なAIを組み立てる。このキャラクターは以下の優先順位で行動する。
- 戦闘不能(死亡)状態なら、倒れモーションを再生する。
- 敵を発見しているなら、攻撃行動をとる。
- 巡回経路が設定されていれば巡回する。
- どれにも該当しなければ待機する。
実装は次のようになる。contextオブジェクトには、キャラクターの状態(isDefeated, hasEnemy, patrolPathなど)や、アクションを実行するためのメソッドを持たせる。
function buildAI(): BTNode {
const root = new BTSelector("Root");
// 戦闘不能シーケンス
const defeatedSeq = new BTSequence("DefeatedSequence");
defeatedSeq.addChild(new BTCondition("IsDefeated", (ctx) => ctx.agent.isDefeated));
defeatedSeq.addChild(new BTAction("PlayDefeat") {
evaluate(ctx: any): BTStatus {
ctx.agent.playDefeatAnimation();
return BTStatus.Running; // 倒れアニメーション中は継続
}
});
// 戦闘シーケンス
const combatSeq = new BTSequence("CombatSequence");
combatSeq.addChild(new BTCondition("HasEnemy", (ctx) => ctx.agent.hasEnemy));
combatSeq.addChild(new BTAction("Attack") {
evaluate(ctx: any): BTStatus {
ctx.agent.performAttack();
return BTStatus.Running;
}
});
// 巡回シーケンス
const patrolSeq = new BTSequence("PatrolSequence");
patrolSeq.addChild(new BTCondition("HasPatrolPath", (ctx) => ctx.agent.patrolPath != null));
patrolSeq.addChild(new BTAction("Patrol") {
evaluate(ctx: any): BTStatus {
ctx.agent.moveAlongPath();
return BTStatus.Running;
}
});
// 待機アクション(条件なしで常に成功)
const idleAction = new BTAction("Idle") {
evaluate(ctx: any): BTStatus {
ctx.agent.playIdle();
return BTStatus.Running;
}
};
root.addChild(defeatedSeq);
root.addChild(combatSeq);
root.addChild(patrolSeq);
root.addChild(idleAction);
return root;
}
// 毎フレーム context を更新し、ルートを評価する
const behaviorTree = buildAI();
const aiContext = { agent: myCharacter };
function updateAI() {
behaviorTree.evaluate(aiContext);
}
このツリーでは、セレクターノードが上から順にシーケンスを評価する。戦闘不能状態でなければ次の戦闘シーケンスに進み、敵がいなければ巡回、それもなければ最終的に待機する。各アクションは「Running」を返すことで複数フレームにわたって動作を継続でき、攻撃や移動が完了したらSuccessを返すように拡張できる。