Unityでルーレットの針を物理で揺らす|Rigidbody2DとHingeJoint2Dを使ってみよう

広告

ルーレットが回ると、針(ニードル)が「カタカタ」と弾かれて揺れます。

あの動きは、針の角度を毎フレーム計算しているわけではありません。

Unityの物理エンジンに任せている

だけです。

今回は、その仕組みを2D物理で作ってみます。

今回作るのは、

ルーレットを回すと、ピンに弾かれて針が揺れる

という処理です。

使用環境はUnity 6(2D / URP)で、入力にはInput Systemを使います。

ルーレットが回転し、針が左右に揺れる完成イメージ

今回の完成イメージ

ルーレットの外側にはピンが並んでいて、その上に針が下がっています。

      針の根本(軸)
          ●
          │
          │ 針
          ▼
   ○   ○   ○   ○   ← ピン(ルーレットの縁)

ルーレットを回すと、ピンが針に当たります。

クリック
↓
ルーレットが回る
↓
ピンが針に当たる
↓
針が弾かれて揺れる
↓
重力で下に戻る

という動きにします。

まずはルーレットを回してみる

最初は、クリックで回って、だんだん止まるルーレットを作ります。

using UnityEngine;
using UnityEngine.InputSystem;

public class RouletteController : MonoBehaviour
{
    float rotSpeed = 0;

    void Start()
    {
        Application.targetFrameRate = 60;
    }

    void Update()
    {
        if (Mouse.current.leftButton.wasPressedThisFrame)
        {
            this.rotSpeed = 3;
        }

        transform.Rotate(0, 0, this.rotSpeed);
        this.rotSpeed *= 0.96f;
    }
}

やっていることは3つです。

クリック
↓
rotSpeedを3にする

毎フレーム
↓
rotSpeed度ずつ回す

rotSpeedに0.96を掛ける
↓
少しずつ遅くなる

見た目だけなら、これで完成です。

なお、0.96を毎フレーム掛ける減速は、フレームレートによって止まるまでの時間が変わります。今回は60fpsに固定しているので、このまま使います。

でも針は揺れない

このルーレットに針を置いても、針は揺れません。

ルーレットが回っているのに、針はただの飾りのままです。

なぜでしょう。

物理で動かすには何が必要?

Unityの物理エンジンに動きを任せるには、2つの部品が必要です。

Collider2D
↓
物の形(当たり判定)

Rigidbody2D
↓
物理エンジンの管理下に置く

たとえるなら、

Collider2D
↓
体の大きさ

Rigidbody2D
↓
物理の世界に参加するための参加証

です。

ここで大事なのは、

Collider2Dだけの物体は、動かない壁のように扱われる

ということです。

重力で落ちたり、ぶつかって動いたりさせたい物体には、Rigidbody2Dが必要です。

Body Typeは3種類ある

Rigidbody2Dには Body Type という設定があります。

Body Type動き
Dynamic重力や衝突で動く
Kinematicスクリプトで決めた動きだけをする。ぶつかっても押し返されない
Static動かない

今回の役割分担は次のとおりです。

ルーレット
↓
Kinematic
↓
自分で決めた通りに回り続ける

針
↓
Dynamic
↓
ぶつかられて揺れて、重力で戻る

でもtransform.Rotateでは安定して弾かれない

ルーレットのピンに、小さな Circle Collider 2D を付けたとします。

それでも、先ほどのスクリプトでは、針がうまく弾かれません。

原因は、

transform.Rotate(0, 0, this.rotSpeed);

です。

transform.Rotate は、向きを直接書き換えます。Transformの変更は、物理シミュレーションの直前に物理エンジンへ同期されます。ただし、これは回転速度を持った動きではなく、瞬間的な向きの書き換えです。

transform.Rotate
↓
向きだけが瞬間的に書き換わる
↓
物理的な速度を持たない
↓
衝突の反応が不安定になる

物理エンジンから見ると、ピンが瞬間移動を繰り返しているようなものです。そのため、針に伝わる衝突の反応が不安定になります。

そこで、ルーレットにRigidbody2Dを付けて、

Rigidbody2Dの回転速度で回す

ようにします。

ルーレットとピンの構成を決める

今回は、次のような構成にします。

Wheel
├ Rigidbody2D(Kinematic)
├ RouletteController
├ Pin(Circle Collider 2D)
├ Pin(Circle Collider 2D)
└ …

