【UE4】L4DのAIDirectorを真似てメタAIを作ってみる その1

More than 3 years have passed since last update.


はじめに

ゲームAIには大きく分けて3つの種類がありそれぞれ

・Navigation AI

・Character AI

・Meta AI

があります。

ここではその中の一つMeta AIの実装の紹介と解説をしていきたいと思います。


L4DにおけるMeta AI

Left 4 Deadシリーズ(L4D)ではAIDirectorというMeta AIが備わっていて非常に優れた機能を持ちその名に恥じぬディレクターっぷりを発揮してくれます。

その機能とは

ルールに基づいた敵の出現(スポーニング)

ドラマチックなペースの生成(ペーシング)

障害物の配置(ルート作成)

回復アイテムや弾薬アイテムの配置

の4つです。

AIが生み出すルールに基づいたランダム性によりプレイヤーはプレイする度に前回とは違った刺激や体験を受け取る事が出来ます。

Character AIとは違って目に見えないAIですのでプレイヤーが実際にその存在を感じることはほぼありませんが、AIDirectorのおかげで他のゲームとは違うゲーム体験をL4Dは生み出せたと言えるでしょう。


要件定義

実際にAIDirectorの機能を真似てMeta AIを作っていきますが、その前に実装する機能をリストアップします。

・ルールに基づいた敵の出現

-プレイヤーから視認出来ない位置

-プレイヤーから出来るだけ遠い位置

-プレイヤーの元へとたどり着ける位置

-この3つの条件を満たす位置に敵を出現させる

・ドラマチックなペースの生成

-感情強度(Emotional Intensity)に基づく敵の出現頻度動的調整

-感情強度が上がる要因は以下の2つ

--敵からダメージを受けた

--プレイヤーの近くで敵が死亡した(プレイヤーが追い詰められている)

-感情強度が最大になると3~5秒間感情強度を最大に維持し続ける

-維持後は徐々に感情強度を下げていく(この間も要因による感情強度上昇あり)

-感情強度が下がりきった後の30~45秒間は敵の出現は行わない

出来る限りシンプルな形にするために4つあるうちの2つに機能を絞りました。

では実際にリストアップした機能を実装していきます。


下地となるプロジェクトをアップています

Meta AIの実装に集中出来るように敵キャラの挙動やプレイヤーの挙動を実装したプロジェクトを上げておきます。


下地プロジェクトのダウンロードリンク



ダウンロードしたプロジェクトの動作チェック

下地となるプロジェクトはMeta AI以外の処理が実装されていますので動作チェックを兼ねて動かしてみます。

レベルは Content > TopDownBP > Maps にあります。

1.PNG

TopDownレベルを開くと以下のような構造になっています。

2.PNG

プレイヤーの操作は一般的な見下ろし型シューティングゲームと同じです。

WASDキーで移動。マウスで方向指定。左クリックで弾の発射となっています。

以降のMeta AIの各処理は下地プロジェクトに追加していく形となります。

では動作チェックが出来た所で早速敵の出現処理(スポーニング)を実装していきます。


敵の出現位置をEQSで求める

L4Dでは出現処理のためにNav Meshを拡張して最適な出現位置を求めていますが、UE4ではEQSと呼ばれる優れたシステムがありますのでこの機能を用いて要件を満たす処理を実装します。

下地プロジェクトを落とされた方はEQSが既に有効になっているはずです。

まずはAIDirectorブループリントを作成します。親クラスはポーン(Pawn)とします。

3.PNG

次にコントローラーブループリントを作成します。親クラスはAIControllerとします。

4.PNG

ポーンクラスを継承したブループリントのAI Controller Classに先ほど作成したコントローラを指定します。

5.PNG

以上2つのブループリントを作成したらEQSの動作を定義する環境クエリを作成します。

EQSが有効になっている場合コンテンツブラウザの 新規追加 > AI に「環境クエリ」という項目が追加されているはずです。

それを選択し適当な名前を付けて作成してください。自分はSpawnSpotとしました。

6.PNG

次に環境クエリの実装をしていきます。

