【Unity初心者】マウス操作で飛行機（乗り物全般）を操縦する（プロジェクト解説・車での使用例）

# 前置き
|MouseFlightのGitHubレポジトリより引用したフライトデモ映像|
|:--------:|
![flight.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3609913/ce307d7e-3dd6-e71a-efc1-2ecc9b1afc0c.gif)

この記事へ訪れた皆様は、おそらく既に`Unity`をある程度まで使っていて、ゲームをより良くする要素を実装するためにこの記事へと迷い込んだのでしょう。
幸運なことに、今回の記事はその目的を達成することに多少なりとも貢献するであろうことを取り上げます（謎の冗長表現）。

具体的には、[`Brian Hernandez`](https://github.com/brihernandez)氏が`GitHub`で公開している[`MouseFlight`](https://github.com/brihernandez/MouseFlight)という、マウス操作を使うことにより`Unity`で直感的に航空機を操縦するプロジェクトのアルゴリズムの解説と使用例を紹介します。

https://github.com/brihernandez/MouseFlight

このプロジェクトは、[`WarThunder`](https://warthunder.com)というクロスプラットフォームで陸海空の戦場を持つコンバットゲームの、航空機におけるマウス操縦を`Unity`上で実装したものになります。

https://warthunder.com

この`MouseFlight`のプロジェクトは汎用性を考えて設計されているため、より緻密な演算をするフライトシミュレーターに留まらず、船舶や車両、人型にも対応させることができます。

このプロジェクトは、マウスを使って航空機を動かすということ以外にも、マウスの動かす方にカメラを動かすという動作についてもうまく実装されています。
その上、`OSS`ライセンスの中でも最も寛容な`MITライセンス`を採用しているため、`Unity`でゲーム開発をするのであれば、是非一度見てもらいたいものとなっています。
長文にはなりますが、どうかお付き合いください。

|使用例|
|:--------:|
![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3609913/e1e2f6c7-d621-6e09-2d80-210353494eb9.gif)


:::note
タイトルに`Unity初心者`と書いてありますが、誰でも理解できるような詳しい説明はしていません（というかできない）。
およそタイトル詐欺のようですが、少なくとも全体的な流れは把握できると思います。
わからない単語は見つけ次第調べたり、`ChatGPT`を使えば理解できるはずです。
:::
:::note warn
また、今回使用する`MouseAim`プロジェクトのバージョンは`Unity 2017.3.1f1`です。
プロジェクトのレポジトリによると、`2018.3.0`や`5.6.4`でもテストされされているようですが、`5.6.4`の場合はライティングをリセットする必要があるそうです。
:::
# 自己紹介
私は今まで、`Unity`で簡易的なアルゴリズムに基づいたフライトシミュレーターを個人で開発したり、自身でもよく理解できない運の巡りからプログラミング講師として`Unity`を教えたり、そこの教材を作成したり、単純な興味から発展してモーターグライダーの訓練生パイロットをしたりしています。

今回は、プログラミング講師としての側面と、飛行機好きの両方向から、このプロジェクトを紹介していきたいと思います。

なお、この記事を`42 Tokyo`のアドベントカレンダーとして投稿したのは、私が来年の1月に`Piscine`を受験する予定だからです。
<details><summary>言い訳ゾーン</summary>

>まだPiscine受験してないならおよそ部外者だって？
ちょっと何言ってるかよくわかr...(略)。
こっちは主催者に許可とってるので別にいいんですぅ〜！！！
この記事のためにシリーズを新しく作ってもらったんじゃないかって？
ちょっと黙r((殴
</details>

:::note
以上により、航空機へ一定の興味関心を持っていることはおよそ伝わったかと思いますが、残念なことに私の知識は十分でなく、`Unity`も特別上手に扱える訳ではありません。
そのため、この記事の不自然な表現や不適切と思われる文は、見つけ次第指摘してもらえると助かります。
:::
# MouseFlightの紹介
[前置き](#前置き)で述べた通り、この記事では[`Brian Hernandez`](https://github.com/brihernandez)氏が`GitHub`で公開している[`MouseFlight`](https://github.com/brihernandez/MouseFlight)という、マウス操作を使うことにより`Unity`で直感的に航空機を操縦するプロジェクトのアルゴリズムの解説と使用例を紹介します。

https://github.com/brihernandez/MouseFlight

このプロジェクトは、[`WarThunder`](https://warthunder.com)というクロスプラットフォームで陸海空の戦場を持つコンバットゲームの、航空機におけるマウス操縦を`Unity`上で実装したものになります。

一般的なフライトシミュレーターでは、物理的な操縦桿として扱えるデバイスがない場合、マウスを操縦桿として使用したり、各種操舵軸をキーボードで細かく操作する必要があるので、まともに飛行機を飛ばすことすら叶えずらい状態となっています。

基本無料のコンバットゲームでもある本作では、ユーザーの参入障壁を下げてプレイヤー人数を確保する必要があります。
こういった背景が、マウスを活用した操作方法を編み出すに至ったと考えられます。

https://warthunder.com

この`MouseFlight`のプロジェクト自体は、力学的にそこまで詳細に作り込まれていません。
しかし、汎用性を考えて設計されているため、より緻密な演算をするフライトシミュレーターに留まらず、船舶や車両、人型にも対応させることができます。

このプロジェクトは、マウスを使って航空機を動かすということ以外にも、マウスの動かす方にカメラを動かすという動作についてもうまく実装されています。
その上、`OSS`ライセンスの中でも最も寛容な`MITライセンス`を採用しているため、`Unity`でゲーム開発をするのであれば、是非一度見てもらいたいものとなっています。

## MouseFlightの導入

早速、プロジェクトをローカルにクローンして`Unity`から開いてみましょう。

まず、ターミナルを開いて`Unity`プロジェクトを配置したい任意のディレクトリへ移動します。
特に指定がないなら`home`や`Desktop`で良いでしょう。
今回は`home`ディレクトリを例に出します。
```shell-session:bash
$ cd ~
```
カレントディレクトリを変更できたら、次のコマンドにより`Git`のレポジトリをクローンします。
```shell-session:bash
$ git clone https://github.com/brihernandez/MouseFlight
```
:::note warn
もし、`git`が入っていない場合は`brew`/`apt`/`winget`などでインストールします。
そのツールが入っていない場合はそのツールをインストールします。
`git`が嫌な場合や、`git clone`が無理そうな場合は`.zip`などでダウンロードしても問題ありません。
:::
このコマンドを実行すると、指定したディレクトリに`Unity`のプロジェクトが作られるので、`Unity Hub`を開いて、右上の`Add`から`clone`したプロジェクトを選択して`UnityHub`の`Projects`に追加しましょう。
|`UnityHub → Add → Add project from disk`を押す|
|:--------:|
![スクリーンショット 2024-12-08 2.27.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3609913/bc365bd4-a81e-47ce-dba2-c42e44c26cc8.png)

追加されたことが確認できたら、指定されたバージョンの`Unity`でプロジェクトを開きましょう。

:::note warn
同じバージョンでなくとも動作はする場合が多いですが、動作が不安定になりやすいのであまりお勧めはできません。
:::
`Unity`のプロジェクトが開けたら、正しいシーン(`DemoFlight`)が選択されていることを確認して、早速`Ctrl+P`か実行ボタンを押して再生しましょう。
操作方法は以下の通りです。
>W/S: Pitch up/down → 上下
A/D: Roll left/right → 左右
C: Enable free look → カメラ操作

なお、`Cキー`を押しながら`WASDキー`を押しても旋回方向をオーバーライドすることができます。

詳しくは`Git`のレポジトリかプロジェクトのコードか`WarThunder`の操作方法を確認しましょう。
|フライトデモ（再登場）|
|:--------:|
![flight.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3609913/ce307d7e-3dd6-e71a-efc1-2ecc9b1afc0c.gif)

## プロジェクトの解説
ここからはプロジェクトの解説をしていきます。
最初にオブジェクトの説明をして、次にスクリプトの簡単な説明をします。

オブジェクトの説明では、まず画像でイメージを掴んで、次にオブジェクトの親子関係を __ディレクトリのツリーを模して__ そのオブジェクトが持つ`Component`と一緒に表示します。
そして、その内の特筆すべきオブジェクトを別個に取り上げます。

スクリプトの説明では、オブジェクトの説明で述べたコードがどう動いているのかについて軽く説明し、理解しづらい点を抜粋して説明します。

### オブジェクトの説明

この`DemoFlight`シーンのヒエラルキーは次のような構造になっています。

#### オブジェクトの概要
|`DemoFlight → Hierarchy`|
|:--------:|
![スクリーンショット 2024-12-08 16.33.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3609913/fa6016c1-b10c-24a5-d070-084fe394dc6d.png)


```:DemoFlight
DemoFlight
├── Directional Light [Light]
├── Scenery(Prefab)
│   ├── Plane
│   └── Cube * 4
├── Plane(Prefab) [Rigidbody, Plane]
│   ├── Model [CapsuleCollider, BoxCollider * 2]
│   │   └── Cube * 4 [MeshFilter, MeshRenderer]
│   ├── TrailLeft [TrailRenderer]
│   └── TrailRight [TrailRenderer]
├── MouseFlightRig(Prefab) [MouseFlightController]
│   ├── MouseAim
│   └── CamRig
│       └── Main Camera [Camera, AudioListener]
└── MouseFlightHud(Prefab) [RectTransform, Canvas, CanvasScaler, Graphic Raycaster, Hud]
    ├── Boresight [CanvasRenderer, Image]
    └── MouseAim [CanvasRenderer, Image]
```

##### Directional Light [Light]

どこにでもあるただの太陽です。
それ以上でもそれ以下でもありません。
##### Scenery(Prefab)

地面（`Plane`）や初期地点付近にある`Cube`をまとめたものです。
紛らわしいことに、こちらの`Plane`は平面としての`Plane`です。
飛行機と間違わないようにしましょう。
##### Plane(Prefab) [Rigidbody, Plane]

飛行機のモデルや翼端から出るスモークをまとめたオブジェクトです。
`Plane`スクリプトにより飛行機の挙動が適用されています。
###### Model [CapsuleCollider, BoxCollider * 2]

|Modelオブジェクト|
|:--------:|
![スクリーンショット 2024-12-14 0.27.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3609913/91308986-239d-6853-38fa-09e895a46f2d.png)

飛行機を構成する直方体をまとめた親オブジェクトで、その親オブジェクトである`Plane`の`Rigidbody`が`Collider`の形によって慣性テンソルが自動的に決まってしまうので、本来子オブジェクトにつけるはずの`Collider`がこちらのオブジェクトについています。
そのため、オブジェクトの当たり判定を良くみると、見た目とは違う形になっていることに気づけるはずです。
上の画像の緑の線が`Model`オブジェクトの当たり判定です。

:::note
今回の記事はこの問題の解決法を[おまけ](#おまけ)で扱っています。
:::
###### Trail(Right/Left) [TrailRenderer]

|TrailLeftオブジェクト|
|:--------:|
![スクリーンショット 2024-12-13 14.14.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3609913/8019e600-36ef-6837-41a9-be1a6b2cf93a.png)

`TrailRenderer`を用いてそのオブジェクトの座標からスモーク（`Trail`）を出します。
今回はそれぞれが翼端にあるので、翼端からスモークが出ているように見えます。
※今回の画像は見えやすいように`Trail`を編集しました
#### MouseFlightRig(Prefab) [MouseFlightController]
`MouseFlightController`スクリプトを管理するオブジェクトで、フレーム毎（`Update`か`FixedUpdate`かはオプションで決められます）に`Plane`と同じ座標へ移動しています。
##### MouseAim
`Transform`しかコンポーネントがないシンプルなオブジェクトですが、担っている役割は大きいです。

まず、このオブジェクトはマウスの移動量により回転の度合いが変わります。
そして、`Plane`の回転はこのオブジェクトを参考に行われます。

具体的には、マウスからのインプット（`Input.GetAxis("Mouse X/Y")`）はこのオブジェクトを回転させます。
そして、このオブジェクトと`Plane`の`Rotation`の差によって[比例制御](https://ja.wikipedia.org/wiki/PID%E5%88%B6%E5%BE%A1)することでマウス操作を実現させています。

これをフローチャートにするとこのようになります。

```mermaid
graph TB
    Start([Start])-->note[/マウスの入力を受け取る/]-->MouseAimオブジェクトを回転させる-->MouseAimとPlaneの回転の差を計算-->この差を参考に比例制御-->End([End]);
```
:::note warn
比例制御によって機体の方向を変えているため、オーバーシュートやアンダーシュートが発生してしまいます。
この問題の解決法は[おまけ](#おまけ)で扱っています。
:::
##### CamRig
こちらも`Transform`しかコンポーネントがないシンプルなオブジェクトですが、担っている役割は同様に大きいです。
本来、カメラは`Plane`オブジェクトから離れた位置に配置しないと、`Plane`のメッシュにめり込んでしまいます。
ただ、コードを書くときはカメラのオブジェクトをオフセットなしで`Plane`と同じ座標に移動させたほうが楽です。

これを親子関係を使うことで解消するキーが、この`CamRig`オブジェクトになります。
つまり、カメラの代わりに回転する位置合わせ（オフセット）用のオブジェクトということですね。
このオブジェクトは`Plane`と同じ座標に移動するので、子オブジェクトである`Main Camera`にオフセットをつけたらそれでうまく動作します。
デフォルトだとオフセットは`(x, y, z) = (0, 9, -30)`ですね。

また、実際に飛ばしてみるとわかりますが、`Cキー`の状態によらず、カメラが回転するときはその移動が滑らかになっています。
これは`CamRig`の角度の変更時に減衰を含んだ[球面線形補完](https://docs.unity3d.com/ja/2023.2/ScriptReference/Quaternion.Slerp.html)（`Sleap`）が使われているからなのですが、説明すると長くなってしまうので、ここでの詳しい言及は避けたいと思います。
#### MouseFlightHud(Prefab) [RectTransform, Canvas, CanvasScaler, Graphic Raycaster, Hud]
沢山のコンポーネントがついていますが、`Hud`スクリプト以外は全てUI関連のコンポーネントです。
このオブジェクトは、実行時に使う`Boresight`オブジェクトや`MouseAim`オブジェクトの親として機能する`Canvas`です。
直接的にやっていることは、`Hud`スクリプトによる子オブジェクトの座標移動のみです。
##### Boresight [CanvasRenderer, Image]

|Boresightイメージ|
|:--------:|
![Crosshair.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3609913/0b8522f0-d88e-54a8-21b6-29ad2021c3b0.png)

機体が向いている方向を`UI`としてプレイヤーに伝えるオブジェクトです。
`MouseFlightController`スクリプトの`BoresightPos`に処理が記述されており、`Plane`オブジェクトの正面方向と、`Plane`オブジェクトからこのイメージをどのくらい前に移動させるかという係数の`aimDistance`を掛け合わせたものに飛行機の`position`を足したものになっています。
```C#:MouseFlightController.cs
public Vector3 BoresightPos
{
    get
    {
        // この条件演算子で値を代入している
        return aircraft == null
             ? transform.forward * aimDistance
             : (aircraft.transform.forward * aimDistance) + aircraft.transform.position;
    }
}
```
つまり、あくまで`UI`はおまけでしかなく、`UI`がなくともマウス操作をすることはできるという訳ですね。

:::note
このプロジェクトでは、まず三次元空間上にUIを配置したと考えた後に二次元座標へ変換させています。
あまりない発想で参考になりますね。
:::
##### MouseAim [CanvasRenderer, Image]

|Boresightイメージ|
|:--------:|
![MouseCrosshair.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3609913/0b6edb80-513d-289f-1ce0-d68938805527.png)

マウスが向いている方向をUIとしてプレイヤーに伝えるオブジェクトです。
`MouseFlightController`スクリプトの`MouseAimPos`に処理が記述されており、`MouseAim`オブジェクトの正面方向と、`Plane`オブジェクトからこのイメージをどのくらい前に移動させるかという係数の`aimDistance`を掛け合わせたものに飛行機の`position`を足したものになっています。
```C#:MouseFlightController.cs
public Vector3 MouseAimPos
{
    get
    {
        if (mouseAim != null)
        {
            // この条件演算子で値を代入している（今回重要なのは後者）
            return isMouseAimFrozen
                ? GetFrozenMouseAimPos()
                : mouseAim.position + (mouseAim.forward * aimDistance);
        }

        else
        {
            return transform.forward * aimDistance;
        }
    }
}
```
こちらも同様に、あくまで`UI`はおまけでしかなく、`UI`がなくともマウス操作をすることは可能です。

---

### スクリプトの説明
どのオブジェクトがどのスクリプトを所持しているかについては前述した通りですね。
スクリプトは次のような振る舞いをします。

#### Plane.cs(Plane)
`Plane.cs`スクリプトは、飛行機の自動操縦と物理演算を制御します。このスクリプトは、`MouseFlightController`から受け取ったデータに基づいて、飛行機の動きを計算します。

##### Plane.csのポイント
`RunAutopilot`メソッド

```C#:Plane.cs
private void RunAutopilot(Vector3 flyTarget, out float yaw, out float pitch, out float roll)
```
:::note
[`out`](https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/out)は参照を渡すための修飾子です。
:::
このメソッドは、飛行機が目標位置に向かうための`yaw`, `pitch`, `roll`を計算します。
計算方法は以下の通りです。

__`localFlyTarget`の計算__

```C#:Plane.cs
Vector3 localFlyTarget = transform.InverseTransformPoint(flyTarget).normalized * sensitivity;
```
これは、旋回の目標位置を飛行機のローカル座標系に変換し、正規化したものです。
`sensitivity`はこの値のスケール（倍率）を調節するものです。

__`yaw`と`pitch`の計算__

```C#:Plane.cs
yaw = Mathf.Clamp(localFlyTarget.x, -1f, 1f);
pitch = -Mathf.Clamp(localFlyTarget.y, -1f, 1f);
```
`localFlyTarget`の`x`成分が`yaw`、`y`成分が`pitch`になります。
これらは`-1`から`1`の範囲に[`Clamp`](https://docs.unity3d.com/ja/560/ScriptReference/Mathf.Clamp.html)されます。
この`yaw`と`pitch`は係数を掛けた後そのまま回転に適用されるため、このプロジェクトの飛行機は実質的な[比例制御](https://ja.wikipedia.org/wiki/PID%E5%88%B6%E5%BE%A1)となっています。


__`roll`の計算__

```C#:Plane.cs
float agressiveRoll = Mathf.Clamp(localFlyTarget.x, -1f, 1f);
float wingsLevelRoll = transform.right.y;
float wingsLevelInfluence = Mathf.InverseLerp(0f, aggressiveTurnAngle, angleOffTarget);
roll = Mathf.Lerp(wingsLevelRoll, agressiveRoll, wingsLevelInfluence);
```
`roll`は、旋回の目標が正面にある場合とそうでない場合で異なります。
`aggressiveTurnAngle`は、旋回の目標の座標が飛行機の前方から見てどの程度離れているかを表します。
`wingsLevelInfluence`は、この角度に基づいて`wingsLevelRoll`と`aggressiveRoll`を線形補間します。

__`FixedUpdate`メソッド__

```C#:Plane.cs
private void FixedUpdate()
{
    rigid.AddRelativeForce(Vector3.forward * thrust * forceMult, ForceMode.Force);
    rigid.AddRelativeTorque(new Vector3(turnTorque.x * pitch, turnTorque.y * yaw, -turnTorque.z * roll) * forceMult, ForceMode.Force);
}
```

ここでは、飛行機に推力とトルクを加えます。
係数である`thrust`, `turnTorque`, `forceMult`はシリアライズされているので、`Inspector`から調整できます。

---

#### MouseFlightController.cs(MouseFlightRig)
`MouseFlightController.cs`スクリプトは、カメラの動きとマウス入力の処理を担当します。
減衰とSlerpを利用したカメラの滑らかな回転や、マウス入力に基づく`aim`位置の計算を行います。

##### MouseFlightController.csのポイント
`RotateRig`メソッド

```C#:MouseFlightController.cs
private void RotateRig()
{
    // ...
    mouseAim.Rotate(cam.right, mouseY, Space.World);
    mouseAim.Rotate(cam.up, mouseX, Space.World);
    // ...
    cameraRig.rotation = Damp(cameraRig.rotation, Quaternion.LookRotation(mouseAim.forward, upVec), camSmoothSpeed, Time.deltaTime);
}
```

このメソッドは、マウス入力に基づいて`mouseAim`を回転させ、続いて`cameraRig`を滑らかに回転させます。
`Main Camera`は`cameraRig`の子オブジェクトであるため、実際にカメラを回転させているのはこのコードです。

`Damp`メソッド

```C#:MouseFlightController.cs
private Quaternion Damp(Quaternion a, Quaternion b, float lambda, float dt)
{
    return Quaternion.Slerp(a, b, 1 - Mathf.Exp(-lambda * dt));
}
```

このメソッドは、 `Quaternion`の値を滑らかに変化させるためのものです。
`lambda`はダンピング係数、`dt`は経過時間です。
球面線形補完に減衰の値を組み合わせることで、滑らかなカメラ回転を実現させています。
このコードは乗り物に限らず、様々な場面で応用できそうですね。

---

#### Hud.cs
`Hud`スクリプトは、`MouseFlightController`から受け取ったデータに基づいて、画面に先程紹介した`Boresight`と`MouseAim`の位置を表示します。

##### Hud.csのポイント(MouseFlightHud)
`UpdateGraphics`メソッド

```C#:Hud.cs
private void UpdateGraphics(MouseFlightController controller)
{
    if (boresight != null)
    {
        boresight.position = playerCam.WorldToScreenPoint(controller.BoresightPos);
        boresight.gameObject.SetActive(boresight.position.z > 1f);
    }

    if (mousePos != null)
    {
        mousePos.position = playerCam.WorldToScreenPoint(controller.MouseAimPos);
        mousePos.gameObject.SetActive(mousePos.position.z > 1f);
    }
}
```

ここでは、`BoresightPos`と`MouseAimPos`をワールド座標からスクリーン座標(`Canvas`)に変換し、`HUD`の要素を更新します。
`z`成分が`1`以上の場合のみ、要素を有効にします。

---

これらスクリプトの動作をまとめてフローチャートで表すと次のようになります。

```mermaid
graph TB
    Start([Start])-->MouseFlightControllerがマウス入力に基づいてmouseAimを回転させる-->計算したmouseAimPosをPlaneスクリプトに提供-->Planeがautopilotに基づいて飛行機の動きを計算-->キーボード入力とマウス操作のどちらかを入力として決定-->HudがboresightとMouseAimの位置をスクリーン上に表示-->計算した値を飛行機に力を回転を加える-->End([End]);
```

:::note
おそらく、私が説明したことだけで内部のアルゴリズムを完全に理解するのは難しいと思います。
そんな時は、動いているコードを眺めて理解しようとしたり、ChatGPTに丸投げしたり、英語のコメントを和訳して解釈したりなど、理解するためのやり方は沢山あります。
:::

# 使用例: 車に応用する
`WheelCollider`を使った車をマウスで制御してみたいと思います。
このデモのプロジェクトはこちらで配布しています。

https://github.com/Neuroi014/UnityCarMouseControlDemo

:::note warn
このプロジェクトは[`Unity 6`](https://unity3d.jp/release-unity6/)の[`HDRP`]()で作られています。
同じバージョンの`HDRP`プロジェクトを動作させられる環境でないと、完全な動作は期待できないかもしれません。
なおこのプロジェクトを動かす場合は、`MouseFlight`と同様に`git clone`してから`Add`してください。
:::

さて、どうやって車に応用するのかという話です。
実は、今まで行っていた立体での処理を平面として行えばできてしまいます。
具体的なイメージが湧かないかもしれませんが、とりあえず実践してみましょう。

:::note
今回、`WheelCollider`の扱いは[こちら](https://smv.hatenablog.com/entry/2023/11/03/173242)の記事を参考にしました。
:::

https://smv.hatenablog.com/entry/2023/11/03/173242

https://docs.unity3d.com/ja/2022.3/Manual/WheelColliderTutorial.html

まず、キーボード操作で動かせるかどうかを確認するために、次のようにオブジェクトを配置し、コンポーネントを割り当てました。

|`CarDemo → Hierarchy（一部）`|
|:--------:|
![スクリーンショット 2024-12-15 19.54.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3609913/e8250990-dea7-9beb-c7d4-aa36fb5f4059.png)

```:CarDemo
CarDemo
├── Car [Rigidbody, SimpleCarController]
│   ├── Body [MeshFilter, MeshRendrer, BoxCollider]
│   ├── Main Camera [Camera, AudioListener, HDAdditionalCameraData]
│   └── Tires
│       └── WheelColider [WheelColider] (前後左右の計4つ)
│           └── GameObject (それぞれに一つづつ)
│               └── Cylinder [MeshFilter, MeshRendrer] (それぞれに一つづつ)
以下省略)
```

これは先ほど紹介したこちらの[記事](https://smv.hatenablog.com/entry/2023/11/03/173242)と完全に同じなので、オブジェクトやスクリプトの詳しい説明は省きます。
記事を見ながら実際に作っている方は、ここで一度動作確認をしましょう。
現時点では、`WASDキー`を使って車両を自由に操作できるはずです。
以降は`SimpleCarController`スクリプトが問題なく動作していることを前提に進めていきます。

キーボードで問題なく操作ができたら、今度はマウスで車両を操作するために、`MouseFlight`プロジェクトのオブジェクトやスクリプトを次のように配置しましょう。

|`CarDemo → Hierarchy（超省略）`|
|:--------:|
![スクリーンショット 2024-12-15 20.02.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3609913/c55dcd3d-a763-a125-f77d-bf6c784ad962.png)


```diff:CarDemo
  CarDemo
- ├── Car [Rigidbody, SimpleCarController]
+ ├── Car [Plane, Rigidbody, SimpleCarController]
+ │   └── (以下は先程から変化なし)
+ ├── MouseFlightRig [MouseFlightController]
+ │   └── (以下はMouseFlightと完全に一致)
+ ├── MouseFlightHud [RectTransform, Canvas, CanvasScaler, Graphic Raycaster, Hud]
+ │   └── (以下はMouseFlightと完全に一致)
```

見てわかる通り、`MouseFlight`の`Hierarchy`と完全に同じですね。
省略した部分は`MouseFlight`のものをそのままコピーすればそれで大丈夫です。

ただ、スクリプトを全く改変しない場合、元のプロジェクトの飛行機と同様に車が空を飛んでしまうので、次に示す複数のスクリプトを変更して、動作が干渉しないようにします。

```diff_c_sharp:SimpleCarController.cs
// ここ以前のメソッドの処理を省略

public void FixedUpdate()
{
    // 変数の宣言を省略

    foreach (AxleInfo axleInfo in axleInfos)
    {
-        if (axleInfo.steering)
+        if (axleInfo.steering && false)
        {
            axleInfo.leftWheel.steerAngle = steering;
            axleInfo.rightWheel.steerAngle = steering;
        }

        // 以降の処理を省略
    }
}

// 最後の中括弧を省略
```
```diff_c_sharp:Plane.cs
// ここ以前のメソッドの処理を省略

private void FixedUpdate()
{
    // コメントを省略
    
-    rigid.AddRelativeForce(Vector3.forward * thrust * forceMult, ForceMode.Force);
-    rigid.AddRelativeTorque(new Vector3(turnTorque.x * pitch,
-                                        turnTorque.y * yaw,
-                                        -turnTorque.z * roll) * forceMult,
-                            ForceMode.Force);
+    if (false)
+    {
+        rigid.AddRelativeForce(Vector3.forward * thrust * forceMult, ForceMode.Force);
+        rigid.AddRelativeTorque(new Vector3(turnTorque.x * pitch,
+                                            turnTorque.y * yaw,
+                                            -turnTorque.z * roll) * forceMult,
+                                ForceMode.Force);
+    }
+
+    rightWheel.steerAngle = turnTorque.y * yaw;
+    leftWheel.steerAngle = turnTorque.y * yaw;
}

// 最後の中括弧を省略
```
:::note
再度有効化する可能性を考えて、その時の編集量が最小になるように、falseの条件をつけています（意図的な冗長設計）。
:::

ここで行っていることを説明します。
まず、前者の変更で、キーボード操作のスクリプトからタイヤの方向が変わらないようにします。

そして、後者の変更では、すべての加える予定の力や回転を一度無効化し、ヨー方向（横方向）のみをタイヤの角度変更に使用しています。

|動作デモ|
|:--------:|
![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3609913/e1e2f6c7-d621-6e09-2d80-210353494eb9.gif)

適切にオブジェクトを割り当てたら、この`gifアニメーション`のように動作するはずです。
どうしてもうまくいかない場合は、こちらの[レポジトリ](リンクをここに)を参考にしてください。

https://github.com/Neuroi014/UnityCarMouseControlDemo

:::note
この車のデモは、マウスによる移動と`WSキー`によるスロットルの調整、Cキーによる視点移動のみに対応しています。
コードの変更が最小限になるような変更をしているため、軽量化のための改善点は多くあります。
:::

# おまけ
本編とは関係のないことを書いていきます。
動作テストに使用例の車のプロジェクトを使っていたりしますが、`MouseFlight`プロジェクト本体であっても問題なく動作するはずです。

### 要素変更の提案とその修正
#### カメラの角度により描画の処理が変更されている処理を単純化する。
`MouseController.cs`には次の記述があります。
```C#:MouseController.cs
// 以前のメソッドを省略

private void RotateRig()
{
    // 以前の処理を省略
    
    Vector3 upVec = (Mathf.Abs(mouseAim.forward.y) > 0.9f) ? cameraRig.up : Vector3.up;
    
    // 以後の処理を省略
}

// 以後のメソッドを省略
```
このコードは、真上や真下を向いた時に発生する不安定な挙動を抑止する役割を担っています。
特に問題がないように感じますが、これはこれで少し微妙な挙動をします（これは人によって感じ方が変わりそう）。

|カメラの動作デモ|
|:--------:|
![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3609913/251e3652-edcc-d9bc-110e-c8240479b935.gif)
※別PCで開発管理せずに並行で作っていたせいでセットが減っています

試した限りでは、コードを変更しても特別おかしな挙動はしなかったので、`Vector3.up`に固定しても大丈夫でしょう。

__変更してみる__

```diff_c_sharp:MouseController.cs
// 以前のメソッドを省略

private void RotateRig()
{
    // 以前の処理を省略
    
-    Vector3 upVec = (Mathf.Abs(mouseAim.forward.y) > 0.9f) ? cameraRig.up : Vector3.up;
+    Vector3 upVec = Vector3.up;
    
    // 以後の処理を省略
}

// 以後のメソッドを省略
```
コードを変更すると、次のようになります。

|カメラの動作デモ|
|:--------:|
![output2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3609913/12259c17-dc00-42b5-587c-c72c9b467ebb.gif)

処理をシンプルにすることによって、より直感的な動作をするようになりましたね。

---

#### Clliderの形状が慣性テンソルに直接影響していて、見かけ上の判定と違う。
`Project`の`Plane`プレハブの`Rigidbody`を確認すると、[`AutomaticTensor`](https://docs.unity3d.com/ja/2023.2/ScriptReference/Rigidbody-automaticInertiaTensor.html)が有効になっていると思います。

|`Project → Plane → Rigidbody → Automatic Tensor`が有効になっている|
|:--------:|
![スクリーンショット 2024-12-08 12.38.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3609913/9d3d2b51-59be-45ec-13b3-994cf95df903.png)

https://docs.unity3d.com/ja/2023.2/ScriptReference/Rigidbody-automaticInertiaTensor.html

これは、その`Rigidbody`が付与されているオブジェクトの回転のしやすさ（[慣性モーメント](https://ja.wikipedia.org/wiki/%E6%85%A3%E6%80%A7%E3%83%A2%E3%83%BC%E3%83%A1%E3%83%B3%E3%83%88)）を当たり判定の形状と重量、重心などから自動的に計算するものとなっています。
そのためか、`Plane`プレハブをよく見てみると、当たり判定が見かけ上の`Cube`などによらない形状となっていることが確認できます。

|`Model`オブジェクト（再掲）|
|:--------:|
![スクリーンショット 2024-12-14 0.27.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3609913/91308986-239d-6853-38fa-09e895a46f2d.png)
この画像の緑色の線が実際の当たり判定です

これは機体の形状を変更した時に予期しない挙動をすることが想像できるので、手動で慣性テンソルを設定しましょう。

__変更してみる__
コードを書いてデフォルトの状態の慣性テンソルを取得したところ、`(x, y, z) = (1977.88, 2261.10, 352.59)`であることがわかりました。
少しくらいなら変えても優位な差は発生しないでしょうし、そもそも特別な意味がある数字ではないので、手動で`(x, y, z) = (2000, 25000, 350)`に変更してしまいましょう。

※慣性テンソルの回転は`(x, y, z) = (0, 0, 0)`でした

こうすることで、それぞれの`Cube`に`CubeCollider`を持たせても飛行機の挙動が変わらないようになりました。

---

#### 比例制御をPID制御に変更する。
車のデモを遊んでいる時、急にハンドルを回したりすると、車両がマウスを通り越して曲がってしまいます（`MouseFlight`でも同様）。
これは、[`P制御`](https://ja.wikipedia.org/wiki/PID%E5%88%B6%E5%BE%A1)で車を動かしている弊害で、オーバーシュートやアンダーシュートを繰り返しやすいという特徴があります。

|`P制御`の動作デモ（2倍速）|
|:--------:|
![output-palette-none.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3609913/a0cac299-2779-de4c-e90b-e29d9b379408.gif)

今回は、これを[`PID制御`](https://ja.wikipedia.org/wiki/PID%E5%88%B6%E5%BE%A1)に変更することで解決します。

https://ja.wikipedia.org/wiki/PID%E5%88%B6%E5%BE%A1

__変更してみる__
次のコードを変更、加筆します。
詳しい説明はしません。

```diff_c_sharp:Plane.cs
// 以前のメソッドを省略

+ private PIDController yawController = new PIDController(1.0f, 0.0f, 0.1f);
+ private PIDController pitchController = new PIDController(1.0f, 0.0f, 0.1f);
+ private PIDController rollController = new PIDController(1.0f, 0.0f, 0.1f);

private void RunAutopilot(Vector3 flyTarget, out float yaw, out float pitch, out float roll)
{
    // 以前の処理を省略
-    yaw = Mathf.Clamp(localFlyTarget.x, -1f, 1f);
-    pitch = -Mathf.Clamp(localFlyTarget.y, -1f, 1f)
+    yaw = Mathf.Clamp(yawController.Update_(localFlyTarget.x, Time.deltaTime), -1f, 1f);
+    pitch = -Mathf.Clamp(-pitchController.Update_(localFlyTarget.y, Time.deltaTime), -1f, 1f);

    // 処理を省略
            
    var wingsLevelInfluence = Mathf.InverseLerp(0f, aggressiveTurnAngle, angleOffTarget);

+    var desiredRoll = Mathf.Lerp(wingsLevelRoll, agressiveRoll, wingsLevelInfluence);

-    roll = Mathf.Lerp(wingsLevelRoll, agressiveRoll, wingsLevelInfluence);
+    roll = Mathf.Clamp(rollController.Update_(desiredRoll, Time.deltaTime), -1f, 1f);
}

// 以後のメソッドを省略
```
```C#:Plane.cs
// 以前のクラスを省略

public class PIDController
{
    private float kp;
    private float ki;
    private float kd;

    private float integral;
    private float lastError;

    public PIDController(float kp, float ki, float kd)
    {
        this.kp = kp;
        this.ki = ki;
        this.kd = kd;
        integral = 0.0f;
        lastError = 0.0f;
    }

    public float Update_(float error, float deltaTime)
    {
        float proportional = kp * error;

        integral += error * deltaTime;
        float integralTerm = ki * integral;

        float derivative = (error - lastError) / deltaTime;
        float derivativeTerm = kd * derivative;

        lastError = error;

        return proportional + integralTerm + derivativeTerm;
    }
}
```

このように変更することで、車が急旋回するときにオーバーシュートを起こさなくなりました。
飛行機の場合も問題なく動作することが確認できています。

|`PID制御`の動作デモ（2倍速）|
|:--------:|
![output-palette-none1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3609913/74948ff0-69ab-baf6-0c6a-fbb1a76401d4.gif)
---
### 改善点の提示（修正案はなし）
#### 機体操作におけるWarThunderとの振る舞いの違いを修正する。
本来`WarThunder`では、`Cキー`を押しながらマウスを動かして視点操作した時、キーボード入力でオーバーライドしていた場合、マウスの方向を表す`MouseAim`イメージに相当するオブジェクト（以後`MouseAim`イメージと呼称）が画面内になかったらその`MouseAim`イメージの方向が機体の向いている方向に上書きされます。
これだけでは何もわからないと思うので、私が`WarThunder`で録画してきた動画を共有します。

|`WarThunder`の動作デモ|
|:--------:|
![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3609913/9d12ad9e-dfde-ce3f-0723-24c5ad2078c4.gif)

実際に`MouseAim`イメージ（に相当するもの）の位置が上書きされていることを確認できますね。
これは`Cキー`を押して正面以外の方向を見ながらドッグファイトをする時に便利です。
今回私は実装しませんでしたが、飛行機のゲームに応用する場合は実装しても良いでしょう。

# ライセンスの明記
今回の記事で紹介した`MouseFlight`は`Brian Hernandez`氏による`MITライセンス`の下にあります。

<details><summary>LICENCE</summary>

MIT License

Copyright (c) 2018 Brian Hernandez

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
</details>