using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Unity.Mathematics; namespace UnityEngine.Splines { /// /// To calculate a value at some distance along a spline, interpolation is required. The IInterpolator interface /// allows you to define how data is interpreted given a start value, end value, and normalized interpolation value /// (commonly referred to as 't'). /// /// /// The data type to interpolate. /// public interface IInterpolator { /// /// Calculate a value between from and to at time interval. /// /// The starting value. At t = 0 this method should return an unmodified 'from' value. /// The ending value. At t = 1 this method should return an unmodified 'to' value. /// A percentage between 'from' and 'to'. Must be between 0 and 1. /// A value between 'from' and 'to'. T Interpolate(T from, T to, float t); } /// /// Describes the unit of measurement used by . /// public enum PathIndexUnit { /// /// The 't' value used when interpolating is measured in game units. Values range from 0 (start of Spline) to /// (end of Spline). /// Distance, /// /// The 't' value used when interpolating is normalized. Values range from 0 (start of Spline) to 1 (end of Spline). /// Normalized, /// /// The 't' value used when interpolating is defined by knot indices and a fractional value representing the /// normalized interpolation between the specific knot index and the next knot. /// Knot } /// /// The SplineData{T} class is used to store information relative to a without coupling data /// directly to the Spline class. SplineData can store any type of data, and provides options for how to index /// DataPoints. /// /// The type of data to store. [Serializable] public class SplineData : IEnumerable> { static readonly DataPointComparer> k_DataPointComparer = new DataPointComparer>(); [SerializeField] PathIndexUnit m_IndexUnit = PathIndexUnit.Knot; [SerializeField] List> m_DataPoints = new List>(); // When working with IMGUI it's necessary to keep keys array consistent while a hotcontrol is active. Most // accessors will keep the SplineData sorted, but sometimes it's not possible. [NonSerialized] bool m_NeedsSort; /// /// Access a by index. DataPoints are sorted in ascending order by the /// value. /// /// /// The index of the DataPoint to access. /// public DataPoint this[int index] { get => m_DataPoints[index]; set => SetDataPoint(index, value); } /// /// PathIndexUnit defines how SplineData will interpret 't' values when interpolating data. /// /// public PathIndexUnit PathIndexUnit { get => m_IndexUnit; set => m_IndexUnit = value; } /// /// How many data points the SplineData collection contains. /// public int Count => m_DataPoints.Count; /// /// The DataPoint Indexes of the current SplineData. /// public IEnumerable Indexes => m_DataPoints.Select(dp => dp.Index); /// /// Invoked any time a SplineData is modified. /// /// /// In the editor this can be invoked many times per-frame. /// Prefer to use when working with /// splines in the editor. /// public event Action changed; #if UNITY_EDITOR bool m_Dirty = false; internal static Action> afterSplineDataWasModified; #endif /// /// Create a new SplineData instance. /// public SplineData() {} /// /// Create a new SplineData instance with a single value in it. /// /// /// A single value to add to the spline data at t = 0.` /// public SplineData(T init) { Add(0f, init); SetDirty(); } /// /// Create a new SplineData instance and initialize it with a collection of data points. DataPoints will be sorted and stored /// in ascending order by . /// /// /// A collection of DataPoints to initialize SplineData.` /// public SplineData(IEnumerable> dataPoints) { foreach(var dataPoint in dataPoints) Add(dataPoint); SetDirty(); } void SetDirty() { changed?.Invoke(); #if UNITY_EDITOR if(m_Dirty) return; m_Dirty = true; UnityEditor.EditorApplication.delayCall += () => { afterSplineDataWasModified?.Invoke(this); m_Dirty = false; }; #endif } /// /// Append a to this collection. /// /// /// The interpolant relative to Spline. How this value is interpreted is dependent on . /// /// /// The data to store in the created data point. /// public void Add(float t, T data) => Add(new DataPoint(t, data)); /// /// Append a to this collection. /// /// /// The data point to append to the SplineData collection. /// /// /// The index of the inserted dataPoint. /// public int Add(DataPoint dataPoint) { int index = m_DataPoints.BinarySearch(0, Count, dataPoint, k_DataPointComparer); index = index < 0 ? ~index : index; m_DataPoints.Insert(index, dataPoint); SetDirty(); return index; } /// /// Append a with default value to this collection. /// /// /// The interpolant relative to Spline. How this value is interpreted is dependent on . /// /// /// The index of the inserted dataPoint. /// public int AddDataPointWithDefaultValue(float t) { var dataPoint = new DataPoint() { Index = t }; if(Count == 0) return Add(dataPoint); if(Count == 1) { dataPoint.Value = m_DataPoints[0].Value; return Add(dataPoint); } int index = m_DataPoints.BinarySearch(0, Count, dataPoint, k_DataPointComparer); index = index < 0 ? ~index : index; dataPoint.Value = index == 0 ? m_DataPoints[0].Value : m_DataPoints[index-1].Value; m_DataPoints.Insert(index, dataPoint); SetDirty(); return index; } /// /// Remove a at index. /// /// The index to remove. public void RemoveAt(int index) { if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException(nameof(index)); m_DataPoints.RemoveAt(index); SetDirty(); } /// /// Remove a from this collection, if one exists. /// /// /// The interpolant relative to Spline. How this value is interpreted is dependent on . /// /// /// True is deleted, false otherwise. /// public bool RemoveDataPoint(float t) { var removed = m_DataPoints.Remove(m_DataPoints.FirstOrDefault(point => Mathf.Approximately(point.Index, t))); if(removed) SetDirty(); return removed; } /// /// Move a (if it exists) from this collection, from one index to the another. /// /// The index of the to move. /// The new index for this . /// The index of the modified . /// public int MoveDataPoint(int index, float newIndex) { if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException(nameof(index)); var dataPoint = m_DataPoints[index]; if(Mathf.Approximately(newIndex, dataPoint.Index)) return index; RemoveAt(index); dataPoint.Index = newIndex; int newRealIndex = Add(dataPoint); return newRealIndex; } /// /// Remove all data points. /// public void Clear() { m_DataPoints.Clear(); SetDirty(); } static int Wrap(int value, int lowerBound, int upperBound) { int range_size = upperBound - lowerBound + 1; if(value < lowerBound) value += range_size * ( ( lowerBound - value ) / range_size + 1 ); return lowerBound + ( value - lowerBound ) % range_size; } int ResolveBinaryIndex(int index, bool wrap) { index = ( index < 0 ? ~index : index ) - 1; if(wrap) index = Wrap(index, 0, Count - 1); return math.clamp(index, 0, Count - 1); } (int, int, float) GetIndex(float t, float splineLength, int knotCount, bool closed) { if(Count < 1) return default; SortIfNecessary(); float splineLengthInIndexUnits = splineLength; if(m_IndexUnit == PathIndexUnit.Normalized) splineLengthInIndexUnits = 1f; else if(m_IndexUnit == PathIndexUnit.Knot) splineLengthInIndexUnits = closed ? knotCount : knotCount - 1; float maxDataPointTime = m_DataPoints[m_DataPoints.Count - 1].Index; float maxRevolutionLength = math.ceil(maxDataPointTime / splineLengthInIndexUnits) * splineLengthInIndexUnits; float maxTime = closed ? math.max(maxRevolutionLength, splineLengthInIndexUnits) : splineLengthInIndexUnits; if(closed) { if(t < 0f) t = maxTime + t % maxTime; else t = t % maxTime; } else t = math.clamp(t, 0f, maxTime); int index = m_DataPoints.BinarySearch(0, Count, new DataPoint(t, default), k_DataPointComparer); int fromIndex = ResolveBinaryIndex(index, closed); int toIndex = closed ? ( fromIndex + 1 ) % Count : math.clamp(fromIndex + 1, 0, Count - 1); float fromTime = m_DataPoints[fromIndex].Index; float toTime = m_DataPoints[toIndex].Index; if(fromIndex > toIndex) toTime += maxTime; if(t < fromTime && closed) t += maxTime; if(fromTime == toTime) return ( fromIndex, toIndex, fromTime ); return ( fromIndex, toIndex, math.abs(math.max(0f, t - fromTime) / ( toTime - fromTime )) ); } /// /// Calculate an interpolated value at a given 't' along a spline. /// /// The Spline to interpolate. /// The interpolator value. How this is interpreted is defined by . /// The that is represented as. /// The to use. A collection of commonly used /// interpolators are available in the namespace. /// The IInterpolator type. /// The Spline type. /// An interpolated value. public T Evaluate(TSpline spline, float t, PathIndexUnit indexUnit, TInterpolator interpolator) where TSpline : ISpline where TInterpolator : IInterpolator { if(indexUnit == m_IndexUnit) return Evaluate(spline, t, interpolator); return Evaluate(spline, SplineUtility.ConvertIndexUnit(spline, t, indexUnit, m_IndexUnit), interpolator); } /// /// Calculate an interpolated value at a given 't' along a spline. /// /// The Spline to interpolate. /// The interpolator value. How this is interpreted is defined by . /// The to use. A collection of commonly used /// interpolators are available in the namespace. /// The IInterpolator type. /// The Spline type. /// An interpolated value. public T Evaluate(TSpline spline, float t, TInterpolator interpolator) where TSpline : ISpline where TInterpolator : IInterpolator { var knotCount = spline.Count; if(knotCount < 1 || m_DataPoints.Count == 0) return default; var indices = GetIndex(t, spline.GetLength(), knotCount, spline.Closed); DataPoint a = m_DataPoints[indices.Item1]; DataPoint b = m_DataPoints[indices.Item2]; return interpolator.Interpolate(a.Value, b.Value, indices.Item3); } /// /// Set the data for a at an index. /// /// The DataPoint index. /// The value to set. /// /// Using this method will search the DataPoint list and invoke the /// callback every time. This may be inconvenient when setting multiple DataPoints during the same frame. /// In this case, consider calling for each DataPoint, followed by /// a single call to . Note that the call to is /// optional and can be omitted if DataPoint sorting is not required and the callback /// should not be invoked. /// public void SetDataPoint(int index, DataPoint value) { if(index < 0 || index >= Count) throw new ArgumentOutOfRangeException("index"); RemoveAt(index); Add(value); SetDirty(); } /// /// Set the data for a at an index. /// /// The DataPoint index. /// The value to set. /// /// Use this method as an altenative to when manual control /// over DataPoint sorting and the callback is required. /// See also . /// public void SetDataPointNoSort(int index, DataPoint value) { if(index < 0 || index >= Count) throw new ArgumentOutOfRangeException("index"); // could optimize this by storing affected range m_NeedsSort = true; m_DataPoints[index] = value; } /// /// Triggers sorting of the list if the data is dirty. /// /// /// Call this after a single or series of calls to . /// This will trigger DataPoint sort and invoke the callback. /// This method has two main use cases: to prevent frequent callback /// calls within the same frame and to reduce multiple DataPoints list searches /// to a single sort in performance critical paths. /// public void SortIfNecessary() { if(!m_NeedsSort) return; m_NeedsSort = false; m_DataPoints.Sort(); SetDirty(); } internal void ForceSort() { m_NeedsSort = true; SortIfNecessary(); } /// /// Given a spline and a target PathIndex Unit, convert the SplineData to a new PathIndexUnit without changing the final positions on the Spline. /// /// The Spline type. /// The Spline to use for the conversion, this is necessary to compute most of PathIndexUnits. /// The unit to convert SplineData to.> public void ConvertPathUnit(TSplineType spline, PathIndexUnit toUnit) where TSplineType : ISpline { if(toUnit == m_IndexUnit) return; for(int i = 0; i(newTime, dataPoint.Value); } m_IndexUnit = toUnit; SetDirty(); } /// /// Given a time value using a certain PathIndexUnit type, calculate the normalized time value regarding a specific spline. /// /// The Spline to use for the conversion, this is necessary to compute Normalized and Distance PathIndexUnits. /// The time to normalize in the original PathIndexUnit.> /// The Spline type. /// The normalized time. public float GetNormalizedInterpolation(TSplineType spline, float t) where TSplineType : ISpline { return SplineUtility.GetNormalizedInterpolation(spline, t, m_IndexUnit); } /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// /// Returns an enumerator that iterates through the DataPoints collection. /// /// /// An IEnumerator{DataPoint{T}} for this collection. public IEnumerator> GetEnumerator() { for (int i = 0, c = Count; i < c; ++i) yield return m_DataPoints[i]; } } }