前置き
MouseFlightのGitHubレポジトリより引用したフライトデモ映像 |
---|
この記事へ訪れた皆様は、おそらく既にUnity
をある程度まで使っていて、ゲームをより良くする要素を実装するためにこの記事へと迷い込んだのでしょう。
幸運なことに、今回の記事はその目的を達成することに多少なりとも貢献するであろうことを取り上げます(謎の冗長表現)。
具体的には、Brian Hernandez
氏がGitHub
で公開しているMouseFlight
という、マウス操作を使うことによりUnity
で直感的に航空機を操縦するプロジェクトのアルゴリズムの解説と使用例を紹介します。
このプロジェクトは、WarThunder
というクロスプラットフォームで陸海空の戦場を持つコンバットゲームの、航空機におけるマウス操縦をUnity
上で実装したものになります。
このMouseFlight
のプロジェクトは汎用性を考えて設計されているため、より緻密な演算をするフライトシミュレーターに留まらず、船舶や車両、人型にも対応させることができます。
このプロジェクトは、マウスを使って航空機を動かすということ以外にも、マウスの動かす方にカメラを動かすという動作についてもうまく実装されています。
その上、OSS
ライセンスの中でも最も寛容なMITライセンス
を採用しているため、Unity
でゲーム開発をするのであれば、是非一度見てもらいたいものとなっています。
長文にはなりますが、どうかお付き合いください。
使用例 |
---|
タイトルにUnity初心者
と書いてありますが、誰でも理解できるような詳しい説明はしていません(というかできない)。
およそタイトル詐欺のようですが、少なくとも全体的な流れは把握できると思います。
わからない単語は見つけ次第調べたり、ChatGPT
を使えば理解できるはずです。
また、今回使用するMouseAim
プロジェクトのバージョンはUnity 2017.3.1f1
です。
プロジェクトのレポジトリによると、2018.3.0
や5.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
プロジェクトを配置したい任意のディレクトリへ移動します。
特に指定がないならhome
やDesktop
で良いでしょう。
今回はhome
ディレクトリを例に出します。
$ cd ~
カレントディレクトリを変更できたら、次のコマンドによりGit
のレポジトリをクローンします。
$ git clone https://github.com/brihernandez/MouseFlight
もし、git
が入っていない場合はbrew
/apt
/winget
などでインストールします。
そのツールが入っていない場合はそのツールをインストールします。
git
が嫌な場合や、git clone
が無理そうな場合は.zip
などでダウンロードしても問題ありません。
このコマンドを実行すると、指定したディレクトリにUnity
のプロジェクトが作られるので、Unity Hub
を開いて、右上のAdd
からclone
したプロジェクトを選択してUnityHub
のProjects
に追加しましょう。
UnityHub → Add → Add project from disk を押す |
---|
追加されたことが確認できたら、指定されたバージョンのUnity
でプロジェクトを開きましょう。
同じバージョンでなくとも動作はする場合が多いですが、動作が不安定になりやすいのであまりお勧めはできません。
Unity
のプロジェクトが開けたら、正しいシーン(DemoFlight
)が選択されていることを確認して、早速Ctrl+P
か実行ボタンを押して再生しましょう。
操作方法は以下の通りです。
W/S: Pitch up/down → 上下
A/D: Roll left/right → 左右
C: Enable free look → カメラ操作
なお、Cキー
を押しながらWASDキー
を押しても旋回方向をオーバーライドすることができます。
詳しくはGit
のレポジトリかプロジェクトのコードかWarThunder
の操作方法を確認しましょう。
フライトデモ(再登場) |
---|
プロジェクトの解説
ここからはプロジェクトの解説をしていきます。
最初にオブジェクトの説明をして、次にスクリプトの簡単な説明をします。
オブジェクトの説明では、まず画像でイメージを掴んで、次にオブジェクトの親子関係を ディレクトリのツリーを模して そのオブジェクトが持つComponent
と一緒に表示します。
そして、その内の特筆すべきオブジェクトを別個に取り上げます。
スクリプトの説明では、オブジェクトの説明で述べたコードがどう動いているのかについて軽く説明し、理解しづらい点を抜粋して説明します。
オブジェクトの説明
このDemoFlight
シーンのヒエラルキーは次のような構造になっています。
オブジェクトの概要
DemoFlight → Hierarchy |
---|
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オブジェクト |
---|
飛行機を構成する直方体をまとめた親オブジェクトで、その親オブジェクトであるPlane
のRigidbody
がCollider
の形によって慣性テンソルが自動的に決まってしまうので、本来子オブジェクトにつけるはずのCollider
がこちらのオブジェクトについています。
そのため、オブジェクトの当たり判定を良くみると、見た目とは違う形になっていることに気づけるはずです。
上の画像の緑の線がModel
オブジェクトの当たり判定です。
今回の記事はこの問題の解決法をおまけで扱っています。
Trail(Right/Left) [TrailRenderer]
TrailLeftオブジェクト |
---|
TrailRenderer
を用いてそのオブジェクトの座標からスモーク(Trail
)を出します。
今回はそれぞれが翼端にあるので、翼端からスモークが出ているように見えます。
※今回の画像は見えやすいようにTrail
を編集しました
MouseFlightRig(Prefab) [MouseFlightController]
MouseFlightController
スクリプトを管理するオブジェクトで、フレーム毎(Update
かFixedUpdate
かはオプションで決められます)にPlane
と同じ座標へ移動しています。
MouseAim
Transform
しかコンポーネントがないシンプルなオブジェクトですが、担っている役割は大きいです。
まず、このオブジェクトはマウスの移動量により回転の度合いが変わります。
そして、Plane
の回転はこのオブジェクトを参考に行われます。
具体的には、マウスからのインプット(Input.GetAxis("Mouse X/Y")
)はこのオブジェクトを回転させます。
そして、このオブジェクトとPlane
のRotation
の差によって比例制御することでマウス操作を実現させています。
これをフローチャートにするとこのようになります。
比例制御によって機体の方向を変えているため、オーバーシュートやアンダーシュートが発生してしまいます。
この問題の解決法はおまけで扱っています。
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イメージ |
---|
機体が向いている方向をUI
としてプレイヤーに伝えるオブジェクトです。
MouseFlightController
スクリプトのBoresightPos
に処理が記述されており、Plane
オブジェクトの正面方向と、Plane
オブジェクトからこのイメージをどのくらい前に移動させるかという係数のaimDistance
を掛け合わせたものに飛行機のposition
を足したものになっています。
public Vector3 BoresightPos
{
get
{
// この条件演算子で値を代入している
return aircraft == null
? transform.forward * aimDistance
: (aircraft.transform.forward * aimDistance) + aircraft.transform.position;
}
}
つまり、あくまでUI
はおまけでしかなく、UI
がなくともマウス操作をすることはできるという訳ですね。
このプロジェクトでは、まず三次元空間上にUIを配置したと考えた後に二次元座標へ変換させています。
あまりない発想で参考になりますね。
MouseAim [CanvasRenderer, Image]
Boresightイメージ |
---|
マウスが向いている方向をUIとしてプレイヤーに伝えるオブジェクトです。
MouseFlightController
スクリプトのMouseAimPos
に処理が記述されており、MouseAim
オブジェクトの正面方向と、Plane
オブジェクトからこのイメージをどのくらい前に移動させるかという係数のaimDistance
を掛け合わせたものに飛行機のposition
を足したものになっています。
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
メソッド
private void RunAutopilot(Vector3 flyTarget, out float yaw, out float pitch, out float roll)
out
は参照を渡すための修飾子です。
このメソッドは、飛行機が目標位置に向かうためのyaw
, pitch
, roll
を計算します。
計算方法は以下の通りです。
localFlyTarget
の計算
Vector3 localFlyTarget = transform.InverseTransformPoint(flyTarget).normalized * sensitivity;
これは、旋回の目標位置を飛行機のローカル座標系に変換し、正規化したものです。
sensitivity
はこの値のスケール(倍率)を調節するものです。
yaw
とpitch
の計算
yaw = Mathf.Clamp(localFlyTarget.x, -1f, 1f);
pitch = -Mathf.Clamp(localFlyTarget.y, -1f, 1f);
localFlyTarget
のx
成分がyaw
、y
成分がpitch
になります。
これらは-1
から1
の範囲にClamp
されます。
このyaw
とpitch
は係数を掛けた後そのまま回転に適用されるため、このプロジェクトの飛行機は実質的な比例制御となっています。
roll
の計算
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
メソッド
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
メソッド
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
メソッド
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
メソッド
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
以上の場合のみ、要素を有効にします。
これらスクリプトの動作をまとめてフローチャートで表すと次のようになります。
おそらく、私が説明したことだけで内部のアルゴリズムを完全に理解するのは難しいと思います。
そんな時は、動いているコードを眺めて理解しようとしたり、ChatGPTに丸投げしたり、英語のコメントを和訳して解釈したりなど、理解するためのやり方は沢山あります。
使用例: 車に応用する
WheelCollider
を使った車をマウスで制御してみたいと思います。
このデモのプロジェクトはこちらで配布しています。
さて、どうやって車に応用するのかという話です。
実は、今まで行っていた立体での処理を平面として行えばできてしまいます。
具体的なイメージが湧かないかもしれませんが、とりあえず実践してみましょう。
今回、WheelCollider
の扱いはこちらの記事を参考にしました。
まず、キーボード操作で動かせるかどうかを確認するために、次のようにオブジェクトを配置し、コンポーネントを割り当てました。
CarDemo → Hierarchy(一部) |
---|
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(超省略) |
---|
CarDemo
- ├── Car [Rigidbody, SimpleCarController]
+ ├── Car [Plane, Rigidbody, SimpleCarController]
+ │ └── (以下は先程から変化なし)
+ ├── MouseFlightRig [MouseFlightController]
+ │ └── (以下はMouseFlightと完全に一致)
+ ├── MouseFlightHud [RectTransform, Canvas, CanvasScaler, Graphic Raycaster, Hud]
+ │ └── (以下はMouseFlightと完全に一致)
見てわかる通り、MouseFlight
のHierarchy
と完全に同じですね。
省略した部分はMouseFlight
のものをそのままコピーすればそれで大丈夫です。
ただ、スクリプトを全く改変しない場合、元のプロジェクトの飛行機と同様に車が空を飛んでしまうので、次に示す複数のスクリプトを変更して、動作が干渉しないようにします。
// ここ以前のメソッドの処理を省略
public void FixedUpdate()
{
// 変数の宣言を省略
foreach (AxleInfo axleInfo in axleInfos)
{
- if (axleInfo.steering)
+ if (axleInfo.steering && false)
{
axleInfo.leftWheel.steerAngle = steering;
axleInfo.rightWheel.steerAngle = steering;
}
// 以降の処理を省略
}
}
// 最後の中括弧を省略
// ここ以前のメソッドの処理を省略
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の条件をつけています(意図的な冗長設計)。
ここで行っていることを説明します。
まず、前者の変更で、キーボード操作のスクリプトからタイヤの方向が変わらないようにします。
そして、後者の変更では、すべての加える予定の力や回転を一度無効化し、ヨー方向(横方向)のみをタイヤの角度変更に使用しています。
動作デモ |
---|
適切にオブジェクトを割り当てたら、このgifアニメーション
のように動作するはずです。
どうしてもうまくいかない場合は、こちらのレポジトリを参考にしてください。
この車のデモは、マウスによる移動とWSキー
によるスロットルの調整、Cキーによる視点移動のみに対応しています。
コードの変更が最小限になるような変更をしているため、軽量化のための改善点は多くあります。
おまけ
本編とは関係のないことを書いていきます。
動作テストに使用例の車のプロジェクトを使っていたりしますが、MouseFlight
プロジェクト本体であっても問題なく動作するはずです。
要素変更の提案とその修正
カメラの角度により描画の処理が変更されている処理を単純化する。
MouseController.cs
には次の記述があります。
// 以前のメソッドを省略
private void RotateRig()
{
// 以前の処理を省略
Vector3 upVec = (Mathf.Abs(mouseAim.forward.y) > 0.9f) ? cameraRig.up : Vector3.up;
// 以後の処理を省略
}
// 以後のメソッドを省略
このコードは、真上や真下を向いた時に発生する不安定な挙動を抑止する役割を担っています。
特に問題がないように感じますが、これはこれで少し微妙な挙動をします(これは人によって感じ方が変わりそう)。
カメラの動作デモ |
---|
※別PCで開発管理せずに並行で作っていたせいでセットが減っています |
試した限りでは、コードを変更しても特別おかしな挙動はしなかったので、Vector3.up
に固定しても大丈夫でしょう。
変更してみる
// 以前のメソッドを省略
private void RotateRig()
{
// 以前の処理を省略
- Vector3 upVec = (Mathf.Abs(mouseAim.forward.y) > 0.9f) ? cameraRig.up : Vector3.up;
+ Vector3 upVec = Vector3.up;
// 以後の処理を省略
}
// 以後のメソッドを省略
コードを変更すると、次のようになります。
カメラの動作デモ |
---|
処理をシンプルにすることによって、より直感的な動作をするようになりましたね。
Clliderの形状が慣性テンソルに直接影響していて、見かけ上の判定と違う。
Project
のPlane
プレハブのRigidbody
を確認すると、AutomaticTensor
が有効になっていると思います。
Project → Plane → Rigidbody → Automatic Tensor が有効になっている |
---|
これは、そのRigidbody
が付与されているオブジェクトの回転のしやすさ(慣性モーメント)を当たり判定の形状と重量、重心などから自動的に計算するものとなっています。
そのためか、Plane
プレハブをよく見てみると、当たり判定が見かけ上のCube
などによらない形状となっていることが確認できます。
Model オブジェクト(再掲) |
---|
この画像の緑色の線が実際の当たり判定です |
これは機体の形状を変更した時に予期しない挙動をすることが想像できるので、手動で慣性テンソルを設定しましょう。
変更してみる
コードを書いてデフォルトの状態の慣性テンソルを取得したところ、(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制御
で車を動かしている弊害で、オーバーシュートやアンダーシュートを繰り返しやすいという特徴があります。
P制御 の動作デモ(2倍速) |
---|
今回は、これをPID制御
に変更することで解決します。
変更してみる
次のコードを変更、加筆します。
詳しい説明はしません。
// 以前のメソッドを省略
+ 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);
}
// 以後のメソッドを省略
// 以前のクラスを省略
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倍速) |
---|
改善点の提示(修正案はなし)
機体操作におけるWarThunderとの振る舞いの違いを修正する。
本来WarThunder
では、Cキー
を押しながらマウスを動かして視点操作した時、キーボード入力でオーバーライドしていた場合、マウスの方向を表すMouseAim
イメージに相当するオブジェクト(以後MouseAim
イメージと呼称)が画面内になかったらそのMouseAim
イメージの方向が機体の向いている方向に上書きされます。
これだけでは何もわからないと思うので、私がWarThunder
で録画してきた動画を共有します。
WarThunder の動作デモ |
---|
実際にMouseAim
イメージ(に相当するもの)の位置が上書きされていることを確認できますね。
これはCキー
を押して正面以外の方向を見ながらドッグファイトをする時に便利です。
今回私は実装しませんでしたが、飛行機のゲームに応用する場合は実装しても良いでしょう。
ライセンスの明記
今回の記事で紹介したMouseFlight
はBrian 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.