エフアンダーバー

個人開発の記録

【Unity】 物理演算の基礎

Unityでアクションゲームを作る上で避けては通れないのが、 コライダー(Collider)やリジッドボディ(Rigidbody)といった物理演算コンポーネントの存在です。 特に物理的な挙動を再現したいわけではなくても、当たり判定に関する処理のためにはこれらが必要になってきます。

この記事ではそんな物理演算周りについて最低限知っておくべきことについてまとめます。

コライダーやリジッドボディの使い方だけはなんとなく覚えたけど、何が起きているかはよくわからないという人向けです。

物理演算とは?

そもそも物理演算とは何なのかという話。

物理演算というとなんだか物が現実の通りに動くような気がしてしまいますが、 実際には(基本的に)古典力学の範囲内でそれっぽく動かしているだけだったりします。 現実の挙動を完全に再現するにはコンピューターの計算速度はまったく足りませんし、 コンピューター上では現実ではあり得ない状態になることも少なくありません。 そのため、物理演算を過信すると痛い目を見ます。

では物理演算(エンジン)は一体何をしてくれるのかというと、変形しない物体(剛体)の運動および衝突処理です。

衝突処理は大まかに次の二つから成ります。

  • 衝突検出 (Collision Detection)
  • 衝突応答 (Collision Response)

衝突検出は物体同士が衝突しているかどうかを判定します。 もっと言うと、物体同士が重なっているかどうかを判定します。 現実では物体が重なって存在することはあり得ませんが、 処理としてとりあえず移動させてしまってから重なりを検出するほうが楽なのです。

衝突応答は衝突が検出された場合に衝突した物体同士がこの後どう動くかを計算します。 検出しただけでは何の意味もないので、 おかしな状態にならないように動きを修正するわけです。

衝突検出と衝突応答

物理演算はこの二つの処理と運動処理を短い時間間隔で何度も繰り返すことによって、 現実世界の物の動きをシミュレーション(模倣)します。

2Dと3D

物理エンジンには2D用のものと3D用のものがあります。 これらは作成するゲームの種類によって使い分けます。

どちらを利用するかはそのゲームに奥行き方向の『移動』があるかどうかによって決めます。 ゲームの見た目は関係ないので注意してください。 例え3Dモデルを使っていようが、平面的な移動しかしないならば2D用の物理エンジンを使います。 逆にドット絵による表現であっても、画面奥に移動できるなら3D用の物理エンジンを使います。

2D用の物理エンジンと3D用の物理エンジンは完全に独立した別のものです。 そのため2D用の物体と3D用の物体との間で衝突処理を行うことはできません。

物理の世界と描画の世界

物理エンジンは現実の物の動きを模倣してくれます。 グラフィックスエンジンはそれに美麗な外見を与えてくれます。 これらは連動して動きますがそれぞれまったく別の世界のものです。

「キャラクターを動かすと何故か倒れる」と悩むUnity初心者の話はよく聞きます。 しかし物理の世界で考えてみれば当たり前のことで、 カプセルの重心に水平方向の力を加えると接地面の摩擦で傾いて倒れるのです。

物理と描画の比較

見た目に騙されないようにしてください。 物理エンジンは物理の世界でしか動きません。

物理演算コンポーネント

Unityでは主に次の三種類のコンポーネントにより物理演算を制御します。

  • Collider (コライダー)
  • Rigidbody (リジッドボディ)
  • Joint (ジョイント)

2D用のものには"2D"という接尾辞がつきます。

Collider (コライダー)

Colliderは衝突する(collide)もの(-er)という意味で、衝突の対象となる物体を表し、物体の形状や材質を保持します。

これがアタッチされていないと、形状がないため一切衝突しません。 物理演算の対象としたいのであれば、多くのケースで必要となるコンポーネントです。

コライダーは形状ごとに別のコンポーネントとなっており、主に次の種類があります。

形状 2D 3D
箱状 BoxCollider2D BoxCollider
球状 CircleCollider2D SphereCollider
カプセル状 CapsuleCollider2D CapsuleCollider
自由形状 PolygonCollider2D, EdgeCollider2D MeshCollider

またプリミティブ(箱状、球状、カプセル状)なコライダーは組み合わせて、 複合コライダーとすることもできます。

トリガー

コライダーには"Is Trigger"という設定があり、これをtrueにすることでトリガーとなります。

トリガー(trigger)は「(銃の)引き金」の意味で、プログラミングにおいてはイベントの発生原因を表します。 余談ですが、イベントを発生させることを発火(fire)と呼ぶこともあります。 「コライダーとの接触をトリガーとしてクリアイベントを発火する」というように使います。

コライダーをトリガーにすると、衝突時の衝突応答がなくなります。 衝突判定のみ行うようになるので、ぶつかる対象というよりもイベントの発生領域を表すようになります。

