【Unity3D BezierCurve繪製曲線】動態添加路徑點繪製可調節曲線(三維空間)

近日,項目需求:前提:三維空間

1.動態添加(刪除)路徑點,通過兩個以上的路徑點來繪製曲線,刪除點之後不影響其他點繪製曲線;

2.每個路徑點都可以被拖拽發生位移,可以通過鎖定某個軸,使該軸不發生位移;

3.每個路徑點處有兩個可調節點(首尾只有一個調節點),可通過調節點來調節曲線切線(速度方向線)斜率,以達到平滑曲線;

4.使該曲線形成路徑,隱藏曲線,Player沿曲線完成自動尋路。

先看效果,再貼代碼。(使用貝塞爾曲線公式與LineRenderer繪製3D可調節曲線

代碼如下:兩個腳本。

1. DMDrawCurve.cs 掛載到任意對象即可

該腳本實現繪製曲線,動態添加(刪除)點,發生位移時更新繪製曲線,以及隱藏該曲線。

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

namespace DM.Editor.View
{
    [RequireComponent(typeof(LineRenderer))]
    public class DMDrawCurve : MonoBehaviour
    {
        public List<Transform> m_allPoints;
        private GameObject m_anchorPoint;
        private GameObject m_controlPoint;
        private GameObject m_pointParent;
        private LineRenderer m_lineRenderer;
        
        private int m_curveCount = 0;
        private int SEGMENT_COUNT = 60;//曲線取點個數(取點越多這個長度越趨向於精確)

        private static DMDrawCurve m_instance;
        public static DMDrawCurve Instance
        {
            get {
                if (null == m_instance)
                    m_instance = new DMDrawCurve();
                return m_instance;
            }
        }
        void Awake()
        {
            if (null == m_instance)
                m_instance = this;
            SetLine();
            if (null == m_anchorPoint)
                m_anchorPoint = Resources.Load("Prefabs/AnchorPoint") as GameObject;
            if (null == m_controlPoint)
                m_controlPoint = Resources.Load("Prefabs/ControlPoint") as GameObject;
        }
        void SetLine()
        {
            if (null == m_lineRenderer)
                m_lineRenderer = GetComponent<LineRenderer>();
            m_lineRenderer.material = Resources.Load("Materials/Line") as Material;
            m_lineRenderer.startColor = Color.red;
            m_lineRenderer.endColor = Color.green;
            m_lineRenderer.widthMultiplier = 0.2f;
        }

        public void Init(GameObject player)
        {//初始化一個基準點(Player)
            if (player == null) return;
            GameObject anchorPoint = LoadPoint(m_anchorPoint, player.transform.position);
            m_allPoints.Add(anchorPoint.transform);
        }      
        public void AddPoint(Vector3 anchorPointPos)
        {
            //初始化時m_allPoints添加了一個player
            if (m_allPoints.Count == 0) return;
            Transform lastPoint = m_allPoints[m_allPoints.Count - 1];
            GameObject controlPoint2 = LoadPoint(m_controlPoint, lastPoint.position+new Vector3(0,0,-1));   
            GameObject controlPoint = LoadPoint(m_controlPoint, anchorPointPos + new Vector3(0, 0, 1));
            GameObject anchorPoint = LoadPoint(m_anchorPoint, anchorPointPos);

            anchorPoint.GetComponent<CurvePointControl>().m_controlObject = controlPoint;
            lastPoint.GetComponent<CurvePointControl>().m_controlObject2 = controlPoint2;

            m_allPoints.Add(controlPoint2.transform);
            m_allPoints.Add(controlPoint.transform);
            m_allPoints.Add(anchorPoint.transform);

            DrawCurve();
        }
        public void DeletePoint(GameObject anchorPoint)
        {
            if (anchorPoint == null) return;
            CurvePointControl curvePoint = anchorPoint.GetComponent<CurvePointControl>();
            if (curvePoint && anchorPoint.tag.Equals("AnchorPoint"))
            {
                if (curvePoint.m_controlObject)
                {
                    m_allPoints.Remove(curvePoint.m_controlObject.transform);
                    Destroy(curvePoint.m_controlObject);
                } 
                if (curvePoint.m_controlObject2)
                {
                    m_allPoints.Remove(curvePoint.m_controlObject2.transform);
                    Destroy(curvePoint.m_controlObject2);
                }
                if (m_allPoints.IndexOf(curvePoint.transform) == (m_allPoints.Count - 1))
                {//先判斷刪除的是最後一個元素再移除
                    m_allPoints.Remove(curvePoint.transform);
                    Transform lastPoint = m_allPoints[m_allPoints.Count - 2];
                    GameObject lastPointCtrObject = lastPoint.GetComponent<CurvePointControl>().m_controlObject2;
                    if (lastPointCtrObject)
                    {
                        m_allPoints.Remove(lastPointCtrObject.transform);
                        Destroy(lastPointCtrObject);
                        lastPoint.GetComponent<CurvePointControl>().m_controlObject2 = null;
                    }
                }
                else
                {
                    m_allPoints.Remove(curvePoint.transform);
                }
                Destroy(anchorPoint);
                if(m_allPoints.Count == 1)
                {
                    m_lineRenderer.positionCount = 0;
                }
            }

            DrawCurve();
        }
        public void UpdateLine(GameObject anchorPoint, Vector3 offsetPos1, Vector3 offsetPos2)
        {
            if (anchorPoint == null) return;
            if (anchorPoint.tag.Equals("AnchorPoint"))
            {
                CurvePointControl curvePoint = anchorPoint.GetComponent<CurvePointControl>();
                if (curvePoint)
                {
                    if (curvePoint.m_controlObject)
                        curvePoint.m_controlObject.transform.position = anchorPoint.transform.position + offsetPos1;
                    if (curvePoint.m_controlObject2)
                        curvePoint.m_controlObject2.transform.position = anchorPoint.transform.position + offsetPos2;
                }
            }
            DrawCurve();
        }
        public List<Vector3> HiddenLine(bool isHidden=false)
        {
            m_pointParent.SetActive(isHidden);
            m_lineRenderer.enabled = isHidden;
            List<Vector3> pathPoints = new List<Vector3>();
            if(!isHidden)
            {
                for(int i = 0; i < m_lineRenderer.positionCount; i++)
                {
                    pathPoints.Add(m_lineRenderer.GetPosition(i));
                }
            }
            return pathPoints;
        }

        private void DrawCurve()//畫曲線
        {
            if (m_allPoints.Count < 4) return;
            m_curveCount = (int)m_allPoints.Count / 3;
            for (int j = 0; j < m_curveCount; j++)
            {
                for (int i = 1; i <= SEGMENT_COUNT; i++)
                {
                    float t = (float)i / (float)SEGMENT_COUNT;
                    int nodeIndex = j * 3;
                    Vector3 pixel = CalculateCubicBezierPoint(t, m_allPoints[nodeIndex].position, m_allPoints[nodeIndex + 1].position, m_allPoints[nodeIndex + 2].position, m_allPoints[nodeIndex + 3].position);
                    m_lineRenderer.positionCount = j * SEGMENT_COUNT + i;
                    m_lineRenderer.SetPosition((j * SEGMENT_COUNT) + (i - 1), pixel);
                }
            }
        }
        private GameObject LoadPoint(GameObject pointPrefab,Vector3 pos)
        {
            if (pointPrefab == null)
            {
                Debug.LogError("The Prefab is Null!");
                return null;
            }
            if (null == m_pointParent)
                m_pointParent = new GameObject("AllPoints");
            GameObject pointClone = Instantiate(pointPrefab);
            pointClone.name = pointClone.name.Replace("(Clone)", "");
            pointClone.transform.SetParent(m_pointParent.transform);
            pointClone.transform.position = pos;

            return pointClone;
        }

        //貝塞爾曲線公式:B(t)=P0*(1-t)^3 + 3*P1*t(1-t)^2 + 3*P2*t^2*(1-t) + P3*t^3 ,t屬於[0,1].
        Vector3 CalculateCubicBezierPoint(float t, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3)
        {
            float u = 1 - t;
            float tt = t * t;
            float uu = u * u;
            float uuu = uu * u;
            float ttt = tt * t;

            Vector3 p = uuu * p0;
            p += 3 * uu * t * p1;
            p += 3 * u * tt * p2;
            p += ttt * p3;

            return p;
        }
    }
}

2. CurvePointControl.cs (掛載到路徑點與調節點上(兩個預製體))

該腳本實現每個路徑點對應的兩個調節點,以及繪製調節點與路徑點之間的切線。

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


namespace DM.Editor.View
{
    public class CurvePointControl : MonoBehaviour
    {
        [Header("鎖定X軸")]
        public bool m_isLockX = false;
        [Header("鎖定Y軸")]
        public bool m_isLockY = true;
        [Header("鎖定Z軸")]
        public bool m_isLockZ = false;
       
        [HideInInspector]
        public GameObject m_controlObject;
        [HideInInspector]
        public GameObject m_controlObject2;


        private Vector3 offsetPos1 = Vector3.zero;
        private Vector3 offsetPos2 = Vector3.zero;
        private LineRenderer lineRenderer;
        void Start()
        {
            if (gameObject.tag.Equals("AnchorPoint") && !lineRenderer)
                lineRenderer = gameObject.AddComponent<LineRenderer>();
            if (lineRenderer)
            {
                lineRenderer.sortingOrder = 1;
                lineRenderer.material = new Material(Shader.Find("Particles/Alpha Blended"));
                lineRenderer.startColor = lineRenderer.endColor = Color.yellow;
                lineRenderer.widthMultiplier = 0.03f;
                lineRenderer.positionCount = 0;
            }
        }
        void OnMouseDown()
        {
            if (!gameObject.tag.Equals("AnchorPoint")) return;
            OffsetPos();
        }
        public List<Vector3> OffsetPos()
        {
            List<Vector3> offsetPosList = new List<Vector3>();
            if (m_controlObject)
                offsetPos1 = m_controlObject.transform.position - transform.position;
            if (m_controlObject2)
                offsetPos2 = m_controlObject2.transform.position - transform.position;
            offsetPosList.Add(offsetPos1);
            offsetPosList.Add(offsetPos2);


            return offsetPosList;
        }
        void OnMouseDrag()
        {
            //if (gameObject.tag.Equals("AnchorPoint")) return;
            Vector3 pos0 = Camera.main.WorldToScreenPoint(transform.position);
            Vector3 mousePos = new Vector3(Input.mousePosition.x, Input.mousePosition.y, pos0.z);
            Vector3 mousePosInWorld= Camera.main.ScreenToWorldPoint(mousePos);
            Vector3 thisPos = mousePosInWorld;
            if (m_isLockX)
                thisPos.x = transform.position.x;
            if (m_isLockY)
                thisPos.y = transform.position.y;
            if (m_isLockZ)
                thisPos.z = transform.position.z;
            transform.position = thisPos;
            DMDrawCurve.Instance.UpdateLine(gameObject, offsetPos1, offsetPos2);   
        }      
        private void DrawControlLine()
        {
            if (!gameObject.tag.Equals("AnchorPoint") || (!m_controlObject && !m_controlObject2)) return;
            if (lineRenderer)
            {
                lineRenderer.positionCount = (m_controlObject && m_controlObject2) ? 3 : 2;
                if (m_controlObject && !m_controlObject2)
                {
                    lineRenderer.SetPosition(0, m_controlObject.transform.position);
                    lineRenderer.SetPosition(1, transform.position);
                }
                if (m_controlObject2 && !m_controlObject)
                {
                    lineRenderer.SetPosition(0, transform.position);
                    lineRenderer.SetPosition(1, m_controlObject2.transform.position);
                }
                if (m_controlObject && m_controlObject2)
                {
                    lineRenderer.SetPosition(0, m_controlObject.transform.position);
                    lineRenderer.SetPosition(1, transform.position);
                    lineRenderer.SetPosition(2, m_controlObject2.transform.position);
                }
            }
        }
        void Update()
        {
            DrawControlLine();
        }
    }
}

對應Inspector,如圖

3. Test.cs (任意掛載)

該腳本實現Player自動尋路。

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

namespace DM.Editor.View
{
    public class Test : MonoBehaviour
    {
        public GameObject m_player;
        public List<Vector3> m_pathPoints;
        void Start()
        {
            DMDrawCurve.Instance.Init(m_player);
        }

        IEnumerator Move()
        {
            if (m_pathPoints.Count == 0) yield break;
            int item = 1;
            while (true)
            {
                m_player.transform.LookAt(m_pathPoints[item]);
                m_player.transform.position = Vector3.Lerp(m_pathPoints[item - 1], m_pathPoints[item], 1f);
                item++;
                if (item >= m_pathPoints.Count)
                {
                    item = 1;
                    yield break;
                }
                yield return new WaitForEndOfFrame();
            }
        }
        void Update()
        {
            if (Input.GetKey(KeyCode.LeftControl) && (Input.GetMouseButtonUp(0) || Input.GetMouseButtonUp(1)))
            {
                Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
                RaycastHit hit;
                if (Physics.Raycast(ray, out hit))
                {
                    if (Input.GetMouseButtonUp(0) && hit.collider.tag.Equals("Terrain"))
                    {
                        Vector3 pointPos = new Vector3(hit.point.x, m_player.transform.position.y, hit.point.z);
                        DMDrawCurve.Instance.AddPoint(pointPos);
                    }
                    else if (Input.GetMouseButtonUp(1) && hit.collider.tag.Equals("AnchorPoint"))
                    {
                        DMDrawCurve.Instance.DeletePoint(hit.collider.gameObject);
                    }
                }
            }
            if (Input.GetKeyUp(KeyCode.A))
                m_pathPoints = DMDrawCurve.Instance.HiddenLine(false);
            else if (Input.GetKeyUp(KeyCode.Escape))
            {
                DMDrawCurve.Instance.HiddenLine(true);
                m_pathPoints.Clear();
            }
            if (Input.GetKeyUp(KeyCode.B))
            {
                StartCoroutine(Move());
            }
        }
    }
}

注意:需要添加兩個Tag值(Terrain,AnchorPoint),也可動態添加,一個給場景地面,一個給路徑點(AnchorPoint),調節點不需要Tag。

開發Demo以及.unitypackage的網盤鏈接在這裏,需要的自行下載:

鏈接:https://pan.baidu.com/s/1dwIOxcMB-Lhq_Tlxenb4fQ 密碼:7g8y

以上就是近日開發結果,如有不足,請批評指正。如有疑問,請留言,看到自然回覆。

如轉載,請註明出處:https://mp.csdn.net/postedit

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章