SuiTechLog

Unity,Arduino,RaspberryPiなど、モノづくり系を気ままに書き残すブログ。

Unity  弾をとばして、跳弾とかさせたい


※18/04/07:一部、デバッグ用レイ表示にミスがありましたので修正させていただきました。

質問

「弾をとばして、あたった面の角度によって跳弾させる処理とかできないか」という質問があったので考えてみました。ほかによさげな方法があれば教えていただければ幸いです。

 単純化のために直進する弾という前提で話を進めます。

 

・コライダー付きの弾を飛ばす作戦

 まず、コライダーを持たせたゲームオブジェクトを単純にまっすぐ飛ばすという考え方があります。これをすると、敵のコライダーにあたれば勝手に反射してくれるのである意味楽です。が、問題になるのは弾の速度が速すぎる場合のすり抜けです。

Unityの物理演算はフレームレートに関わらず

Edit=>Project Settings=>Time

のFixed Time Stepsによって何秒毎に行うか設定されています。(デフォルトは0.02秒毎)

 

docs.unity3d.com

 

 つまり、このタイムステップ毎に、あたっているかの判定を行うのですが・・・たとえば弾の速度が速すぎる場合、敵の当たり判定を飛び越えてしまったりすることがあり、いわゆるすり抜け現象が発生します。

 f:id:sui332015:20180408014042j:plain

弾の速度が遅い場合

 

f:id:sui332015:20180408013648j:plain

弾の速度が速い場合(すりぬけ)

 

 タイムステップは小さくすることができますが、小さくしすぎると物理演算・コライダの数が多くなった時に重くなりすぎて処理落ちが発生したり、デバイスの違いによって、開発PCで考えていた挙動とは大きく変わってしまうことになります。

 

一応、この作戦、遅い球なら問題はなさそうですが・・・。

 

 余談ですが、Fixed Update()メソッドもこの間隔で呼ばれます。ので、物理演算をつかって行う処理はなるべくFixedUpdateの中で書くのが良いと思います。

 

参考

docs.unity3d.com

・レイを使う作戦

 UnityにはRay(レイ)というその名の通り光線のようなものがありそれを使うことで、そのレイがあたったコリジョンの情報を取得することができます。

 

処理内容は以下。

public class Shooter_Ray : MonoBehaviour {

    //発射元オブジェクト
    public GameObject Emitter_object;

    //判定処理をしたくないレイヤーを無視するためのレイヤーマスク
    public LayerMask mask=-1;

	// Update is called once per frame
	void FixedUpdate() {

        //発射位置と方向
        Vector3 emitpos = Emitter_object.transform.position;
        Vector3 emitdir = Emitter_object.transform.forward;

        //新しいレイを作成する(発射位置と方向をあらわす情報)
        Ray ray = new Ray(emitpos, emitdir);

        //あたったものの情報が入るRaycastHitを準備
        RaycastHit hit;

        //物理コリジョン判定をさせるためにレイをレイキャストしてあたったかどうか返す
        bool isHit = Physics.Raycast(ray, out hit, 100.0f, mask);

        //あたっているなら
        if (isHit)
        {
            //あたったゲームオブジェクトを取得
            GameObject hitobject = hit.collider.gameObject;

            //マテリアルカラー変更
            //hitobject.GetComponent().material.color = Color.green;

            //あたったオブジェクト削除
            //Destroy(hitobject);

            //レイがあたった座標
            Vector3 position = hit.point;

            //レイがあたった当たり判定オブジェクトの面の法線
            Vector3 normal = hit.normal;

            //レイの方向ベクトル
            Vector3 direction = ray.direction;

            //反射ベクトル(反射方向を示すベクトル)
            Vector3 reflect_direction = 2 * normal * Vector3.Dot(normal, -direction) + direction;

            //レイと反射ベクトルのなす角度(ラジアン)
            float rad = Mathf.Acos(Vector3.Dot(-ray.direction, reflect_direction) / ray.direction.magnitude * reflect_direction.magnitude );

            //ラジアンを度に変換
            float deg = rad * Mathf.Rad2Deg;

            //反射角度が90度以上だったら・・・
            //if(deg>90)
            //{  
            //}

            ////////////// デバッグ用 ////////////////

            //(デバッグ用)角度を表示
            Debug.Log(deg);

            //(デバッグ用)新しい反射用レイを作成する
            Ray reflect_ray = new Ray(position, reflect_direction);

            //(デバッグ用)レイを画面に表示する
            Debug.DrawLine(reflect_ray.origin, reflect_ray.origin+reflect_ray.direction * 100, Color.blue, 0);

        }

        ////////////// デバッグ用 ////////////////

        //(デバッグ用)発射レイを表示
        Debug.DrawLine(ray.origin, ray.origin+ray.direction * 100, Color.red, 0);

    }
}

 

 問題としてレイは1fの中ですべての処理を終えてしまうので、このままだと目に見えないほどの超高速弾ということになります。今回は発射点からレイを飛ばしましたが、たとえば、弾の位置から毎フレーム進む速度にあわせて次のフレームに移動するであろう位置まで毎フレーム前方にレイを飛ばす方法があると思います。

