
パシフィコ横浜ノースの現地とオンラインの両面で開催されたゲーム開発者向け大規模カンファレンスCEDEC 2023。ここでは、「『ブループロコトル』のAI実装~企画意図をふるまいに反映させるために行ったこと~」のセッションのレポートをお届けします。
このセッションには、バンダイナムコエンターテインメントの第4プロダクションリードプログラマである佐竹敏久氏が登壇しました。

状況判断の分岐から行動へ移るAIの挙動
『ブループロコトル』のAIは階層型タスクネットワーク(Hierarchical Task Network)によって行動を決めています。この場合のタスクは問題を解決するための操作や行為で、「近づく」と「吠える」、そして「こんぼうで叩く」がタスクに該当します。ネットワークグラフということで、「近づく」→「吠える」→「こんぼうで叩く」という順番で動かすことが出来ます。

このタスクはより小さいタスク分解できます。「戦う」は「近づく」と「攻撃する」に、「攻撃する」は「吠える」と「こんぼうで叩く」の2つを引き出せます。これを深度優先で回収すると、「近づく」→「吠える」→「こんぼうで叩く」となります。

HTNプランニングはこのようにルートタスクを指定して、タスクをサブタスクに分解して行動を得る手法です。タスクは、サブタスクに分解できるコンパウンドタスクと、分解出来ないツリーの葉であるプリミティブタスクの2種類が存在。またプリミティブタスクは、エージェント(この場合は敵の3Dモデル)が実行できる行動の単位です。

コンパウンドタスクには分解の手法をメソッドという形で複数持てます。このメソッドを開くためのプリコンディション(前提条件)と、どれぐらい分解されるかを示すサブタスクが存在します。

メソッドとプリコンディションの観点からコンパウンドタスクを見ると、メソッドにおいてこんぼうを持っている(true)か、いないか(false)で、「吠える」と「こんぼうで叩く」、そして「ひっかく」に分かれ、バリエーションを持たせられるのがわかります。プリミティブタスクには他にも、ワールドステートへ影響を与えるエフェクトとアクションが存在します。

HTNプランニングシステムは、ざっくり分けると「戦う」や「近づく」などの行動を内包したドメインと、こんぼうを持っているか(true)か距離が近いか遠いか(Far)を判定するワールドステートと、行動の推移を決めるプラナーがあります。


ドメインをもう少し詳しく説明すれば、活動や知識の領域を表す言葉で、プランニングに使用できるタスクを内包しています。こんぼうを持ったウォリアーなら「近づく」ことと「こんぼうで叩く」こと、弓を持ったアーチャーなら「弓で射る」や「離れる」などの知識が入っており、ドメインを変えることで行動が変わります。


ワールドステートは、プランニングに使用する知識表現で必要な情報を都合の良い形で保持し、こんぼうの有無や距離の近遠などで攻撃などを分けるものです。プランナーにルートタクスを与えると、近づくことでEffectsが「Distance = Near」と更新し、「攻撃する」のコンパウンドタスクはPreconditionsが「bHasClub == true」となっているためこんぼうを持っています。しかし、こんぼうで叩いてしまうと壊れるため「bHasClub = false」へ変化。それをワールドステートに反映してしまったら、次のタスクでも実行しようにもこんぼうが壊れているためプランが成立しなくなってしまいます。


HTNプランニングシステムは、まとめると上位タスクから下位タスクへ分解することで、行動プランを得る手法です。つまり曖昧な命令から具体的な行動を行うことでした。

HTNタスク記述はBehavior Treeで記述しているものです。この図を簡単にしてみるとRootから始まり、Sequenceを経てCheckStateとSubTaskの2つが左から順に実行されます。

プリミティブタスクも同様で、ルートに記述されておりPreconditionがCheckStateでこんぼうを持ち、ModifyStateはワールドステートの書き換えでこんぼうが折れたこと表し、そしてSendAnimCommnadでBashというコマンドを実行します。


ここでは別紙に『ブループロコトル』のタスクネットワークの細かいところをマーカーで塗って解説したものです。事細かな挙動が様々な処理を経て実行される手順が描かれています。このHTNをより汎用的なメレー攻撃に定義し直してみましょう。

