このノートでは衝突検出と Rigidbody コンポーネントの特性を扱います。
物理はいつ更新されるのか
Unity の物理は Update ではなく FixedUpdate で回ります。両者の違いは次のとおりです。
Updateは描画 1 フレームにつき 1 回呼ばれ、間隔はフレームレートに応じて変動する。FixedUpdateは固定タイムステップで呼ばれ、既定では 0.02 秒(50 Hz)。フレームレートとは無関係。重いフレームでは複数回呼ばれることもあり、非常に速いフレームでは一度も呼ばれないこともある。
ここから基本原則が導かれます。リジッドボディへの力と速度の変更は、すべて FixedUpdate に書く。Update に書くと力を加える回数がフレームレートで変わり、マシンによって操作感が変わってしまいます。
C#void FixedUpdate()
{
rb.AddForce(direction * force, ForceMode.Force);
}
逆に、入力の取得は Update に書かなければなりません。Input.GetKeyDown は 1 フレームだけ true になるため、FixedUpdate では取りこぼします。定石は Update で意図を記録し、FixedUpdate で消費する形です。
C#private bool jumpQueued;
void Update()
{
if (Input.GetKeyDown(KeyCode.Space)) jumpQueued = true;
}
void FixedUpdate()
{
if (jumpQueued)
{
rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
jumpQueued = false;
}
}
Time.fixedDeltaTime を小さくすれば物理精度は上がりますが、CPU コストは線形に増えます。モバイルではむしろ 0.033(30 Hz)へ上げることが多いです。
Rigidbody
Rigidbody を付けた時点で、そのオブジェクトは物理エンジンの管理下に入ります。重要なプロパティは次のとおり。
Mass(質量)。衝突時の運動量交換にのみ影響し、落下速度には影響しません。真空中では羽根も鉄球も同じ速さで落ちますし、Unity もそう振る舞います。「速く落としたい」から質量を上げても無意味で、Gravity Scale(2D)を変えるか自分で力を足すべきです。
Drag / Angular Drag(減衰)。物理ステップごとに velocity *= (1 - drag * fixedDeltaTime) で速度を減衰させます。空気抵抗の物理モデルではなく、手触りを決めるつまみにすぎません。
Is Kinematic(キネマティック)。力と衝突の影響を受けなくなりますが、他の物体を押しますし、衝突コールバックも発火します。移動する足場やアニメーション駆動のキャラクターで使います。注意点がひとつ。キネマティックなボディは transform.position への代入ではなく rb.MovePosition() で動かしてください。前者は物理システム内で補間されるため衝突検出が正しく働きますが、後者はテレポートであり、壁を貫通します。
Interpolate(補間)。物理は 50 Hz、描画は 120 Hz ということがあり、その間のフレームではボディが動かないため動きがカクついて見えます。Interpolate を有効にすると、描画が二つの物理ステップの間を補間します。代償は物理ステップ 1 つ分の遅れです。プレイヤー操作のボディでは有効に、それ以外では無効にしておくのが普通です。
衝突検出
離散と連続
既定の Discrete モードは、物理ステップごとに現在位置が重なっているかを 1 回だけ調べます。弾丸のような高速な物体は 1 ステップで壁を丸ごと通り抜けてしまい、どちらのサンプルでも重なりが検出されません。これが**トンネリング(すり抜け)**です。
対策は Collision Detection を次のいずれかに変えることです。
- Continuous — 静的コライダーに対してスイープテストを行い、レベルジオメトリのすり抜けを防ぐ。
- Continuous Dynamic — 同じく Continuous な動的ボディに対してもスイープする。
- Continuous Speculative — 推測接触(speculative contacts)に基づく方式。前二者より安価で回転にも対応する。近年の推奨既定値。
連続検出は高価なので、本当に必要な少数のオブジェクトにだけ有効にします。もう一つの方法は、そもそもリジッドボディを使わず、毎フレーム前フレーム位置から現在位置へ Physics.Raycast を飛ばすやり方です。実際、多くの弾丸はこれで実装されています。
Collider と Trigger
衝突コールバックが発火する条件は、両方のオブジェクトが Collider を持ち、かつ少なくとも一方が非キネマティックな Rigidbody を持つことです。Rigidbody のないコライダー同士では何も起こりません。「コールバックが呼ばれない」原因として、これが群を抜いて多いです。
Is Trigger を有効にすると物理的な反発がなくなり、重なりの報告だけが残ります。
| 実体衝突 | トリガー |
|---|---|
OnCollisionEnter | OnTriggerEnter |
OnCollisionStay | OnTriggerStay |
OnCollisionExit | OnTriggerExit |
OnCollisionEnter(Collision other) は接触点・法線・力積を持ちますが、OnTriggerEnter(Collider other) は相手のコライダーしか渡しません。衝撃音を選ぶなど「どれくらい強くぶつかったか」が必要なときは、前者の collision.impulse を使います。
コライダーの形状
安い順に Sphere < Capsule < Box < 凸 Mesh < 凹 Mesh(静的のみ)。
Mesh Collider は既定で凹形状であり、動的衝突には参加できません。Convex を有効にすればリジッドボディを持てますが、頂点数は 255 に制限されます。実務上の原則は、可能な限りプリミティブで近似し、Mesh Collider は静的なレベルジオメトリに限ることです。キャラクターにはカプセル 1 つで十分で、指ごとにコライダーを付ける必要はありません。
レイヤー衝突マトリクス
Project Settings → Physics のマトリクスは、どのレイヤーの組み合わせを検査するかを決めます。これはもっとも安価な物理最適化です。弾丸レイヤーと収集物レイヤーのチェックを外せば、エンジンはブロードフェーズの時点でその組み合わせを考慮しなくなります。
レイヤーの多いプロジェクトでは、全チェック状態は O(n²) の組み合わせをすべて検査することを意味します。この表に 10 分かけるほうが、個々のスクリプトを最適化するより効くことがよくあります。
実践上のメモ
OnCollisionEnterの中で重い処理をしない。 コールバックは物理ステップの内側で走ります。大量の Instantiate やDestroyはそのステップをそのまま引き延ばします。イベントを記録してUpdateか次フレームで処理しましょう。Physics.autoSimulationはオフにできる。 ターン制や決定論的なゲームプレイでは、Physics.Simulate(step)を自分で呼ぶことでシミュレーションを完全に再現可能にできます。Rigidbody.Sleep。 静止したボディはスリープしてソルバから外れます。押しても動かない物体があれば、たいていは眠っているだけです。WakeUp()するか、十分な大きさの力を加えてください。- スケールは罠。 非一様スケール(x, y, z が等しくない)は Sphere / Capsule コライダーの挙動を予測不能にします。Unity がどれか一つの成分で近似するためです。非一様スケールのキャラクターで物理がおかしいときは、まずここを疑ってください。
物理まわりの「原因不明」のバグは、追い詰めるとたいてい四つのどれかです。更新フックの間違い(Update と FixedUpdate)、検出モードの不足(離散と連続)、コールバック条件の未達(Rigidbody がない)、あるいは汚れたスケール。