unity实现根据路径生成贝塞尔曲线路径并且物体跟随路径移动

Source

参考视频:Unity如何实现物体沿指定路线移动_哔哩哔哩_bilibili

Unity 功能合集之物体沿自定义路径移动_哔哩哔哩_bilibili

一,根据定点生成贝塞尔曲线

1.创建空对象Path,添加自定义脚本

path.cs脚本内容如下:

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

public class path : MonoBehaviour
{
    private List<Vector3> totalPath = new List<Vector3>();
    public List<LineRenderer> lineRenderers = new List<LineRenderer>();

    private void Awake()
    {
        GetComponentsInChildren(lineRenderers);
    }

    public List<Vector3> GetPathPoint()
    {
        totalPath.Clear();
        for (int i = 0; i < lineRenderers.Count; i++)
        {
            Vector3[] path = new Vector3[lineRenderers[i].positionCount];
            lineRenderers[i].GetPositions(path);
            totalPath.AddRange(path);
        }

        return totalPath;
    }
}

2,Path的子对象下创建空对象path(小写),添加LineRenderer和自定义脚本Line

Line.cs脚本如下

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

[ExecuteInEditMode]
public class Line : MonoBehaviour
{
    public LineRenderer lineRenderer;
    public Transform pointA;
    public Transform pointB;
    public Transform pointC;
    public Transform pointD;

    // Start is called before the first frame update
    void Start()
    {
       

    }



    // Update is called once per frame
    void Update()
    {
        List<Vector3> path = BezierUtility.BeizerIntepolate4List(pointA.position, pointB.position, pointC.position, pointD.position, 40);

        lineRenderer.positionCount = path.Count;
        lineRenderer.SetPositions(path.ToArray());
    }
}

其中BezierUtility.cs脚本如下

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

public class BezierUtility 
{
    internal static Vector3 CalculateBezierPoint(Vector3 p0,Vector3 p1,Vector3 p2,float t)
    {
        float u = 1 - t;
        float tt = t * t;
        return u * u * p0 + 2 * t * u * p1 + tt * p2;

    }

    internal static Vector3 BezierIntepolate3(Vector3 p0,Vector3 p1,Vector3 p2,float t)
    {
        Vector3 p0p1 = Vector3.Lerp(p0, p1, t);
        Vector3 p1p2 = Vector3.Lerp(p1, p2, t);
        return Vector3.Lerp(p0p1, p1p2, t);

    }

    internal static Vector3 BeizerIntepolate4(Vector3 p0,Vector3 p1,Vector3 p2,Vector3 p3,float t)
    {
        Vector3 p0p1 = Vector3.Lerp(p0, p1, t);
        Vector3 p1p2 = Vector3.Lerp(p1, p2, t);
        Vector3 p2p3 = Vector3.Lerp(p2, p3, t);

        Vector3 px = Vector3.Lerp(p0p1, p1p2, t);
        Vector3 py = Vector3.Lerp(p1p2, p2p3, t);

        return Vector3.Lerp(px, py, t);
    }

    internal static List<Vector3> BeizerIntepolate4List(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float pointCount)
    {
        List<Vector3> pointList = new List<Vector3>();
        for(int i = 0; i < pointCount; i++)
        {
            pointList.Add(BeizerIntepolate4(p0, p1, p2, p3, i / pointCount));
        }
        return pointList;
    }
}

3,Line脚本的线段渲染器参数是path(小写)本身,pointA,pointB,pointC,pointD分别可以是自定义的位置类型

4,创建一个要根据路线移动的物体,这里随便创建了一个cube,添加自定义脚本AlongPathMove

参数:V是速度,Point0-Ponit3分别是路径点参数,在这里为path下的cube(1)-cube(4)

脚本Along Path Move脚本内容如下

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

public class AlongPathMove : MonoBehaviour
{
    public float v;//km/h

    private List<Vector3> path = new List<Vector3>();
    
    public Transform point0;
    public Transform point1;
    public Transform point2;
    public Transform point3;

    private float totalLength;//路的总长度

    private float currentS;//当前已经走过的路程

    private int index;

    public LineRenderer lineRenderer;

    // Start is called before the first frame update
    void Start()
    {
        path = BezierUtility.BeizerIntepolate4List(point0.position, point1.position, point2.position, point3.position,50);

        lineRenderer.SetPositions(path.ToArray());
        lineRenderer.positionCount = path.Count;

        for(int i = 1; i < path.Count; i++)
        {
            totalLength += (path[i] - path[i - 1]).magnitude;

        }
    }
    Vector3 dir;
    Vector3 pos;
    private void FixedUpdate()
    {
        float s = (v * 10 / 36) * Time.time;//得到应该运行的路程

        if (Input.GetKey(KeyCode.Space))
        {
            return;
        }

        if (currentS < totalLength)
        {
            for(int i = index; i < path.Count-1; i++)
            {
                currentS += (path[i + 1] - path[i]).magnitude;//计算下一个点的路程
                if (currentS>s)
                {
                    index = i;
                    currentS -= (path[i + 1] - path[i]).magnitude;
                     dir = (path[i + 1] - path[i]).normalized;
                     pos = path[i] + dir * (s - currentS);
                    break;
                }
                
            }
            transform.position = pos;

            transform.rotation = Quaternion.Lerp(transform.rotation, Quaternion.LookRotation(dir, transform.up), Time.deltaTime * 5);
        }
        else
        {
            Debug.LogError("抵达终点");
        }
    }
}

