【Unity2D】壁に反射後の弾道予測線を表示する方法

Unity2Dで壁に反射後の弾道予測線を表示する方法を備忘録として残します。
完成すると下記のような動きとなります。

実現したい内容

今回実現したい内容は下記の通りです。

  • マウスクリック後ドラッグした距離で、弾を打ち出す強さを変える
  • 弾を打ち出す強さに応じて、弾道予測線の長さを変更する
  • 弾道予測線は壁にぶつかると反射後の軌道を表示する

ソースコード

下記が壁に反射後の弾道予測線を表示するソースコードです。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Cysharp.Threading.Tasks;
using System;

public class PlayerShooter : MonoBehaviour
{
    [Header("手球の速度")]
    public float speed;
    /// <summary>
    /// メインカメラ座標
    /// </summary>
    private Camera mainCamera = null;
    /// <summary>
    /// ドラッグ開始点
    /// </summary>
    private Vector2 dragStart = Vector2.zero;
    private Rigidbody2D rigidBody;
    /// <summary>
    /// 発射方向の力
    /// </summary>
    private Vector2 currentForce = Vector2.zero;
    /// <summary>
    /// 最大付与量
    /// </summary>
    [SerializeField]
    private float MaxMagnitude = 5f;
    /// <summary>
    /// 予測線を描画する
    /// </summary>
    [SerializeField]
    private LineRenderer predictionLineRenderer;
    /// <summary>
    /// Wallレイヤーでマスク
    /// </summary>
    [SerializeField]
    private LayerMask wallLayer;

    /// <summary>
    /// PlayerBallへの紐付け
    /// </summary>
    [SerializeField]
    private PlayerBall playerBallPrefab;
    /// <summary>
    /// 現在のプレイヤーの位置を格納する
    /// </summary>
    Transform thisTransform;

    /// <summary>
    /// 起動時に呼び出される関数
    /// </summary>
    private void Awake()
    {
        rigidBody = GetComponent<Rigidbody2D>();
        this.mainCamera = Camera.main;
        thisTransform = this.transform;
    }

    /// <summary>
    /// マウスをワールド座標に変換して取得
    /// </summary>
    /// <returns></returns>
    private Vector2 GetMousePosition()
    {
        Vector2 position = Input.mousePosition;
        position = this.mainCamera.ScreenToWorldPoint(position);
        return position;
    }

    /// <summary>
    /// ドラッグ開始イベントハンドラ
    /// </summary>
    public void OnMouseDown()
    {
        predictionLineRenderer.enabled = true;
        this.dragStart = this.GetMousePosition();
        this.predictionLineRenderer.SetPosition(0, this.rigidBody.position);
        this.predictionLineRenderer.SetPosition(1, this.rigidBody.position);
    }
    /// <summary>
    /// ドラッグ中イベントハンドラ
    /// </summary>
    public void OnMouseDrag()
    {
        Vector2 position = this.GetMousePosition();
        this.currentForce = this.dragStart - position;
        if (this.currentForce.magnitude > MaxMagnitude)
        {
            this.currentForce *= MaxMagnitude / this.currentForce.magnitude;

        }
        this.predictionLineRenderer.SetPosition(0, this.rigidBody.position);
        this.predictionLineRenderer.SetPosition(1, this.rigidBody.position + this.currentForce);
        //弾道予測線を描画
        DrawBulletLine();
    }
    /// <summary>
    /// 弾道予測線の描画
    /// </summary>
    private void DrawBulletLine()
    {
        Vector2 bulletLine = this.currentForce + (Vector2)predictionLineRenderer.GetPosition(0);
        int hitCount = 0;
        bool hitCountFlug = true;
        while (hitCountFlug)
        {
            Vector2 startPos = predictionLineRenderer.GetPosition(hitCount);
            Vector2 endPos = predictionLineRenderer.GetPosition(hitCount + 1);
            RaycastHit2D[] hits = Physics2D.LinecastAll(startPos, endPos, wallLayer);

            if (hits.Length > hitCount)
            {
                if (hits[hitCount].collider != null && hits[hitCount].collider.CompareTag("Wall"))
                {
                    Vector2 reflectDir = Vector2.Reflect(bulletLine - hits[hitCount].point, hits[hitCount].normal);
                    Vector2 reflectPos = hits[hitCount].point + reflectDir;
                    predictionLineRenderer.positionCount = hitCount + 3;

                    predictionLineRenderer.SetPosition(hitCount + 1, hits[hitCount].point);
                    predictionLineRenderer.SetPosition(hitCount + 2, reflectPos);
                    bulletLine = reflectPos;
                    hitCount++;
                }
            }
            else
            {
                predictionLineRenderer.positionCount = hitCount + 2;
                hitCountFlug = false;
            }
        }
    }

