【UE5 初心者】球を転がして迷路を攻略するTPSゲームを作る(3) -カメラの追従と視点移動を実装する-

# はじめに
以下の点に注意して頂ければ幸いです。
・初心者による初心者のための記事です。文章に誤りのある場合がありますので注意ください。
・UE(Unreal Engine)のバージョンは5.5.3-39772772+++UE5+Release-5.5、OSはWindows10です。
・UE5のインストールについては完了している想定とします。
・本記事ではスクリプト(C++)は使用せず、UE5で使用されるビジュアルスクリプティングであるブループリントのみを使用します。
# 概要
本記事ではカメラがプレイヤーを追尾する仕組みと、視点を操作する機能の実装を行います。
加えて、視点とプレイヤーの移動方向をシンクロさせる機能、カメラが障害物へめり込まないようにする機能も実装します。
# もくじ
- [1.カメラをプレイヤーに追従させる](#1.カメラをプレイヤーに追従させる)
- [2.視点操作の実装](#2.視点操作の実装)
- [3.視点の方向と移動方向をシンクロさせる](#3.視点の方向と移動方向をシンクロさせる)
- [4.カメラが床を貫通しないようにする](#4.カメラが床を貫通しないようにする)

# 1.カメラをプレイヤーに追従させる
現状ではカメラが固定されており、プレイヤーが移動してもそれに追従しません。常にカメラ中央にプレイヤーを捉えていて欲しいので、プレイヤーの座標に応じてカメラを移動させることで対処します。
この実装にはTickイベントを用いることで、細かい間隔でプレイヤーの位置を取得してカメラの座標を移動させます。イメージとしてはカメラがプレイヤーを監視して追従する感じです。
BP_Characterのイベントグラフを開いてみると、すでにEvent Tickノードがあると思います。

まずどのように機能するか確認するため、以下のようにノードを作成してSphereの座標とカメラの座標を同じ値にセットしてみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/e7781268-e6e8-4287-b644-a5cb5e739bd8.png)

この状態でコンパイルし、保存してゲーム画面に映ってください。カメラが球にめりこんではいますが、球の移動にカメラが追従するようになったと思います。

後は、位置を調整してカメラがプレイヤーから距離を取るようにすれば実装できます。
単純にカメラの位置を後ろにするだけであれば以下のようにカメラの位置を後ろに移動させることで実装可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/127ecd65-9047-437f-9468-c4e9a8401bc7.png)

ただ、この実装では常にカメラが球の後ろに位置するため、以下のように横を向いた際に球が映らなくなってしまいます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/affc9c12-d533-4840-a7d8-71e069bac13e.png)

ので、以下のようにプレイヤーを中心に球状の範囲をカメラが動くようにする必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/33c229a4-46b2-4dc5-b8aa-f3e3823a76b2.png)

この実装にはカメラの角度からベクトルを用いて計算します。UE5には専用のノードが用意されているため、サインだのコサインだので計算せずとも簡単に求めることができます。
まず、Cameraコンポーネントのピンを伸ばして、Get Relative Rotationノードを作成して現在のカメラの角度を取得します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/70ab6cc3-3379-47a7-869b-669c592cfc79.png)

そうしたら、Rotationノードからピンを伸ばしてGet Forward Vectorノードを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/55a5c4c9-938d-434e-b15a-a8d7c79890f7.png)

このノードは与えられた回転角度から長さ1のベクトルを作成してくれます。
そうしたら、カメラ距離の変数を追加して型をFloat、初期値を500、変数名はCameraDistanceとしてMultiplyでベクトルの長さを伸ばします。
その後、以下のように球の中心からSubtractノードを用いてカメラのベクトル分引いて球の後ろの座標を取得します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/8008dd92-6adb-4895-8696-f9f0d1b43abd.png)

ゲームを開始して確認してみましょう。また、カメラコンポーネントのY軸の回転を-30などに設定してみて確認してみましょう。以下のようにどちらの場合でも球が画面中央に表示されればokです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/ab66cf04-c583-4e6f-abb9-5ded800640a4.png)

確認出来たら、ノードを関数にまとめます。ノードをドラッグ(またはCTRLを押しながら左クリック)して選択し、右クリックして関数にまとめます。名前はFollowCameraとします。
あとは以下のようにノードの位置を調整したり、コメントや説明を追加してまとめたら完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/1cb09b8b-3e8c-44d2-9b3f-cb3f03c66f18.png)

# 2.視点操作の実装
まずは視点操作のアクションを追加する必要があります。コンテンツドロワーを開いてActionフォルダにIA_Lookを作成します。IA_Moveを作成した時と同様にValue TypeをAxis 2Dに変更しておいてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/fcf2ea4c-7f17-487d-9d05-89a7cd096568.png)

