ゲームAI
UE4

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

はじめに

この記事は「【UE4】AIのExample Projectを作ってみました(フォルダ構造編)」の続きとなっております。

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

プロジェクトはこちらです!

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

StealthGameExample_Ver1_5.zip

C++

BaseCharacterクラス

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

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

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

AISightTargetInterface.h

このヘッダにはインターフェースが定義されており関数をオーバーライドすることでAIPerceptionAISense_Sightで使われる視線チェックの動作を変更することが出来ます。
詳しくは
【UE4】AISense_Sightの視線チェックを改良する
こちらを読んでいただければと思います。

GenericTeamAgentInterface.h

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

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

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

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

    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
を参考にしました。

Core

BP_BaseCharacter

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

2018-01-07_19h21_58.png

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

リロードアニメーション再生処理

2018-01-07_19h31_11.png

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

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

今回は「アニメーション再生時間分だけDelayをし武器のリロード処理を呼ぶ」といった処理になりましたが、ここをアニメーション通知を用いた方法でも問題無いと思います。

ラグドール化

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スタイルガイド - アセット命名規則に則っています。

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

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

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

Characters

BP_Player

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

イベントグラフ

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

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

CrouchやUnCrouchについては以下のサイトとツイートを参考にしました。
[UE4] Characterアクター by もんしょの巣穴blog


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

Weaponグラフ

WeaponSetup

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

IAim & IUnAim

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

IFireWeapon

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

IEquipThrowableWeapon

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

IChangeWeapon

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

BP_StealthGamePlayerController

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

カメラの切り替え処理

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

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

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

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

Set Camera Angle Limitではプレイヤカメラマネージャから「View Pitch Max&Min」「View Yaw Max&Min」にそれぞれセットします。これにより簡単にプレイヤーカメラの角度制限を実現しています。(参考:How to Limit View/Rotation on my player character?

Unpossessed

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

Tick(敵へのスポット処理)

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

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

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

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

第2章はこちら