【Unity】地形に沿った着弾マーカーを表示する

概要
==============
何か物を飛ばすとき、着弾地点にマーカーを表示したい時がありませんか？
本記事は、飛ばす物体の軌跡の予測線と、着弾座標にマーカーを表示する実装例を紹介します。
凸凹した地形など、着弾地点の高さがあらかじめ分からない場合に重宝します。

![イメージGIF](https://i.gyazo.com/5b4b6579139656126cfc59215687ed30.gif)
弾の射出速度だけを調節して、地形に沿った着弾マーカーを表示する実装例

実装
============

## スクリプト
以下の3つのスクリプトを使用します。(長いので折り畳んでいます)
（初速度と開始座標が既に決まっているのであれば、DrawArc.csをちょっと変えるだけで十分です。）

<dl>
<dt>DrawArc.cs</dt>
<dd>・初速度と開始座標を元に放物線を表示<dd>
<dd>・コライダーと衝突した場所にマーカーを表示<dd>
</dl>

<details><summary>スクリプト</summary><div>

```C#:DrawArc.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DrawArc : MonoBehaviour
{
    /// <summary>
    /// 放物線の描画ON/OFF
    /// </summary>
    //[SerializeField]
    private bool drawArc = true;

    /// <summary>
    /// 放物線を構成する線分の数
    /// </summary>
    //[SerializeField]
    private int segmentCount = 60;

    /// <summary>
    /// 放物線を何秒分計算するか
    /// </summary>
    private float predictionTime = 6.0F;

    /// <summary>
    /// 放物線のMaterial
    /// </summary>
    [SerializeField, Tooltip("放物線のマテリアル")]
    private Material arcMaterial;

    /// <summary>
    /// 放物線の幅
    /// </summary>
    [SerializeField, Tooltip("放物線の幅")]
    private float arcWidth = 0.02F;

    /// <summary>
    /// 放物線を構成するLineRenderer
    /// </summary>
    private LineRenderer[] lineRenderers;

    /// <summary>
    /// 弾の初速度や生成座標を持つコンポーネント
    /// </summary>
    private ShootBullet shootBullet;

    /// <summary>
    /// 弾の初速度
    /// </summary>
    private Vector3 initialVelocity;

    /// <summary>
    /// 放物線の開始座標
    /// </summary>
    private Vector3 arcStartPosition;

    /// <summary>
    /// 着弾マーカーオブジェクトのPrefab
    /// </summary>
    [SerializeField, Tooltip("着弾地点に表示するマーカーのPrefab")]
    private GameObject pointerPrefab;

    /// <summary>
    /// 着弾点のマーカーのオブジェクト
    /// </summary>
    private GameObject pointerObject;


    void Start()
    {
        // 放物線のLineRendererオブジェクトを用意
        CreateLineRendererObjects();

        // マーカーのオブジェクトを用意
        pointerObject = Instantiate(pointerPrefab, Vector3.zero, Quaternion.identity);
        pointerObject.SetActive(false);
        
        // 弾の初速度や生成座標を持つコンポーネント
        shootBullet = gameObject.GetComponent<ShootBullet>();
    }

    void Update()
    {
        // 初速度と放物線の開始座標を更新
        initialVelocity = shootBullet.ShootVelocity;
        arcStartPosition = shootBullet.InstantiatePosition;

        if (drawArc)
        {
            // 放物線を表示
            float timeStep = predictionTime / segmentCount;
            bool draw = false;
            float hitTime = float.MaxValue;
            for (int i = 0; i < segmentCount; i++)
            {
                // 線の座標を更新
                float startTime = timeStep * i;
                float endTime = startTime + timeStep;
                SetLineRendererPosition(i, startTime, endTime, !draw);

                // 衝突判定
                if (!draw)
                {
                    hitTime = GetArcHitTime(startTime, endTime);
                    if (hitTime != float.MaxValue)
                    {
                        draw = true; // 衝突したらその先の放物線は表示しない
                    }
                }
            }

            // マーカーの表示
            if (hitTime != float.MaxValue)
            {
                Vector3 hitPosition = GetArcPositionAtTime(hitTime);
                ShowPointer(hitPosition);
            }
        }
        else
        {
            // 放物線とマーカーを表示しない
            for (int i = 0; i < lineRenderers.Length; i++)
            {
                lineRenderers[i].enabled = false;
            }
            pointerObject.SetActive(false);
        }
    }

    /// <summary>
    /// 指定時間に対するアーチの放物線上の座標を返す
    /// </summary>
    /// <param name="time">経過時間</param>
    /// <returns>座標</returns>
    private Vector3 GetArcPositionAtTime(float time)
    {
        return (arcStartPosition + ((initialVelocity * time) + (0.5f * time * time) * Physics.gravity));
    }

    /// <summary>
    /// LineRendererの座標を更新
    /// </summary>
    /// <param name="index"></param>
    /// <param name="startTime"></param>
    /// <param name="endTime"></param>
    private void SetLineRendererPosition(int index, float startTime, float endTime, bool draw = true)
    {
        lineRenderers[index].SetPosition(0, GetArcPositionAtTime(startTime));
        lineRenderers[index].SetPosition(1, GetArcPositionAtTime(endTime));
        lineRenderers[index].enabled = draw;
    }

    /// <summary>
    /// LineRendererオブジェクトを作成
    /// </summary>
    private void CreateLineRendererObjects()
    {
        // 親オブジェクトを作り、LineRendererを持つ子オブジェクトを作る
        GameObject arcObjectsParent = new GameObject("ArcObject");

        lineRenderers = new LineRenderer[segmentCount];
        for (int i = 0; i < segmentCount; i++)
        {
            GameObject newObject = new GameObject("LineRenderer_" + i);
            newObject.transform.SetParent(arcObjectsParent.transform);
            lineRenderers[i] = newObject.AddComponent<LineRenderer>();

            // 光源関連を使用しない
            lineRenderers[i].receiveShadows = false;
            lineRenderers[i].reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off;
            lineRenderers[i].lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
            lineRenderers[i].shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;

            // 線の幅とマテリアル
            lineRenderers[i].material = arcMaterial;
            lineRenderers[i].startWidth = arcWidth;
            lineRenderers[i].endWidth = arcWidth;
            lineRenderers[i].numCapVertices = 5;
            lineRenderers[i].enabled = false;
        }
    }    

    /// <summary>
    /// 指定座標にマーカーを表示
    /// </summary>
    /// <param name="position"></param>
    private void ShowPointer(Vector3 position)
    {
        pointerObject.transform.position = position;
        pointerObject.SetActive(true);
    }

    /// <summary>
    /// 2点間の線分で衝突判定し、衝突する時間を返す
    /// </summary>
    /// <returns>衝突した時間(してない場合はfloat.MaxValue)</returns>
    private float GetArcHitTime(float startTime, float endTime)
    {
        // Linecastする線分の始終点の座標
        Vector3 startPosition = GetArcPositionAtTime(startTime);
        Vector3 endPosition = GetArcPositionAtTime(endTime);

        // 衝突判定
        RaycastHit hitInfo;
        if (Physics.Linecast(startPosition, endPosition, out hitInfo))
        {
            // 衝突したColliderまでの距離から実際の衝突時間を算出
            float distance = Vector3.Distance(startPosition, endPosition);
            return startTime + (endTime - startTime) * (hitInfo.distance / distance);
        }
        return float.MaxValue;
    }
}

```

</div></details>


<dl>
<dt>ShootBullet.cs</dt>
<dd>・弾の初速度と初期座標を保持<dd>
<dd>・キー入力で弾を生成して射出<dd>
</dl>

<details><summary>スクリプト</summary><div>

```C#:ShootBullet.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ShootBullet : MonoBehaviour
{
    /// <summary>
    /// 弾のPrefab
    /// </summary>
    [SerializeField, Tooltip("弾のPrefab")]
    private GameObject bulletPrefab;

    /// <summary>
    /// 砲身のオブジェクト
    /// </summary>
    [SerializeField, Tooltip("砲身のオブジェクト")]
    private GameObject barrelObject;

    /// <summary>
    /// 弾を生成する位置情報
    /// </summary>
    private Vector3 instantiatePosition;
    /// <summary>
    /// 弾の生成座標(読み取り専用)
    /// </summary>
    public Vector3 InstantiatePosition
    {
        get { return instantiatePosition; }
    }

    /// <summary>
    /// 弾の速さ
    /// </summary>
    [SerializeField, Range(1.0F, 20.0F), Tooltip("弾の射出する速さ")]
    private float speed = 1.0F;

    /// <summary>
    /// 弾の初速度
    /// </summary>
    private Vector3 shootVelocity;
    /// <summary>
    /// 弾の初速度(読み取り専用)
    /// </summary>
    public Vector3 ShootVelocity
    {
        get { return shootVelocity; }
    }
    
    void Update ()
    {
        // 弾の初速度を更新
        shootVelocity = barrelObject.transform.up * speed;

        // 弾の生成座標を更新
        instantiatePosition = barrelObject.transform.position; 

        // 発射
        if (Input.GetKeyDown(KeyCode.Space))
        {
            // 弾を生成して飛ばす
            GameObject obj = Instantiate(bulletPrefab, instantiatePosition, Quaternion.identity);
            Rigidbody rid = obj.GetComponent<Rigidbody>();
            rid.AddForce(shootVelocity * rid.mass, ForceMode.Impulse);

            // 5秒後に消える
            Destroy(obj, 5.0F);
        }
    }
}

```

</div></details>

<dl>
<dt>AngleController.cs</dt>
<dd>・砲台の角度をキー入力で変更する<dd>
</dl>

<details><summary>スクリプト</summary><div>

```C#:AngleController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AngleController : MonoBehaviour
{
    /// <summary>
    /// 感度
    /// </summary>
    [SerializeField, Range(0.01F, 5.0F), Tooltip("感度")]
    private float sensitivity = 1.0F;
    
    /// <summary>
    /// 砲身のオブジェクト
    /// </summary>
    [SerializeField, Tooltip("砲身のオブジェクト")]
    private GameObject barrelObject;
    
	void Update ()
    {
        if (Input.GetKey(KeyCode.LeftArrow))
        {
            this.transform.Rotate(new Vector3(0F, -1.0F * sensitivity, 0F));
        }
        if (Input.GetKey(KeyCode.RightArrow))
        {
            this.transform.Rotate(new Vector3(0F, 1.0F * sensitivity, 0F));
        }

        if (Input.GetKey(KeyCode.UpArrow))
        {
            barrelObject.transform.Rotate(new Vector3(-1.0F * sensitivity, 0F, 0F));
        }
        if (Input.GetKey(KeyCode.DownArrow))
        {
            barrelObject.transform.Rotate(new Vector3(1.0F * sensitivity, 0F, 0F));
        }
        
    }
}


```

</div></details>

## スクリプト以外

- 砲台
砲台っぽいオブジェクトを作り、前述の3つのスクリプトをアタッチします。（下図参照）
![メモ描き.png](https://qiita-image-store.s3.amazonaws.com/0/232140/e1cdaa3f-4077-9712-369e-3c2a35e7fd65.png)


- 弾
RigidBodyを付けたSphereなどをPrefab化し、ShootBulletスクリプトのBulletPrefabにアタッチします。
レイヤーは『Ignore Raycast』に設定しておきます。
![弾のレイヤー.png](https://qiita-image-store.s3.amazonaws.com/0/232140/e63f70f8-579a-3315-3b5f-06fe23a00716.png)




- マーカー
着弾点を示すマーカーをPrefab化し、DrawArcスクリプトのPointerPrefabにアタッチします。
今回は下図のような、LineRendererで矢印を描いたものを使用します。
![DrawArc_004.png](https://qiita-image-store.s3.amazonaws.com/0/232140/b52460d7-efb1-9ca1-8d5b-d9b4174de655.png)



作業環境
=================
- Unity2017.3



参考
==============
- [Steam VR Plugin](https://assetstore.unity.com/packages/templates/systems/steamvr-plugin-32647)
- [Physics.Linecast](https://docs.unity3d.com/ja/2017.3/ScriptReference/Physics.Linecast.html)