【UE4】パトロールをする敵AIの作り方

# 最初に
どうも、ろっさむです。

今回はUE4でパトロールを行う敵AIを作る手法についてまとめようと思います。

本記事では `Behaviour Tree` 関連や `AIPerception`に関する詳細な説明は省きますのでご了承下さい。
興味がある方は以下の記事を参照してください。

[【UE4】味方AIの作り方！AIとは何かを学びながら、ブループリントで味方キャラクターを実装しよう](https://qiita.com/4_mio_11/items/9e8af2ce82ee2a7625b5)


# 作り方

## BTとBBの作成

まずは敵用のBTとBBを作成してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/77b61ca8-a101-3b55-1258-33504a204b8b.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/9957d571-5647-d76c-26d8-21d17f03c423.png)

`BT_Enemy`の方では`BB_Enemy`を設定しておいてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/6585e9f2-3c72-b8bc-7170-f804ca658bfb.png)

今回、BB内で必要となる情報はPlayerのActor情報と、移動地点のVector情報です。
予め設定しておきましょう。

`New Key`を押下し、`Key Type`を`Object`、`Base Class`を `Actor` にして`PlayerActor`という名前のKeyを作成してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/6f1d98ed-7b39-a6fa-9728-5703ce02774b.png)


また、同様に`New Key`を押下し、`Key Type`を`Vector`にして`RoutePoint`という名前のKeyを作成してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/0da42fab-8d98-ccd1-297e-1bad7c5657f1.png)


これでBBの設定は終わりです。
次にBTの設定を行いましょう。

## 敵AIのBTの土台を構築

今回のAIでは敵がプレイヤーを発見しているかどうかで行動パターンが異なってきます。
ですので最初のコンポジットノードは `Selector`に設定しておきましょう。
ノード名をわかりやすい任意の名前に変更しておいてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/2de89456-f7a2-9c56-5325-4f4199b3c1ab.png)


まずはパトロールを行うパターンを作っていきたいと思います。

コンポジットノードの `Sequence`を追加して、Selectorから繋いでください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/d02d7132-b804-99ef-1afe-0c3627fcdb71.png)


パトロールを行っているということは、まだプレイヤーは未発見状態です。
ですので、BBの `PlayerActor`には何もSetされていないということになります。
デコレーターノードを使用してプレイヤーが未発見の状態の時のみ、この `Sequence`ノードを通るようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/b63c599d-3cd5-2d92-3b9a-2e557b02bee9.png)

この`Sequence`では、敵は設定されている `RoutePoint`を移動することになります。この`RoutePoint`は後程設定するように処理を作成します。`MoveTo`ノードを追加して、`Blackboard Key`を`RoutePoint`に設定しておいてください。また、 `Acceptable Radius`もできるだけ小さい値にしておいてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/c6ac9f15-4496-7948-445c-58615f75f480.png)


設定後は`Sequence`に繋いでください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/972fdf19-20c2-fc5d-ee16-1b27a4857837.png)

次に、プレイヤーを発見した際に、プレイヤーを追従するパターンを作ります。

新しく`Sequence`を追加してわかりやすい任意の名前を設定した後、`Selector`に繋いでください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/473b40b1-54cf-ec1a-0cea-ced93691348c.png)

こちらのパターンは単純にプレイヤーを追うだけなので、`MoveTo`ノードを追加して `Blackboard Key`を `PlayerActor`に設定するだけです。`Acceptable Radius`は任意の値に設定してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/edee0682-2022-32ca-0bb9-fce2d03cdde4.png)

設定が完了後、`Sequence`ノードに繋いでください。
これで一旦土台は完成です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/81ef0e7a-c050-352a-ba75-89707bcc8d50.png)

使用している敵AIControllerの`BeginPlay`にてBTを走らせる処理を入れておきましょう。またここでBBの変数を作成し、BBの参照を持っておいてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/ae937210-bdb6-2c0b-9198-1639284c13c9.png)


## 敵に視覚情報を追加する

敵のAIController用BPを開いて、`Add Component`から `AIPerception`を追加してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/64fb4b33-a417-9823-f7c9-f27bf981fc01.png)

追加したら、`AIPerception`の`Details`を確認し、`Scenes Config`の+ボタンを押下して`AI Sight config`を追加してください。
これで視覚の機能が備わったことになります。

`Sight Radius`は敵を中心とする視覚範囲内の半径にプレイヤーが入ってきたら検知するために使用します。
`Lose Sight Radius`はその逆で、この半径を超えると見失ったことになります。
この二つは任意の値に設定しておいてください。

また、`Detection by Affiliation`は全てにチェックマークをつけてください。プレイヤーかどうかの判断は今回はキャストで行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/1a8c2cff-0118-b3ce-66cb-de19ff427a38.png)

