Unreal Engine 5.3 でアクター（物体、オブジェクト）をマウスで操作するプログラムを書く

# 主旨

画面内に表示されているアクターをマウスでクリック（ドラッグ）して、そのまま動かしたい先にアクターを移動できるようにするブループリントの書き方についての忘備録です。

内容としては、下記の記事の UE5 版です。基本的には下記の記事とほぼ同様の操作で実現可能です。

- [Unreal Engine 4.25 でアクター（物体、オブジェクト）をマウスで操作するプログラムを書く](https://qiita.com/mml/items/ce2433d94b5ee497b7b8)

上の記事の方法では、プレイヤーが動くとつかんでいるオブジェクトがすっ飛んでいきました。この記事ではその部分を修正し、プレイヤーが移動するとオブジェクトがプレイヤーの動きについてくるようにしてます。

# 前提

Unreal Engine 5 で Blueprint を使って実現します。C++ でも同様のことは可能ですが、UE5 で C++ を使ってプログラムを書けるスキルのある人ならこの記事を見たらできると思うので、説明は割愛します。

# 準備

UE5 を起動します。「ゲーム」の項目にある「ブランク」というテンプレートを選択します。右側のメニューにある「スターターコンテンツ」のチェックを外します。ここでは、プロジェクト名は GrabObject としています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/fe060dbe-76e0-2a1d-2455-c3258499dc9e.png)

選択がすんだら、右下の「作成」ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/3547d72e-ff78-7e22-a49e-1d37e0c29d55.png)

プロジェクトが作成されると、上のような画面がでるはずです。この状態になったら、プレイボタンを押してシミュレーションを開始させ、カメラの位置を地面の上に移動させます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/a9d346fd-210c-fb07-7005-83ed8575481b.png)

シミュレーション開始のボタンは、画面上部にあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/f9179e88-f0d1-5bf1-3cff-e0846eea8934.png)

カメラが地面の上に移動したら、シミュレーションを停止させます。

## テスト

先に、マウスイベントが拾えることを確認しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/52cf4fbd-4a2b-3548-286d-7c38fabe158d.png)

上記メニューから「レベルブループリントを開く」を選びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/6219ee99-c8f6-6dd7-5b1e-3bc290ce66a2.png)

イベントグラフの中に、Left Mouse Button イベントを配置します。イベントグラフの画面が表示されないときは、画面の左側にある EventGraph という項目をダブルクリックすると開きます。

イベントグラフの画面内で右クリックして Left Mouse と入力すると簡単に見つかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/1dcf01a6-b16a-8bc2-2d46-59cd5dcf8cdb.png)

Left Mouse Event ノードを配置したところです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/9cf2a857-8739-fad1-e985-d51e60d0ff1a.png)


同様にして、Print String ノードを配置します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/9ea6e4ac-6026-3c4a-15af-1af4eec1bd4d.png)

Left Mouse Event ノードの Pressed から Print String までマウスでドラッグしてリンクを作成します。

これでシミュレーションを実行します。ゲーム画面内をクリックして Hello という水色の文字が表示されたら、マウスイベントの取得に成功しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/36cb0e89-eadb-0948-ae26-e621bb7ed76f.png)


# つかむ対象のアクター（オブジェクト）を配置する

ここでは、つかむ対象として Cube を配置します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/e4b22ec8-6099-736c-9449-e010e44ab158.png)

メイン画面の画面上部にある「プロジェクトに迅速に追加します」と表示されるアイコンを押して「形状」「キューブ」と選びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/f3f5389a-8c28-a3d0-5d38-ab9773f7437e.png)

すると、立方体が画面内に追加されます。デフォルトでは少し浮いている状態になっているかもしれません。その場合は、オブジェクトを移動させるかZ座標を0に設定するなどして、接地した状態にしておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/d5af385f-e10f-ce07-776e-9c939c4bfb11.png)

Cube を選んだ状態で、画面右側にある「詳細」のタブの中から Simulate Physics という項目を探して、これにチェックを入れます。

このチェックを入れることで、プログラムを使って Cube を物理的に操作（移動や回転など）できるようになります。

# マウスカーソルを表示させる

