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

#はじめに
ゲームAIには大きく分けて３つの種類がありそれぞれ
・Navigation AI
・Character AI
・Meta AI
があります。
ここではその中の一つMeta AIの実装の紹介と解説をしていきたいと思います。

#L4DにおけるMeta AI
**Left 4 Deadシリーズ（L4D）**では**AIDirector**というMeta AIが備わっていて非常に優れた機能を持ちその名に恥じぬディレクターっぷりを発揮してくれます。
その機能とは
・**ルールに基づいた敵の出現（スポーニング）**
・**ドラマチックなペースの生成（ペーシング）**
・**障害物の配置（ルート作成）**
・**回復アイテムや弾薬アイテムの配置**
の４つです。
AIが生み出す**ルールに基づいたランダム性**によりプレイヤーはプレイする度に前回とは違った刺激や体験を受け取る事が出来ます。
Character AIとは違って**目に見えないAI**ですのでプレイヤーが実際にその存在を感じることはほぼありませんが、AIDirectorのおかげで他のゲームとは違うゲーム体験をL4Dは生み出せたと言えるでしょう。

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

**・ルールに基づいた敵の出現**
－プレイヤーから視認出来ない位置
－プレイヤーから出来るだけ遠い位置
－プレイヤーの元へとたどり着ける位置
－この３つの条件を満たす位置に敵を出現させる

**・ドラマチックなペースの生成**
－感情強度（Emotional Intensity）に基づく敵の出現頻度動的調整
－感情強度が上がる要因は以下の２つ
－－敵からダメージを受けた
－－プレイヤーの近くで敵が死亡した（プレイヤーが追い詰められている）
－感情強度が最大になると3～5秒間感情強度を最大に維持し続ける
－維持後は徐々に感情強度を下げていく（この間も要因による感情強度上昇あり）
－感情強度が下がりきった後の30～45秒間は敵の出現は行わない

出来る限りシンプルな形にするために4つあるうちの2つに機能を絞りました。
では実際にリストアップした機能を実装していきます。

#下地となるプロジェクトをアップています
Meta AIの実装に集中出来るように敵キャラの挙動やプレイヤーの挙動を実装したプロジェクトを上げておきます。
> [下地プロジェクトのダウンロードリンク][link-1]
[link-1]:https://onedrive.live.com/redir?resid=99121011A816BCEF!661&authkey=!AMHYxM8_eZFAl6s&ithint=file%2c7z

#ダウンロードしたプロジェクトの動作チェック
下地となるプロジェクトはMeta AI以外の処理が実装されていますので動作チェックを兼ねて動かしてみます。
レベルは Content > TopDownBP > Maps にあります。
![1.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/6b93a971-21ae-8ac1-530f-889b12933b3c.png)
TopDownレベルを開くと以下のような構造になっています。
![2.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/ca0d618a-08ed-77a1-5927-0dae662c2327.png)
プレイヤーの操作は一般的な見下ろし型シューティングゲームと同じです。
WASDキーで移動。マウスで方向指定。左クリックで弾の発射となっています。
以降のMeta AIの各処理は下地プロジェクトに追加していく形となります。
では動作チェックが出来た所で早速**敵の出現処理（スポーニング）**を実装していきます。

#敵の出現位置をEQSで求める
L4Dでは出現処理のためにNav Meshを拡張して最適な出現位置を求めていますが、UE4ではEQSと呼ばれる優れたシステムがありますのでこの機能を用いて要件を満たす処理を実装します。
下地プロジェクトを落とされた方はEQSが既に有効になっているはずです。

まずはAIDirectorブループリントを作成します。親クラスは**ポーン（Pawn）**とします。
![3.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/6286e6a5-a0e2-2f22-35b4-4a52f309a552.png)
次にコントローラーブループリントを作成します。親クラスは**AIController**とします。
![4.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/17851c94-299c-36dd-4e34-bd30bef04405.png)
ポーンクラスを継承したブループリントの**AI Controller Class**に先ほど作成したコントローラを指定します。
![5.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/d54ee846-89e1-7584-38b5-a16c0e303d11.png)