まずAttackMainというルートタスクから、MoveToAttackとP_Melee(このPはプリミティブタスク)に分解されます。MoveToAttack内に常にTureを返すNullが入っているのは、隣にあるLocomoteがターゲットに近いと失敗し、処理自体が止まることを防ぐためです。攻撃時に近ければ何でもよいためにModifyStateを最大距離割る2で処理し、Parallelノードにて、MoveToで目標に移動しつつCommandという攻撃が当たるかをCanHitAttackで常にチェックするという実行を並行して行います。

P_Melee内では、攻撃の当たる距離が赤枠のAttackRengeMaxに該当します。またMeleeは攻撃のひな形として利用でき、Meleeを継承してSlashという斬撃に切り替えることも可能です。これによって近接攻撃を量産しています。

行動に幅を持たせるタクティカルスキル―行動を選ぶ抽選テーブル
タクティカルスキルは先の攻撃量産化で使ったいたものです。Meleeのタクティカルスキルをみると、右側にTS_BashとTS_Slashの2つがあります。


エネミーに2つのアクションを持たせるには、エネミー独自のHTNドメインを持っているためにTS_BashとTS_SlashがMeleeとしてマージされますが、名称が被って衝突してしまうためにMelee_BashとMelee_Slashに変更しています。

こうすると、エネミー側のHTNドメインのAttackMainのタスクが攻撃アクションのルートタスクとなるために、バッシュとスラッシュという行動をプランニングすることができるのです。また使用させたいアクションを、タクティカルスキルテーブルから与え行動を増やす事ができます。


HTNプランニングにおいて、HTNは深さ優先で検索し先に見つけたものをブランドして出力するために、先にジャブを見つけたら、それがプランに組み込まれてジャブばかりを繰り出してしまいます。そのため、「ジャブで牽制しつつフルスイングのように振る舞わせたい」ように動かすには、アクションを決める仕組みと、決めたものをHTNに反映する仕組みの2つが必要です。そこでアクションを決める仕組みとしては、確率テーブルで決める「アクション抽選テーブル」という方法があります

アクション抽選テーブルは、左の行の名前とAction Nameがアクション名で、Phaseから右のRange Maxまでが前提条件(スタンスはHPが減った時の怒りモードへの切り替えなど)、そしてPriorityから右が本題となる抽選パラメータです。


抽選は、「抽選ウェイト」の計算によって選び出されると共に、アクションが実行されると抽選ウェイト値が、WeightGroupもまとめて初期値にリセット。そのため、実行されなかったアクションは、行動と共に抽選ウェイト値が上がり続けるために選択されやすくなり、「大技を使うのでは?」という期待に応えられるのです(もちろん、大技もWeight値が0でなければいきなり使う場合がある)。

例えば、ジャブのWeightとWeightincが50で、フルスイングがそれぞれ10づつだとしたら、1回目にジャブをするとフルスイングが20に、2回目もジャブを放つとフルスイングは30と増えます。3回目にフルスイングをしたら、フルスイングのWeightincが初期の10に戻る変わりに、ジャブが100となるため初期値の行動に戻ったような動きに出来るのです。他にも、意外性を考慮して大技もWeightが0でなければ開幕使用する可能性があります。

抽選の流れとしては、抽選候補を収集すると共に「タクティカルスキルが有効か」や「アクション抽選テーブルの前提条件を満たすか」によってフィルタリングし、Priorityの中から抽選ウェイト値の重みで抽選します。


この結果をHTNプランニング反映し、AttackMainのブランチになりますが、抽選結果の順位に応じた評価値を付けて結果をプランニングに反映させます。優先順位は0始まりで順位ごとに5が掛けられますが、評価値は小さいほど良いために、この場合では計算結果1位のSlashが選ばれます。
なお抽選結果に無ければ1万となりますが、ここまで大きくなるとBattleMainにある待機コードのWaitが評価値を200持っているため、1万の値がついたブランチはWaitより後ろに追いやってしまうためにプランに表れなくなります。

なお抽選結果1位プランニングで失敗した時は、2位以降がプランとして出力されます。なおアクション抽選を1つに絞らずリストで出力するのは、プランが失敗した時に結果が1つだけだとエネミーが停止してしまうから。また連続で失敗したら、失敗したアクションを除外して抽選からやり直しです。


敵が徒党を組めるようにするチームAI
CombatCoordinatorはエネミーの上位に位置するチームAIです。エネミ―は位置関係などによって1戦闘単位としてグルーピングされており、戦闘単位につき1つ存在し配下のエネミーを調停しています。調停内容には、同目標の同時攻撃を管理するエネミー数管理の攻撃権や、誰が回復させるかなどの支援を内包しています。