マテリアル

コライダーには物理的な材質(マテリアル)を設定できます。

物理的な材質とは何かというと、反発係数と摩擦係数を決めるものになります。 ざっくり言うと「どれだけ跳ね返るか」と「どれだけざらついているか」です。

材質はアセットとして作成してからコライダーに設定します。 このアセット、2D用はPhysicsMaterial2Dなのですが、 3D用は歴史的な理由でPhysicMaterialと"s"が抜けています。 スクリプトの型も同名なのでタイプ時にはご注意を。

材質を明示的に指定しなかった場合にはプロジェクトのデフォルトの材質が設定されます。 デフォルトの材質は"Project Settings"から変更できます。

Rigidbody (リジッドボディ)

Rigidbodyは剛体を意味します。 剛体は質点に大きさと形状を加えた概念で、変形しない物体を表します。

これがアタッチされていないと、剛体ではない、つまり物理に従って動く物体ではないということになります。 移動する物体であるならば、多くのケースで必要となるコンポーネントです。

わかりづらい設定がいくつかありますが、次のものだけ理解しておけばとりあえずは問題ありません。

設定 説明
Mass 剛体全体の質量(重さ)。
(Linear) Drag 移動(並進運動)にかかる抵抗の大きさ。
Angular Drag 回転(回転運動)にかかる抵抗の大きさ。
Use Gravity 重力の影響を受けるかどうか。
Gravity Scale 【2D】 重力の影響度。
Is Kinematic キネマティックかどうか(後述)。
Body Type 【2D】 コライダーの種類(後述)。

Joint (ジョイント)

Jointは「接合」や「関節」などを意味する英語で、リジッドボディ間の関係や制約を表します。 『関係や制約』というのは「一緒に動く」とか「離れすぎない」といった類のものです。

描画の世界ではTransformコンポーネントやスクリプトによって関係や制約を表しましたが、 物理の世界ではこれらが利用できないため、 代わりにジョイントを利用します。

3D用のジョイントは制約ごとに次の種類があります。

コンポーネント名 説明
FixedJoint ひとつの物体であるかのように拘束。
SpringJoint バネで繋がれているかのように拘束。
HingeJoint 蝶番*1で繋がれているかのように拘束。
CharacterJoint ラグドール用。
ConfigurableJoint カスタマイズ用。

2D用は数が多いため省略しますが、概ね同じようなものがあります。

コンポーネントまとめ

コンポーネント 説明
コライダー 衝突できる物体であることとその形状・材質を表す。
リジッドボディ 物理によって移動する物体であることを表す。
ジョイント リジッドボディ同士の関係・制約を表す。

コライダーの種類

コライダーは利用方法によって次の三つに分類されます *2

  • 静的コライダー (static collider)
    • (3D) Rigidbodyをアタッチしていない
    • (2D) Rigidbody2Dの"Body Type"がStatic
  • 動的コライダー (dynamic collider)
    • (3D) Rigidbodyをアタッチしている かつ "Is Kinematic"がfalse
    • (2D) Rigidbody2Dの"Body Type"がDynamic
  • キネマティックコライダー (kinematic collider)
    • (3D) Rigidbodyをアタッチしている かつ "Is Kinematic"がtrue
    • (2D) Rigidbody2Dの"Body Type"がKinematic

静的コライダーは基本的にゲーム中動かしてはならないコライダーです (動かさないことを前提に動作速度が最適化されます)。 他のコライダーに衝突されても動くことはありません。 主にステージの壁や床に使用します。

動的コライダーは力学に従って移動するコライダーです。 物理演算においてはこれが最も基本的なコライダーになります。 主にステージ上のオブジェクトやキャラクターに使用します。

キネマティックコライダーはスクリプトによって動きを制御するコライダーです。 kinematicは「運動学的な」という意味で、力ではなく動きそのものを記述することを示します。 キネマティックコライダーは静的コライダー同様に他から影響を受けませんが、 スクリプトによって動かしてもよいことになっています。 主にステージのギミックに使用します。

衝突対象表

コライダーはその種類によって衝突可能な対象が制限されます。

例えば、静的コライダー同士はどちらも動かず衝突のしようがないため、 衝突検出の負荷を減らすために衝突の組み合わせから除外されます (元から重なっていた場合は物理エンジンでは考慮されません)。

衝突可能な組み合わせは次の通りです。

静的 動的 キネマティック
静的 × ×
動的
キネマティック × ×

ただし、片方または両方がトリガーの場合やRigidbody2Dで"Use Full Kinematic Contacts"がtrueの場合は次の通りになります。

静的 動的 キネマティック
静的 ×
動的
キネマティック

