Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

概要

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

イメージGIF
弾の射出速度だけを調節して、地形に沿った着弾マーカーを表示する実装例

実装

スクリプト

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

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

スクリプト
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;
    }
}

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

スクリプト
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);
        }
    }
}

AngleController.cs
・砲台の角度をキー入力で変更する

スクリプト
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));
        }

    }
}


スクリプト以外

  • 砲台
    砲台っぽいオブジェクトを作り、前述の3つのスクリプトをアタッチします。(下図参照)
    メモ描き.png


  • RigidBodyを付けたSphereなどをPrefab化し、ShootBulletスクリプトのBulletPrefabにアタッチします。
    レイヤーは『Ignore Raycast』に設定しておきます。
    弾のレイヤー.png

  • マーカー
    着弾点を示すマーカーをPrefab化し、DrawArcスクリプトのPointerPrefabにアタッチします。
    今回は下図のような、LineRendererで矢印を描いたものを使用します。
    DrawArc_004.png

作業環境

  • Unity2017.3

参考

_udonba
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away