9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

42TokyoAdvent Calendar 2024

Day 18

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

Last updated at Posted at 2024-12-17

前置き

MouseFlightのGitHubレポジトリより引用したフライトデモ映像
flight.gif

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

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

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

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

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

使用例
output.gif

タイトルにUnity初心者と書いてありますが、誰でも理解できるような詳しい説明はしていません(というかできない)。
およそタイトル詐欺のようですが、少なくとも全体的な流れは把握できると思います。
わからない単語は見つけ次第調べたり、ChatGPTを使えば理解できるはずです。

また、今回使用するMouseAimプロジェクトのバージョンはUnity 2017.3.1f1です。
プロジェクトのレポジトリによると、2018.3.05.6.4でもテストされされているようですが、5.6.4の場合はライティングをリセットする必要があるそうです。

自己紹介

私は今まで、Unityで簡易的なアルゴリズムに基づいたフライトシミュレーターを個人で開発したり、自身でもよく理解できない運の巡りからプログラミング講師としてUnityを教えたり、そこの教材を作成したり、単純な興味から発展してモーターグライダーの訓練生パイロットをしたりしています。

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

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

言い訳ゾーン

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

以上により、航空機へ一定の興味関心を持っていることはおよそ伝わったかと思いますが、残念なことに私の知識は十分でなく、Unityも特別上手に扱える訳ではありません。
そのため、この記事の不自然な表現や不適切と思われる文は、見つけ次第指摘してもらえると助かります。

MouseFlightの紹介

前置きで述べた通り、この記事ではBrian Hernandez氏がGitHubで公開しているMouseFlightという、マウス操作を使うことによりUnityで直感的に航空機を操縦するプロジェクトのアルゴリズムの解説と使用例を紹介します。

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

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

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

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

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

MouseFlightの導入

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

まず、ターミナルを開いてUnityプロジェクトを配置したい任意のディレクトリへ移動します。
特に指定がないならhomeDesktopで良いでしょう。
今回はhomeディレクトリを例に出します。

bash
$ cd ~

カレントディレクトリを変更できたら、次のコマンドによりGitのレポジトリをクローンします。

bash
$ git clone https://github.com/brihernandez/MouseFlight

もし、gitが入っていない場合はbrew/apt/wingetなどでインストールします。
そのツールが入っていない場合はそのツールをインストールします。
gitが嫌な場合や、git cloneが無理そうな場合は.zipなどでダウンロードしても問題ありません。

このコマンドを実行すると、指定したディレクトリにUnityのプロジェクトが作られるので、Unity Hubを開いて、右上のAddからcloneしたプロジェクトを選択してUnityHubProjectsに追加しましょう。

UnityHub → Add → Add project from diskを押す
スクリーンショット 2024-12-08 2.27.19.png

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

同じバージョンでなくとも動作はする場合が多いですが、動作が不安定になりやすいのであまりお勧めはできません。

Unityのプロジェクトが開けたら、正しいシーン(DemoFlight)が選択されていることを確認して、早速Ctrl+Pか実行ボタンを押して再生しましょう。
操作方法は以下の通りです。

W/S: Pitch up/down → 上下
A/D: Roll left/right → 左右
C: Enable free look → カメラ操作

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

詳しくはGitのレポジトリかプロジェクトのコードかWarThunderの操作方法を確認しましょう。

フライトデモ(再登場)
flight.gif

プロジェクトの解説

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

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

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

オブジェクトの説明

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

オブジェクトの概要

DemoFlight → Hierarchy
スクリーンショット 2024-12-08 16.33.41.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

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

今回の記事はこの問題の解決法をおまけで扱っています。

Trail(Right/Left) [TrailRenderer]
TrailLeftオブジェクト
スクリーンショット 2024-12-13 14.14.35.png

TrailRendererを用いてそのオブジェクトの座標からスモーク(Trail)を出します。
今回はそれぞれが翼端にあるので、翼端からスモークが出ているように見えます。
※今回の画像は見えやすいようにTrailを編集しました

MouseFlightRig(Prefab) [MouseFlightController]

MouseFlightControllerスクリプトを管理するオブジェクトで、フレーム毎(UpdateFixedUpdateかはオプションで決められます)にPlaneと同じ座標へ移動しています。

MouseAim

Transformしかコンポーネントがないシンプルなオブジェクトですが、担っている役割は大きいです。

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