レイを飛ばす距離は、

bool isHit = Physics.Raycast(ray, out hit, 1000.0f, mask);

の1000fのところで調整できますので応用してみてください。

 

 ちなみにこのレイ、発射点が、コライダーのうちがわだったりするとちゃんとそのコライダーは無視してくれます。

 

 反射ベクトルと入射ベクトルとの角度の二つがあれば、反射方向にエフェクトを出す、オブジェクトを発射する、死亡判定、移動などなどさせるなど色々できるのかなと思います。

そのあたりは単純なif文なので割愛。

簡単な解説。

 

Ray ray = new Ray(Emitter_object.transform.position, Emitter_object.transform.forward);

発射点の座標と発射方向をもったレイを新規作成。このレイ単体では特に意味を成しません。

 

bool isHit = Physics.Raycast(ray, out hit, 1000.0f, mask);

 続いて、物理演算Raycastメソッドと、作成したレイをつかってコリジョン判定処理を行います。引数は、レイ、ヒット情報(後述)、レイを飛ばす距離、レイヤーマスクで、戻り値はなにかに当たったか当たっていないか。になります。

 ヒット情報はoutとついていますがこれは参照渡しといって、通常の引数と違って引数に渡した値をメソッドの中で変更すると呼び出し元にも反映されるものになります。つまりこのメソッドが終わった後、hitにはあたったものの情報が書き込まれていることになります。

 

Vector3 reflect_direction = 2 * normal * Vector3.Dot(normal, -direction) + direction;

 内積をつかって、入射ベクトルと、法線を中心に線対象なベクトルを求めています。

f:id:sui332015:20180408010527j:plain

 ベクトルなので原点はないのですが、簡略化のためにOが原点だとイメージしてください。わかっているのは

ベクトルOP=入射ベクトル(の-反転)

n=法線ベクトル(大きさは1)のふたつです。

ここではP'を求められればよいので、

それはベクトルOBとBP’(=PO)を足したものになります。

OBはnの軸にOPベクトルを射影した長さ(内積スカラー値)に、ベクトルn(nは長さ1のベクトル)をかけてベクトル値にして、それを二倍したものになります。

つまり

2 * normal * Vector3.Dot(normal, -direction)

そして、BP'はPOそのものなのでそれを足して

2 * normal * Vector3.Dot(normal, -direction) + direction;

 

 

あとは入射ベクトルと反射ベクトルの角度を「度」で出せれば条件をつけたりなど色々都合がよさそうなので

radをラジアンの角度

(a,b)をaとbの内積とすると内積の定義から

(a,b)=a*b*cos(rad)

radを求めるように変形して

rad = acos((a,b)/a*b)

となるので以下の式が求められます

float rad = Mathf.Acos(Vector3.Dot(-ray.direction, reflect_direction) / ray.direction.magnitude * reflect_direction.magnitude );

 

あとは、ラジアンを度に直して

float deg = rad * Mathf.Rad2Deg;

判定することができるかと思います。

 

参考

Ray: 単体ではほぼなにもできないマン。単純に位置と方向を持っているだけですね。

docs.unity3d.com

 

RayCast:Rayをつかって接触判定させることができる。実はレイを使わずに位置と方向を直接打ち込むこともできます。

docs.unity3d.com

 

DrawLine:デバッグに使います。Lineは長いですが短い直線を描くDrawRayというのもあります。なお、DrawLineでも距離を短くすれば同じことができるので・・・。

docs.unity3d.com