また、3Dで非トリガー同士の場合でも"Project Settings" > "Physics" > "Contact Pairs Mode"の設定により後者の衝突表へと変更が可能です。

スクリプト操作

物理演算自体はコンポーネントのアタッチのみで動作しますが、それだけではゲームとして成り立ちません。 そこで、ユーザ入力時や衝突時にスクリプトによる操作が必要となってきます。

ここではごく基本的な操作のみ扱います。 詳細は過去記事や他所の記事を参照してください。

www.f-sp.com

www.f-sp.com

力を加える

動的コライダーは基本的にリジッドボディに力を加えることにより移動や回転をします。

力を加える方法はいくつかありますが、最も基本的なのはAddForceAddTorqueです。

AddForce

AddForceはリジッドボディの重心に対して力を加えます。

void FixedUpdate() {
    var rigidbody = GetComponent<Rigidbody>();     // 2DならRigidbody2D
    var force = new Vector3(1f, 2f, 3f);           // 2DならVector2

    rigidbody.AddForce(force, ForceMode.Force);    // 2DならForceMode2D
}

ForceModeは力の与え方を指定するもので、 継続的に与える力(物を押すなど)にはForceを、瞬間的に与える力(釘を打つなど)にはImpulseを使います。

AddTorque

AddTorqueはリジッドボディにトルクを加えます。

トルクというのは物体を回転させる強さを表すもので、ざっくり言うと回転版の力です。

void FixedUpdate() {
    var rigidbody = GetComponent<Rigidbody>();     // 2DならRigidbody2D
    var torque = new Vector3(1f, 2f, 3f);          // 2Dならfloat

    rigidbody.AddTorque(torque, ForceMode.Force);  // 2DならForceMode2D
}

ForceModeAddForceと同様です。

指定位置へ動かす

キネマティックコライダーは力の影響を受けないため、直接位置を指定して動かす必要があります。

位置を指定した移動にはMovePositionを使います。

void FixedUpdate() {
    var rigidbody = GetComponent<Rigidbody>();     // 2DならRigidbody2D
    var position = new Vector3(1f, 2f, 3f);        // 2DならVector2

    rigidbody.MovePosition(position);
}

同様に、回転にはMoveRotationを使います。

衝突に反応する

衝突に対して何らかの処理を実行するには、OnCollisionから始まる次のメソッドのいずれかを定義します (2Dの場合にはそれぞれ接尾辞に"2D"がつき、引数もCollision2D型になります)。

  • void OnCollisionEnter(Collision collision)
  • void OnCollisionExit(Collision collision)
  • void OnCollisionStay(Collision collision)

これらのメソッドは衝突が発生した際に自動的に呼ばれます。

private void OnCollisionEnter(Collision collision) {
    var player = collision.gameObject.GetComponent<Player>();
    if (player) {
        player.Damage(1);

        Destroy(gameObject);
    }
}

"Enter", "Exit", "Stay" というのはそれぞれ呼ばれるタイミングを示しています。 "Enter"が衝突の開始時、"Exit"が衝突の終了時、"Stay"が衝突中です。 "Enter"と"Exit"は一回の衝突に一度しか呼ばれませんが、"Stay"は何度も呼ばれます。

衝突の開始だの終了だの言われてもわかりづらいかもしれませんが、 衝突というのは連続で発生することがあり、その始めと終わりのことです。 キャラクターと地面との衝突で考えると、 接地した瞬間が"Enter"で、立っている状態、つまりは重力によって地面と連続的に衝突している状態が"Stay"、ジャンプして離れた瞬間が"Exit"です。 「衝突」を「接触」と言い換えてもいいかもしれません。

コライダーがトリガーの場合には前述のメソッドの代わりにOnTriggerから始まる次のメソッドのいずれかを定義します (2Dの場合にはそれぞれ接尾辞に"2D"がつき、引数もCollider2D型になります)。

  • void OnTriggerEnter(Collider other)
  • void OnTriggerExit(Collider other)
  • void OnTriggerStay(Collider other)

各メソッドの意味は同様ですが、引数が衝突相手のコライダーになっていることに注意してください。

Collisionクラス

OnCollision系のメソッドにはCollisionクラスのオブジェクトが引数として渡されてきます。 このクラスからは衝突時の状況に関する情報を取得できます。

主なものは次の通りです。

プロパティ名 説明
collider 衝突相手のコライダー。
rigidbody 衝突相手のリジッドボディ。
gameObject 衝突相手のゲームオブジェクト。
relativeVelocity 衝突相手の相対速度。
contactCount 接点の数。

また、GetContactメソッドにより取得できる接点情報には主に次の内容が含まれます。

プロパティ名 説明
point 接点の座標。
normal 衝突相手の接点における法線。
separation 接点におけるコライダー同士の距離(負値でどれだけめり込んだか)。