具体的には、マウスからのインプット(Input.GetAxis("Mouse X/Y"))はこのオブジェクトを回転させます。
そして、このオブジェクトとPlaneRotationの差によって比例制御することでマウス操作を実現させています。

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

比例制御によって機体の方向を変えているため、オーバーシュートやアンダーシュートが発生してしまいます。
この問題の解決法はおまけで扱っています。

CamRig

こちらもTransformしかコンポーネントがないシンプルなオブジェクトですが、担っている役割は同様に大きいです。
本来、カメラはPlaneオブジェクトから離れた位置に配置しないと、Planeのメッシュにめり込んでしまいます。
ただ、コードを書くときはカメラのオブジェクトをオフセットなしでPlaneと同じ座標に移動させたほうが楽です。

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

また、実際に飛ばしてみるとわかりますが、Cキーの状態によらず、カメラが回転するときはその移動が滑らかになっています。
これはCamRigの角度の変更時に減衰を含んだ球面線形補完Sleap)が使われているからなのですが、説明すると長くなってしまうので、ここでの詳しい言及は避けたいと思います。

MouseFlightHud(Prefab) [RectTransform, Canvas, CanvasScaler, Graphic Raycaster, Hud]

沢山のコンポーネントがついていますが、Hudスクリプト以外は全てUI関連のコンポーネントです。
このオブジェクトは、実行時に使うBoresightオブジェクトやMouseAimオブジェクトの親として機能するCanvasです。
直接的にやっていることは、Hudスクリプトによる子オブジェクトの座標移動のみです。

Boresight [CanvasRenderer, Image]
Boresightイメージ
Crosshair.png

機体が向いている方向をUIとしてプレイヤーに伝えるオブジェクトです。
MouseFlightControllerスクリプトのBoresightPosに処理が記述されており、Planeオブジェクトの正面方向と、Planeオブジェクトからこのイメージをどのくらい前に移動させるかという係数のaimDistanceを掛け合わせたものに飛行機のpositionを足したものになっています。

MouseFlightController.cs
public Vector3 BoresightPos
{
    get
    {
        // この条件演算子で値を代入している
        return aircraft == null
             ? transform.forward * aimDistance
             : (aircraft.transform.forward * aimDistance) + aircraft.transform.position;
    }
}

つまり、あくまでUIはおまけでしかなく、UIがなくともマウス操作をすることはできるという訳ですね。

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

MouseAim [CanvasRenderer, Image]
Boresightイメージ
MouseCrosshair.png

マウスが向いている方向をUIとしてプレイヤーに伝えるオブジェクトです。
MouseFlightControllerスクリプトのMouseAimPosに処理が記述されており、MouseAimオブジェクトの正面方向と、Planeオブジェクトからこのイメージをどのくらい前に移動させるかという係数のaimDistanceを掛け合わせたものに飛行機のpositionを足したものになっています。

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メソッド

Plane.cs
private void RunAutopilot(Vector3 flyTarget, out float yaw, out float pitch, out float roll)

outは参照を渡すための修飾子です。

このメソッドは、飛行機が目標位置に向かうためのyaw, pitch, rollを計算します。
計算方法は以下の通りです。

localFlyTargetの計算

Plane.cs
Vector3 localFlyTarget = transform.InverseTransformPoint(flyTarget).normalized * sensitivity;

これは、旋回の目標位置を飛行機のローカル座標系に変換し、正規化したものです。
sensitivityはこの値のスケール(倍率)を調節するものです。

yawpitchの計算

Plane.cs
yaw = Mathf.Clamp(localFlyTarget.x, -1f, 1f);
pitch = -Mathf.Clamp(localFlyTarget.y, -1f, 1f);

localFlyTargetx成分がyawy成分がpitchになります。
これらは-1から1の範囲にClampされます。
このyawpitchは係数を掛けた後そのまま回転に適用されるため、このプロジェクトの飛行機は実質的な比例制御となっています。

rollの計算

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は、この角度に基づいてwingsLevelRollaggressiveRollを線形補間します。

FixedUpdateメソッド

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メソッド

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 CameracameraRigの子オブジェクトであるため、実際にカメラを回転させているのはこのコードです。

Dampメソッド

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から受け取ったデータに基づいて、画面に先程紹介したBoresightMouseAimの位置を表示します。

Hud.csのポイント(MouseFlightHud)

UpdateGraphicsメソッド

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);
    }
}

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


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

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