    /// <summary>
    /// ドラッグ終了イベントハンドラ
    /// </summary>
    public void OnMouseUp()
    {

        this.predictionLineRenderer.enabled = false;
        if (boolBallShoot == true)
        {
            return;
        }
        // ゲームステータスがショットOKの場合球を打ち出す
        if (questManager.LunchBall())
        {
            Vector2 ballForce = this.currentForce;
            ShootAsync(ballForce).Forget();
        }
    }
}

Inspectorはこんな感じです。

一つずつ説明していきます。

    /// <summary>
    /// マウスをワールド座標に変換して取得
    /// </summary>
    /// <returns></returns>
    private Vector2 GetMousePosition()
    {
        Vector2 position = Input.mousePosition;
        //ワールド座標に変換
        position = this.mainCamera.ScreenToWorldPoint(position);
        return position;
    }

    /// <summary>
    /// ドラッグ開始イベントハンドラ
    /// </summary>
    public void OnMouseDown()
    {
        //描画線の予測を有効にする
        predictionLineRenderer.enabled = true;
        //ドラッグの開始位置をワールド座標で取得する
        this.dragStart = this.GetMousePosition();
        //直線の2点の頂点の座標を設定する
        this.predictionLineRenderer.SetPosition(0, this.rigidBody.position);
        this.predictionLineRenderer.SetPosition(1, this.rigidBody.position);
    }

ドラッグが開始された時に呼び出されるイベントハンドラです。
下記の部分で表示する直線の開始点と終了点の座標を入れています。開始時なのでどちらも同じ座標となっています。

this.predictionLineRenderer.SetPosition(0, this.rigidBody.position);
this.predictionLineRenderer.SetPosition(1, this.rigidBody.position);
    /// <summary>
    /// ドラッグ中イベントハンドラ
    /// </summary>
    public void OnMouseDrag()
    {
        //ドラッグ中のマウスの位置をワールド座標で取得する。
        Vector2 position = this.GetMousePosition();
        // ドラッグ開始点からの距離を取得する
        this.currentForce = this.dragStart - position;
        // MaxMagnitudeに直線の長さの制限を指定しておきそれを超える場合は、最大値となるようにします。
        if (this.currentForce.magnitude > MaxMagnitude)
        {
            this.currentForce *= MaxMagnitude / this.currentForce.magnitude;
        }
        this.predictionLineRenderer.SetPosition(0, this.rigidBody.position);
        this.predictionLineRenderer.SetPosition(1, this.rigidBody.position + this.currentForce);
        //弾道予測線を描画
        DrawBulletLine();
    }

ドラッグ中に呼び出されるイベントハンドラです。
下記の部分でドラッグした距離が、設定した最大値を超える場合は最大値となるようにしています。

        // MaxMagnitudeに直線の長さの制限を指定しておきそれを超える場合は、最大値となるようにします。
        if (this.currentForce.magnitude > MaxMagnitude)
        {
            this.currentForce *= MaxMagnitude / this.currentForce.magnitude;
        }

例えば、MaxMagnitude10であり、this.currentForce.magnitudeが20の場合、計算は次のようになります。
this.currentForce *= 10 / 20;
ベクトルの大きさ(magnitude)は、次のように計算されます。
magnitude = √(x^2 + y^2)
このとき、ベクトルに0.5をかけると、新しいベクトルの成分は次のようになります。
(x’, y’) = (0.5x, 0.5y)
新しいベクトルの大きさ(magnitude’)を計算すると
magnitude’ = √((0.5x)^2 + (0.5y)^2)
      = √(0.25x^2 + 0.25y^2)
      = 0.5 * √(x^2 + y^2)
      = 0.5 * magnitude
ベクトル自体の向きは変わらず、元のベクトルと同じ方向を向いたままです。0.5倍されたベクトルは、元のベクトルと同じ方向を示し、長さは元のベクトルの長さの0.5倍になります。