物理演算の注意点

最後に、物理演算を使う上で間違えやすいポイントや注意点についてまとめておきます。

Transformによる操作は控える

Rigidbody(2D)をアタッチしたオブジェクトをTransformによって操作することは基本的にすべきではありません。 これはRigidbody(2D)が物理の世界のもので、Transformが描画の世界のものだからです。 Unityがある程度それっぽく動くようにはしてくれますが *3、 根本的に管理方法が違います。

Transform.positionRigidbody.positionTransform.rotationRigidbody.rotationを代わりに使用します。 また、もしその位置または回転の変更がテレポートでないのであれば、 動的コライダーに対してAddForceAddTorqueを、 キネマティックコライダーに対してMovePositionMoveRotationを使うべきです。 スケールや親子関係は物理の世界には存在しませんが、似たような機能としてコライダーの形状設定やジョイントがあります。

例外的なケースも多々あるのですが(特にアニメーション周り)、まずは物理の世界で同じことができないか調べてみるべきです。 物理の世界を描画の世界が表現するのであって、描画の世界に物理の世界を合わせることは難しいのです。

Update内で操作しない

物理の世界と描画の世界では状態更新の間隔が違います。 そのため描画の世界の更新間隔で呼び出されるUpdate内やLateUpdate内、 あるいはこれらから呼び出されるメソッド内でリジッドボディに変更を加えてしまうと、 おかしな挙動となる場合があります*4

そこで、リジッドボディに変更を加える場合には代わりにFixedUpdateというメソッド内で処理を行います。 使い方はUpdateLateUpdateと同じです。

一応正確には更新間隔に依存するリジッドボディの一部のメソッドだけの問題ですが、 わかりづらいので『物理関係の操作はFixedUpdate内』とされることが多いです。 ただこれには少し問題もあり、Inputの一部のメソッドはFixedUpdateでは使用できないため、これが絡むと工夫が必要になります。 この辺りは結構ややこしいので他所の記事に投げときます。

qiita.com

MeshColliderを乱用しない

3D用の物理エンジンにはMeshColliderという万能に見えるコライダーがあります。 これはコライダーの形状にMeshを利用できるというものです。

ただ勘違いしてはいけないのは、『コライダーの形状にMeshが利用できる』のであって、 『Mesh(3Dモデル)につけるためのコライダー』ではないということです。

描画用の3Dモデルというのは見栄えをよくするために数千から数万超の三角形からできています。 これをMeshColliderに指定するのは、数千から数万個のコライダーをアタッチするようなものです*5。 当然相当重くなります。

MeshColliderの基本的な用途は地形の描画結果と形状を合わせることだと思います。 その他のオブジェクトやキャラクターはカプセルなどである程度形を合わせるだけで十分です。

高速移動に気を付ける

衝突検出は先に移動させてから重なりを検出すると説明しました。 これは1更新サイクル(1フレーム)の間に移動する量がそれほど大きくないことを前提としています。 なぜなら移動経路上の衝突を一切考慮していないためです。

移動速度が遅い場合にはサイクル当たりの移動量が小さいためあまり問題にならないのですが、 移動速度が速い場合にはサイクルあたりの移動量が大きく、 移動前の位置と移動後の位置の間に何か障害物があったとしても突き抜けて進んでしまいます。

この問題を解消するにはもちろん速度を抑えるのが一番手っ取り早いのですが、銃弾などの表現ではそうもいきません。 本記事では扱いませんが、そういった場合のためにリジッドボディに"Collision Detection Mode"という設定があるので調べてみてください。

ユニティちゃんライセンス

ユニティちゃんライセンス

この作品はユニティちゃんライセンス条項の元に提供されています

おわりに

数年前に書きかけたまま放置していた記事だったのですが、少し気合を入れて書ききりました。 もともとこれほど長い記事の予定ではなかったのですが、あらゆることを少しずつ書いた結果なかなかの長文記事に……。 改めて物理演算のややこしさを実感しました。

ひそかにリンク等を追加していたのですが、最近pixivFANBOXにて支援者の募集を始めました。 継続的な記事執筆のため、コーヒー代程度でも支援してもらえると助かります。 ご協力よろしくお願いします。 詳細はこちら



執筆時のUnityのバージョン:2018.3.2f1

*1:「ちょうつがい」と読みます。ドアなどに使われる留め具のことです。

*2:用語はいまいち定まっていないようなので慣用的なものです

*3:わかりづらくさせるだけなので個人的にはやめた方がいい気がしますが……

*4:なんかヘン、というレベルのわかりづらいバグなので知らないと見逃しがちかもしれません

*5:もちろんいろいろと最適化はされますが……