設定ができたら、`OnPerceptionUpdated`と`OnTargetPerceptionUpdated`の二つのEventを追加します。前者はプレイヤーを発見した際に使用し、後者はプレイヤーを見失った際に使用します。

まずは`OnPerceptionUpdated`を作っていきましょう。

「On Perception Updated」からは発見したアクター群の配列が出力されているため、一つ一つPlayerのBPであるかどうかを確認していく必要があります。ただし、Playerを見つけることができたらその後の処理は行う必要がないため、途中でループ処理を中断できる「ForEachLoopWithBreak」を呼び出します。

"Array Element"として出力されているActorがどのように発見されたか知ることができる「Get Actors Perception」という関数があります。この関数にActorを渡し、ターゲットとして自身のコンポーネントであるAIPerceptionを繋げます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/4bc96eb7-d648-0671-dc05-b954df420b80.png)

出力された"info"というデータを分解し、"Last Sensed Stimuli"という配列の0番目（つまり視覚）を取り出します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/f5389ff3-1844-dba0-76bc-aaff8aba5613.png)

これを更に分解し、要素の最後に存在している"Successfully Sensed"（感知が成功しているか）をブランチに繋ぎ成功していれば、次はプレイヤーを登録する処理に移っていきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/7149df31-cfb0-8574-919b-6fc3f68a9428.png)

感知されたActorがプレイヤーBPであれば、ブラックボードのキー「PlayerActor」に設定を行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/5c146c95-82b0-27cb-5561-21c5e0c9f033.png)

設定後は、もう検索ループを掛ける必要がないので、ノードを先ほどの「ForEachLoopWithBreak」にある"Break"へと繋ぎます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/cfb0bde6-216e-8e87-37f7-3c7e5e1dfd67.png)


次に、プレイヤーを見失った場合の処理を作成しましょう。
`OnTargetPerceptionUpdated`の実装に移ります。

こちらでは出力されている `Simulus Successfully Sensed`が`false`であれば、BBから`PlayerActor`の参照を外すという処理だけを行っています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/8b9b8b53-a5f8-2fa4-3982-644435929e8a.png)

これでAIController側での処理は完了です。

## Splineを使用してパトロールルートを設定する。

`Spline Component`を使用することでレベル上に曲線や直線を設定して、今回のAIのようなパトロールルートとして扱うことができます（もちろんそれ以外にもレベル作成周りとかでバリバリ使えます）。

まずは`Actor`を継承したBPを作成して `BP_EnemyPatrolRoute`と名付けます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/8b5475d4-036b-7ee3-2498-20da9bafe485.png)

ダブルクリックでBPを開き、`Add Component`から `Spline`を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/7a20b3d7-b1ad-f96e-c6d0-78a84a0f504d.png)

追加できたらレベル上に  `BP_EnemyPatrolRoute`を配置します。配置すると、ぴょろっと点と線が表示されているかと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/75b35b90-4731-a6c2-88de-52202ed24b5b.png)

この点が後々パトロールポイントとなります。点は移動させたり回転させることができます。
点を増やしていくことでルートを作成していきます。
ちなみにルート作成するのにあたって、点を右クリックすると表示される `Spline Generation Panel`が役立つかもしれません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/6b2bba29-6e1b-ee14-9c20-936c729eb2b0.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/d8cddeeb-260f-ea30-3bcf-be24a0f95d95.png)

実際に使ってみるとよくわかるのですが、円や曲線、四角形などをぱっと簡単に作成してくれます。
また点の数や長さ、点の追加開始位置なども設定が可能です。

他にも点をもし上下で動かしてしまった場合などに、点を選択して右クリックするとFloorへスナッピングなどできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/11a587c8-6b67-1929-2488-a18c169182cd.png)

点をもりもり増やしてパトロール経路を作成しましょう....

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/d9d3e599-fb77-5cb7-0be6-61a4935af5d5.png)

こんな感じにしてみました。

次の段階に進みましょう。

## 敵BPに経路用のアクターを設定

パトロールの経路を作成した後は、敵側でそのパトロール経路を取得できるようにしましょう。

任意の敵BPを開いて、変数を追加してください。

名前を`RouteActor`にして、型を先ほどパトロール経路作成用のActor、`BP_EnemyPatrolRoute`に設定します。
また、レベル上に敵BPを配置した際にエディタ側から設定ができるように `Instance Editable`にチェックをつけておいてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/d2c5fddd-8a20-af6b-0ad4-aa8b18e14b4f.png)

設定ができたらコンパイルして、レベル上に敵BPをパトロール経路近くに配置してください。

配置したら `Details`から先ほどの変数`RouteActor`を検索し、レベル上に設置している`BP_EnemyPatrolRoute`を設定してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/f03a7cb3-4ff3-28f5-98a5-1e1f906a9276.png)


これでレベル上での下準備は終わりです。

