枝で組む — ビヘイビアツリー
判断を木に組む。状態どうしを直接つなぐのをやめ、根から葉へ向かって毎フレーム評価が降りていき、葉で実際の行動が起きる。
部品はどれも、ひとつの約束だけ守る。呼ばれたら "success" か "failure" を返す。Sequence は子を左から順に呼んで、ひとつでも失敗したらそこで止める。全部成功して初めて成功。Selector は逆に、ひとつでも成功したらそこで止める。
// 子を順に呼ぶ。ひとつでも失敗したら止まる = AND
const sequence =
(...kids) =>
(ctx) => {
for (const k of kids) if (k(ctx) === "failure") return "failure"
return "success"
}
// 子を順に呼ぶ。ひとつでも成功したら止まる = OR
const selector =
(...kids) =>
(ctx) => {
for (const k of kids) if (k(ctx) === "success") return "success"
return "failure"
}Sequence は子を AND で、Selector は OR でまとめる枝。葉になるのは二種類。Condition はテストの真偽を成否に変えるだけ。Action は何かを実行して、成功を返す。これで「条件を確かめてから行動する」が枝として書ける。
// 真偽 → 成否
const condition = (test) => (ctx) => (test(ctx) ? "success" : "failure")
// 実行して、やったことにする
const action = (run) => (ctx) => {
run(ctx)
return "success"
}四つを入れ子にすると、見張りエージェントの小さな木になる。「敵が近ければ追う、そうでなければ巡回する」を Selector ひとつで表す。追跡の枝は Sequence で「敵が見えるか」を確かめてから「追う」に進む。
const tree = selector(
sequence(
condition((ctx) => dist(ctx) < sight), // 敵が視界内なら
action(() => moveTo(player.x, player.y, 2.2)), // 追う
),
action(() => moveTo(/* 次の巡回点へ */)), // どちらも無理なら巡回
)Selector は左の枝(追跡)を先に試すので、追える状況なら追跡が勝つ。下では、どの枝が今フレーム通ったかを視界の輪の濃さと下部のバーで光らせている。
プレイヤーが視界の輪に入ると、左のバーが点いて見張りが追跡へ切り替わる。輪から出ると右のバーが点いて巡回へ戻る。枝の並びがそのまま優先順位になっていて、selector のいちばん左に「体力が低ければ逃げる」の枝を挿せば、追跡より逃走が先に試される。遷移を一本ずつ書かず、優先順位を木の並びで宣言できる。
ビヘイビアツリーは 2000 年代に商用ゲーム(Halo 2 などが初期の例)で広まった。状態と、状態どうしの遷移を持つ有限状態機械に対し、ビヘイビアツリーはタスクの木を毎フレーム上から評価する。Sequence と Selector に加えて、子を装飾する Decorator(反転・繰り返し・成否の上書き)や、複数の振る舞いを並列に走らせる Parallel が標準的な部品。失敗した枝を記録して次フレームは途中から再開する、といった最適化もよく入る。状態が増えると遷移が n×(n−1) 本まで膨らむのを、遷移を消して木の構造に押し込むことで避ける。