Needle
├ Rigidbody2D(Dynamic)
├ Circle Collider 2D
└ Hinge Joint 2D

ポイントは3つです。

各Pinには、別のRigidbody2Dを付けない
↓
子のCollider2Dは、親(Wheel)のRigidbody2Dに属する

Needleは、Wheelの子にしない
↓
ルーレットの回転に巻き込まれてしまうため

こうすると、WheelのRigidbody2Dが回るのに合わせて、すべてのピンが一緒に動きます。

ルーレットをKinematicにして回す

ルーレットに Rigidbody2D を付けて、Body Type を Kinematic にします。

スクリプトは次のようになります。

using UnityEngine;
using UnityEngine.InputSystem;

public class RouletteController : MonoBehaviour
{
    float rotSpeed = 0;
    Rigidbody2D rb;

    void Awake()
    {
        rb = GetComponent<Rigidbody2D>();

        if (rb == null)
        {
            rb = gameObject.AddComponent<Rigidbody2D>();
        }

        rb.bodyType = RigidbodyType2D.Kinematic;
        rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
    }

    void Start()
    {
        Application.targetFrameRate = 60;
    }

    void Update()
    {
        if (Mouse.current.leftButton.wasPressedThisFrame)
        {
            this.rotSpeed = 3;
        }

        this.rotSpeed *= 0.96f;
    }

    void FixedUpdate()
    {
        // 1フレーム(60fps)あたり rotSpeed 度 → 1秒あたりの角度に換算
        rb.angularVelocity = this.rotSpeed * 60f;
    }
}

コードを順番に追ってみよう

変わったところは3つです。

回し方を変えた

transform.Rotate(0, 0, this.rotSpeed);

をやめて、

rb.angularVelocity = this.rotSpeed * 60f;

にしました。

angularVelocity は、

1秒あたりに回る角度

です。

もともとの rotSpeed は「1フレームあたり何度回すか」でした。60fpsなら1秒に60フレームなので、60を掛けて1秒あたりの角度に直しています。

rotSpeed
↓
1フレームあたりの角度

rotSpeed × 60
↓
1秒あたりの角度

物理の処理はFixedUpdateに書く

回転を設定する処理は、Update ではなく FixedUpdate に書いています。

Update
↓
フレームごとに呼ばれる

FixedUpdate
↓
物理エンジンと同じ間隔で呼ばれる

物理に関わる動きは、物理エンジンと同じタイミングで決めるのが基本です。

なお、クリックの判定と rotSpeed の減速は、これまでどおり Update に残しています。

衝突判定をContinuousにした

rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;

ルーレットが速く回ると、ピンが一瞬で動いてしまい、針をすり抜けることがあります。

Continuous にすると、動いている途中の衝突も調べるので、すり抜けにくくなります。

針を用意する

次は針です。針には次のコンポーネントを付けます。

Sprite Renderer
↓
見た目

Circle Collider 2D
↓
ピンに当たる部分
Offsetで、根本(軸)より下の先端側にずらす
Is Triggerはオフ

Rigidbody2D
↓
Body Typeは Dynamic
Gravity Scaleは 1
Simulatedはオン
Freeze Rotation Zはオフ

ここで大事なのは、コライダーの位置を、根本より下にずらしておくことです。

コライダーの中心が根本(軸)と同じ位置にあると、重力で下に戻る回転力が生まれません。針が揺れたまま戻ってこないときは、Offsetを確認してください。

位置を固定すれば軸になる?

ここで、こう考えたくなります。

針が飛んでいかないように、位置を固定しよう

そこで、Rigidbody2D の Constraints で、

Freeze Position X
Freeze Position Y

にチェックを入れてみます。

でも先端を中心にくるくる回る

実際に動かすと、針は先端を中心にくるくる回ってしまいます。

なぜでしょう。

Rigidbody2Dは、

重心を中心に回る

からです。

今回のように、円形のコライダーが1つだけの場合、重心はコライダーの中心になります。

今回のコライダーは針の先端側にあります。

針の根本
  ●
  │
  │
  ▼
 [○] ← コライダー(ここが重心)

位置を固定すると、この重心が動かない点になります。

位置を固定
↓
重心(先端側)が動かない
↓
先端を中心に回る

つまり、

位置の固定では、根本を軸にすることはできません。

HingeJoint2Dで軸を決める

そこで使うのが Hinge Joint 2D です。

ヒンジとは蝶番のことです。ドアが蝶番を軸にして開閉するように、