デフォルトではマウスカーソルは表示されません。しかしその状態だと、オブジェクトをつかむときに困るので（操作する人間が）、マウスカーソルを表示する設定に変更します。なお、マウスカーソルが非表示状態でも、オブジェクトをマウスでつかむこと自体はできます。

## MousePlayerController クラスの作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/be9696a0-e2ff-ed84-0bee-98ba1d91dc16.png)

ブループリント操作のメニューから「新規の空のブループリントクラス」の項目を選びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/72140102-7f57-1769-b472-de3dab202f66.png)

Player Controller クラスを選びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/f9e912f1-4c23-0589-de57-29551e68d16f.png)

ブループリントに MousePlayerController という名前を付けて「作成」を押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/b071bb85-874d-d9b5-bd3e-cd105a65e124.png)

この画面が表示されたら、画面上部にある「クラスのデフォルト」のタブを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/ca440a8c-185a-f545-0337-f95f3fd4911f.png)

右側のリストの中にある Show Mouse Cursor にチェックを入れます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/c601ea55-3812-299d-55c8-fd9c390c850b.png)

チェックを入れたら、ファイルのメニューから「すべてを保存」を選んで、変更を保存しておきます。

## GameModeBase クラスの作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/be9696a0-e2ff-ed84-0bee-98ba1d91dc16.png)

再び、ブループリント操作のメニューから「新規の空のブループリントクラス」の項目を選びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/d90012f5-6d23-194f-bb13-7f8399159181.png)

Game Mode Base を選びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/b1e0d3e4-16da-9abe-edab-7df3ece50fa5.png)

クラス名を MouseGameModeBase として「作成」を押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/5d5d2a3c-66f1-6a84-2f0c-766b0e7a3aab.png)

上のような画面が出たら「クラスのデフォルト」のタブを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/219f732a-fd9a-a12b-e3dc-a8cc3fabdc7a.png)

右側のリストから「クラス」という項目を探し、その中にある Player Controller Class のプルダウンメニューを開いて MousePlayerController を選びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/c601ea55-3812-299d-55c8-fd9c390c850b.png)

選び終えたら、ファイルのメニューから「すべてを保存」を選んで、変更を保存しておきます。

## MouseGameModeBase クラスをプロジェクトで使用するように設定する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/9ac6c1d0-686c-ddcb-4b09-856d33752d40.png)

ブループリント関連の操作メニューから「ゲームモード」「GameModeBaseクラスを選択」と選んで、出てくるリストから MouseGameModeBase を選びます。

以上の操作で、マウスカーソルが表示されるようになります。シミュレーションをスタート（プレイ）させて、実行中にマウスカーソルが表示されることを確認しておきます。

# マウスでつかむ操作を Blueprint で記述する

マウスでオブジェクトをつかむプログラムをレベルブループリントに記述します。オブジェクトを直接マウスで移動させるのではなく、移動させたいオブジェクトを別のロボットアームのようなオブジェクトで「掴んだ」状態にして、ロボットアームを移動させることで対象のオブジェクトを移動するというようにプログラムを書きます。

マウスで移動させたいオブジェクトを直接移動させないのは、重力や慣性の影響、衝突時の処理などを自力で書かずに済ますためです。

プログラムは、おおまかに次の三つのパートにわけて記述します。

- マウスの左ボタンを押したときに、マウスカーソルの位置につかめるオブジェクトがあったら、ロボットアームオブジェクトを生成して、そのオブジェクトをつかんだ状態にする。
- マウスの左ボタンを離したときに、ロボットアームオブジェクトが何らかのオブジェクトをつかんでいる状態なら、そのオブジェクトを離す（つかんだ状態から解放する）
- オブジェクトをつかんでいる状態でマウスカーソルを動かしたときに、その動きに合わせてロボットアームオブジェクトを移動させる（結果的につかまれているオブジェクトも移動する）。

これらのコードを記述することで、マウスを使ってオブジェクトをつかみ、移動させるという動作を実現します。

## レベルブループリントにプログラムを書く

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/52cf4fbd-4a2b-3548-286d-7c38fabe158d.png)

上記メニューから「レベルブループリントを開く」を選びます。

## 変数リスト