IMC_Playerにも忘れずに追加します。入力は「マウスのXY 2D軸」に設定します。移動時はモディファイアを設定しましたが、今回の入力ではすでにマウスの移動方向に応じてその値が変換されるため不要です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/b7ff0f90-7511-4835-99b7-7b72e375c3c1.png)

そうしたらBP_Characterに戻ってロジックの実装を行います。関数を作成して名前はLookPlayerとします。Inputに引数としてVector 2Dの値を追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/2e4bea97-1dba-4196-a99e-d5e85ab092be.png)

今回は１でカメラの角度に応じて自動的にカメラがプレイヤーを追尾してくれるようになっているので、入力に応じてカメラを回転させることで実装します。
まず、入力(Vector 2D)を回転(Rotator)に変換する必要があります。
Inputのピンを伸びしてBreak Vector 2Dノードを作成して分解し、Make Rotatorノードを作成し、X -> Z(Yaw), Y -> Y(Pitch)に繋げてRotatorに変換します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/1cd8f8f4-6364-4001-afd0-8a9f1ac9adde.png)

そして、Make RotatorのReturn Valueのピンを伸ばしてAdd Relative Rotationを作成します。TargetにはCameraコンポーネントを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/3bc385b3-beed-4a9c-8ce7-f795f29ee36f.png)

その後、イベントグラフに戻りIA_Lookイベントを作成してLookPlayer関数を繋ぎこんだらゲームを開始してみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/de4fbe58-da4a-41c6-96dc-cfc6375c753a.png)

マウスを動かすとプレイヤーを中心としてカメラを動かせるようになったと思います。
しかし現状の実装だと、以下の問題点を修正する必要があります。

・上(または下)に目いっぱい視点移動するとカメラが反転する点
・視点移動と進行方向がシンクロしてない点
・カメラが床を貫通する点

下二つの点については３，４でそれぞれ修正します。カメラの反転については本章で修正を行います。

カメラが反転する原因は、カメラの回転のYが90(または-90)を超えようとするときにカメラ自身が回転するためです。ゲームプレイ中にカメラのRotateのXの値を確認すると、-180と0の値を反復しているのが確認できると思います。これを防ぐためにカメラの回転のYの値に制限を設けます。
LookPlayerを開いて、以下のように「現在の回転+入力値」の合計値を求めます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/d9201734-b7f7-4134-a27e-4a8446458237.png)

次に、制限の値の変数をFloat型、変数名CameraPitchLimit、デフォルト値は80で変数を作成します。
Yが80(下向き)の場合と-80(上向き)の場合それぞれで検証したいため、Addノードからピンを伸ばしてAbsoluteノードを作成して絶対値を得ます。
次に、AbsoluteノードのピンからLessノードを作成してLessのもう一方の入力ピンにCameraPitchLimitを接続して、出力ピンからBranchノードを作成して、Branchノードを以下のようにLookPlayerの実行ピンに接続します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/b28909c2-2dc9-4b9b-ad0c-54ee8f445f2c.png)

Branchノードを追加したことにより条件分岐が可能となりました。
今回の条件式は「変更後の値<制限値」なので、Trueの場合には今までの処理を、Falseの場合にはY方向の回転を制御させた処理を行わせます。
まず、整理するためにBranchの手前までの条件式を関数にまとめます。関数名はCheckPitchLimitとします。
後は、Falseの場合にはYを変更しないように処理を追加します。あとは整理のために引数のValueをGETすることで見やすくしたり、コメントを追加してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/916ecdf9-279c-46cb-9488-d5d2a44481c6.png)

ゲームを開始してカメラを移動させてみると上下の移動範囲に制限が付き、これにより反転することはなくなったと思います。

# 3.視点の方向と移動方向をシンクロさせる
移動方向とカメラの向きをシンクロさせるために、カメラの追尾の際にも使用したGet Forward Vectorと、その横方向版であるGet Right Vectorを使用することでプレイヤーの向きと進行方向を合致させます。

まず、MovePlayer関数を開いて、カメラコンポーネントからForward VectorとRight Vectorを取得することでカメラ方向のベクトルを取得します。
プレイヤーの移動は平面方向だけにしたいので、To Vector 2DノードでZ方向を除きます。このままだと、Z方向を除いたことで長さが一定でなくなり、カメラの角度に比例して移動距離が小さくなってしまうので、Normalizeノードで再度長さが1になるように修正します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/830ac15e-50cf-49af-bf7d-185f822d8790.png)

あとはこのベクトルと入力値を掛けて足し合わせることでカメラの向きと移動方向を合致させます。
入力値をBreak Vector 2Dで分解し、それぞれX(WキーとSキーの入力値)をForward Vectorの値と、Y(DキーとAキー)をRight Vectorの値とMultiplyノードで掛け合わせ、Addノードで合算します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/d7ede292-90c5-4880-9ce5-956db032199d.png)

これでゲームを開始すると視点の方向と移動方向がシンクロするようになったと思います。

# 4.カメラが床を貫通しないようにする

