#はじめに
Watch Dogsをプレイしていて警備兵に対し「ルアー」と呼ばれる注意を引きつけるアイテムを使って遊んでいたのですが、引き付けられた時の振る舞いを見てみると以下のような手順になっていました。
異音方向を見る → 異音が鳴った位置へ移動する → 元の巡回位置へ戻る
警備兵という立場ですので「異音の位置へ移動した後、元の位置へ戻る」というのは自然ではあると思うのですが**「異音の周辺を調査」**をさせてみるとよりそれっぽくなるのではないかと思い実際に実装してみました。
#プロジェクトはこちら
https://1drv.ms/u/s!Au-8FqgREBKZhSxPCPt1iiJBPMPc
今回解説するプロジェクトは上記のURLからダウンロード出来ます。
コメントを多く書いていますので「何をどうしているのか」というのは分かりやすくなっているはずです。この記事を見る前にザッと確認することをオススメします。
#異音周辺調査のためのノードとノード生成
##BP_SearchNode
異音が聞こえた際の調査にはBP_SearchNodeアクタを活用します。このアクタはグラフ探索アルゴリズムでのノードのような役割を果たします。
このアクタは以下のような
SearchNodeBase.h
~~ 省略 ~~
#include "GameplayTagAssetInterface.h" // ADD
~~ 省略 ~~
UCLASS() // ADD
class SEARCHAITEST_API ASearchNodeBase : public AActor, public IGameplayTagAssetInterface
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GameplayTags")
FGameplayTagContainer MyTagContainer;
~~ 省略 ~~
UFUNCTION(BlueprintCallable, Category = GameplayTags)
virtual void GetOwnedGameplayTags(FGameplayTagContainer& TagContainer) const override
{
TagContainer = MyTagContainer;
}
IGameplayTagAssetInterfaceを備えた独自のアクタクラスを継承して作成しています。このインターフェースはEQSのGameplayTagsテストをきちんと動作させるために必要なものです。
GameplayTagContainerは**「訪問済みノードか?」、「未訪問のノードか?」というタグを保存しています。EQSテストの際に「訪問済み」のタグを持つノードはフィルタリングにより除外され警備兵は可能な限り一度チェックした地点を再び訪れない**ようになります。
異音の地点から周囲をランダムに移動するとした場合同じ場所を行ったり来たりします。
こういった動きは時に自然に見えることもありますが、やはり「スクリプト感」が否めない不自然な動きをすることもあるため今回のような一度訪れた地点は可能な限り避けることで「らしい動き(振る舞い)」をさせることが出来ます。
##BP_SearchNodeGenerator_Grid
このアクタは上記のBP_SearchNodeを格子状(グリッド状)に生成&配置させるためのアクタです。EQSのSimpleGridジェネレータのソースを参考に組んでみました。
警備兵が異音を聞いたときには異音の位置にこのアクタをスポーンさせます。
#EQS_Search
ジェネレータはActors Of Classを使用しています。これを使った理由はただ1つ、GameplayTagsテストを使うためです。
GameplayTagsテストはIGameplayTagAssetInterfaceを備えたアクタを対象とします。頻繁に使われるであろう「Circle」「Cone」「Donut」「Grid」等のジェネレータはFNavLocationというEQSアイテムを生成するのですがFNavLocationはインターフェースを備えておらずGameplayTagsテストではスキップされてしまいます。
Actors Of Classテストはアクタを対象にEQSアイテムを生成するので検索対象にIGameplayTagAssetInterfaceを備えたBP_SearchNodeを指定することで問題をクリアしています。
詳しくはエンジンソースのEnvQueryTest_GameplayTags.h/cppを参照していただければと思います。
#BehaviorTree
ノード数が多いので複雑そうに見えますが案外やっていることは単純です。上から順に解説していきます。
-
一番上にあるデコレーター。BlackBoardにあるEnemyキーがセットされていなければ以降の処理を続けます。Enemyキーは警備兵がプレイヤーを視認した時に値を格納するようにしているので、今回のプロジェクトで値が格納されることはありません。(聴覚のみの設定しか行っていないため)
-
SearchNodeHolderキーにはBP_SearchNodeGenerator_Gridのリファレンスが格納されます。SearchNodeHolderキーに値が格納された時は**「異音が聞こえた」**ということを示しています。
-
警備兵には簡易な状態遷移を実装しています。パトロール状態か調査状態に応じて移動速度の変化やメッシュカラーを変更させています。Move Toは異音の位置が格納されているキーへと移動します。これはWatch Dogsでの振る舞いと同じです。
-
Set All TRUEとなっているノードは自作のノードでBoolean型のキーの配列を全てTrueにセットします。このノードによりReached Noise LocationデコレータからFalseが返されますのでSELECTORコンポジットを次の処理へと進める事が出来ます。
-
異音周辺を調査させる実際のノード群です。EQS_Searchで得られた位置がDestinationキーに格納されMove Toで移動しています。
-
Run EQS QueryノードはEQSアイテム全てがフィルタリングによりスコアが付けられないという状況になるとFailedを返します。EQS_SearchではGameplayTagsテストで「訪問済みノード」を除外します。警備兵が探索するべき地点を全て巡るとノードには全て「訪問済みタグ」がセットされているはずなのでRun EQS QueryはFailedを返し「異音周辺の調査が終了した」と判断しSELECTORコンポジットを次の処理へと進めます。
-
これらのノードは調査が終了した時に呼ばれます。調査が終了したことを示すデバッグ表示(自作タスクです)して不要になったSearchNodeHolderをDestroyしてNULLをキーに格納します。
-
Has Not SearchNodeHolderデコレータはSerchNodeHolderに何も値が入っていない、つまり異音を聞いていない事をチェックしています。Has Patrol Holderデコレータでは巡回のための位置情報を格納しているBP_PatrolPointsHolderを持っているかをチェックし警備兵が巡回するタイプならば巡回処理を行います。
#BP_CommanderAI
異音が聞こえた時「どの警備兵を調査へ向かわせるか」を決定し適切な警備兵へと指示を送るPawnです。
調査へ向かわせる警備兵はレベル上にいる全ての警備兵の中から最も異音発生地点から近い者が担当します。CommanderAIにピックアップされた警備兵は異音発生地点にBP_SearchNodeGenerator_Gridをスポーンし調査行動を行います。
異音が発生する度にGet All Actors Of Classで警備兵を取得しても良いのですが当時は何を思ったのか「登録制にしよ」と考えてしまいこのような実装になりました。
警備兵は異音が聞こえた時RegisterNoiseListenerイベントを呼び出し、自身を登録します。この時「Begun to Registration」がFalse、つまり誰もイベントを呼び出していなかった場合は登録受付の制限時間をセットしReception Time秒の間だけ登録を受け付けます。
Reception Time秒経過すると登録受付を切り上げ登録された警備兵から異音に対し最も近い者をピックアップします。
ピックアップされた警備兵はPreparation for Search Enemyイベントで異音周辺調査のための準備を行います。
#AIC_ExampleAI
##On Target Perception Updated
これはOn Target Perception Updatedの後半部分です。警備兵の調査状態はここから始まります。
Enemy変数がNullの場合のみ処理を行います。Enemy変数に値が入っている時(プレイヤーを視認した時)は、わざわざ異音の調査をする必要がありませんのでこのようにしました。
AIPerceptionから受け取った刺激が有効な場合いきなりBehaviorTreeを停止させ、異音方向へ警備兵を振り向かせます。メタルギアソリッドでは空マガジンを投げると警備兵が「ん?」とその方向へ振り向きます。これを実現してみたかったのでこのように処理を組みました。
異音方向への振り向きが終了するとイベントディスパッチャによりOnFinishLookAtNoiseLocationイベントが呼ばれます。
CommannderAIに対し「異音が聞こえました」と登録をし受付終了まで待機した後にBehaviorTreeを再開させ、調査へ向かうか巡回状態へ戻るか何れかの行動を取ります。
#PreparationForSearchEnemy
このイベントはCommanderAIから「異音の調査へ向かってくれ!」と指示された場合のみ呼び出されます。
SearchNodeHolder変数がNullか否かをチェックしています。調査中の状態で改めてCommanderAIから別の地点への調査の指示があった場合にレベル上にスポーンしていたBP_SearchNodeGenerator_GridをDestroyします。これが無ければ調査中に割り込まれる度にBP_SearchNodeGenerator_Gridがどんどんスポーンされ永遠に削除されなくなります。
チェックが終わり適切な処理が呼び出された後はBP_SearchNodeGenerator_GridをスポーンしBlackBoardに値をセットしていきます。
#おわりに
完成してみると想像していたよりも良い感じに動いてくれたなと満足しています。
個人的に「使い道なさそうだなぁ」と思っていたEQSのActors Of ClassやGameplayTagsテストが大活躍したので「侮れないなぁ」と反省しきりでした。
以上で紹介は終了です。何か質問があればコメントやTwitterに書いていただければと思います。
お疲れ様でした。