## BT上でのサービスノードでパトロールポイントを更新する

先ほど作成した`BT_Enemy`を開いてください。
開いたら、上部メニューの`New Service`を押下して`BTService_BlueprintBase`を継承したサービスノードを新規で作ってください。

名前を`BTS_UpdatePatrolPoint`としておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/f1908872-715a-aac6-ce50-d0d13299327d.png)

`BTS_UpdatePatrolPoint`を開いてください。
Functionsの`override`から`Event Receive Tick AI`というサービスノード自身がアクティブ状態の際のTickと、`Event Receive Activation AI`というサービスノードがアクティブになった際に呼び出されるメソッドを選択してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/bc766913-142d-6b1e-3c7d-7a83c3aa03b5.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/5b51799c-204b-9685-4e45-30fe6c89d9b4.png)

次に今回必要な変数を定義しておきます。
`Integer`型の`CurrentPatrolPoint`という名前の変数を作成してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/5c1b06cc-762e-d129-fb0e-af91c6c4b5c9.png)

それから、BBの方で定義している`RoutePoint`に値を設定するために、BT側からKeyを登録するための`Blackboard Key`型の`RoutePointKey`という変数も作成してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/79c0fce1-daeb-51a3-ce88-8b9b35e45739.png)

ではまず、`Event Receive Activation AI`にて初期化処理から実装していきます。
ただ今回は初期化時にパトロールポイントをランダムに設定するための処理をしています。もし不必要な場合はこのイベント自体なくても大丈夫です。消しちゃってください。

まずは、`Controlled Pawn`から敵BPにキャストを行います。成功したらパトロール経路用に設定した`RouteActor`からSplineコンポーネントをgetしてください。getしたら、そのSplineで設定されている点の数が何個あるかを取得できる`GetNumberOfSplinePoints`を追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/6171a051-494d-81fd-8658-36e0be6c84ca.png)

この値をMAX値としてランダムなint型を生成する`RandomInteger`を繋げ、出力された数値を`CurrentPatrolPoint`に設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/24d71ef1-cbec-a192-cca3-500b5cc86350.png)


これで初期化処理は完了です。

次はサービス自体のTick処理です。

初期化処理時と同様に`Controlled Pawn`から敵BPにキャストを行います。成功したらパトロール経路用に設定した`RouteActor`からSplineコンポーネントをgetしてください。getしたら、Splintで設定した何番目の点の位置を取得するかという`GetLocationSplinePoint`を追加してください。ここでの`PointIndex`には`CurrentPatrolPoint`を設定します。また`CoordinateSpace`は`World`を設定しておいてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/eb4994d9-8264-ef88-e612-e6a98d7cf0b8.png)

取得したVector値をBlackboardの`RoutePoint`に設定します。`Key`には`RoutePointKey`を設定してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/417790d7-dbb6-e428-54b0-94ac8b407920.png)

最後に次のパトロールポイントへ進むための準備をします。`CurrentPatrolPoint`をインクリメントして、もしその結果がSpline上での最後のポイントであれば最初のポイントを設定するように`0`に初期化します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/9cfa4259-5cc2-2358-f8bd-604f9cf7b3a6.png)

これでサービスの処理は完了です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/27671c5e-93dc-dff6-24e9-a6e0f06ccec9.png)

BTに戻って、パトロール中の`Sequence`ノードに追加してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/b26dd84e-9246-49b6-cae2-337032cd0436.png)


## 仕上げとして、BTにて値が切り替わったら処理を中止するように変更

現在の状態でも動くには動きますが、パトロール場所へ移動中にプレイヤーを見かけても無視して移動が終わってから追いかけようとしたり、プレイヤーを見失うことができません。
これを可能にするために、デコレーターに細工をします。

`Notify Observer`を`On Value Change`にし、`Observer aborts`を`Both`にしておきます。こうすることで、`PlayerActor`の値が変わった瞬間に現在実行しているタスクの処理は中断されROOtから判定し直しとなります。`Both`は二つの`Sequence`どちらも中断の対象となるように設定する項目です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/1edfc80d-4dee-4b18-46ca-117b7c1b6c78.png)

これでBTは完了となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/102b29f1-8d1c-7a76-d0fe-af6b888e77a9.png)

## 結果

実際に実行してみてください。以下のツイートが実装例となります。(Gifでは限界がありました)

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Splineを使ってランダムにSplineの点パトロールする敵を作ったー<br>視界にプレイヤーが入ったら追いかけてくるけど一定距離離れたら（見失ったら）パトロールに戻る <a href="https://t.co/0Nd8UlMpKX">pic.twitter.com/0Nd8UlMpKX</a></p>&mdash; ろっさむ (@4_mio_11) <a href="https://twitter.com/4_mio_11/status/1326874539809148928?ref_src=twsrc%5Etfw">November 12, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>