ここを軸にして回ってね

と決める部品です。

Freeze Position のチェックは外して、針に Hinge Joint 2D を追加します。

設定値意味
Connected Rigid Body空ワールドに固定される
Anchor(0, 0)針のTransformの位置が軸になる
Auto Configure Connected Anchorオン軸の位置を自動で合わせる
HingeJoint2DのInspector。Connected Rigid Bodyは空、Anchorは(0, 0)、Auto Configure Connected Anchorはオン

ポイントは、

針のTransformの位置を、針の根本に置いておく

ことです。

Anchor が (0, 0) なら、そのTransformの位置がそのまま軸になります。

これで、

根本を軸にして
↓
重力で下に戻りながら
↓
ピンに弾かれて揺れる

動きになります。

揺れ方を調整する

動くようになったら、数値を変えて好みの動きに近づけます。

項目場所効果
Angular DampingRigidbody2D大きいほど、揺れがすぐ収まる
Use LimitsHingeJoint2D振れる角度の範囲を制限できる(基本はオフ。調整するときだけオン)
Lower Angle / Upper AngleHingeJoint2D例: -40 〜 40 度

目安は次のとおりです。

軽く回りすぎる
↓
Angular Dampingを3〜5に上げる
Use Limitsで角度を制限する

動きが鈍い
↓
Angular Dampingを1〜2に下げる

正解の数値はありません。

再生しながら少しずつ値を変えて、ちょうどよい動きを探してみてください。

速く回すと軸がぶれる

ルーレットを激しく回すと、針の軸がぶれることがあります。

ピンの動きが速すぎて、物理の計算が追いつかなくなるためです。

そんなときは、物理の精度を上げます。

設定場所例
Velocity IterationsPhysics 2D8 → 16
Position IterationsPhysics 2D3 → 10
Fixed TimestepTime0.02 → 0.01
Iterationsを増やす
↓
衝突や関節のズレの補正が丁寧になる

Fixed Timestepを小さくする
↓
物理の計算回数が増える
↓
すり抜けにくくなる

その分、処理は少し重くなります。設定を変えたあとは、実行して負荷を確認してください。

それでもぶれる場合は、

クリック時のrotSpeedを下げる
針のコライダー半径を少し大きくする

を試してみてください。

なお、Physics 2Dの設定項目の場所や名前は、Unityのバージョンによって変わることがあります。見つからない場合は、Project Settingsの検索欄で「Iterations」と入力すると探せます。

ここまでで学んだものを整理する

この小さなルーレットには、2D物理の基本がまとまっています。

Collider2D
↓
物の形(当たり判定)

Rigidbody2D
↓
物理エンジンの管理下に置く

Body Type: Kinematic
↓
決めた通りに動く側(ルーレット)

Body Type: Dynamic
↓
動かされる側(針)

FixedUpdate
↓
物理の処理を書く場所

angularVelocity
↓
1秒あたりの回転角度

HingeJoint2D
↓
回転の軸を決める

Angular Damping / Mass / Use Limits
↓
揺れ方の調整

まとめ

今回の重要な部分は、

void FixedUpdate()
{
    rb.angularVelocity = this.rotSpeed * 60f;
}

と、針に付けた

Hinge Joint 2D

です。

ルーレットは transform.Rotate ではなく、

KinematicのRigidbody2Dに回転速度を設定して回す

ことで、針に安定して衝突を伝えています。

そして針は、位置を固定するのではなく、

HingeJoint2Dで軸を決める

ことで、根本を中心に揺れるようになります。

一言で整理すると、

ルーレット
↓
Kinematic + angularVelocity
↓
物理の世界で回す

針
↓
Dynamic + HingeJoint2D
↓
軸を決めて、あとは物理に任せる

です。

この考え方は、ルーレットだけでなく、ドア、振り子、シーソーなど、

軸を中心に動くもの

にそのまま使えます。

関連記事:Unityでキャラクターが動く床に乗ると一緒に動く仕組みガイドでは、Kinematic のRigidbody2Dで床を動かす方法と、FixedJoint2Dによる物理的な接続を解説しています。

関連記事:Unity 6 ゲーム制作 補足解説シリーズまとめ ― 6つのサンプルゲームで学ぶ全10本(PC版)では、ルーレットを含むサンプルゲームの補足解説をまとめています。

訪問数 7 回, 今日の訪問数 7回

広告

Physics,Unity

Posted by hidepon