Spline.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Collections.ObjectModel;
  5. using UnityEngine;
  6. using UnityEngine.Events;
  7. namespace SplineMesh {
  8. /// <summary>
  9. /// A curved line made of oriented nodes.
  10. /// Each segment is a cubic Bézier curve connected to spline nodes.
  11. /// It provides methods to get positions and tangent along the spline, specifying a distance or a ratio, plus the curve length.
  12. /// The spline and the nodes raise events each time something is changed.
  13. /// </summary>
  14. [DisallowMultipleComponent]
  15. [ExecuteInEditMode]
  16. public class Spline : MonoBehaviour {
  17. /// <summary>
  18. /// The spline nodes.
  19. /// Warning, this collection shouldn't be changed manualy. Use specific methods to add and remove nodes.
  20. /// It is public only for the user to enter exact values of position and direction in the inspector (and serialization purposes).
  21. /// </summary>
  22. public List<SplineNode> nodes = new List<SplineNode>();
  23. /// <summary>
  24. /// The generated curves. Should not be changed in any way, use nodes instead.
  25. /// </summary>
  26. [HideInInspector]
  27. public List<CubicBezierCurve> curves = new List<CubicBezierCurve>();
  28. /// <summary>
  29. /// The spline length in world units.
  30. /// </summary>
  31. public float Length;
  32. [SerializeField]
  33. private bool isLoop;
  34. public bool IsLoop {
  35. get { return isLoop; }
  36. set {
  37. isLoop = value;
  38. updateLoopBinding();
  39. }
  40. }
  41. /// <summary>
  42. /// Event raised when the node collection changes
  43. /// </summary>
  44. public event ListChangeHandler<SplineNode> NodeListChanged;
  45. /// <summary>
  46. /// Event raised when one of the curve changes.
  47. /// </summary>
  48. [HideInInspector]
  49. public UnityEvent CurveChanged = new UnityEvent();
  50. /// <summary>
  51. /// Clear the nodes and curves, then add two default nodes for the reset spline to be visible in editor.
  52. /// </summary>
  53. private void Reset() {
  54. nodes.Clear();
  55. curves.Clear();
  56. AddNode(new SplineNode(new Vector3(5, 0, 0), new Vector3(5, 0, -3)));
  57. AddNode(new SplineNode(new Vector3(10, 0, 0), new Vector3(10, 0, 3)));
  58. RaiseNodeListChanged(new ListChangedEventArgs<SplineNode>() {
  59. type = ListChangeType.clear
  60. });
  61. UpdateAfterCurveChanged();
  62. }
  63. private void OnEnable() {
  64. RefreshCurves();
  65. }
  66. public ReadOnlyCollection<CubicBezierCurve> GetCurves() {
  67. return curves.AsReadOnly();
  68. }
  69. private void RaiseNodeListChanged(ListChangedEventArgs<SplineNode> args) {
  70. if (NodeListChanged != null)
  71. NodeListChanged.Invoke(this, args);
  72. }
  73. private void UpdateAfterCurveChanged() {
  74. Length = 0;
  75. foreach (var curve in curves) {
  76. Length += curve.Length;
  77. }
  78. CurveChanged.Invoke();
  79. }
  80. /// <summary>
  81. /// Returns an interpolated sample of the spline, containing all curve data at this time.
  82. /// Time must be between 0 and the number of nodes.
  83. /// </summary>
  84. /// <param name="t"></param>
  85. /// <returns></returns>
  86. public CurveSample GetSample(float t) {
  87. int index = GetNodeIndexForTime(t);
  88. return curves[index].GetSample(t - index);
  89. }
  90. /// <summary>
  91. /// Returns the curve at the given time.
  92. /// Time must be between 0 and the number of nodes.
  93. /// </summary>
  94. /// <param name="t"></param>
  95. /// <returns></returns>
  96. public CubicBezierCurve GetCurve(float t) {
  97. return curves[GetNodeIndexForTime(t)];
  98. }
  99. private int GetNodeIndexForTime(float t) {
  100. if (t < 0 || t > nodes.Count - 1) {
  101. throw new ArgumentException(string.Format("Time must be between 0 and last node index ({0}). Given time was {1}.", nodes.Count - 1, t));
  102. }
  103. int res = Mathf.FloorToInt(t);
  104. if (res == nodes.Count - 1)
  105. res--;
  106. return res;
  107. }
  108. /// <summary>
  109. /// Refreshes the spline's internal list of curves.
  110. // </summary>
  111. public void RefreshCurves() {
  112. curves.Clear();
  113. for (int i = 0; i < nodes.Count - 1; i++) {
  114. SplineNode n = nodes[i];
  115. SplineNode next = nodes[i + 1];
  116. CubicBezierCurve curve = new CubicBezierCurve(n, next);
  117. curve.Changed.AddListener(UpdateAfterCurveChanged);
  118. curves.Add(curve);
  119. }
  120. RaiseNodeListChanged(new ListChangedEventArgs<SplineNode>() {
  121. type = ListChangeType.clear
  122. });
  123. UpdateAfterCurveChanged();
  124. }
  125. /// <summary>
  126. /// Returns an interpolated sample of the spline, containing all curve data at this distance.
  127. /// Distance must be between 0 and the spline length.
  128. /// </summary>
  129. /// <param name="d"></param>
  130. /// <returns></returns>
  131. public CurveSample GetSampleAtDistance(float d) {
  132. if (d < 0 || d > Length)
  133. throw new ArgumentException(string.Format("Distance must be between 0 and spline length ({0}). Given distance was {1}.", Length, d));
  134. foreach (CubicBezierCurve curve in curves) {
  135. // test if distance is approximatly equals to curve length, because spline
  136. // length may be greater than cumulated curve length due to float precision
  137. if(d > curve.Length && d < curve.Length + 0.0001f) {
  138. d = curve.Length;
  139. }
  140. if (d > curve.Length) {
  141. d -= curve.Length;
  142. } else {
  143. return curve.GetSampleAtDistance(d);
  144. }
  145. }
  146. throw new Exception("Something went wrong with GetSampleAtDistance.");
  147. }
  148. /// <summary>
  149. /// Adds a node at the end of the spline.
  150. /// </summary>
  151. /// <param name="node"></param>
  152. public void AddNode(SplineNode node) {
  153. nodes.Add(node);
  154. if (nodes.Count != 1) {
  155. SplineNode previousNode = nodes[nodes.IndexOf(node) - 1];
  156. CubicBezierCurve curve = new CubicBezierCurve(previousNode, node);
  157. curve.Changed.AddListener(UpdateAfterCurveChanged);
  158. curves.Add(curve);
  159. }
  160. RaiseNodeListChanged(new ListChangedEventArgs<SplineNode>() {
  161. type = ListChangeType.Add,
  162. newItems = new List<SplineNode>() { node }
  163. });
  164. UpdateAfterCurveChanged();
  165. updateLoopBinding();
  166. }
  167. /// <summary>
  168. /// Insert the given node in the spline at index. Index must be greater than 0 and less than node count.
  169. /// </summary>
  170. /// <param name="index"></param>
  171. /// <param name="node"></param>
  172. public void InsertNode(int index, SplineNode node) {
  173. if (index == 0)
  174. throw new Exception("Can't insert a node at index 0");
  175. SplineNode previousNode = nodes[index - 1];
  176. SplineNode nextNode = nodes[index];
  177. nodes.Insert(index, node);
  178. curves[index - 1].ConnectEnd(node);
  179. CubicBezierCurve curve = new CubicBezierCurve(node, nextNode);
  180. curve.Changed.AddListener(UpdateAfterCurveChanged);
  181. curves.Insert(index, curve);
  182. RaiseNodeListChanged(new ListChangedEventArgs<SplineNode>() {
  183. type = ListChangeType.Insert,
  184. newItems = new List<SplineNode>() { node },
  185. insertIndex = index
  186. });
  187. UpdateAfterCurveChanged();
  188. updateLoopBinding();
  189. }
  190. /// <summary>
  191. /// Remove the given node from the spline. The given node must exist and the spline must have more than 2 nodes.
  192. /// </summary>
  193. /// <param name="node"></param>
  194. public void RemoveNode(SplineNode node) {
  195. int index = nodes.IndexOf(node);
  196. if (nodes.Count <= 2) {
  197. throw new Exception("Can't remove the node because a spline needs at least 2 nodes.");
  198. }
  199. CubicBezierCurve toRemove = index == nodes.Count - 1 ? curves[index - 1] : curves[index];
  200. if (index != 0 && index != nodes.Count - 1) {
  201. SplineNode nextNode = nodes[index + 1];
  202. curves[index - 1].ConnectEnd(nextNode);
  203. }
  204. nodes.RemoveAt(index);
  205. toRemove.Changed.RemoveListener(UpdateAfterCurveChanged);
  206. curves.Remove(toRemove);
  207. RaiseNodeListChanged(new ListChangedEventArgs<SplineNode>() {
  208. type = ListChangeType.Remove,
  209. removedItems = new List<SplineNode>() { node },
  210. removeIndex = index
  211. });
  212. UpdateAfterCurveChanged();
  213. updateLoopBinding();
  214. }
  215. SplineNode start, end;
  216. private void updateLoopBinding() {
  217. if(start != null) {
  218. start.Changed -= StartNodeChanged;
  219. }
  220. if(end != null) {
  221. end.Changed -= EndNodeChanged;
  222. }
  223. if (isLoop) {
  224. start = nodes[0];
  225. end = nodes[nodes.Count - 1];
  226. start.Changed += StartNodeChanged;
  227. end.Changed += EndNodeChanged;
  228. StartNodeChanged(null, null);
  229. } else {
  230. start = null;
  231. end = null;
  232. }
  233. }
  234. private void StartNodeChanged(object sender, EventArgs e) {
  235. end.Changed -= EndNodeChanged;
  236. end.Position = start.Position;
  237. end.Direction = start.Direction;
  238. end.Roll = start.Roll;
  239. end.Scale = start.Scale;
  240. end.Up = start.Up;
  241. end.Changed += EndNodeChanged;
  242. }
  243. private void EndNodeChanged(object sender, EventArgs e) {
  244. start.Changed -= StartNodeChanged;
  245. start.Position = end.Position;
  246. start.Direction = end.Direction;
  247. start.Roll = end.Roll;
  248. start.Scale = end.Scale;
  249. start.Up = end.Up;
  250. start.Changed += StartNodeChanged;
  251. }
  252. public CurveSample GetProjectionSample(Vector3 pointToProject) {
  253. CurveSample closest = default(CurveSample);
  254. float minSqrDistance = float.MaxValue;
  255. foreach (var curve in curves) {
  256. var projection = curve.GetProjectionSample(pointToProject);
  257. if (curve == curves[0]) {
  258. closest = projection;
  259. minSqrDistance = (projection.location - pointToProject).sqrMagnitude;
  260. continue;
  261. }
  262. var sqrDist = (projection.location - pointToProject).sqrMagnitude;
  263. if (sqrDist < minSqrDistance) {
  264. minSqrDistance = sqrDist;
  265. closest = projection;
  266. }
  267. }
  268. return closest;
  269. }
  270. }
  271. public enum ListChangeType {
  272. Add,
  273. Insert,
  274. Remove,
  275. clear,
  276. }
  277. public class ListChangedEventArgs<T> : EventArgs {
  278. public ListChangeType type;
  279. public List<T> newItems;
  280. public List<T> removedItems;
  281. public int insertIndex, removeIndex;
  282. }
  283. public delegate void ListChangeHandler<T2>(object sender, ListChangedEventArgs<T2> args);
  284. }