使用例: 車に応用する

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

このプロジェクトはUnity 6HDRPで作られています。
同じバージョンのHDRPプロジェクトを動作させられる環境でないと、完全な動作は期待できないかもしれません。
なおこのプロジェクトを動かす場合は、MouseFlightと同様にgit cloneしてからAddしてください。

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

今回、WheelColliderの扱いはこちらの記事を参考にしました。

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

CarDemo → Hierarchy(一部)
スクリーンショット 2024-12-15 19.54.36.png
CarDemo
CarDemo
├── Car [Rigidbody, SimpleCarController]
│   ├── Body [MeshFilter, MeshRendrer, BoxCollider]
│   ├── Main Camera [Camera, AudioListener, HDAdditionalCameraData]
│   └── Tires
│       └── WheelColider [WheelColider] (前後左右の計4つ)
│           └── GameObject (それぞれに一つづつ)
│               └── Cylinder [MeshFilter, MeshRendrer] (それぞれに一つづつ)
以下省略)

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

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

CarDemo → Hierarchy(超省略)
スクリーンショット 2024-12-15 20.02.54.png
CarDemo
  CarDemo
- ├── Car [Rigidbody, SimpleCarController]
+ ├── Car [Plane, Rigidbody, SimpleCarController]
+ │   └── (以下は先程から変化なし)
+ ├── MouseFlightRig [MouseFlightController]
+ │   └── (以下はMouseFlightと完全に一致)
+ ├── MouseFlightHud [RectTransform, Canvas, CanvasScaler, Graphic Raycaster, Hud]
+ │   └── (以下はMouseFlightと完全に一致)

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

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

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;
        }

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

// 最後の中括弧を省略
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;
}

// 最後の中括弧を省略

再度有効化する可能性を考えて、その時の編集量が最小になるように、falseの条件をつけています(意図的な冗長設計)。

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

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

動作デモ
output.gif

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

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

おまけ

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

要素変更の提案とその修正

カメラの角度により描画の処理が変更されている処理を単純化する。

MouseController.csには次の記述があります。

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

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

// 以後のメソッドを省略

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

カメラの動作デモ
output.gif
※別PCで開発管理せずに並行で作っていたせいでセットが減っています

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

変更してみる

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

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

// 以後のメソッドを省略

コードを変更すると、次のようになります。

カメラの動作デモ
output2.gif

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


Clliderの形状が慣性テンソルに直接影響していて、見かけ上の判定と違う。

ProjectPlaneプレハブのRigidbodyを確認すると、AutomaticTensorが有効になっていると思います。

Project → Plane → Rigidbody → Automatic Tensorが有効になっている
スクリーンショット 2024-12-08 12.38.37.png

これは、そのRigidbodyが付与されているオブジェクトの回転のしやすさ(慣性モーメント)を当たり判定の形状と重量、重心などから自動的に計算するものとなっています。
そのためか、Planeプレハブをよく見てみると、当たり判定が見かけ上のCubeなどによらない形状となっていることが確認できます。

Modelオブジェクト(再掲)
スクリーンショット 2024-12-14 0.27.54.png
この画像の緑色の線が実際の当たり判定です

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

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

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

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


比例制御をPID制御に変更する。

車のデモを遊んでいる時、急にハンドルを回したりすると、車両がマウスを通り越して曲がってしまいます(MouseFlightでも同様)。
これは、P制御で車を動かしている弊害で、オーバーシュートやアンダーシュートを繰り返しやすいという特徴があります。

P制御の動作デモ(2倍速)
output-palette-none.gif

今回は、これをPID制御に変更することで解決します。

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

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);
}

// 以後のメソッドを省略
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

改善点の提示(修正案はなし)

機体操作におけるWarThunderとの振る舞いの違いを修正する。

本来WarThunderでは、Cキーを押しながらマウスを動かして視点操作した時、キーボード入力でオーバーライドしていた場合、マウスの方向を表すMouseAimイメージに相当するオブジェクト(以後MouseAimイメージと呼称)が画面内になかったらそのMouseAimイメージの方向が機体の向いている方向に上書きされます。
これだけでは何もわからないと思うので、私がWarThunderで録画してきた動画を共有します。

WarThunderの動作デモ
output.gif

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

ライセンスの明記

今回の記事で紹介したMouseFlightBrian Hernandez氏によるMITライセンスの下にあります。

LICENCE

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.

9
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?