環境クエリを開くとルートノードのみが存在するグラフが表示されます。

ルートノードの下にある黒く塗りつぶされた範囲をドラッグすると検索画面が出ますのでPathと入力すればPoints:Pathing Gridが候補表示されますので選択します。

10.PNG

選択後はルートノードの下にPathing Gridノードが追加されます。

11.PNG

Pathing Gridにはプロパティが設定出来ますので、選択して右側の詳細パネルにある各項目を以下のように編集します。

12.PNG

次にPathing Gridに対しテストを追加していきます。このテストを追加することによりPathing Gridはテストに対し最もスコアの高い(条件を満たす)位置を初めて取得出来ます。

スポーニングの肝となる部分ですので少し詳しく書いていきます。

最初に追加するテストはプレイヤーから離れているというテストです。

Pathing Gridノードを右クリック > テストを追加 > Distance

Distanceテストを選択してください。

13.PNG

Distanceノードをクリックし表示された詳細パネルを以下のように変更します。

14.PNG

Distance To にPlayer Contextを渡すことでプレイヤーから離れている距離でスコアリングします。

Test PurposeのScoreOnlyですが、他にもFilter Only・Filter And Scoreがありテストの結果が悪いクエリを排除する事が出来ます。今回はScore Onlyなので排除はしません。

このDistance Toによりプレイヤーから最も離れているクエリを取得出来るのですが、離れているほどスコアが高いというのはClamping > Scoring Equationが判定しています。Previewにグラフが表示されていますがこのグラフの左端はDistance Toで指定したContextに最も近い場合のスコアを表しています。逆の右端は指定したContextに最も遠い場合のスコアを表しています。右端に近いほどスコアが高い。すなわちプレイヤーから遠い程スコアが高いとなるわけです。

ちなみにScoring EquationをInverse Linearに設定すると結果が正反対になるので、その場合はプレイヤーから近いほどスコアが高いとなります。

以上で敵の出現要件を満たす機能の一つであるプレイヤーから遠い位置を取得する事が出来ました。

2つ目はプレイヤーから見えない位置を取得するようにしますが、これにはTraceテストを使用します。Traceテストは主に視線チェック(その位置から視認できるか否か)に用いるテストです。

Distanceテストを追加した時と同様の手順で、選択するテストはTraceです。

15.PNG

追加されたTraceテストを選択し詳細パネルでContextPlayerContextに変更します。

16.PNG

TraceテストではTestPurposeを変更しませんので、このテストはプレイヤーから見える(視線チェックが通る)クエリを排除するとなります。逆に言えばプレイヤーから見えないクエリを残すということにもなります。

3つ目はプレイヤーへ到達可能なクエリを残すというテストを作成します。これにはPathingテストを使用します。このテストは到達不可能なクエリを破棄する仕事をします。

今までと同様の手順でテストを追加します。選択するテストはPathingです。

17.PNG

このテストも詳細パネルで使用するContextをPlayerContextへと変更します。

18.PNG

これで環境クエリの作成は終了です。最終的には以下のようになるはずです。

19.PNG

もしBP_PlayerContextの部分がQuerierから変わっていなかった場合、再度環境クエリ(ここではSpawnSpot)を開き直せばきちんと反映されるはずです。

以上で敵の出現の要件にあった

-プレイヤーから視認出来ない位置

-プレイヤーから出来るだけ遠い位置

-プレイヤーの元へとたどり着ける位置

を全て満たす位置が取得出来るようになりました。

ですが現段階では位置が取得出来るだけです。環境クエリでは敵の出現処理までは実装出来ないので、実際にレベル上に出現させる処理は別のブループリントに書く必要があります。


実際に敵を出現させる

出現処理ですが実装するブループリントはAIDirectorのコントローラブループリントになります。

出現位置をEQSから取得するための処理を書きます。

イベント ティックからピンを引き出し検索欄にRun EQSQueryと打ち、候補から選択します。

Query Templateには先ほど作成した環境クエリ(ここではSpawnSpot)を指定します。

