Unityでルーレットの針を物理で揺らす|Rigidbody2DとHingeJoint2Dを使ってみよう
ルーレットが回ると、針(ニードル)が「カタカタ」と弾かれて揺れます。
あの動きは、針の角度を毎フレーム計算しているわけではありません。
Unityの物理エンジンに任せている
だけです。
今回は、その仕組みを2D物理で作ってみます。
今回作るのは、
ルーレットを回すと、ピンに弾かれて針が揺れる
という処理です。
使用環境はUnity 6(2D / URP)で、入力にはInput Systemを使います。
[画像: 完成イメージ(ルーレットと針が揺れているGIFまたはスクリーンショット)]
今回の完成イメージ
ルーレットの外側にはピンが並んでいて、その上に針が下がっています。
針の根本(軸)
●
│
│ 針
▼
○ ○ ○ ○ ← ピン(ルーレットの縁)
ルーレットを回すと、ピンが針に当たります。
クリック
↓
ルーレットが回る
↓
ピンが針に当たる
↓
針が弾かれて揺れる
↓
重力で下に戻る
という動きにします。
まずはルーレットを回してみる
最初は、クリックで回って、だんだん止まるルーレットを作ります。
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を掛ける
↓
少しずつ遅くなる
見た目だけなら、これで完成です。
でも針は揺れない
このルーレットに針を置いても、針は揺れません。
ルーレットが回っているのに、針はただの飾りのままです。
なぜでしょう。
物理で動かすには何が必要?
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.Rotate
↓
向きだけ書き換わる
↓
物理エンジンは「ピンがぶつかってきた」と気づけない
物理エンジンから見ると、ピンが何も言わずにワープしているようなものです。
そこで、ルーレットに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
↓
ピンに当たる部分(針の先端側にずらしておく)
Rigidbody2D
↓
Body Typeは Dynamic
[画像: 針のInspector(Sprite Renderer、Circle Collider 2D、Rigidbody2D)]
位置を固定すれば軸になる?
ここで、こう考えたくなります。
針が飛んでいかないように、位置を固定しよう
そこで、Rigidbody2D の Constraints で、
Freeze Position X
Freeze Position Y
にチェックを入れてみます。
でも先端を中心にくるくる回る
実際に動かすと、針は先端を中心にくるくる回ってしまいます。
なぜでしょう。
Rigidbody2Dは、
重心(コライダーの位置)を中心に回る
からです。
今回のコライダーは針の先端側にあります。
針の根本
●
│
│
▼
[○] ← コライダー(ここが重心)
位置を固定すると、この重心が動かない点になります。
位置を固定
↓
重心(先端側)が動かない
↓
先端を中心に回る
つまり、
位置の固定では、根本を軸にすることはできません。
HingeJoint2Dで軸を決める
そこで使うのが Hinge Joint 2D です。
ヒンジとは蝶番のことです。ドアが蝶番を軸にして開閉するように、
ここを軸にして回ってね
と決める部品です。
Freeze Position のチェックは外して、針に Hinge Joint 2D を追加します。
| 設定 | 値 | 意味 |
|---|---|---|
| Connected Rigid Body | 空 | ワールドに固定される |
| Anchor | (0, 0) | 針のTransformの位置が軸になる |
| Auto Configure Connected Anchor | オン | 軸の位置を自動で合わせる |
[画像: HingeJoint2DのInspector]
ポイントは、
針のTransformの位置を、針の根本に置いておく
ことです。
Anchor が (0, 0) なら、そのTransformの位置がそのまま軸になります。
これで、
根本を軸にして
↓
重力で下に戻りながら
↓
ピンに弾かれて揺れる
動きになります。
揺れ方を調整する
動くようになったら、数値を変えて好みの動きに近づけます。
| 項目 | 場所 | 効果 |
|---|---|---|
| Angular Damping | Rigidbody2D | 大きいほど、揺れがすぐ収まる |
| Mass | Rigidbody2D | 大きいほど、弾かれにくくなる |
| Use Limits | HingeJoint2D | 振れる角度の範囲を制限できる |
| Lower Angle / Upper Angle | HingeJoint2D | 例: -40 〜 40 度 |
目安は次のとおりです。
軽く回りすぎる
↓
Angular Dampingを3〜5に上げる
Use Limitsで角度を制限する
動きが鈍い
↓
Angular Dampingを1〜2に下げる
正解の数値はありません。
再生しながら少しずつ値を変えて、ちょうどよい動きを探してみてください。
速く回すと軸がぶれる
ルーレットを激しく回すと、針の軸がぶれることがあります。
ピンの動きが速すぎて、物理の計算が追いつかなくなるためです。
そんなときは、物理の精度を上げます。
| 設定 | 場所 | 例 |
|---|---|---|
| Velocity Iterations | Physics 2D | 8 → 16 |
| Position Iterations | Physics 2D | 3 → 10 |
| Fixed Timestep | Time | 0.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のまま、物理エンジン経由で回す
ことで、針に衝突を伝えています。
そして針は、位置を固定するのではなく、
HingeJoint2Dで軸を決める
ことで、根本を中心に揺れるようになります。
一言で整理すると、
ルーレット
↓
Kinematic + angularVelocity
↓
物理の世界で回す
針
↓
Dynamic + HingeJoint2D
↓
軸を決めて、あとは物理に任せる
です。
この考え方は、ルーレットだけでなく、ドア、振り子、シーソーなど、
軸を中心に動くもの
にそのまま使えます。
関連記事:Unityでキャラクターが動く床に乗ると一緒に動く仕組みガイドでは、Kinematic のRigidbody2Dで床を動かす方法と、FixedJoint2Dによる物理的な接続を解説しています。
関連記事:Unity 6 ゲーム制作 補足解説シリーズまとめ ― 6つのサンプルゲームで学ぶ全10本(PC版)では、ルーレットを含むサンプルゲームの補足解説をまとめています。










ディスカッション
コメント一覧
まだ、コメントがありません