次に、FollowCameraを修正して、カメラが床を貫通しないようにします。
カメラが床を貫通しているかどうかを調べるにはSphere Traceというノードを使用します。このノードを用いると2点間の周辺の球状の範囲に障害物がないかを調べることができます。
範囲のイメージは以下の塗りつぶし範囲
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/16c2a6d2-11b6-446d-992b-e4835dd08345.png)

まず条件分岐を行うためにFollowCamera関数を開きSphere Trace For ChannelノードとBranchノードを作成し、以下のようにつなぎます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/6cd95116-2be0-44f1-b821-313649b48a0c.png)

次に条件として衝突判定の範囲を設定します。そのためにTraceを行う開始点として球の座標を、終点としてカメラの位置を設定するのですが、これらの値はこの関数では何度か使用するので、使いまわせるようにローカル変数にします。
SphereのGet World Locationノードと、Subtractノードからそれぞれピンを引っ張り、Promote to local variableを選択するとローカル変数のSETノードが作成されます。このノードの実行ノードをSphere Traceノードの前につなぎこんで、それぞれの変数名をSphereLocationとCameraLocationとしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/b1095c20-e451-4279-98a3-9c54bf9c0a82.png)
こうすることでローカル変数となった値はマイブループリントの下部に表示され、この関数内限定で他の変数同様使いまわすことができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/4fc75d37-870e-40b6-a9f4-b228a7156892.png)

そうしたら、Sphere TraceのStartとEndに今作成したローカル変数を繋ぎこみ、Radiusの値を10に設定します。
Set World LocationのLocationの値もローカル変数から引っ張れるので、同様に繋ぎこんで整理します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/eb2ea8b8-496b-45fb-a03a-28e03990ed73.png)

これで条件式が完成しました。ので、次は障害物がある場合のカメラの位置を考えます。イメージとしては以下の赤丸のような障害物とプレイヤーの間の位置にしたいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/1a61f3cd-7d22-4402-8ee4-3a870d044d9c.png)

そのために、まず衝突点の座標を取得します。Sphere TraceノードのOut Hitのピンを伸ばし、Break Hitノードを作成し分解します。
これを展開してLocationのピンを探してください。これが衝突点の座標になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/843b8313-03ad-471d-8432-699ef99bab7d.png)

衝突点の座標と球の座標の間の点を取得するにはLerpノードを使用します。このノードを使用するとA:B=Alpha:1-Alphaの地点の座標を取得できます。Locationピンを伸ばしてLerp(Vector)ノードを作成します。さらにLocal変数からSphereの座標をLerpノードのBの入力ピンにつなぎ、LerpノードのAlphaを0.2に設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/881998ec-ad3a-46d9-a477-2cd7f8193d07.png)

あとはSet World Locationノードでこの座標をセットするだけです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/2af3f14e-2140-4fa5-9d5c-42cff6cd6bdb.png)

ゲームを開始してみてください。カメラが床を貫通しなくなったと思います。

<details><summary>(補足)TPSシューターで実装する場合の注意点</summary>

今回の実装をTPSシューターゲームで行う場合、たぶん衝突時に照準がずれます。
理由は衝突の有無でベクトルが異なるためです。カメラが衝突すると以下のように赤点部分にカメラが移動するため、照準がずれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992369/6d905768-e085-4c26-b3d3-0a77b2eb5583.png)


これに対する簡単な対処は、Sphere Traceの代わりにLine Traceを用いることです。これは線上の範囲の衝突のみを感知するため、衝突点は常にカメラとプレイヤーの軌道上になります。
ただし、浅い角度では壁や床の内部がすり抜けてしまうため、その対処が別で必要になります。(例えば、カメラの角度に応じてLerpのAlphaの値を変更する　など)

今回については別にどっちでもいいのでなるべく簡単に、ということでSphere Traceを利用しています。
</details>


# おわりに
今回でプレイヤーの操作の実装を完了しました。自由にキャラクターを動かせるようになりよりゲームらしくなったと思います。
次回はゲームの内容の部分である迷路ステージの作成を行っていきます。
最後まで読んでいただき、ありがとうございました。

# おまけ　カメラを自撮り棒のように扱える Spring Armコンポーネントについて
<a href=https://dev.epicgames.com/documentation/ja-jp/unreal-engine/using-spring-arm-components-in-unreal-engine?application_version=5.5>公式ドキュメント Spring Armコンポーネント</a>
これを使うとドキュメントの動画にある通り、今回実装したような「障害物がある場合にカメラの位置を修正する」動作が簡単に実装できます。
ただし、これを使用する場合は追尾対象とカメラが親子関係にある必要があります。
今回はカメラと球を親子関係にするとカメラがぐるぐる回るため、親子関係を取って実装できないのでロジックを自前で作成してます。

このコンポーネントに限らずですが、UEには便利なものが揃っていると思います。特に人型のキャラクター想定で。
利用できる場合は積極的に利用したいですね。