Querierは、このコントローラーを所持しているポーンを指定しますのでGet Controlled PawnをQuerierへと接続します。

Run ModeSingle Random Item from Best 5%に変更します。

このRun Modeですが選んだものによってEQSから取得出来るクエリの個数が異なります。

デフォルトのSingle Best Itemではスコアリングされたクエリの中から最も点数の高いクエリが取得出来ます。

Single Random Item from Best XX%はスコアリングされたクエリの上位XX%のクエリ一つだけ取得出来ます。取得出来るクエリは上位XX%の内からランダムで選出されます。

All Matching条件を満たすクエリを取得出来ます。条件を満たしているクエリであれば全て取得出来てしまうので、プレイヤーから見えないがたった数メートルしか離れていないというようなクエリも混ざっています。

出現位置に関してはSingle Best Itemでも問題は無いのですが、個人的にある程度バラけていた方が面白いかなと思ってBest 5%の方を指定しました。

これでEQSを実行することが出来たので次はEQSから返されるクエリを取得するようにします。

クエリを取得するには一手間必要でOnQueryFinishedEventを通さなければなりません。

RunEQSQueryのピンを引き出し検索欄でOnQueryFinishedEventと打ちます。一番上に出るOnQueryFinishedを割り当てるを選択します。

20.PNG

するとイベントを OnQueryFinishedEvent とバインド OnQueryFinishedEvent_イベント_0の2つのノードが表示されます。

21.PNG

RunEQSQueryのReturn Valueとイベントを OnQueryFinishedEvent とバインドのターゲットを接続します。

22.PNG

次にOnQueryFinishedEvent_イベント0からQuery Instanceピンを引き出しGet Results as Locationsを検索欄へと打ち込み選択します。

Get Results as LocationsはEQSのクエリの位置を配列で返します。Get Results as LocationsのReturn Valueピンを引き出し変数へ昇格を選択し新しく変数を作成します。名前はLocationsとします。

23.PNG

以上で位置が取得出来たので、Spawn AI From Classを利用してレベルに敵キャラを出現させていきます。

Set Locationsからピンを引き出しSpawn AI From Classと打ち込み選択します。

Pawn Classには敵キャラであるBP_Infectedを指定し、Behavior TreeBT_Infectedを指定します。この2つは下地プロジェクトに入っているので新たに作成する必要は無いです。

Locationは先程作成した変数Locationsをゲットし、配列操作のGetノードを追加して指定します。インデックスは0で構いません。

24.PNG

画像ではDelayを挟んでいますが好みです。

配列Locationsから0番目の要素から取得出来ないようになっていますが、Run EQSQueryノードで指定したRun ModeでSingle Random Item from Best XX%を指定したためです。

上で説明しましたがこのRun Modeで返ってくるクエリは一つだけなので配列の0番目を取得するだけで良いのです。

以上で全ての要件を満たす敵の出現処理が出来ました。

実際にレベル上にAIDirectorを配置して動作を確認します。AIDirectorの位置はレベルの中心(X=0.0, Y=0.0, Z=100)としました。

恐らく画像の丸で囲まれた位置から敵がポコポコ出現すると思います。

25.PNG

敵を倒しつつ移動すると敵のスポーン位置も変化しているはずです。

EQSのスコアリングの様子を見たい方は実行中にコンソールでEnableGDTと打ち込んで見てください。候補には出ませんがこのコマンドは各種AIの動きをデバッグ表示する事が出来ます。

EnableGDTと打ち込みキーパッドの3を押すとEQSが表示され、Tabキーでカメラを自由に動かせます。

26.PNG


まとめ

長かったですがこれで敵の出現処理は終了です。

EQSは実験段階の機能ですがかなり強力ですね。ルールに基づいた位置を割り出すのにこれ以上ない程優れた機能を持っていてスポーニングに必要な位置の割り出しも簡単な3つのテストで実現出来てしまいました。

出現処理は終わりましたが、要件定義にも書いたドラマチックなペース生成が残っています。この処理は結構ヘビーですので、次の記事で続きを書いていきます。

お疲れ様でした。