レベルブループリントでは下記の変数を使います。それぞれの変数の意味については、プログラムの説明の中で説明していきます。変数はプログラムを書いていく途中で「変数へ昇格(Promote to Variable)」として作成したほうが良いものもあります。それらについてもプログラムを記述する説明の中で触れます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/9296da93-64d4-27ef-e62e-fb710364445d.png)

- PhysicsObject: 移動させるオブジェクトを「つかむ」ために作成したオブジェクト（ロボットアーム）を保持する変数です。Promote to Variable を使って作成します。
- HandledObject：「つかまれた」オブジェクトを保持するオブジェクトです。Promote to Variable を使って作成します。

- PreCameraLocation (Vector): 直前のプレイヤーの視点の位置を記録します。
- Distance (Integer): つかめるオブジェクトとプレイヤーとの間の最大距離をこの変数で指定します。この数値を越えた距離にあるオブジェクトはつかめません。初期値（デフォルト値）は 1000 程度にするとよいでしょう。
- TargetObject (EObject Tyep Query [Array]): つかめるオブジェクトのタイプを指定します。この変数は、Promote to Variable を使って作成します。
- Handled (Boolean)：オブジェクトをつかんだ状態か否かを表すフラグです。デフォルト値は False （チェックなし）にします。
- PreLocation (Vector)：つかんだオブジェクトの直前の位置を記録します。
- MotionRage (Float)：マウスの移動に対してつかんだオブジェクトを移動させる割合をこの変数で指定します。デフォルト値は 0.2 から 0.5 の間にするとよいでしょう。

なお、変数のデフォルト値を設定するには、変数を作成した後に一度「コンパイル」の操作をする必要があります。

# マウスの左ボタンを押した時にマウスカーソルの先にあるオブジェクトをつかむ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/94886d51-ae13-7ec2-1f38-abc53289bda0.png)

オブジェクをマウスを使って掴んだり話したりする全体のコードは上のとおりです。以下、詳しく説明します。

## マウスカーソルの先にあるオブジェクトを調べる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/72f0f057-9e43-07d4-65fe-e99a7bcfa7bb.png)

上の図は、オブジェクトをつかむプログラムの前半部分です。マウスのボタンをクリックしたときに、マウスカーソルがある位置の先にあるオブジェクトを探し、つかめるオブジェクトがある場合はそのオブジェクトをつかむという処理を行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/64855628-6a55-5929-4eef-21d8cd141f4e.png)

Left Mouse Event ノードで、マウスの左ボタンが押されたときと、ボタンが離されたときのイベントを拾います。ここでは、ボタンが押されたとき (Pressed) の処理を記述しています。

マウスのボタンが押されたときの、プレイヤーの視点の位置を PreCameraLocation に保存します。これは、プレイヤーが移動したときに、オブジェクトをその移動に追随させるために必要になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/7e47f881-86d6-a95c-e5e5-fb96e73aeeff.png)

Convert Mouse Location To World Space ノードで、マウスの左ボタンがクリックされた位置を、ゲームの世界の中の座標値に変換します。

マウスカーソルは二次元の画面（ウィンドウ）の上を移動するので、二次元の位置情報しかもっていません。マウスカーソルがゲームの世界（三次元世界）の中のどの方向を指示しているか知るためには、二次元の情報をゲームの世界（三次元世界）の中の座標値や方向を表す情報（ベクトル）に変換する必要があります。Convert Mouse Location To World Space はその変換の処理を行ってくれます。

Location のピンは、マウスカーソルに対応する「プレイヤーの視線の開始点」の位置情報（ベクトル）を出力します。Direction は、プレイヤーの視線の開始点からマウスカーソの指し示す先の方向を示すベクトル（単位ベクトル）です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/605cde5f-7395-721f-06c3-096ff5f21f0c.png)

Line Trace for Objects というノードは、Start で指定した位置から End で指定した位置の方向に見えないビームを飛ばして、そのビームに当たったオブジェクトを取得するノードです。このノードを使って、マウスカーソルの先にビームが当たるオブジェクトが存在するかどうかを調べます。