また、DrawBulletLine();を呼び出して弾道予測線を描画します。

    /// <summary>
    /// 弾道予測線の描画
    /// </summary>
    private void DrawBulletLine()
    {
        //ドラッグした距離と始点を指定し、暫定の弾道予測線を生成
        Vector2 bulletLine = this.currentForce + (Vector2)predictionLineRenderer.GetPosition(0);
        //2回目までの反射予測線を描く。
        for (int loopCount = 0; loopCount <= 1; loopCount++)
        {
            //弾道予測線の始点と終点を設定 2回目以降は壁の衝突点と終点を設定
            Vector2 startPos = predictionLineRenderer.GetPosition(loopCount);
            Vector2 endPos = predictionLineRenderer.GetPosition(loopCount + 1);
            //始点から終点までの間にある全ての物体との衝突を判定。ただし、wallLayerで指定されたタグを持つ物体のみが対象
            RaycastHit2D[] hits = Physics2D.LinecastAll(startPos, endPos, wallLayer);
            //hitsから取り出す衝突位置の格納場所 初回の場合は0 以降のループでは1
            int hitsCount;
            //初回の衝突位置は0の要素に格納される。2回目以降は1の要素に格納される(始点が壁に接地しているため)
            if (loopCount == 0)
            {
                hitsCount = 0;
            }
            else
            {
                hitsCount = 1;
            }
            //Null参照を防ぐための判定
            if (hits.Length > hitsCount)
            {
                //壁に衝突しているかどうかの判定
                if (hits[hitsCount].collider != null && hits[hitsCount].collider.CompareTag("Wall"))
                {
                    //反射ベクトルを求める
                    Vector2 reflectDir = Vector2.Reflect(bulletLine - hits[hitsCount].point, hits[hitsCount].normal);
                    //壁の衝突位置から終点までのベクトルを求める
                    Vector2 reflectPos = hits[hitsCount].point + reflectDir;
                    //直線の頂点数を増やす
                    predictionLineRenderer.positionCount = loopCount + 3;
                    //始点から衝突位置までの直線
                    predictionLineRenderer.SetPosition(loopCount + 1, hits[hitsCount].point);
                    //衝突地点から終点までの直線
                    predictionLineRenderer.SetPosition(loopCount + 2, reflectPos);
                    //次のループで衝突地点から終点までの直線で同様の処理を行う
                    bulletLine = reflectPos;
                }
                else
                {
                    //衝突点がない場合、反射後の直線は描画せずに終了
                    predictionLineRenderer.positionCount = loopCount + 2;
                    break;
                }
            }
            else
            {
                //衝突点がない場合、反射後の直線は描画せずに終了
                predictionLineRenderer.positionCount = loopCount + 2;
                break;
            }
        }
    }

弾道予測線を描画するためのメソッドです。
現在の力と予測線の開始位置から予測線の終点を計算し、物体が壁に衝突するかどうかをチェックします。衝突がある場合は反射方向を計算し、予測線を更新します。衝突がない場合は予測線を終了します。
予測線を表示する最大の反射回数は2回とします。(反射回数が増えると描画がチラつくため)

上記のソースコードはイメージとしてこんな感じです。

1.始点と終点を決定し、その間にある衝突点を決定する

2.衝突点での反射ベクトルを求め、反射後の壁の衝突点から終点までのベクトルを求める

3.反射する場合、直線の頂点数を2(始点、終点)から3(始点、衝突点、終点)に増やす

4.始点から衝突点、衝突点から終点の直線を描画する。

5.衝突点から終点の直線で再び最初に戻り、衝突点の有無を判定する。
 ただし、始点での衝突点を含まないようにするためリストの2番目の値を使用する。

    /// <summary>
    /// ドラッグ終了イベントハンドラ
    /// </summary>
    public void OnMouseUp()
    {

        this.predictionLineRenderer.enabled = false;
        if (boolBallShoot == true)
        {
            return;
        }
        // ゲームステータスがショットOKの場合球を打ち出す
        if (questManager.LunchBall())
        {
            Vector2 ballForce = this.currentForce;
            ShootAsync(ballForce).Forget();
        }
    }

球を発射するためのメソッドです。ドラッグ終了時球を発射します。

    課題

    壁への衝突回数が3回以上の場合うまく動かない。回数制限なく一般化できなかったため今後修正していきたい。→(5/21追記 ループで3回以上を描画できたが重くなるためやらない)
    また、それぞれの関数の説明がしっかりできていないためこれも後ほど追記します。→(5/21追記 少し分かりやすくなったはず)

    コメント

    タイトルとURLをコピーしました