using System; using System.Collections.Generic; using UnityEngine; namespace WSMGameStudio.Splines { public class SplineFollower : MonoBehaviour { [Tooltip("One or more splines to be followed")] public List splines; [Tooltip("Following speed")] public float speed; [Tooltip("Speed Unit")] public SplineFollowerSpeedUnit speedUnit; [Tooltip("Following behaviour")] public SplineFollowerBehaviour followerBehaviour; [Tooltip("Customize start position along the spline (From 0% to 100%)")] [Range(0, 100)] public float customStartPosition = 0f; [Tooltip("Apply spline rotation on object")] public bool applySplineRotation = true; [Tooltip("Configure stops")] public SplineFollowerStops cycleEndStops; [Tooltip("Configure stop time")] public float cycleStopTime = 0f; [Tooltip("Offset between master and linked followers")] public float followersOffset = 0; [Tooltip("WRAP: Loop around spline list | OVERFLOW: Position can overflow spline")] public LinkedFollowerBehaviour linkedFollowersBehaviour; [Tooltip("Linked followers connected to this follower")] public List linkedFollowers; [Tooltip("Visualize follower path for debugging purposes")] public bool visualizePathOnEditor = false; [System.NonSerialized] private float _currentStopTime = 0f; [System.NonSerialized] private float _cicleDuration; [System.NonSerialized] private float _splineProgress; [System.NonSerialized] private bool _goingForward = true; [System.NonSerialized] private float _currentSplineLength; [System.NonSerialized] protected int _currentSplineIndex = 0; [System.NonSerialized] protected int _lastSplineIndex = 0; [System.NonSerialized] private float _oldSpeedValue; [System.NonSerialized] private bool _oldGoingForward; [System.NonSerialized] private Transform _transform; [System.NonSerialized] private Vector3 _currentPosition = Vector3.zero; [System.NonSerialized] private Quaternion _currentRotation = Quaternion.identity; [System.NonSerialized] protected OrientedPoint[][] _normalizedOrientedPoints; [System.NonSerialized] private Vector3 _startPosition = Vector3.zero; [System.NonSerialized] private Vector3 _endPosition = Vector3.zero; [System.NonSerialized] private Quaternion _startRotation = Quaternion.identity; [System.NonSerialized] private Quaternion _endRotation = Quaternion.identity; [System.NonSerialized] private int _lastFramePointIndex = -1; [System.NonSerialized] private int _currentPointIndex = -1; [System.NonSerialized] private int _nextPointIndex = 0; [System.NonSerialized] private float _segmentdistance; [System.NonSerialized] private float _segmentTime; [System.NonSerialized] private float _segmentStep; [System.NonSerialized] private float _segmentProgress; [System.NonSerialized] private float _segmentPercent; public bool SpeedChanged { get { return _oldSpeedValue != speed; } } public bool DirectionChanged { get { return _oldGoingForward != _goingForward; } } public bool CurrentPointChanged { get { return _lastFramePointIndex != _currentPointIndex; } } public bool FirstFrame { get { return _currentPointIndex == -1; } } //Properties used by linked followers public int CurrentSplineIndex { get { return _currentSplineIndex; } } public int CurrentPointIndex { get { return _currentPointIndex; } } public float SegmentProgress { get { return _segmentProgress; } } public OrientedPoint[][] NormalizedOrientedPoints { get { if (_normalizedOrientedPoints == null) NormalizeOrientedPoints(); return _normalizedOrientedPoints; } } public bool GoingForward { get { _goingForward = (speed >= 0); return _goingForward; } } /// /// Initialize /// protected void Start() { _transform = GetComponent(); InitializeFollower(); } /// /// Update each frame /// protected void Update() { if (cycleEndStops != SplineFollowerStops.Disabled && _currentStopTime > 0f) { _currentStopTime -= Time.deltaTime; return; } FollowSpline(); _oldSpeedValue = speed; _oldGoingForward = _goingForward; } /// /// Initialize follower and set it starting position /// private void InitializeFollower() { _oldSpeedValue = speed; _oldGoingForward = _goingForward; InitializeLinkedFollowers();//initialize even if splines not set if (splines == null || splines.Count == 0) return; _lastFramePointIndex = -1; _currentPointIndex = -1; _currentSplineIndex = 0; _currentSplineLength = splines[_currentSplineIndex].Length; _splineProgress = (customStartPosition * 0.01f); cycleStopTime = Mathf.Abs(cycleStopTime); NormalizeOrientedPoints(); CalculateCicleDuration(); } /// /// Initialize linked followers if any /// private void InitializeLinkedFollowers() { if (linkedFollowers != null) { foreach (LinkedSplineFollower link in linkedFollowers) { link.Master = this; link.FollowerBehaviour = linkedFollowersBehaviour; } } } /// /// Populate the normalized orientes points collection to use as follower reference /// protected void NormalizeOrientedPoints() { if (splines == null || splines.Count == 0) return; _normalizedOrientedPoints = new OrientedPoint[splines.Count][]; float optimalSpacing = 2f, splineLength; for (int i = 0; i < splines.Count; i++) { splineLength = splines[i].GetTotalDistance(true); optimalSpacing = splineLength / Mathf.Floor(splineLength); _normalizedOrientedPoints[i] = splines[i].CalculateOrientedPoints(optimalSpacing, false); } } /// /// Follow spline path /// private void FollowSpline() { if (splines == null || splines.Count == 0) return; _goingForward = (speed >= 0); if (SpeedChanged) { CalculateCicleDuration(); if (DirectionChanged) { Vector3 tempPos = _startPosition; _startPosition = _endPosition; _endPosition = tempPos; Quaternion tempRot = _startRotation; _startRotation = _endRotation; _endRotation = tempRot; } } if (_cicleDuration > 0f) _splineProgress = _goingForward ? (_splineProgress + (Time.deltaTime / _cicleDuration)) : (_splineProgress - (Time.deltaTime / _cicleDuration)); //Avoid getting stuck on high speeds if (followerBehaviour == SplineFollowerBehaviour.Loop) { int splinesCount = _normalizedOrientedPoints.Length; _splineProgress = (_goingForward && _splineProgress > splinesCount) ? (_splineProgress % splinesCount) : ((!_goingForward && _splineProgress < -splinesCount) ? (_splineProgress % -splinesCount) : _splineProgress); } // Reached end of spline if ((_goingForward && _splineProgress > 1f) || (!_goingForward && _splineProgress < 0f)) { _lastSplineIndex = _currentSplineIndex; // Moving to next spline if ((_goingForward && _currentSplineIndex < splines.Count - 1) || (!_goingForward && _currentSplineIndex > 0)) { _currentSplineIndex = _goingForward ? (_currentSplineIndex + 1) : (_currentSplineIndex - 1); _splineProgress = CalculateProgressOnSplineTransition(_lastSplineIndex, _currentSplineIndex); CalculateCicleDuration(); if (cycleEndStops == SplineFollowerStops.EachSpline) _currentStopTime = cycleStopTime; } else //Reached end of spline list { switch (followerBehaviour) { case SplineFollowerBehaviour.StopAtTheEnd: _splineProgress = _goingForward ? 1f : 0f; OnStopAtTheEnd(); break; case SplineFollowerBehaviour.Loop: _currentSplineIndex = _goingForward ? 0 : splines.Count - 1; _splineProgress = CalculateProgressOnSplineTransition(_lastSplineIndex, _currentSplineIndex); CalculateCicleDuration(); OnLoop(); break; case SplineFollowerBehaviour.BackAndForward: _splineProgress = _goingForward ? 1f : 0f; speed *= -1; _goingForward = !_goingForward; OnBackAndForward(); break; } if (cycleEndStops == SplineFollowerStops.LastSpline || cycleEndStops == SplineFollowerStops.EachSpline) _currentStopTime = cycleStopTime; } } _lastFramePointIndex = _currentPointIndex; _currentPointIndex = CalculateStartPointIndex(); CalculateSegmentPositionsAndDirection(!_goingForward); _segmentPercent = (1f / (_normalizedOrientedPoints[_currentSplineIndex].Length - 1)); if (_goingForward) _segmentProgress = _splineProgress - (_currentPointIndex * _segmentPercent); else _segmentProgress = _splineProgress - (_nextPointIndex * _segmentPercent); _segmentProgress = _segmentProgress / _segmentPercent; _segmentProgress = _goingForward ? _segmentProgress : (1f - _segmentProgress); _currentPosition = Vector3.Lerp(_startPosition, _endPosition, _segmentProgress); _currentRotation = Quaternion.Slerp(_startRotation, _endRotation, _segmentProgress); _transform = _transform == null ? GetComponent() : _transform; _transform.position = _currentPosition; if (applySplineRotation) _transform.rotation = _currentRotation; UpdateLinkedFollowers(); } /// /// Update linked followers positions along the splines /// private void UpdateLinkedFollowers() { if (linkedFollowers != null) { float acumulatedOffset = 0; for (int i = 0; i < linkedFollowers.Count; i++) { acumulatedOffset = CalculateLinkedFollowersAcumulatedOffset(i, acumulatedOffset); linkedFollowers[i].FollowMaster(acumulatedOffset); } } } /// /// Since splines can have different lengths, the spline progress needs to be proportionally adjusted when transitioning between splines /// /// /// /// private float CalculateProgressOnSplineTransition(int lastSplineIndex, int currentSplineIndex) { float newSplineProgress = _splineProgress; if (ValidateOrientedPoints(lastSplineIndex) && ValidateOrientedPoints(currentSplineIndex)) { float newSplineRatio = CalculateSplineTransitionRatio(lastSplineIndex, currentSplineIndex); newSplineProgress = _goingForward ? ((_splineProgress - 1f) * newSplineRatio) : (1f - (Mathf.Abs(_splineProgress) * newSplineRatio)); } return newSplineProgress; } private float CalculateSplineTransitionRatio(int lastSplineIndex, int currentSplineIndex) { return splines[lastSplineIndex].GetTotalDistance(true) / splines[currentSplineIndex].GetTotalDistance(true); } /// /// Calculates segment start point index based on follower direction /// /// private int CalculateStartPointIndex() { int ret = 0; if (ValidateOrientedPoints(_currentSplineIndex)) { ret = (int)((_normalizedOrientedPoints[_currentSplineIndex].Length - 1) * _splineProgress); if (_goingForward) { ret = (ret >= _normalizedOrientedPoints[_currentSplineIndex].Length - 1) ? (_normalizedOrientedPoints[_currentSplineIndex].Length - 2) : ret; ret = ret < 0 ? 0 : ret; } else { ret = (ret >= _normalizedOrientedPoints[_currentSplineIndex].Length - 1) ? (_normalizedOrientedPoints[_currentSplineIndex].Length - 1) : ret; ret = ret < _normalizedOrientedPoints[_currentSplineIndex].Length - 1 ? (ret + 1) : ret; ret = ret <= 1 ? 1 : ret; } } return ret; } /// /// Calculate current seggment position and direction for the follow terrain method /// /// private void CalculateSegmentPositionsAndDirection(bool reversed = false) { if (CurrentPointChanged || splines[_currentSplineIndex].StaticSpline == false) { _nextPointIndex = reversed ? (_currentPointIndex - 1) : (_currentPointIndex + 1); //Out of bounds safety check _nextPointIndex = _nextPointIndex < 0 ? _currentPointIndex : (_nextPointIndex >= _normalizedOrientedPoints[_currentSplineIndex].Length ? _currentPointIndex : _nextPointIndex); if (ValidateOrientedPoints(_currentSplineIndex)) { _startPosition = _normalizedOrientedPoints[_currentSplineIndex][_currentPointIndex].Position; _startRotation = _normalizedOrientedPoints[_currentSplineIndex][_currentPointIndex].Rotation; _endPosition = _normalizedOrientedPoints[_currentSplineIndex][_nextPointIndex].Position; _endRotation = _normalizedOrientedPoints[_currentSplineIndex][_nextPointIndex].Rotation; } } } /// /// Calculate cicle duration to keep consistent speed /// private void CalculateCicleDuration() { if (ValidateOrientedPoints(_currentSplineIndex)) { _currentSplineLength = splines[_currentSplineIndex].GetTotalDistance(true); _cicleDuration = speed == 0f ? 0f : _currentSplineLength / Mathf.Abs(GetConvertedSpeedUnit()); } } /// /// Convert speed to selected Speed Unit /// /// public float GetConvertedSpeedUnit() { float convertedSpeed = 0f; switch (speedUnit) { case SplineFollowerSpeedUnit.ms: convertedSpeed = speed; break; case SplineFollowerSpeedUnit.kph: convertedSpeed = (speed / 3.6f); break; case SplineFollowerSpeedUnit.mph: convertedSpeed = (speed / 2.237f); break; case SplineFollowerSpeedUnit.kn: convertedSpeed = (speed / 1.944f); break; } return convertedSpeed; } /// /// Validates if oriented points were generated /// /// /// private bool ValidateOrientedPoints(int splineIndex) { // Out of bounds and null validation if (splines == null || splineIndex < 0 || splineIndex > splines.Count - 1) return false; // Control points validation if (splines[splineIndex].OrientedPoints == null || splines[splineIndex].OrientedPoints.Length == 0 || (splines[splineIndex].StaticSpline == false && splines[splineIndex].CheckChanges())) { float splineLength = splines[splineIndex].GetTotalDistance(); float optimalSpacing = splineLength / (int)splineLength; _normalizedOrientedPoints[splineIndex] = splines[splineIndex].CalculateOrientedPoints(optimalSpacing, true); } return true; } /// /// Move object to start position /// public void MoveToStartPosition() { if (splines == null || splines.Count == 0) return; NormalizeOrientedPoints(); float t = customStartPosition * 0.01f; OrientedPoint[] points = _normalizedOrientedPoints[0]; _currentPointIndex = (int)((points.Length - 1) * t); OrientedPoint start = points[_currentPointIndex]; _transform = _transform == null ? GetComponent() : _transform; _transform.position = start.Position; if (applySplineRotation) _transform.rotation = start.Rotation; MoveLinkedFollowersToStartPosition(); } /// /// Set linked followers to their initial positions along the splines /// private void MoveLinkedFollowersToStartPosition() { if (linkedFollowers != null) { float acumulatedOffset = 0; for (int i = 0; i < linkedFollowers.Count; i++) { linkedFollowers[i].Master = this; linkedFollowers[i].FollowerBehaviour = linkedFollowersBehaviour; acumulatedOffset = CalculateLinkedFollowersAcumulatedOffset(i, acumulatedOffset); linkedFollowers[i].MoveToStartPosition(acumulatedOffset); } } } private float CalculateLinkedFollowersAcumulatedOffset(int index, float acumulatedOffset) { acumulatedOffset = index == 0 ? followersOffset + (linkedFollowers[index].followingOffset / 2f) : (acumulatedOffset + (linkedFollowers[index].followingOffset / 2f) + (linkedFollowers[index - 1].followingOffset / 2f)); return acumulatedOffset; } /// /// Restarts follower to its initial position /// public void RestartFollower() { InitializeFollower(); } /// /// Executes when follower reaches the end of the spline list and follow behaviour is set to BACK AND FORWARD /// protected virtual void OnBackAndForward() { } /// /// Executes when follower reaches the end of the spline list and follow behaviour is set to LOOP /// protected virtual void OnLoop() { } /// /// Executes when follower reaches the end of the spline list and follow behaviour is set to STOP AT THE END /// protected virtual void OnStopAtTheEnd() { } } }