なお、Line Trace for Objects ノードの Draw Debug Type を For Duration に設定しておくと、シミュレーションのプレイ時にビームがオブジェクトに当たった位置を赤く表示してくれます。

ビームは Object Types のピンに入力する配列に入っているオブジェクトのタイプにだけ反応します。ビームに反応するオブジェクトのリストは TypeObject 変数で指定しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/ddaf8ddf-6403-4eea-3fb6-faf9193f559a.png)

この変数は、Line Trace for Objects の Object Types ピンから引き出して、Promote to Variable （変数へ昇格）を使って変数にすると楽に作れます（ここでは TypeObject という名前に設定しています）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/4bf8ae27-d0cd-a225-a878-7bb1dbab8da4.png)

ビームに反応するオブジェクトのタイプは、変数のデフォルト値で設定します。ここでは、PhysicsBody をビームに反応するオブジェクトのタイプとして指定しています。なお、デフォルト値を指定するためには Promote to Variable の操作を行った後で、一度コンパイルの操作をする必要があります。

## マウスカーソルの先にあるオブジェクトをつかむ

Line Trace for Objects でビームが当たったオブジェクトを、ロボットアームに相当するオブジェクトで「つかむ」処理をします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/81222c5c-ed92-f577-e87a-7267cc06940c.png)

Line Trace for Objects ノードで放ったビームが PhysicsBody タイプのオブジェクトにぶつかると、最初にぶつかったオブジェクトやビームがぶつかった状況などの詳細情報が入った構造体が Hit ピンから出力されます。

Add Physics Handle Component ノードでは、マウスカーソルの先にあるオブジェクトをつかむためのロボットアームオブジェクトを作成します。そして、Grab Component at Location and Rotation ノードでビームがぶつかったオブジェクトを「掴んだ」状態にします。

## つかんだオブジェクトの情報を記録する

何らかのオブジェクトをつかんだら、「つかんだ」状態であることを記録します。さらに、つかんだオブジェクトの情報、ロボットアームオブジェクトの情報も、それぞれ個別の変数に記録しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/b9f007b3-293b-7477-8d2d-34385c81de53.png)

PhysicsObject 変数と HandledObject 変数は、Promote to Variable （変数へ昇格）で作成すると楽に作れます。Handled 変数は「何かをつかんでいる」という状態を記録する変数です。Handled の項目にチェックを入れる (True にする) ことで「掴んでいる」という状態を記録します。

# マウスの左ボタンを離したらオブジェクトを離す

左ボタンを離したら、オブジェクトも手放すようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/5ca9652f-531c-4b40-33d1-0b2acf65c1f0.png)

Left Mouse Event の Released のピンから先のノードは、マウスの左ボタンを離した時に実行されます。

最初に、オブジェクトがつかまれた状態にあるかを Handled 変数を調べることで確認します。オブジェクトがつかまれていなければ、何も処理は行いません。

Release Component は、オブジェクトをつかんでいたロボットアーム (PhysicsObject) を消滅 (Release) させます。これで、オブジェクトをつかんでいる状態が解消されます。

最後に Handled 変数を「つかんでいない」状態にセットします。Handled のチェックを外した状態 (False) にすることで、何もつかんでいない状態を表します。

# マウスカーソの移動に合わせてオブジェクトを移動させる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/c1b3e5df-cdda-dfaf-f0e6-19fc32c8f928.png)

オブジェクトの移動は、Event Tick のイベントの処理として行います。先に説明したように、対象のオブジェクト自体を動かすのではなく、そのオブジェクトをつかんでいるロボットアームのオブジェクトを動かすことで、間接的に対象のオブジェクトを移動させます。

## ロボットアームの位置を更新する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/16de7ea8-a005-c9a8-44e4-72b8cfff4481.png)

オブジェクトがつかまれていない状態 (Handled 変数にチェックが入っていない状態）のときは何もしません。

Get Target Location and Rotation ノードで、PhysicsObject （ロボットアームオブジェクト）の現在の位置や回転角度の情報を取得します。この Location の情報に、計算した移動量を加えたものを、PhysicsObject の新しい位置の情報 (New Location) として設定します。

## オブジェクトの移動量をマウスカーソルの移動量とプレイヤーの移動量から計算する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/cbdbd1c7-b532-2b2c-49da-81064b7ca6b2.png)

この部分は複雑なので、プログラムを二つの部分に分けて説明します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/5e2954dd-197b-9219-a62b-f75e920e9561.png)

