#はじめに
第1章の続きです。
この章ではNPCであるGuardやBehaviorTree、EQSを紹介&解説していきます。
#プロジェクトはこちらです!
今回解説するプロジェクトはOneDriveにアップしてあります。バージョンは4.18.2です。
#BP_Guard
実際にレベル上にスポーンされるキャラクタでゲーム世界との相互作用する役割を担います。
初期化を行うBeginPlayイベントです。
ISetTeamIDでGuardに設定されたTeamIDをAIControllerへと渡しています。渡したTeamIDはAIPerceptionのDetect by Affiliationを動作させるために必要なIDです。
デフォルトでは「1」が入っていますが0~254の範囲であれば好きな数字を入れてしまっても構いません。しかしBP_Playerとは異なる数字を入れてください。TeamIDが異なるキャラクタをGuardは敵と認識します。
###ISetPatrolPointsHolder
ISetPatrolPointsHolderはBP_PatrolPointsHolderの参照を保持しているPatrolPointsHolderをAIControllerへと渡します。
BP_PatrolPointsHolderはGuardの巡回地点をVector配列で保持しGuardからの要求で「次に向かうべき位置」を配列から取り出し返します。
BeginPlayの最後で呼ばれていたIChangeStateです。Guardには物凄く簡易ではありますが状態機械を実装しています。
各状態に応じて移動速度の変更と現在どの状態に入っているかをわかりやすくするためにキャラクターメッシュの色を変更しています。
ChangeWalkSpeedではCharacterMovementのMaxWalkSpeedに変数で渡された新しい速度を設定し、ChangeBodyColorはBeginPlayで作成したキャラクターメッシュのDynamicMaterialInstanceに新しい色情報を設定します。
武器を使用するIFireWeaponはBP_Playerと同様にIFireを通じて武器に命令を出しますがこの時に引数のRandomizeをTRUEに、RandomRadiusに値を設定しています。
詳しくはWeaponの項目で説明しますがRandomizeをTRUEにすると射撃時のブレが発生します。RandomRadiusはブレの大きさを表します。
BP_SecurityCameraから呼ばれたISpottedはここで処理されます。物凄く単純です。Create Widgetでターゲットマーカーウィジェットを生成しAdd To Viewportで画面表示を行っています。
生成したウィジェットの参照を保持しているのはDestroy時にRemove From Parentするためです。
IDoCoverはカバー処理を行います。この関数を呼び出しているのはBehaviorTreeTaskのBTT_DoCoverで引数StandCoverに入る値もそちらで求められています。
BTT_DoCoverについては後ほど説明します。
Healthが0になった時に呼ばれるInDeadイベントとワールドから削除される(Destroy)時に呼ばれるDestroyedイベントです。
InDeadでは親クラスのInDeadを呼びラグドール化をします。ラグドール化した後はGuard独自の処理としてBrain ComponentからStop LogicをしてBehaviorTreeの実行を停止しています。
Stop Logicについては以下のサイトを参考にしました
UE4でAI(ビヘイビアツリー)を一時停止する方法 Written by WassyPGさん
UE4 AIで使う移動と停止のまとめ by Let's Enjoy Unreal Engine
画像のように配布中のプロジェクトではGuardが死亡してもDestroyされることはありません。
Guardには仲間の死体を見つけた時の振る舞いも実装されています。是非こういった振る舞いも見てほしかったので今回はわざとDestroyを実行させないようにしています。
#AIC_Guard
BP_Guardは世界と相互作用する役割を担いますがAIC_Guardは相互作用する方法を決定します。つまりは意思決定です。
AIC_Guardはセンサー(ここではAIPerception)を通じて世界の情報を収集します。集めた情報を基にAIC_Guardは適切なオブジェクトに適切な命令を出します。
AIPerceptionそのものについての説明はここではしません。AIPerceptionは一体何をしてくれるのか?どのような機能があるのか?とAIPerceptionについて不安をお持ちの方は以下のページを参考にしていただければと思います。
【UE4】味方AIの作り方!AIとは何かを学びながら、ブループリントで味方キャラクターを実装しよう
【UE4】AI Perception の紹介と使い方
##OnTargetPerceptionUpdated(AIPerception)
AIPerceptionが何かしらの刺激を受け取るとこのイベントが呼び出され実行されます。
引数Actorには刺激を与えたアクタが渡されます。引数Stimulusは刺激を受けた場所(Stimulus Location)やアクタが見えた・見えなくなった(Successfully Sensed)等の情報が渡されます。
SwitchToAIStimulisは引数Stimulusのクラスに応じた実行ピンに遷移するようなマクロを組んでいます。
調べるクラスはAISense_Sight(視覚から受けた刺激)、AISense_Hearing(聴覚から受けた刺激)の2つです。このプロジェクトではAIPerceptionが受ける刺激は視覚と聴覚だけなのでこのような実装にしました。AIPerceptionにはこれ以外にもいくつかの種類がありますが殆どの場合はこの2つを分類出来れば十分でしょう。
###Hearing(不審な音を聞いた時)
SwitchToAIStimulisでHearingが実行された場合に呼び出される処理です。Guardは不審な音を聞いた時音が鳴った地点の周囲を探索しGuardにとって敵であるプレイヤーを探します。
IsValidでNullチェックを行い未だプレイヤーを視認していないことを確認します。(探索は敵を未だ視認していない場合のみ実行されます。敵が目の前に居るのに敵を探す必要はありませんから。)
視認していなければGetValueAsObjectで返ってくる値はNULLなのでIsNotValidに処理が流れ敵の探索を実行します。SearchEnemyでは異音が聞こえた地点を中心に周りを探索するので変数CurrentStimulusからStimulus Location(異音が鳴った位置)を引数に渡します。
SearchEnemyという名前が付けられていますがこの関数では敵の探索は行ないません。実際に探索をするのはBehaviorTreeです。
しかしBehaviorTreeで振る舞う際に必要な情報はこの関数で渡し、BehaviorTreeも渡された時点で探索を開始しますので、このような関数名にしました。
関数に入るとSpawnActorFromClassでBP_SearchNodeGenerator(このアクタについては後ほど説明します)が生成されBlackboardに格納されます。
格納する前に既にBlackboard内に別のBP_SearchNodeGeneratorの参照を保存していないかを調べます。参照が保存されていた場合はそれをワールドから削除し、新しく生成したBP_SearchNodeGeneratorと置き換えます。
###Sight(何かを見た時、見失った時)
敵が見えた時、見失った時、仲間の死体を見つけた時はこれらの処理が実行されます。
Branchでは見えた何かがプレイヤーかをチェックしています。TRUEであればプレイヤーキャラクタの参照をBlackboardのEnemyへ格納します。
次に変数EventTimerHundleをNullチェックしています。変数EventTimerHundleは敵を見失った時に実行されるSetTimerByEventのタイマーで、Nullではない(敵を見失った)場合タイマーを停止&クリアし、SetTimerByEventで設定したイベントの実行を停止させます。(敵が見えてしまったので「見失った処理」は実行する必要がありません)
見えた何かがプレイヤーではない場合、それは「自分たちの仲間」と判断されます。仲間と判断されると次に「こいつは死んでいる?」と尋ねます。
尋ねた結果TRUEだった場合Seeing Friend Dead Bodyイベントを実行します。Seeing Friend Dead BodyイベントはHearing時の処理に繋がっており、死体の周囲を探索します。
AIPerceptionのAISightは「見失った時」もイベントを実行し、そのときStimulusのSuccessfullySensedにはFalseが入っています。
それによりBranchはFALSEを通り敵を見失った処理が実行されます。
敵を見失った場合はすぐにBlackboardの情報は更新せず、ある時間待機します。
ある時間というのはAIPerceptionのAI_Sight_Configで設定できるMaxAgeのことです。(単位は秒)
SetTimerByEventの引数Timeには変数CurrentStimulusのExpiration Ageを渡していますが、このExpiration Ageには先程のMax Ageで設定した数値が入っています。
MaxAge秒後にイベントClearSeeingEnemyが実行され、ここで初めてBlackboardの情報が更新されます。BlackboardのEnemyがNullになるとGuardはパトロールに勤しみます。
#EQS(Environment Query System)
EQSは環境の情報を収集し、目的を達成するための最高の位置を求める機能です。AIを戦略的に動かすためには欠かせません。
UE4のEQSについてよく知らない方は公式のページが参考になるかと思います。チュートリアルもありますので是非。
Environment Query System
##EnvQueryContext(コンテキスト)
EnvQueryContextはEQSを実行する上で必要な情報を提供するデータです。位置を求めるためにEQSはジェネレータを通じてアイテムを生成します。コンテキストはアイテムを生成する位置やアクタをジェネレータへ提供する役割を担います。
###EQC_Player
EQC_Playerはプレイヤーキャラクタを表します。プレイヤーキャラクタへの参照を取得し提供します。
Provide Single Actor関数をオーバーライドして返り値にGet Player Characterで返った値を渡します。
Provide Single Actorは文字通り一つのアクタの参照を提供します。他にも複数のアクタを提供する「Provide Actors Set」や位置を表すVectorを提供する「Provide Single Location」、複数の位置を提供する「Provide Locations Set」があります。
Provide Single Actorを使用したのは、このプロジェクトにおいてプレイヤーキャラクターはBP_Playerだけであり、シングルプレイ専用なのでBP_Playerが何人もレベルに存在するわけでもないからです。
もしもマルチプレイに対応していたりプレイヤーのパートナーAI等がいた場合はProvide Actors Setを利用するほうが良いでしょう。
###EQC_CoverObjects
Guardがカバーリング(障害物に身を隠す)するための障害物を提供するコンテキストです。
GetAllActorsWithTagでCoverObjectとTagを付けられたレベル上にあるオブジェクトを全て取得し渡します。
###EQC_SearchNodes
敵を探索する際にBP_SearchNodeGeneratorが生成したBP_SearchNodeを提供するコンテキストです。
BP_SearchNodeGeneratorはグリッド状(格子状)にBP_SearchNodeを生成します。GridPointはその生成されたBP_SearchNodeの配列を表しています。
##EQS_SearchEnemy
このEQSでは敵を探索するのに最も良い位置を求めます。
一番上にあるActorsOfClass:以下略は「SearchCenterを中心に半径SearchRadius内にあるSearched Actor Classを探し、そのアクタをアイテムとする」ということをしています。
SearchCenterに指定されているEQC_SearchNodesはレベルにグリッド状に配置されているBP_SearchNodeを表しているのでBP_SearchNodeがそのままアイテムとして扱われます。
ActorsOfClass:以下略の下にある青いやつらはEQSテストを表します。ジェネレータが生成したアイテムから最も良い位置を求めるスコアリングはテストを通して行われます。
###GameplayTagsテスト
UE4にはActor TagやComponent TagがありますがGameplayTagsは少し特殊なタグです。
GameplayTagそのものについては以下のサイトが参考になるでしょう。
UE4 Gameplay Tagを使ってゲームプレイ時のタグ管理をより扱いやすくする
GameplayTagsテストではアイテム(BP_SearchNode)に設定されているGameplayTagをチェックし、合致していれば1点、外れていれば0点を与えます。
今回の場合アイテムに**UnVisited(未訪問)**のGameplayTagが付けられていれば、そのアイテムは1点もらえます。
###PathExist:from Querier
PathExistテストは3つのTestModeを備えています。
アイテムに対する経路は存在しているか(到達可能か)を対象とするPath Exist
アイテムまでの移動コストを対象とするPath Cost
アイテムまでの移動距離(コストとは別です)を対象とするPath Length
今回はアイテムまで到達可能かを対象とするPath Existでテストしています。
「from Querier」とありますが、QuerierはこのEQSを実行したアクタを表します。(今回のプロジェクトではGuard)このテストの場合は「Querierはアイテムまで到達可能か?」をチェックし可能であれば1点与えます。
アイテムからDistance toに指定されたコンテキストまでの距離に応じた点数を与えるテストです。点数は1~0までの値が与えられます。
画像ではScoring EquationにInverse Linearが指定されています。Distanceテストはデフォルト設定の場合遠ければ遠いほどスコアが高いという具合になっているのですが、Scoring EquationをInverse Linearとすることで、その結果を反転させます。つまり近ければ近い程スコアが高いとなります。
###Dot 2D : 以下略
このテストは少しむずかしいテストです。Dotと名が付いているので、このテストは内積の結果で点数が付けられます。
このテストに関して詳しく知りたい方は以下の記事を読んでいただければと思います。
【UE4】EQSを使ってInfluence Mapを作った
このテストでは「Querierが向いている方向にあるアイテムは高得点」という点数付けをしています。
##EQG_HidingSpot
Guardのカバーポイント(身を隠す位置)を生成するジェネレータです。EQSジェネレータはエンジン側で用意されているもの以外にもEnv Query Generator Blueprint Baseを継承することでカスタムメイドのジェネレータを作成できます。
実装自体は公式のEnvironment Query System ユーザーガイドを参考にしました。一番下にExample Generatorとして実装例が掲載されています。
(今回のプロジェクトでは少し簡略化しています)
Environment Query System ユーザーガイド
引数ContextLocationsから一つ要素を取り出しQuerier Locationとします。その時、-Z方向に少し移動させた値を保存します。
引数から取り出した位置はプレイヤーキャラクタの腰の位置が入っています。この位置にTraceさせると腰の位置より低い高さの障害物に衝突せず十分なカバーポイントを作成することが出来なくなってしまうので、引数の位置から少し下の位置を保存するようにしました。
ここでは障害物だけをトレースで検知したいのでプレイヤーキャラクタやAIは検知対象とならないようにGet All Actors Of Classで検索しトレース除外対象として保存します。
ここでのLineTraceは円の外側から内側(Querier Location)に向かってトレースをします。外から内に向かってトレースすることで確実にプレイヤーキャラクタとの間に障害物がある位置を取得できます。
トレースが衝突すると、衝突した面の法線(Normal)を取得し変数Offset From Wallでスケーリングします。つまり衝突面から離れた位置を求めます。
Normalから求めた位置を衝突した位置(Impact Point)へと加えると衝突位置から変数Offset From Wall分壁から離れた位置が取得出来ます。
取得できた位置はAdd Generated Vectorに渡され、アイテムを生成する位置として処理されます。
##EQS_FindCover
最も良いカバーポイントを求めます。EQS_HidingPointで求められたカバーポイント候補に対してスコアリングをします。
###Trace : to EQC_Player on Weapon
カバーポイント候補からプレイヤーキャラクタに向かってトレースします。通常のTraceテストはトレースがヒットすると1点もらえるのですが、今回の場合はその結果を反転し「トレースが何かにヒットしなければ1点」としています。
結果を反転させることでプレイヤーへの射線が通る位置を求めることが出来ます。
Traceテストはデフォルトのままだとアイテムの位置からContextへ向かってトレースを行ないますが、今回のような射線が通るか?というテストをする際には良い結果が得られない事がほとんどだと思います。
アイテムの多くは地面に接した位置に生成されます。実際の射線は手元から敵に向かうものですので、そのままトレースしてしまうと腰の高さまでしか無い障害物からでも「ここからは狙えません!」という結果が返ってしまいます。
そこで上の画像のItem Height Offsetに数値を入力します。この数値によりアイテムの位置がZ方向(上)に移動します。今回は100と入力することで鳩尾・胸くらいの高さまでアイテムをオフセットさせています。
###Dot 2D & Path Exist
EQS_SearchEnemyで説明した内容と同一ですので、省略します。
#BehaviorTree
BehaviorTreeではGuardの振る舞いを実装し、実際にフィールドをパトロールさせたり、攻撃させたりカバーさせたりします。
今までBP_GuardやAIC_Guardで紹介&解説してきた多くの関数やイベントはBehaviorTreeから実行されます。
BehaviorTreeの動作が分からない方は以下のスライドを読むのをオススメします。
【自作ゲーム】 Behavior TreeでAIを作る 【Unity】
UE4のBehaviorTreeについては公式ページが参考になるでしょう。
ビヘイビアツリー
##BT_Guard
AIC_GuardのBeginPlayで「Run Behavior Tree」を実行していますが、その際に実行するBT Assetとして指定されているのは、このBT_Guardです。
BT_Guardは各状態(戦闘、探索、パトロール)に応じたBehavior Treeを実行させる役割を担います。
各BehaviorTreeへの遷移は単純な比較です。
ツリー右側の「Has No Enemy」は敵を発見したときに値が格納されるキーEnemyをチェックしています。キーEnemyがNULL(敵を発見していない)の場合は以下2つのいずれかを実行します。
Has No Enemyにはオブザーバーを中止に「Both」が設定されており、Guardが敵を見つけると即座にパトロールもしくは敵の探索を中止し攻撃を開始します。
「Has SearchNodeHolder」もHas No Enemyと同様です。この場合はキーSearchNodeHolderをチェックし値が格納されていれば(異音を聞いた・仲間の死体を見つけた)探索を実行します。
##BT_Patrol
パトロールを行うBehaviorTreeです。
これは以下のような順番で実行されます。
- Change Stateで状態をパトロール状態へと変更します。
- 「Has Patrol Points Holder」でBP_PatrolPointsHolderの参照を所持しているか確認します。所持していれば下の2つのタスクを実行します。
- BTT_GetNextPatrolLocationで「次に向かうべき巡回地点」を取得します。
- Move Toで取得した地点へ移動します。
- 1から4まで繰り返します。
このBehaviorTreeでは2つのカスタムメイドされたBehavior Tree Taskがあります。
###BTT_ChangeState
BTT_ChangeStateはGuardの状態を変更します。このタスクは各BehaviorTreeで実行されており、BehaviorTreeに応じた状態に遷移します。
引数ControlledPawnにはこのBehaviorTreeを実行するアクタの参照が入っています。それに対しインターフェースを通じて新しい状態へと変更します。
新しい状態を表すキーNewStateは「インスタンス編集可能」にチェックが入っておりBehaviorTreeエディタで目的の状態を設定することが出来ます。
最後にFinish Executeでこのタスクが「無事に実行された」としています。
###BTT_GetNextPatrolLocation
このタスクはBT_Patrolでのみ呼び出されます。タスク名の通り「次に向かうべき巡回地点」を取得しBlackboardへ適したキーへ格納されます。
このタスクには2つのキーが定義されています。
LocationKey : 巡回地点を格納するBlackboardで定義されたキー。Vector型。BehaviorTreeエディタから値を設定する。
PatrolHolderKey : 実行しているGuardが巡回するべき地点全てを保持するBP_PatrolPointsHolderが格納されているキー。BehaviorTreeエディタから値を設定する。
このタスクが実行されるとPatrolHolderKeyからBP_PatrolPointsHolderの参照を取り出します。取り出した参照から関数GetNextPatrolPointを実行し次へ向かう巡回地点を取得します。
取得した地点はLocationKeyへ格納されます。
##BT_SearchEnemy
異音を聞いた時に探索を行うBehaviorTreeです。
以下の順番で実行されます。
- Change Stateで探索状態へと変更します。
- Move To Noise Location(Move Toタスクを新しく命名しています)で異音が鳴った地点へ移動します。
- Run EQS QueryでEQS_SearchEnemyを実行し向かうべき地点を取得します。
- Rotate(Rotate To Face BBEntryタスクを新しく命名しています)で向かうべき地点へ体を向けます。(これが無くても移動方向へ向くのですが一瞬で振り向いてしまうのでそれを防ぐために使用しています)
- Move Toで向かうべき地点へ移動します。
- Run EQS Queryの実行が失敗するまで1から5を繰り返します。失敗するとEnd Searchが実行されます。
- Release Node Holderで所持しているBP_SearchNodeGeneratorのアクタをDestroyします。
- Change Stateでパトロール状態へと変更します。
###BTT_ReleaseSearchNodeHolder
このタスクではBlackboardに保存されているSearchNodeGeneratorの参照を取得するのに必要なBlackboardKeySelector定義されていません。
代わりにMake BlackboardKeySelectorを使用しタスク上でBlackboardKeySelectorを作成しSearchNodeGeneratorの参照を取得しています。
この方法を取ったのは「エディタでの指定忘れ」が発生したためです。BlackboardKeySelectorを定義しインスタンス編集可能とすることでエディタから目的のキーを指定するのですが、自分は度々指定忘れをしてしまいます。処理は問題無いのに指定するキーが間違っているため目的の位置へ移動しない等のバグが発生しました。
自分のせいですが結構イラッと来てしまったので指定忘れを防ぐためにMake BlackboardKeySelectorで直接取得するキー名を指定し取得する方法を取りました。
敵を視認すると直ちにこのBehaviorTreeが実行されます。ここではカバーポイントへの移動、カバーリング(しゃがみ)、敵への射撃をしています。
以下の順番で実行されます。
- Change Stateで戦闘状態へと変更します。
- Search Node Holderを削除します。(これは探索中に敵を発見した場合の処置です)
- Weighted Randomは左右いずれかのツリーをそれぞれ50%の確率で実行します。
- 左のSequenceが実行された時
- 強制的に立ち上がります
- EQS_FindCoverを実行しカバーポイントを取得します
- 敵に銃口を向けながらカバーポイントへ移動します
- カバーリングを行ないます
- 右のSequenceが実行された時
- BTD_CloseEnoughで敵との距離が十分離れているかチェックします(近ければ即座に以下のタスクは中止します。)
- しばらく待機します
- カバーリングでしゃがんでいる場合は立ち上がります
- Simple Parallelで以下のタスクを並行して実行します
- 敵への射撃を行ないます
- 常に敵の方向へ銃口が向くように回転します
###WeightedRandom
このコンポジットノードはエンジンが標準で実装しているノードではなく自作したものです。
WeightedRandomノードの実装や仕組みについては以下のブログを参考にしました。
このタスクではBP_Guardが実装しているイベントI Do Coverを引数Stand CoverにTRUEを入れて実行しています。
I Do Coverは引数がTRUEであればUn Crouchを実行しますので、強制的に立ち上がることになります。
BehaviorTreeには標準の機能に「Move Toタスク」があるのですが、BTT_MoveTo_LookAtでは少し改良して指定されたアクタ(もしくは位置)を向きながら移動するようにしています。
イベントグラフは複雑なように見えますが実際にはキーDestinationKeyに格納されている型がObject型かVector型かで分岐させているだけで、やっていることはほとんど同じです。
ここではKeyがVecotr型の場合の処理を見ていきます。
Branchを通ると関数Look At TargetでキーLookAtTargetKeyが持つアクタ(もしくは位置)を見るようにAI Controllerに命令をします。
次にAI Move ToでDestinationKeyが持つ位置に向かって移動をします。
移動が終わると関数ClearFocusを呼び出しFocus状態を解除します。
LookAtTargetKeyが持つアクタ(もしくは位置)を見るようにするための関数です。
この関数もBTT_MoveTo_LookAt同様にLookAtTargetKeyがObject型かVector型を持っているかで処理を分岐させています。
ここではObject型だった場合の処理(上側)を見ていきます。
Branchを通ると関数SetFocusに引数で渡されたInput Pin(BTT_MoveTo_LookAtのOwner Controllerが渡されます)とLookAtTargetKeyが持つObjectをActorへ変換してNew Focusへと渡します。
関数SetFocusはエンジン側に実装されている関数で引数New Focusに指定されたアクタ方向を向くようにAI Controllerに命令するものです。これは一度呼ばれるとずっと対象となるアクタを向き続けます。
向き続ける動作を停止させるには対となる関数ClearFocusを呼びます。BTT_MoveTo_LookAtでも最後にClearFocusを呼び出しています。
アクタ方向へAIを向かせるにはSetFocusを使いましたが、位置(Vector型)へAIを向かせるには関数SetFocalPointを使用します。
ちなみにSetFocusの実装では引数で渡されたアクタから位置を取り出し(GetActorLocation)それをSetFocalPointへと渡しています。
カバーリングを行うタスクです。
ここでの処理は関数TraceAroundが重要ですのでそちらを解説します。
関数TraceAroundはEQG_HidingSpotを参考に処理を組みました。
引数Centerにはトレースする際の中心位置が渡されます。
あとの流れはEQG_HidingSpotと同じです。トレースが何かにヒットすれば即座にTRUEを返し処理を抜けます。
この関数はGuardの腰あたりから周囲にトレースをします。そのトレースが何かにぶつかった場合、「あ、目の前には背の高い壁があるな」と判断されGuardは立った状態でのカバーリングをします。(実際は立ちっぱなしなだけ)
トレースが何にもぶつからなかった場合、周囲にはGuard全体をカバー出来るほどの障害物は無く背の低い障害物しかないためGuardはしゃがんだ状態のカバーリングをします。