【UE4】AIのExample Projectを作ってみました（プログラム紹介＆解説編）第1章

#はじめに
この記事は「[【UE4】AIのExample Projectを作ってみました（フォルダ構造編）](https://qiita.com/Dv7Pavilion/items/28e44c307b6c960c51a8)」の続きとなっております。

興味のある方は是非読んでいただければ幸いです。


#プロジェクトはこちらです！
今回解説するプロジェクトはOneDriveにアップしてあります。バージョンは**4.18.2**です。

[StealthGameExample_Ver1_5.zip](https://1drv.ms/u/s!Au-8FqgREBKZhTC3k-2_VILJrrXl)


#C++
##BaseCharacterクラス
このクラスには主に**「AIの動作」**に必要な機能の実装を行っています。

```cpp
//~~~~~~~ Add ~~~~~~~
#include "Perception/AISightTargetInterface.h"
#include "GenericTeamAgentInterface.h"
//~~~~~~~~~~~~~~~~~~~
```

ヘッダの上部には新たに2つのヘッダをインクルードしています。

###AISightTargetInterface.h
このヘッダにはインターフェースが定義されており関数をオーバーライドすることで**AIPerception**の**AISense_Sight**で使われる視線チェックの動作を変更することが出来ます。
詳しくは
[【UE4】AISense_Sightの視線チェックを改良する](https://qiita.com/Dv7Pavilion/items/730a853b30dcd1eae199)
こちらを読んでいただければと思います。

###GenericTeamAgentInterface.h
このヘッダは**TeamId**と呼ばれる**敵か味方か、それとも中立か**を決定するためのIDを取り扱えるようにするインターフェースが定義されています。

このインターフェースを継承し適切な関数をオーバーすることでAIPerceptionの各Senseで設定できる**Detect by Affiliation**の項目をきちんと動作させることが出来ます。

```cpp
//ヘッダ
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "TeamID")
		FGenericTeamId TeamId;
```

これがTeamIDを格納する変数で0～255までの値が入ります。
TeamIDが**自身と同一**であれば（0と0、1と1等）味方であると判断され、**異なる場合**は敵と判断されます。最大値である**255を入れれば**中立であると判断されます。



```cpp
	UFUNCTION(BlueprintCallable, Category = "GenericTeamID")
		virtual FGenericTeamId GetGenericTeamId() const override;
```

GetGenericTeamId関数は文字通り定義したTeamIDを返すGetter関数です。
AISense_SightやAISense_Hearingではこの関数を通して設定されているTeamIDを取得しますので忘れずにoverrideするようにしてください。

TeamIDに関する実装では
[AI Perception in Unreal Engine 4 – How to Setup Written by Denis Rizov](https://denisrizov.com/2017/05/08/ai-perception-in-unreal-engine-4-how-to-setup/)
を参考にしました。

# Core
##BP_BaseCharacter
BP_BaseCharacterはBaseCharacterを継承したBP版のBaseCharacterです。AIとプレイヤーに共通する処理はこちらに定義されています。

![2018-01-07_19h21_58.png](https://qiita-image-store.s3.amazonaws.com/0/104467/f2d7d081-9e56-87d0-ef80-fd6548c45a42.png)

多くの処理がゲームを作る上で頻出する処理だと思います。ここではリロードアニメーション再生処理とラグドール化解説します。

###リロードアニメーション再生処理
![2018-01-07_19h31_11.png](https://qiita-image-store.s3.amazonaws.com/0/104467/bbc5f349-6083-d292-6fc8-1c894eec3480.png)

BeginPlayではBP_BaseCharacterで作成しているイベントディスパッチャOnReloadがカスタムイベントReloadWeaponをバインドしています。
OnReloadは武器の基底クラス（BP_BaseWeapon）で武器が持つ弾数が0になると呼び出されバインドされているReloadWeaponが実行されます。

GetAnimInstanceでアニメーションBPを取得しインターフェースを通じてリロードアニメーションの再生を行います。IPlayReloadAnimからはアニメーションの再生時間が返ってくるのでその秒数分Delayで待機し、リロード中の武器に対してOnFinishReloadAnimを呼び「リロードアニメーションの再生終わりましたよ」と通知します。

今回は「アニメーション再生時間分だけDelayをし武器のリロード処理を呼ぶ」といった処理になりましたが、ここを**[アニメーション通知](https://docs.unrealengine.com/latest/JPN/Engine/Animation/Sequences/Notifies/index.html)**を用いた方法でも問題無いと思います。


###ラグドール化
<img src="https://qiita-image-store.s3.amazonaws.com/0/104467/6d2403b3-1763-0fd5-b7c8-55b578c1834e.png" width=80%>

Healthが0になった時に呼ばれるInDeadでは**ラグドール化**を行っています。
Set All Bodies Simulate PhysicsをTRUEにすることで**物理演算の有効化**を行っています。物演算の対象はTargetにしていされているMeshです。
これでラグドール化はOKなのですがコリジョンが床をすり抜けてしまいますのでSet Collision EnabledでCollision Typeを**Physics Only**に変更します。
最後にCapsuleComponent（Mesh全体を覆うコリジョン）を**No Collision**に設定します。この設定を忘れてしまうと死体にプレイヤーやAIが衝突判定により移動が阻害されます。ゲームプレイ上非常に邪魔なのでここではNo Collisionに設定しました。


##BPI_Character
このプロジェクトにおいて**「BPI」**は**ブループリントインターフェース**を指します。ココらへんのアセット命名規則は[Gamemakin UE4スタイルガイド - アセット命名規則](https://github.com/akenatsu/ue4-style-guide/blob/master/README.jp.md#anc)に則っています。

今回プロジェクトを作成するにあたり課したルールとして「**外部で呼び出される関数**はインターフェースで宣言する」というものです。
このルールを守ることで可能な限り**特定のキャラクタやアクタに依存しない**ようにする事が狙いでした。

BPI_Characterで宣言されている関数は「プレイヤーやAIに共通する処理で、かつ外部から呼び出される関数」となります。
IFireWeaponも実装しているのはBP_Playerですが呼び出す側は**BP_StealthGamePlayerController**です。
AIの場合は実装側はBP_Guardですが呼び出す側は**AIC_Guard**であったりビヘイビアツリーのタスクです。

インターフェースで宣言している関数には頭文字に必ず**I**の文字を付けています。これによりIが先頭にある関数名は「あ、インターフェースのやつだな」と分かりやすくしある程度検索にも引っかかりやすくしたつもりです。

#Characters
##BP_Player
BP_Playerには2つのグラフがあります。移動やしゃがみ、走り等が定義されているイベントグラフと武器の使用に関する処理が定義されているWeaponグラフです。
まずはイベントグラフの方を先に解説します。

###イベントグラフ
<img src="https://qiita-image-store.s3.amazonaws.com/0/104467/e7936fcb-1e67-1dcf-df19-7e3cb0c26efc.png" width=80%>

イベントグラフでは直接入力を検知することはしません。プレイヤーコントローラが入力を検知しインターフェースを通じて命令をします。
ですのでここでは殆どがインターフェースの実装となっています。実装内容の殆どはThirdPersonサンプルにある「Add Controller Yaw Input」「Add Movement Input」等お馴染みのものばかりだと思います。

CrouchやUnCrouchはあまり馴染みがないかもしれません。Crouch、UnCrouchはエンジン側で実装されている機能です。
この2つは文字通り「しゃがみ状態にする」「しゃがみ状態を解除」します。

>CrouchやUnCrouchについては以下のサイトとツイートを参考にしました。
[[UE4] Characterアクター by もんしょの巣穴blog](http://monsho.blog63.fc2.com/blog-entry-144.html)
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Characterクラスから呼び出せる「Crouch」「Un Crouch」関数は文字通り「しゃがみ」を行う。しかしCrouchを呼び出ししゃがみ状態にしてもIs Crouched」変数にはFALSEしか入っていない。<br>Is Crouchedを望む動作をさせるにはCharacter Movement Componentにある「Can Crouch」にチェックを入れれば良い。 <a href="https://t.co/rF0wLwlJSg">pic.twitter.com/rF0wLwlJSg</a></p>&mdash; PavilionDV7 (@Dv7Pavilion) <a href="https://twitter.com/Dv7Pavilion/status/941678216766414848?ref_src=twsrc%5Etfw">2017年12月15日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


PossessやUnpossessedはコントローラーがこのキャラクタを所有したときや切り離した時のイベントです。Possessでは画面左下に出す操作説明ウィジェットを表示するための処理が書かれています。Unpossessedはその逆で表示したウィジェットを削除しています。

###Weaponグラフ
<img src="https://qiita-image-store.s3.amazonaws.com/0/104467/8be50e41-868e-a2fc-0cc3-2930de4b26d2.png" width=80%>

####WeaponSetup
WeaponSetupイベントは武器を装備した時に呼び出されます。ChilcActorコンポーネントを利用して武器の表示を行なっているのでここではChildActorコンポーネントが処理するべき武器クラスをセットし適切な位置へとスナップさせます。（今回の場合は右手にスナップさせています）

####IAim & IUnAim
<img src="https://qiita-image-store.s3.amazonaws.com/0/104467/c80fbbd6-f90c-eab9-a0cd-868e1af170a0.png" width=80%>

IAimとIUnAimイベントは武器を構えた時下ろした時のイベントです。武器を構えた時だけカメラの向いている方向へと体を向けさせたいのでUse Controller Rotation YawをFALSEにしています。MaxWalkSpeedも同様の理由です。構えた時でもJogと同じスピードで動けるのは少々違和感があったのでMaxWalkSpeedを遅めに設定しています。
必要な設定をした後にはタイムラインでカメラの位置を移動させています。カメラの位置はDefault Camera LocationやAim Camera Locationが示しており**Sceneコンポーネント**を使用しています。

####IFireWeapon
<img src="https://qiita-image-store.s3.amazonaws.com/0/104467/b98e5c2b-6c48-b686-4e65-d1cb94bede3d.png" width=80%>

IFireWeaponイベントはコントローラーが左クリック入力を検知した時に呼び出されます。文字通り武器の発射処理を行います。
ここだけは特別にブランチで「現在装備している武器は何か？」というのを調べます。Current Weaponが「BP_Bolt」（投擲可能武器）であるならばそれに応じた処理を行います。（ボルトをSpawn Actor From Classで生成し数秒待機をして再度投げられるようにします）
Current Weaponが「BP_Bolt」以外の場合は武器に備えられたインターフェースを通して「弾を打て（IFire）」という命令を出します。

####IEquipThrowableWeapon
<img src="https://qiita-image-store.s3.amazonaws.com/0/104467/87cff3f5-c563-9377-e0e8-8fef22967d72.png" width=80%>

IEquipThrowableWeaponイベントは投擲可能な武器を装備した時に呼ばれます。今回のプロジェクトの場合は２キーを押すことでこれが呼ばれます。
イベントが呼ばれると「ShowThrowableComponents関数」を実行します。ShowThrowableComponents関数では投擲武器を装備した状態で構えた時に表示される「投擲物の軌道」の表示を担う「BP_ThrowLineComponent（シーンコンポーネントを継承したカスタムコンポーネント）」に対し「軌道を表示してくれ」と命令を出します。

####IChangeWeapon
<img src="https://qiita-image-store.s3.amazonaws.com/0/104467/072137aa-5df5-c344-0053-1ee887df5876.png" width=80%>

IChangeWeaponイベントは投擲物以外の武器を装備した時に呼び出されるイベントです。1キーを押すことで呼ばれます。
このイベントでは新しく装備する武器クラスを保存しWeaponSetupイベントを呼び出しきちんと右手に武器が収まるようにしています。最後にHiddenThrowableComponentsを呼び出し軌道表示を非表示にしています。

##BP_StealthGamePlayerController
プレイヤーの入力を検知し入力に応じた処理をインターフェースを通じて命令する役割を担っています。
多くは見慣れている処理ですしインターフェースの関数を呼び出しているだけなのでここでは「カメラの切り替え処理」を解説します。

###カメラの切り替え処理
<img src="https://qiita-image-store.s3.amazonaws.com/0/104467/00b41830-fdf5-82e0-c151-d51520f795f8.png" width=80%>

プロジェクト作成時にはWatch Dogsをプレイしていて「カメラをハックして死角にいる敵をスポットする」という偵察方法が好きだったのでこのプロジェクトにも入れてみました。
Eキーを押すとカメラの切り替え処理が実行されます。Eキーが押されるとプレイヤーカメラの前方にSphereトレースを行います。トレースがハック可能なオブジェクト（このプロジェクトでは監視カメラ）を検知すると検知したオブジェクトを保存し**Set View Target with Blend**を呼びます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/104467/c536224b-04ee-ab20-bfd5-61b826e6999c.png" width=80%>

Set View Target with BlendによりGetControlledPawnが持つカメラからNew View Targetアクタが持つカメラへと切り替えます。切替時にはいきなり変わるのではなくLerpしたような変化をしますが、この遷移アニメーションはSet View Targe twith Blendの**Blend Time**を設定することで簡単に実現出来ます。何かしらカメラを切り替えたい時にはオススメの処理です。

<img src="https://qiita-image-store.s3.amazonaws.com/0/104467/8a1d01f4-9bc0-5cc7-8df2-56616b02c312.png" width=80%>

Set View Target with Blendが呼ばれた後にはDisable Inputで入力を無効化してBlend Time秒間待機します。待機後にUnPossessし現在コントロールしているアクタからコントローラーを切り離します。Delayの前にUnPossessしてしまうとSet View Target with Blendの遷移がその場で止まってしまいDelay秒の間フリーズしたような動作になってしまうためこのような順序で処理する必要がありました。
UnPossessしたあとは検知したハック対象アクタにPossessしコントローラーを所有させ入力を有効化します。
最後にIUnAimを呼び出しているのはキャラクターがAim状態のままカメラをハックしてキャラクターに視点を戻した時、Aim状態が継続されてしまうという不具合を防ぐためです。

###IReturnToPlayer
IReturn to Playerイベントは「どのカメラからでもプレイヤーキャラクタに視点を戻せる」ようにするための処理です。もし現在の視点が監視カメラだった場合Get Controlled Pawnで取得するアクタは監視カメラになってしまいますのでGet All Actors of ClassでBP_Playerを検索しビュー遷移の対象キャラクタとして保存しています。


## BP_SecurityCamera
###Possess
<img src="https://qiita-image-store.s3.amazonaws.com/0/104467/9beeefaf-a665-cc24-905b-6f4a11020197.png" width=80%>

コントローラーにアクタが所有されるとSet Camera Angle Limit関数で首の回転角度を制限しTickを有効化、操作説明用ウィジェットを表示します。
Possessed Camera View Yaw Max&Minにはアクタが向いているYawを加えています。これによりそれぞれの監視カメラ正面方向から+YawMax分、-YawMin分角度制限をつけています。
試しにPossessed Camera View Yaw Max&Minの値を直接Set Camera Angle Limit関数に渡してみてください。全ての監視カメラが同じ方向に角度制限されるようになります。

<img src="https://qiita-image-store.s3.amazonaws.com/0/104467/7329a6b9-e658-d2bd-4d82-06c30577f271.png" width=80%>

Set Camera Angle Limitではプレイヤカメラマネージャから「View Pitch Max&Min」「View Yaw Max&Min」にそれぞれセットします。これにより簡単にプレイヤーカメラの角度制限を実現しています。（[参考：How to Limit View/Rotation on my player character?](https://answers.unrealengine.com/questions/35476/limit-viewrotation.html)）


###Unpossessed
<img src="https://qiita-image-store.s3.amazonaws.com/0/104467/9943c424-f87a-fde8-469d-6a51654b2a48.png" width=80%>

Unpossessedでは新しく設定したカメラの角度制限は初期設定に戻します。初期設定に戻さなければBP_Playerや他の監視カメラにビューが遷移した際にPossessedで設定したカメラ角度制限がそのまま引き継がれてしまい、BP_Playerのカメラが自由に動かせなくなるバグが発生してしまいます。


###Tick（敵へのスポット処理）
<img src="https://qiita-image-store.s3.amazonaws.com/0/104467/e614ca85-f54f-cd43-01c8-1361549bf081.png" width=80%>

監視カメラでAIを見ると「スポット（ターゲットマークを付与）」出来ます。この処理はTickイベントで行っており「AIを見た」というのはトレースを行って実現しています。ここでは**MultiSphereTraceByChannel**を用います。

LineTraceByChannelではなくMultiSphereTraceByChannelを使用したのは「厳密にAIを狙うのが面倒くさかったから」です。
自分は普段トラックボールマウスを利用しているのですがトラックボールでAIMをするのは少々骨が折れます。そこで「ある程度大雑把に敵方向を見ただけでスポット出来るMultiSphereTraceByChannelを採用しました。

トレースが何かしらにヒットするとまずはPawnに変換します。壁や障害物などには変換失敗するので以降の処理は不要です。
ヒットしたアクタがPawnであればIIsSpottedを呼び「君は既にスポットされてるの？」と問い合わせます。問い合わせた結果「未だスポットされていない」場合は「じゃあターゲットマークを表示してね」と命令をし対象となるPawnにターゲットマークを表示させます。

>ISpottedの実装はBP_Guardにあります。ISpottedを解説するのはまだ先になりますが気になる方は覗いてみても良いでしょう。

#[第2章はこちら](https://qiita.com/Dv7Pavilion/items/b1f259b82c2b8bd5fd8b)