上の図はマウスカーソルの移動した量を計算する部分だけを抜き出したプログラムです。具体的には下記の処理をしています。

- Get Player Controller でプレイヤーのオブジェクトを取得し、Convert Mouse Location to World Space ノードで、マウスカーソルの現在の位置を取得します。。
- PreLocation と現在のマウスカーソルの位置情報の差分を計算します。
- 計算した差分に MotionRate を乗じます。

次に、プレイヤーの移動量に関する処理をしている部分だけを抜き出して説明します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/a0608f19-7275-b510-387a-d70beb25d054.png)

この部分の上半分は、プレイヤーの前の位置と、現在の位置の差分を計算しています。この計算の結果は、プレイヤーが移動したときにマウスカーソルの位置を補正するために使用します。下半分は、つかんでいるオブジェクトとプレイヤーとの間の距離を計算しています。この計算結果は、つかんだオブジェクトを移動させるときの、マウスカーソルとの移動量との比率を計算するために使用します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/cbdbd1c7-b532-2b2c-49da-81064b7ca6b2.png)

上記で説明したマウスカーソルの移動量と、プレイヤーの移動量の両方を使って、オブジェクトの移動量を計算しています。

- マウスカーソル自体の移動量はとても小さく、その値をそのままオブジェクトの移動量として使っても、オブジェクトはほとんど移動してくれません。そのため移動量をある程度増幅してオブジェクトの位置情報に加えてやる要があります。定数倍でもよいのですが、このプログラムでは少々工夫して、プレイヤーとオブジェクトの間の距離を使って移動量の増幅率（倍率）を計算しています。
- プレイヤーが移動すると、それにあわせてマウスカーソルがプレイヤーの移動量と同じだけ移動してしまいます。マウスカーソルの位置は、プレイヤーの目の前の位置を表すので、プレイヤーの移動によってマウスカーソルの位置が移動するのは当然です。このプログラムでは、マウスカーソルの移動量からプレイヤーの移動量を引くことで、純粋にマウスカーソルが移動した量だけを算出しています。
- プレイヤーの移動量の影響を除去した後で、距離を使って計算した移動量の倍率を乗じることで、マウスカーソルの移動量だけからオブジェクトの移動量を計算します。
- 最後に、プレイヤーの移動量を再び加算しています。このようにすることで、プレイヤーの移動量がオブジェクトの移動量に等倍で加算され、プレイヤーの移動に合わせてオブジェクトが完全に同期してついてくるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/0e97c07b-6adf-2122-8c79-a841283260c5.png)

上記のプログラムでは、ロボットアームを移動させた後、現在のマウスカーソルの位置と、現在のプレイヤーの位置を保存しています。これらの値は、次の Tick イベントの処理の時に使用されます。

以上のようにプログラムを書くことで、マウスでオブジェクトをつかんで動かす処理を実現できます。

なお、マウスカーソルの移動量に乗じている倍率の計算は、この方法が正解というわけではありません。あくまで簡易的なものです。ほとんどのアプリケーションでは、この計算で問題なく対応できると思います。MotionRate の数値を操作することで、移動量を大きくしたり小さくしたりできます。0.2 から 0.5 の間に設定し、必要に応じて増減するといいでしょう。

マウスカーソルの移動に厳密に合致するオブジェクトの移動量（の倍率）を計算するには、プレイヤーの位置を中心とする極座標系を作成して、マウスカーソルの移動量を極座標系上の移動量として計算して・・・みたいなことをする必要が（多分）あります。

# 補足

上記のプログラムでは、オブジェクトを上下左右方向に動かすことはできますが、前後方向には動かせません。プログラムを工夫することで、前後方向に動かすようにすることは可能です。

このプログラムは UE4 時代の方法をそのまま使っています。UE5 では他の方法もあるかもしれませんが、ちょっと調べた限りでは見つけられませんでした。もっと良い方法を知っている方がいたらぜひ教えてください。