げーむ開発徒然日記~怠惰のために勤勉~

Unreal Engineや3DCG制作について学んだことを記事にしていきます

【UE5】EQSを使って遮蔽物によるインタラクションアイコンの表示・非表示を実装してみる

本記事は、Unreal Engine (UE) Advent Calendar 2022の18日目の記事になります。

目次

はじめに

 本記事では、UEのEQS(Environment Query System;環境クエリシステム)を使って、アイテムピックアップ、ドア開閉などのインタラクション可能なオブジェクトのアイコンを遮蔽物の有無によって表示・非表示を切り替える機能を実装していきたいと思います。

※後述しますが、この方法、正直効果的ではないです。あくまで「EQSを使ってみた」だけのもので、利点は、実装がシンプルでわかりやすいくらいかなと思ってます(インタラクティブなオブジェクトにトレースなどの処理を書かなくて良いため)。プロトタイプや簡単なプロジェクトに使う分にはいいかもしれません。

 なお、今回使用したバージョンは5.1です。

docs.unrealengine.com

 上記の公式ドキュメントによれば「EQS のユースケースの例としては、最も近い体力のピックアップや弾薬を探したり、どの敵が最も脅威となっているのかを把握したり、プレイヤーが見える位置を見つけたりすることが挙げられます」と記載されており、EQSはUE標準のAIビヘイビアツリーなどと組み合わせることで様々な環境に対応可能なAIの作成に活用できるものですが、今回はあえてオブジェクトのアイコン表示に使ってみました。

完成形

 作成したものの完成形は以下のような感じ。

 インタラクション可能なオブジェクトが遮蔽物の奥に隠れている場合、アイコンが非表示になります

 厳密に言えば、Querierとなるキャラクターのカメラ位置からインタラクティブなオブジェクトの原点位置との間に遮蔽物があるかどうかを見ています。この時点でなんとなく、最初に注意点として述べた理由がわかるかと思います。

ウィジェットコンポーネントについて

 インタラクティブなオブジェクトにアイコンを表示しようとした場合、ウィジェットコンポーネントをアクターに取り付けることが多いかと思います。

 そしてウィジェットの描画方法としては、

スクリーン空間 ⇒ワールド座標をもとにスクリーン座標に2Dウィジェットを描画

ワールド空間 ⇒ワールド座標に3Dウィジェットを描画

があります。

 ワールド空間であれば特に何もせずとも遮蔽物の奥のアイコンは隠れるのですが、

「カメラとの距離に応じて表示サイズが変わる」「3Dウィジェットのため3次元の向きを持つ(後ろから見ると反転して見える)」などといった挙動となります。

 反対に、スクリーン空間であれば上記とは逆で「カメラとの距離によって表示サイズは変わらない」「常に正面を向いている」といった挙動にはなりますが、遮蔽物の奥にあったとしても関係なしに画面に描画されます。

 このように、どちらの描画方法も一長一短ですが、今回のようなインタラクションアイコンや敵のHPバーなどといったものに対しては、一般にスクリーン空間での描画が適しています。

 しかし、繰り返しにはなりますがスクリーン空間への描画をそのまま使うと冒頭で示した動画のような、遮蔽によるアイコンの表示・非表示というのができません。仮にこれをBPのみで実装しようとすると、ちょっと面倒だったりします。

 このため、今回は上述のEQS機能のひとつである「プレイヤーが見える位置を見つけたりすること」を使ってサクッと簡単に実装していきます。

実装

準備

 使用するUEのバージョンによっては、プロジェクト内の[Editor Preferences (エディタの環境設定)] > [Experimental (実験的)] > [AI]セクションで、Environment Querying Systemをオンにする必要があります。UE5.1ではそもそも項目が見つからないはずです。

EQS_QuickStart_01.png

※画像は公式ドキュメントから引用

