おもちゃラボ

Unityで遊びを作ってます

【Unity】Physicsを使わずに平面でボールを反射させる

Unityで物理挙動に従って物体を動かしたい場合にはPhysicsを使うのが便利です。ただ、物理挙動に従わない動きや、Physicsが使えない場合には自分で物理挙動を計算する必要があります。

この記事では、平面とボールの衝突と反射を計算する方法を紹介します。「Unityの教科書」にも書いたように、物理挙動を実現するには

  1. 衝突判定
  2. 衝突応答

の2つを計算する必要があります。

ここで、衝突判定は平面とボールが当たったかどうか、衝突応答はボールの跳ね返る向きの計算になりますね。まずは衝突判定の部分から考えていきましょう。

f:id:nn_hokuson:20180330201116p:plain:w500

平面とボールの当たり判定

平面とボールの当たり判定はそんなに複雑ではありません。ボールの中心から平面に向けておろした垂線hの長さが、ボールの半径以上あれば衝突、半径未満なら衝突していないとみなします。

f:id:nn_hokuson:20180330194724p:plain:w300

垂線の長さhは、法線nの長さが1であることを利用して、dとnの内積を計算することで求められます。

{ \displaystyle
d \cdot n = |d||n|cos \theta = |d|cos \theta  = h
}

いきなり数学が出てきましたが、ゲーム作りにおいて数学は超大切です。ベクトルとか三角関数が苦手!って方は次の書籍がものすごく分かりやすいのでオススメです(高校参考書ですが。笑)

[asin:4098374021:detail]

これで垂線hの長さが求められたので、ボールの半径と比べて衝突判定ができそうですね。ただ、ここでは簡略化のため、無限遠の平面を考えています。ボールが平面からはみ出ていた場合の処理については省略しています。

ボールの反射方向の計算

平面とボールの当たり判定ができたので、第二の難関(というほどでもない)の衝突応答を作っていきましょう。

平面とボールの衝突応答は比較的簡単です。平面に当たったとき、ボールは平面に対して逆側に反射します。この方向を反射ベクトルと呼びます。この反射ベクトルを求めるには次の図のベクトルrを求めればよさそうですね。

f:id:nn_hokuson:20180330194954p:plain:w300

反射ベクトルrの求め方は少しトリッキィというか、コロンブスの卵というか、面白い方法で計算できます。衝突地点から移動ベクトルをそのまま伸ばして、ボールと平面の距離ぶん法線ベクトルを2回足してやると、なんと反射ベクトルと同じところにたどり着きますね。

f:id:nn_hokuson:20180330195312p:plain:w300

ベクトルでは経路が違っていても、スタート地点とゴール地点が同じなら、同じベクトルとみなすことが出来ます。つまり上の図で、遠回りしている緑の線(dir + 2*a)と、求めたい反射ベクトル(r)は同じベクトルになるのです。この分かりやすさは結構好きです(笑)

ここで、{ \displaystyle a=(dir \cdot n) * n} なので、まとめると反射ベクトルは次のように計算できます。

{ \displaystyle r = dir + 2 * a = dir + 2 * (dir \cdot n) * n}

実際に平面とボールで反射するスクリプトを作る

さて、反射ベクトルの計算方法もわかったので実際のプログラムを作っていきましょう。まずはシーンビューに平面とボールを配置してください。

f:id:nn_hokuson:20180330200124j:plain:w600

次にBallにアタッチするプログラムを作成して、次のスクリプトを入力してください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Ball : MonoBehaviour 
{
    public GameObject plane;
    Vector3 moveDir;    // 移動方向ベクトル
    float speed = 0.2f; // 移動速度

    void Start () 
    {
        moveDir = new Vector3(1, -2, 1).normalized * speed;
    }
		
    void Update () 
    {
        // 移動
        transform.position += moveDir;

        // ボールと平面の距離
        Vector3 d = transform.position - plane.transform.position;
        float h = Vector3.Dot(d, plane.transform.up);

        // 当たり判定
        if( h < transform.localScale.x){
            Collision();
        } 
    }

    void Collision()
    {
    	// 反射ベクトルを計算する
        Vector3 n = plane.transform.up;
        float   h = Mathf.Abs(Vector3.Dot(moveDir, n));
        Vector3 r = moveDir + 2 * n * h;
        moveDir = r;
    }
}

Updateメソッドの中で毎フレーム平面とボールの距離hを計算し、衝突しているかどうかをチェックしています。Collisionメソッドの中では、反射ベクトルを計算するため、ボールの移動ベクトル(moveDir)と平面の法線ベクトル(transform.up)を使って計算しています。

スクリプトが作成できたら、ヒエラルキーウインドウのSphereにアタッチしてください。また、インスペクタからBallスクリプトのPlane欄にPlaneをドラッグ&ドロップしてください。

f:id:nn_hokuson:20180330200515j:plain

作成したスクリプトをアタッチできたら、Unityを実行してみましょう。次図のようにちゃんと平面で跳ね返りましたね!

f:id:nn_hokuson:20180330200541g:plain

複数の平面で当たり判定をしたい場合には、全ての平面に対してボールと衝突判定を行う必要があります。つまり平面の数が増えればそれだけ衝突判定の回数も増えるわけです。

f:id:nn_hokuson:20180330200556g:plain

平面だけでなくボールの数も増えると、その組み合わせ数はすぐに爆発します。ボールN個と平面M個で単純な組合せはN*M個ですから・・・

このままではリアルタイムでの計算はできない、高速化するための様々なアルゴリズムが考えられています。有名なものにはオクトツリーを用いる方法があります。ここでは説明しませんが、次のサイトや参考書で説明されているので、ぜひ読んでみてください

その15 8分木空間分割を最適化する!