using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using UnityEngine; using UnityEngine.Events; namespace SplineMesh { /// /// A curved line made of oriented nodes. /// Each segment is a cubic Bézier curve connected to spline nodes. /// It provides methods to get positions and tangent along the spline, specifying a distance or a ratio, plus the curve length. /// The spline and the nodes raise events each time something is changed. /// [DisallowMultipleComponent] [ExecuteInEditMode] public class Spline : MonoBehaviour { /// /// The spline nodes. /// Warning, this collection shouldn't be changed manualy. Use specific methods to add and remove nodes. /// It is public only for the user to enter exact values of position and direction in the inspector (and serialization purposes). /// public List nodes = new List(); /// /// The generated curves. Should not be changed in any way, use nodes instead. /// [HideInInspector] public List curves = new List(); /// /// The spline length in world units. /// public float Length; [SerializeField] private bool isLoop; public bool IsLoop { get { return isLoop; } set { isLoop = value; updateLoopBinding(); } } /// /// Event raised when the node collection changes /// public event ListChangeHandler NodeListChanged; /// /// Event raised when one of the curve changes. /// [HideInInspector] public UnityEvent CurveChanged = new UnityEvent(); /// /// Clear the nodes and curves, then add two default nodes for the reset spline to be visible in editor. /// private void Reset() { nodes.Clear(); curves.Clear(); AddNode(new SplineNode(new Vector3(5, 0, 0), new Vector3(5, 0, -3))); AddNode(new SplineNode(new Vector3(10, 0, 0), new Vector3(10, 0, 3))); RaiseNodeListChanged(new ListChangedEventArgs() { type = ListChangeType.clear }); UpdateAfterCurveChanged(); } private void OnEnable() { RefreshCurves(); } public ReadOnlyCollection GetCurves() { return curves.AsReadOnly(); } private void RaiseNodeListChanged(ListChangedEventArgs args) { if (NodeListChanged != null) NodeListChanged.Invoke(this, args); } private void UpdateAfterCurveChanged() { Length = 0; foreach (var curve in curves) { Length += curve.Length; } CurveChanged.Invoke(); } /// /// Returns an interpolated sample of the spline, containing all curve data at this time. /// Time must be between 0 and the number of nodes. /// /// /// public CurveSample GetSample(float t) { int index = GetNodeIndexForTime(t); return curves[index].GetSample(t - index); } /// /// Returns the curve at the given time. /// Time must be between 0 and the number of nodes. /// /// /// public CubicBezierCurve GetCurve(float t) { return curves[GetNodeIndexForTime(t)]; } private int GetNodeIndexForTime(float t) { if (t < 0 || t > nodes.Count - 1) { throw new ArgumentException(string.Format("Time must be between 0 and last node index ({0}). Given time was {1}.", nodes.Count - 1, t)); } int res = Mathf.FloorToInt(t); if (res == nodes.Count - 1) res--; return res; } /// /// Refreshes the spline's internal list of curves. // public void RefreshCurves() { curves.Clear(); for (int i = 0; i < nodes.Count - 1; i++) { SplineNode n = nodes[i]; SplineNode next = nodes[i + 1]; CubicBezierCurve curve = new CubicBezierCurve(n, next); curve.Changed.AddListener(UpdateAfterCurveChanged); curves.Add(curve); } RaiseNodeListChanged(new ListChangedEventArgs() { type = ListChangeType.clear }); UpdateAfterCurveChanged(); } /// /// Returns an interpolated sample of the spline, containing all curve data at this distance. /// Distance must be between 0 and the spline length. /// /// /// public CurveSample GetSampleAtDistance(float d) { if (d < 0 || d > Length) throw new ArgumentException(string.Format("Distance must be between 0 and spline length ({0}). Given distance was {1}.", Length, d)); foreach (CubicBezierCurve curve in curves) { // test if distance is approximatly equals to curve length, because spline // length may be greater than cumulated curve length due to float precision if(d > curve.Length && d < curve.Length + 0.0001f) { d = curve.Length; } if (d > curve.Length) { d -= curve.Length; } else { return curve.GetSampleAtDistance(d); } } throw new Exception("Something went wrong with GetSampleAtDistance."); } /// /// Adds a node at the end of the spline. /// /// public void AddNode(SplineNode node) { nodes.Add(node); if (nodes.Count != 1) { SplineNode previousNode = nodes[nodes.IndexOf(node) - 1]; CubicBezierCurve curve = new CubicBezierCurve(previousNode, node); curve.Changed.AddListener(UpdateAfterCurveChanged); curves.Add(curve); } RaiseNodeListChanged(new ListChangedEventArgs() { type = ListChangeType.Add, newItems = new List() { node } }); UpdateAfterCurveChanged(); updateLoopBinding(); } /// /// Insert the given node in the spline at index. Index must be greater than 0 and less than node count. /// /// /// public void InsertNode(int index, SplineNode node) { if (index == 0) throw new Exception("Can't insert a node at index 0"); SplineNode previousNode = nodes[index - 1]; SplineNode nextNode = nodes[index]; nodes.Insert(index, node); curves[index - 1].ConnectEnd(node); CubicBezierCurve curve = new CubicBezierCurve(node, nextNode); curve.Changed.AddListener(UpdateAfterCurveChanged); curves.Insert(index, curve); RaiseNodeListChanged(new ListChangedEventArgs() { type = ListChangeType.Insert, newItems = new List() { node }, insertIndex = index }); UpdateAfterCurveChanged(); updateLoopBinding(); } /// /// Remove the given node from the spline. The given node must exist and the spline must have more than 2 nodes. /// /// public void RemoveNode(SplineNode node) { int index = nodes.IndexOf(node); if (nodes.Count <= 2) { throw new Exception("Can't remove the node because a spline needs at least 2 nodes."); } CubicBezierCurve toRemove = index == nodes.Count - 1 ? curves[index - 1] : curves[index]; if (index != 0 && index != nodes.Count - 1) { SplineNode nextNode = nodes[index + 1]; curves[index - 1].ConnectEnd(nextNode); } nodes.RemoveAt(index); toRemove.Changed.RemoveListener(UpdateAfterCurveChanged); curves.Remove(toRemove); RaiseNodeListChanged(new ListChangedEventArgs() { type = ListChangeType.Remove, removedItems = new List() { node }, removeIndex = index }); UpdateAfterCurveChanged(); updateLoopBinding(); } SplineNode start, end; private void updateLoopBinding() { if(start != null) { start.Changed -= StartNodeChanged; } if(end != null) { end.Changed -= EndNodeChanged; } if (isLoop) { start = nodes[0]; end = nodes[nodes.Count - 1]; start.Changed += StartNodeChanged; end.Changed += EndNodeChanged; StartNodeChanged(null, null); } else { start = null; end = null; } } private void StartNodeChanged(object sender, EventArgs e) { end.Changed -= EndNodeChanged; end.Position = start.Position; end.Direction = start.Direction; end.Roll = start.Roll; end.Scale = start.Scale; end.Up = start.Up; end.Changed += EndNodeChanged; } private void EndNodeChanged(object sender, EventArgs e) { start.Changed -= StartNodeChanged; start.Position = end.Position; start.Direction = end.Direction; start.Roll = end.Roll; start.Scale = end.Scale; start.Up = end.Up; start.Changed += StartNodeChanged; } public CurveSample GetProjectionSample(Vector3 pointToProject) { CurveSample closest = default(CurveSample); float minSqrDistance = float.MaxValue; foreach (var curve in curves) { var projection = curve.GetProjectionSample(pointToProject); if (curve == curves[0]) { closest = projection; minSqrDistance = (projection.location - pointToProject).sqrMagnitude; continue; } var sqrDist = (projection.location - pointToProject).sqrMagnitude; if (sqrDist < minSqrDistance) { minSqrDistance = sqrDist; closest = projection; } } return closest; } } public enum ListChangeType { Add, Insert, Remove, clear, } public class ListChangedEventArgs : EventArgs { public ListChangeType type; public List newItems; public List removedItems; public int insertIndex, removeIndex; } public delegate void ListChangeHandler(object sender, ListChangedEventArgs args); }