環境クエリの作成

 まずはコンテンツブラウザの右クリックメニューから環境クエリを作成します。

 作成されたアセットを開くと、ビヘイビアツリーのようなエディタ(クエリグラフ)が開きます。

 最初から置いてある、ルートノードからドラッグ操作で出てくる[Actors Of Class]を選択します。

 これは、GetActorsOfClassに近いものであって、詳細パネルから取得したいアクタークラスと、球状のサーチを行うかどうかなどを設定できます。

 今回は、BP_Interactableというクラスをサーチするので、先に作成し、以下のように[Searched Actor Class]に設定しておきます。

 Generate Only Actors in Radiusは、TrueであればSearch Radiusで設定された半径の球状の範囲内のSearched Actor Classオブジェクトをサーチします。Falseであればワールド全体からサーチします。

 Search Radiusは後ほどキャラクタークラスからクエリにパラメータを与えるため、Query Paramsにしておきます。

 Search Centerはサーチの中心位置です。デフォルトのEnvQueryContext_Querierのままでも大丈夫ですが、今回はカメラ位置を中心にしたいので、コンテンツブラウザ右クリックから[EnvQueryContext_BrueprintBase]を作成します。これによって、Querierの情報をもとにカスタムの位置情報などを作成できます。

 作成したらアセットを開き、Provide Single Location関数をオーバーライドします。これで、Querierの位置をキャラクター原点ではなくてカメラ位置にオーバーライドできます。

 これができたら、先ほどのSearch Centerに、作成したEnvQueryContextを設定します。

 次に、クエリグラフに戻ってActors Of Classノードを右クリックして[テストを追加]>[Trace]を選びます。

 Actors Of ClassノードにTraceという項目が追加されるので選択すると、詳細を設定できます。

 設定は以下のようにします。

 [Context]に先ほど作成したカメラ位置を使えるようにするEnvQueryContextを設定します。

 

 これで、このクエリを実行すると、

カメラ位置を中心とするSearch Radius半径の球内に存在するBP_Interactableオブジェクトを取得⇒取得された全てのBP_Interactableオブジェクトの原点位置からカメラ位置にトレースによって遮蔽物があるオブジェクトについてはクエリ終了後の最終的な出力から弾かれる。といった具合になります。

プレイヤーキャラクターの作成

 まずは、近くにあるBP_Interactableを検出するためのコリジョンコンポーネントを追加します。

※InteractableCollisonは詳細パネルから全チャンネルへの応答をオーバーラップにしていますが、インタラクティブなオブジェクトのみをオーバーラップ、他のアクターやコンポーネントに対しては応答を無視にするとなお良いです。

 BeginPlayイベントで色々とセットアップするので、Enable Interactable Detectionというカスタムイベントを追加します。

 以下がイベントの中身です。コリジョンの半径を設定したり、オーバーラップイベントなどを作成しています。

 コリジョン内のBP_InteractableはInteractable配列内に格納されます。

 次はSequenceノードの2つ目からの内容です。先ほど、クエリグラフのActors Of Classノードにてサーチ半径にパラメータを使うことに設定していたので、キャラクターのBPから与えてやります。

 UpdateInteractionIconイベントでは、球体内に存在するBP_Interactableに対して、それぞれの可視・不可視に応じてウィジェットの可視性をトグルしています(次項参照)。

インタラクティブなオブジェクトの作成

 Actorクラスを継承したBP_Interactableは、スタティックメッシュコンポーネントと、ウィジェットコンポーネントを追加しただけの単純なものです。

 ウィジェットコンポーネントの詳細設定にて描画をScreenにしておき、ウィジェットの可視性(Visibility)はデフォルト値をFalseにしておきます。ウィジェットクラスには自作のアイコン表示用ウィジェットを設定してください(作成は割愛)。

 キャラクタークラスから実行していたToggle Iconイベントは以下の通り。

 

⇒実装おしまい

おわりに

 というわけで、今回はEQSを使って遮蔽物の有無によるインタラクションアイコンの表示・非表示をできるようにしてみました。

 実際にはトレースを飛ばし続けてさらに近づいたときに、あとはボタンを押すだけでインタラクトできることを示すようにアイコンの見た目を変えたり、もちろんインタラクション処理も追加実装する必要があります。

 そしてかなりデメリットであるのが、プロジェクトによっては複数の引き出しをコンポーネントとして持つタンスなどのアクターを作成する場合です。この場合、今回の方法を用いると、アクター(タンス自体)の原点の位置情報が使われるため、タンスの原点さえ見えていれば全ての引き出しに対してアイコンが表示されてしまうといったことが生じうるため根本から方法を変えるか大幅な変更を行う必要などが出てきますので、それでも良ければ構わないのですが、できれば今回の方法でも十分なプロジェクトやプロトタイプ程度に使うのが良いかと思います。

 また、タイマーでクエリを実行していますが、For Each Loopによるオブジェクト毎へのウィジェット情報の更新を行いますし、クエリのActors Of Classも決して処理負荷的に優しいものではないという点にご留意ください。

 なので、ちゃんと実装する場合は、素直にC++を使うのがいいです。