/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.Motion { using UnityEngine; /// /// Allows for a user-defined path that objects can follow. /// public class Path : MonoBehaviour { [Tooltip("The points which represent the curve.")] [SerializeField] protected Vector3[] m_ControlPoints; public Vector3[] ControlPoints { get { return m_ControlPoints; } set { m_ControlPoints = value; } } private CubicBezierCurve[] m_Curve; /// /// Initialize the default values. /// private void Awake() { if (m_ControlPoints == null || m_ControlPoints.Length <= 1) { return; } m_Curve = new CubicBezierCurve[(m_ControlPoints.Length / 3)]; for (int i = 0; i < m_Curve.Length; ++i) { var startIndex = i * 3; m_Curve[i] = new CubicBezierCurve(transform.TransformPoint(m_ControlPoints[startIndex]), transform.TransformPoint(m_ControlPoints[startIndex + 1]), transform.TransformPoint(m_ControlPoints[startIndex + 2]), transform.TransformPoint(m_ControlPoints[startIndex + 3])); } } /// /// Returns the tangent of the curve near the specified position. /// /// The position to retrieve the tangent of. /// The index of the last curve segnement. /// The tangent of the curve near the specified position. public Vector3 GetTangent(Vector3 position, ref int index) { var time = m_Curve[index].GetTime(position); if (time == 1 && index < m_Curve.Length - 1) { // If the time is equal to 1 then the position is at an endpoint. Determine if the current curve is closer to the given position or if the next curve is closer. var distance = (m_Curve[index].GetClosestPoint(position) - position).sqrMagnitude; var nextDistance = (m_Curve[index + 1].GetClosestPoint(position) - position).sqrMagnitude; if (nextDistance < distance) { // The next curve is closer - increase the index and retrieve a new time. index++; time = m_Curve[index].GetTime(position); } } else if (time == 0 && index > 0) { // If the time is equal to 0 then the position is at an endpoint. Determine if the current curve is closer to the given position or if the previous curve is closer. var distance = (m_Curve[index].GetClosestPoint(position) - position).sqrMagnitude; var prevDistance = (m_Curve[index - 1].GetClosestPoint(position) - position).sqrMagnitude; if (prevDistance < distance) { // The previous curve is closer - decrease the index and retrieve a new time. index--; time = m_Curve[index].GetTime(position); } } return m_Curve[index].GetTangent(time); } /// /// Returns the tangent of the curve near the specified position. /// /// The position to retrieve the tangent of. /// The index of the last curve segnement. /// The tangent of the curve near the specified position. public Vector3 GetClosestPoint(Vector3 position, ref int index) { var time = m_Curve[index].GetTime(position); if (time == 1 && index < m_Curve.Length - 1) { // If the time is equal to 1 then the position is at an endpoint. Determine if the current curve is closer to the given position or if the next curve is closer. var distance = (m_Curve[index].GetClosestPoint(position) - position).sqrMagnitude; var nextDistance = (m_Curve[index + 1].GetClosestPoint(position) - position).sqrMagnitude; if (nextDistance < distance) { // The next curve is closer - increase the index and retrieve a new time. index++; } } else if (time == 0 && index > 0) { // If the time is equal to 0 then the position is at an endpoint. Determine if the current curve is closer to the given position or if the previous curve is closer. var distance = (m_Curve[index].GetClosestPoint(position) - position).sqrMagnitude; var prevDistance = (m_Curve[index - 1].GetClosestPoint(position) - position).sqrMagnitude; if (prevDistance < distance) { // The previous curve is closer - decrease the index and retrieve a new time. index--; } } return m_Curve[index].GetClosestPoint(position); } /// /// Represents one segment of a cubic bezier curve. /// public class CubicBezierCurve { private const int c_StepCount = 300; private Vector3 m_P0; private Vector3 m_P1; private Vector3 m_P2; private Vector3 m_P3; /// /// Four parameter constructor. /// /// The first point that makes up the curve. /// The second point that makes up the curve. /// The third point that makes up the curve. /// The fourth point that makes up the curve. public CubicBezierCurve(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3) { m_P0 = p0; m_P1 = p1; m_P2 = p2; m_P3 = p3; } /// /// Returns the point of the bezier curve at the normalized position of the curve. /// /// The normalized position within the curve. /// The point of the bezier curve at the normalized position of the curve. public Vector3 GetPoint(float time) { return (((-m_P0 + 3 * (m_P1 - m_P2) + m_P3) * time + (3 * (m_P0 + m_P2) - 6 * m_P1)) * time + 3 * (m_P1 - m_P0)) * time + m_P0; } /// /// Returns the tangent. This tangent is the first derivative of the curve. /// /// The normalized position within the curve. /// The tangent of the curve. public Vector3 GetTangent(float time) { return (3 * (1 - time) * (1 - time) * (m_P1 - m_P0) + 6 * (1 - time) * time * (m_P2 - m_P1) + 3 * time * time * (m_P3 - m_P2)).normalized; } /// /// Returns the closest time at the specified position. /// /// The position to retrieve the time of. /// Should the end cap be included? /// The closest time at the specified position. public float GetTime(Vector3 position) { return GetTime(position, 0, 1); } /// /// Returns the closest time at the specified position. /// /// The position to retrieve the time of. /// The minimum time to search within the curve. /// The maximum time to search within the curve. /// The closest time at the specified position. public float GetTime(Vector3 position, float minTime, float maxTime) { float closestTime = 0f; float closestDistance = float.MaxValue; float step = (maxTime - minTime) / c_StepCount; var steps = c_StepCount + 1; // Walk the curve looking for the closest point to the specified position. Store the closest point and return the corresponding time to that point. for (int i = 0; i < steps; ++i) { var t = minTime + step * i; var distance = (GetPoint(t) - position).sqrMagnitude; if (distance < closestDistance) { closestDistance = distance; closestTime = t; } } return closestTime; } /// /// Returns the closest point on the curve to the specified position. /// /// The position to retrieve the closest point on the curve of. /// Should the curve's end cap be included? /// The closest point on the curve to the specified position. public Vector3 GetClosestPoint(Vector3 position) { return GetPoint(GetTime(position)); } } } }