地図/探索/エージェントAI

枝で組む — ビヘイビアツリー

判断を木に組む。状態どうしを直接つなぐのをやめ、根から葉へ向かって毎フレーム評価が降りていき、葉で実際の行動が起きる。

部品はどれも、ひとつの約束だけ守る。呼ばれたら "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) 本まで膨らむのを、遷移を消して木の構造に押し込むことで避ける。