以上2つのブループリントを作成したらEQSの動作を定義する**環境クエリ**を作成します。
EQSが有効になっている場合コンテンツブラウザの 新規追加 > AI に「環境クエリ」という項目が追加されているはずです。
それを選択し適当な名前を付けて作成してください。自分は**SpawnSpot**としました。
![6.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/452e4c03-ba17-855f-58ab-c6ebab2e17ae.png)

次に環境クエリの実装をしていきます。
環境クエリを開くと**ルートノード**のみが存在するグラフが表示されます。
ルートノードの下にある黒く塗りつぶされた範囲をドラッグすると検索画面が出ますので**Path**と入力すれば**Points:Pathing Grid**が候補表示されますので選択します。
![10.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/26f9bca1-3933-7cae-8db5-8e4be587dbb4.png)
選択後はルートノードの下にPathing Gridノードが追加されます。
![11.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/81a4d7f0-635e-f8d8-bb69-2f9b154a8cf8.png)
Pathing Gridにはプロパティが設定出来ますので、選択して右側の詳細パネルにある各項目を以下のように編集します。
![12.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/3114b72d-ead0-3449-c836-adeb15e6b03f.png)
次にPathing Gridに対し**テスト**を追加していきます。このテストを追加することによりPathing Gridはテストに対し最もスコアの高い（条件を満たす）位置を初めて取得出来ます。
スポーニングの肝となる部分ですので少し詳しく書いていきます。
最初に追加するテストは**プレイヤーから離れている**というテストです。
**Pathing Gridノードを右クリック > テストを追加 > Distance**
で**Distanceテスト**を選択してください。
![13.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/749b9ab7-37b5-7c1c-8807-f16927332410.png)
Distanceノードをクリックし表示された詳細パネルを以下のように変更します。
![14.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/4b62a42f-e5fb-4143-8a32-851a20595106.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に設定すると結果が正反対になるので、その場合はプレイヤーから近いほどスコアが高いとなります。
以上で敵の出現要件を満たす機能の一つである**プレイヤーから遠い位置**を取得する事が出来ました。

２つ目は**プレイヤーから見えない位置**を取得するようにしますが、これには**Traceテスト**を使用します。Traceテストは主に視線チェック（その位置から視認できるか否か）に用いるテストです。
Distanceテストを追加した時と同様の手順で、選択するテストは**Trace**です。
![15.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/d5f9fd32-7b0e-cc35-f9ba-8c1568580cad.png)
追加された**Traceテスト**を選択し詳細パネルで**Context**を**PlayerContext**に変更します。
![16.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/64f95d20-2906-ee4e-9f72-3a90fec35e00.png)
TraceテストではTestPurposeを変更しませんので、このテストは**プレイヤーから見える（視線チェックが通る）クエリを排除する**となります。逆に言えば**プレイヤーから見えないクエリを残す**ということにもなります。

３つ目は**プレイヤーへ到達可能なクエリを残す**というテストを作成します。これには**Pathingテスト**を使用します。このテストは**到達不可能なクエリを破棄する**仕事をします。
今までと同様の手順でテストを追加します。選択するテストは**Pathing**です。
![17.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/95ea8493-3922-79e9-cccd-c2ee59483286.png)
このテストも詳細パネルで使用するContextを**PlayerContextへと変更**します。
![18.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/e3f47886-601f-65ae-ec75-ad0a37fe1446.png)

これで環境クエリの作成は終了です。最終的には以下のようになるはずです。
![19.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/1b984cdc-bc3c-bcba-c73b-32c09df7cfc0.png)
もしBP_PlayerContextの部分がQuerierから変わっていなかった場合、再度環境クエリ（ここではSpawnSpot）を開き直せばきちんと反映されるはずです。

以上で敵の出現の要件にあった
－プレイヤーから視認出来ない位置
－プレイヤーから出来るだけ遠い位置
－プレイヤーの元へとたどり着ける位置
を全て満たす位置が取得出来るようになりました。
ですが現段階では**位置が取得出来るだけ**です。環境クエリでは敵の出現処理までは実装出来ないので、実際にレベル上に出現させる処理は別のブループリントに書く必要があります。

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

出現位置をEQSから取得するための処理を書きます。
**イベント ティック**からピンを引き出し検索欄に**Run EQSQuery**と打ち、候補から選択します。
**Query Template**には先ほど作成した環境クエリ（ここではSpawnSpot）を指定します。
**Querier**は、このコントローラーを所持しているポーンを指定しますので**Get Controlled Pawn**をQuerierへと接続します。
**Run Mode**は**Single 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](https://qiita-image-store.s3.amazonaws.com/0/104467/e54bd24e-4cd6-2575-98ae-e0ee91279e4d.png)
すると**イベントを OnQueryFinishedEvent とバインド** **OnQueryFinishedEvent_イベント_0**の2つのノードが表示されます。
![21.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/630977ca-02dc-cfe0-28c9-19e7ab912d42.png)
RunEQSQueryの**Return Value**とイベントを OnQueryFinishedEvent とバインドの**ターゲット**を接続します。
![22.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/c659f6ab-ad97-7882-0546-b36e5ae91bc2.png)
次にOnQueryFinishedEvent_イベント0から**Query Instance**ピンを引き出し**Get Results as Locations**を検索欄へと打ち込み選択します。
Get Results as LocationsはEQSのクエリの位置を配列で返します。Get Results as Locationsの**Return Value**ピンを引き出し**変数へ昇格**を選択し新しく変数を作成します。名前は**Locations**とします。
![23.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/4cd8f554-de84-2c7f-23e7-5f061a7e4ea3.png)
以上で位置が取得出来たので、**Spawn AI From Class**を利用してレベルに敵キャラを出現させていきます。
**Set Locations**からピンを引き出し**Spawn AI From Class**と打ち込み選択します。
**Pawn Class**には敵キャラである**BP_Infected**を指定し、**Behavior Tree**は**BT_Infected**を指定します。この2つは下地プロジェクトに入っているので新たに作成する必要は無いです。
Locationは先程作成した**変数Locations**をゲットし、配列操作の**Get**ノードを追加して指定します。インデックスは0で構いません。
![24.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/50b2e11c-2b71-55aa-1421-c414cb976ac0.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](https://qiita-image-store.s3.amazonaws.com/0/104467/c4d8a020-49e3-7a2e-6ee6-a548a2e9ac6f.png)
敵を倒しつつ移動すると敵のスポーン位置も変化しているはずです。
EQSのスコアリングの様子を見たい方は実行中にコンソールで**EnableGDT**と打ち込んで見てください。候補には出ませんがこのコマンドは各種AIの動きをデバッグ表示する事が出来ます。
EnableGDTと打ち込みキーパッドの3を押すとEQSが表示され、Tabキーでカメラを自由に動かせます。
![26.PNG](https://qiita-image-store.s3.amazonaws.com/0/104467/56b6815a-e80f-95db-3c07-754d46d2fda2.png)

#まとめ
長かったですがこれで敵の出現処理は終了です。
EQSは実験段階の機能ですがかなり強力ですね。ルールに基づいた位置を割り出すのにこれ以上ない程優れた機能を持っていてスポーニングに必要な位置の割り出しも簡単な3つのテストで実現出来てしまいました。
出現処理は終わりましたが、要件定義にも書いた**ドラマチックなペース生成**が残っています。この処理は結構ヘビーですので、次の記事で続きを書いていきます。
お疲れ様でした。