4,点击运行后效果如图所示,方块沿着生成的路线移动

5,这种方式下没办法控制物体的停止,物体走到终点也就停止运动,以下是增加这两个功能的AlongPathMove脚本的写法,上面的步骤都不需要改变,只需要替换AlongPathMove脚本即可

using System.Collections;
using System.Collections.Generic;
using System.IO;
using Oculus.Interaction;
using Unity.VisualScripting;
using UnityEngine;

//移动类型,Once是只移动到终点就停止,Loop是进行走到终点再重新回到起点,Yoyo是循环往返
public enum MoveType
{
    Once,
    Loop,
    Yoyo
}
public class AlongPathMove2 : MonoBehaviour
{
    public float v;
    public Path path;

    public MoveType moveType;
    private List<Vector3> pathPoint = new List<Vector3>();

    private float totalLength;//路的总长度

    private float currentS;//当前已经走过的路程

    private int index = 0;//当前走到哪一个点


    private Vector3 dir;
    private Vector3 pos;

    private float s;

    public GameObject gameobject;

    private bool iscollider=false;

    // Start is called before the first frame update
    void Start()
    {
        Once();
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Alpha1))
        {
            v = 0;
        }

        else if (Input.GetKeyDown(KeyCode.Alpha2))
        {
            v = 3;
        }
       else if (Input.GetKeyDown(KeyCode.Alpha3))
        {
            v = 6;
        }
        else if (Input.GetKeyDown(KeyCode.Alpha4))
        {
            v = 9;
        }
    }

    void OnTriggerStay(Collider collider)
    {
        if (collider.gameObject.name== gameobject.name)
        {
            Debug.Log("开始碰撞");
            iscollider = true;
        }
    }
    //碰撞结束
    void OnTriggerExit(Collider collider)
    {
        Debug.Log("接触结束");
        iscollider = false;
    }

    private void FixedUpdate()
    {
        if (iscollider)
        {
            s += (v * 10 / 36) * Time.fixedDeltaTime;//得到应该运行的路程

            if (currentS < totalLength)
            {
                for (int i = index; i < pathPoint.Count - 1; i++)
                {
                    currentS += (pathPoint[i + 1] - pathPoint[i]).magnitude;//计算下一个点的路程
                    if (currentS > s)
                    {
                        index = i;
                        currentS -= (pathPoint[i + 1] - pathPoint[i]).magnitude;
                        dir = (pathPoint[i + 1] - pathPoint[i]).normalized;
                        pos = pathPoint[i] + dir * (s - currentS);
                        pos.y = 0.8f;
                        break;
                    }

                }
                transform.position = pos;

                transform.rotation = Quaternion.Lerp(transform.rotation, Quaternion.LookRotation(dir, transform.up), Time.deltaTime * 5);
            }
            else
            {
                Debug.LogError("抵达终点");
                switch (moveType)
                {
                    case MoveType.Once:
                        Once();
                        break;
                    case MoveType.Loop:
                        Loop();
                        break;
                    case MoveType.Yoyo:
                        Yoyo();
                        break;
                    default:
                        break;
                }
            }
        }
        else
        {
            return;
        }
        
    }


    private void Once()
    {
        currentS = 0;
        totalLength = 0;
        index = 0;
        s = 0;

        pathPoint = path.GetPathPoint();

        for(int i = 1; i < pathPoint.Count; i++)
        {
            totalLength += (pathPoint[i] - pathPoint[i - 1]).magnitude;
        }
    }

    private void Loop()
    {
        currentS = 0;
        totalLength = 0;
        index = 0;
        s = 0;

        pathPoint = path.GetPathPoint();
        for(int i = 1; i < pathPoint.Count; i++)
        {
            totalLength += (pathPoint[i] - pathPoint[i - 1]).magnitude;
        }
    }

    private void Yoyo()
    {
        currentS = 0;
        totalLength = 0;
        index = 0;
        s = 0;
        pathPoint = path.GetPathPoint();
        if (Vector3.SqrMagnitude(pathPoint[0] - transform.position) > Vector3.SqrMagnitude(pathPoint[pathPoint.Count - 1] - transform.position))
        {
            pathPoint.Reverse();
        }
        for(int i = 1; i < pathPoint.Count; i++)
        {
            totalLength += (pathPoint[i] - pathPoint[i - 1]).magnitude;
        }
    }
   
}

设置isCollider标志位,本案例实现的是当相机靠近引导物体时引导物体才会走,通过OnTriggerStay,OnTriggerExit方法来改变isCollider的bool值,从而改变它的运动状态。OnTrigger...方法的触发条件是碰撞二者必须都添加碰撞盒对象并且其中之一勾选是触发器

6,移动物体添加AlongPathMove2脚本

参数V是速度,路径是上面介绍过的Path,MoveType可以自己选择。GameObject是相机(当相机和引导物体碰撞盒相碰时,引导机器人移动)

7,运行效果如图