using System; using System.Collections.Generic; using UnityEngine; namespace Pathfinding { [AddComponentMenu("Pathfinding/Modifiers/Advanced Smooth")] [System.Serializable] /// Smoothing by dividing path into turns and straight segments. [HelpURL("http://arongranberg.com/astar/documentation/stable/class_pathfinding_1_1_advanced_smooth.php")] public class AdvancedSmooth : MonoModifier { public override int Order { get { return 40; } } public float turningRadius = 1.0F; public MaxTurn turnConstruct1 = new MaxTurn(); public ConstantTurn turnConstruct2 = new ConstantTurn(); public override void Apply (Path p) { Vector3[] vectorPath = p.vectorPath.ToArray(); if (vectorPath == null || vectorPath.Length <= 2) { return; } List newPath = new List(); newPath.Add(vectorPath[0]); TurnConstructor.turningRadius = turningRadius; for (int i = 1; i < vectorPath.Length-1; i++) { List turnList = new List(); TurnConstructor.Setup(i, vectorPath); turnConstruct1.Prepare(i, vectorPath); turnConstruct2.Prepare(i, vectorPath); TurnConstructor.PostPrepare(); if (i == 1) { turnConstruct1.PointToTangent(turnList); turnConstruct2.PointToTangent(turnList); } else { turnConstruct1.TangentToTangent(turnList); turnConstruct2.TangentToTangent(turnList); } EvaluatePaths(turnList, newPath); //Last point if (i == vectorPath.Length-2) { turnConstruct1.TangentToPoint(turnList); turnConstruct2.TangentToPoint(turnList); } EvaluatePaths(turnList, newPath); } newPath.Add(vectorPath[vectorPath.Length-1]); p.vectorPath = newPath; } void EvaluatePaths (List turnList, List output) { turnList.Sort(); for (int j = 0; j < turnList.Count; j++) { if (j == 0) { turnList[j].GetPath(output); } } turnList.Clear(); if (TurnConstructor.changedPreviousTangent) { turnConstruct1.OnTangentUpdate(); turnConstruct2.OnTangentUpdate(); } } [System.Serializable] /// Type of turn. public class MaxTurn : TurnConstructor { Vector3 preRightCircleCenter = Vector3.zero; Vector3 preLeftCircleCenter = Vector3.zero; Vector3 rightCircleCenter; Vector3 leftCircleCenter; double vaRight, vaLeft, preVaLeft, preVaRight; double gammaLeft, gammaRight; double betaRightRight, betaRightLeft, betaLeftRight, betaLeftLeft; double deltaRightLeft, deltaLeftRight; double alfaRightRight, alfaLeftLeft, alfaRightLeft, alfaLeftRight; public override void OnTangentUpdate () { rightCircleCenter = current + normal * turningRadius; leftCircleCenter = current - normal * turningRadius; vaRight = Atan2(current-rightCircleCenter); vaLeft = vaRight + Math.PI; } public override void Prepare (int i, Vector3[] vectorPath) { preRightCircleCenter = rightCircleCenter; preLeftCircleCenter = leftCircleCenter; rightCircleCenter = current + normal * turningRadius; leftCircleCenter = current - normal * turningRadius; preVaRight = vaRight; preVaLeft = vaLeft; vaRight = Atan2(current-rightCircleCenter); vaLeft = vaRight + Math.PI; } public override void TangentToTangent (List turnList) { alfaRightRight = Atan2(rightCircleCenter - preRightCircleCenter); // + Math.PI*0.5; //Angle tangent to the angle the previous circle (from the current circle) alfaLeftLeft = Atan2(leftCircleCenter - preLeftCircleCenter); // + Math.PI*0.5; alfaRightLeft = Atan2(leftCircleCenter - preRightCircleCenter); // + Math.PI*0.5; //RightLeft means: from the previous right circle to the current left circle alfaLeftRight = Atan2(rightCircleCenter - preLeftCircleCenter); // + Math.PI*0.5; //LeftRight means: from the previous left circle to the current right circle double magnRightLeft = (leftCircleCenter - preRightCircleCenter).magnitude; double magnLeftRight = (rightCircleCenter - preLeftCircleCenter).magnitude; bool noRightLeft = false; bool noLeftRight = false; //Discard RightLeft and LeftRight paths if the circles lie to close to each other if (magnRightLeft < turningRadius*2) { magnRightLeft = turningRadius*2; noRightLeft = true; } if (magnLeftRight < turningRadius*2) { magnLeftRight = turningRadius*2; noLeftRight = true; } deltaRightLeft = noRightLeft ? 0 : (ThreeSixtyRadians * 0.25) - Math.Asin(turningRadius*2 / magnRightLeft); //turn*2 should be r1 + r2 for circles with different radiis deltaLeftRight = noLeftRight ? 0 : (ThreeSixtyRadians * 0.25) - Math.Asin(turningRadius*2 / magnLeftRight); //turn*2 should be r1 + r2 for circles with different radiis //Length for the first turn betaRightRight = ClockwiseAngle(preVaRight, alfaRightRight - ThreeSixtyRadians*0.25); // ThreeSixtyRadians * 0.25 = 90 degrees betaRightLeft = ClockwiseAngle(preVaRight, alfaRightLeft - deltaRightLeft); betaLeftRight = CounterClockwiseAngle(preVaLeft, alfaLeftRight + deltaLeftRight); betaLeftLeft = CounterClockwiseAngle(preVaLeft, alfaLeftLeft + ThreeSixtyRadians*0.25); //Add length for the second turn betaRightRight += ClockwiseAngle(alfaRightRight - ThreeSixtyRadians*0.25, vaRight); betaRightLeft += CounterClockwiseAngle(alfaRightLeft + deltaRightLeft, vaLeft); betaLeftRight += ClockwiseAngle(alfaLeftRight - deltaLeftRight, vaRight); betaLeftLeft += CounterClockwiseAngle(alfaLeftLeft + ThreeSixtyRadians*0.25, vaLeft); betaRightRight = GetLengthFromAngle(betaRightRight, turningRadius); betaRightLeft = GetLengthFromAngle(betaRightLeft, turningRadius); betaLeftRight = GetLengthFromAngle(betaLeftRight, turningRadius); betaLeftLeft = GetLengthFromAngle(betaLeftLeft, turningRadius); Vector3 pRightRight1, pRightRight2, pRightLeft1, pRightLeft2, pLeftRight1, pLeftRight2, pLeftLeft1, pLeftLeft2; //Debug.Log ("=== DELTA VALUES===\nRightLeft "+ToDegrees (deltaRightLeft)+" - LeftRight "+ToDegrees (deltaLeftRight)); //Set up points where the straigh segments starts and ends (between the turns) pRightRight1 = AngleToVector(alfaRightRight - ThreeSixtyRadians*0.25)*turningRadius + preRightCircleCenter; pRightLeft1 = AngleToVector(alfaRightLeft - deltaRightLeft)*turningRadius + preRightCircleCenter; pLeftRight1 = AngleToVector(alfaLeftRight + deltaLeftRight)*turningRadius + preLeftCircleCenter; pLeftLeft1 = AngleToVector(alfaLeftLeft + ThreeSixtyRadians*0.25)*turningRadius + preLeftCircleCenter; pRightRight2 = AngleToVector(alfaRightRight - ThreeSixtyRadians*0.25)*turningRadius + rightCircleCenter; pRightLeft2 = AngleToVector(alfaRightLeft - deltaRightLeft + Math.PI)*turningRadius + leftCircleCenter; pLeftRight2 = AngleToVector(alfaLeftRight + deltaLeftRight + Math.PI)*turningRadius + rightCircleCenter; pLeftLeft2 = AngleToVector(alfaLeftLeft + ThreeSixtyRadians*0.25)*turningRadius + leftCircleCenter; betaRightRight += (pRightRight1 - pRightRight2).magnitude; betaRightLeft += (pRightLeft1 - pRightLeft2).magnitude; betaLeftRight += (pLeftRight1 - pLeftRight2).magnitude; betaLeftLeft += (pLeftLeft1 - pLeftLeft2).magnitude; if (noRightLeft) { betaRightLeft += 10000000; } if (noLeftRight) { betaLeftRight += 10000000; } turnList.Add(new Turn((float)betaRightRight, this, 2)); turnList.Add(new Turn((float)betaRightLeft, this, 3)); turnList.Add(new Turn((float)betaLeftRight, this, 4)); turnList.Add(new Turn((float)betaLeftLeft, this, 5)); } public override void PointToTangent (List turnList) { bool noRight = false, noLeft = false; float rightMagn = (prev-rightCircleCenter).magnitude; float leftMagn = (prev-leftCircleCenter).magnitude; if (rightMagn < turningRadius) noRight = true; if (leftMagn < turningRadius) noLeft = true; double alfa = noRight ? 0 : Atan2(prev-rightCircleCenter); double delta = noRight ? 0 : (ThreeSixtyRadians * 0.25) - Math.Asin(turningRadius / (prev-rightCircleCenter).magnitude); //Angle to the point where turning ends on the right circle gammaRight = alfa + delta; double betaRight = noRight ? 0 : ClockwiseAngle(gammaRight, vaRight); double alfaLeft = noLeft ? 0 : Atan2(prev-leftCircleCenter); double deltaLeft = noLeft ? 0 : (ThreeSixtyRadians * 0.25) - Math.Asin(turningRadius / (prev-leftCircleCenter).magnitude); //Angle to the point where turning ends gammaLeft = alfaLeft - deltaLeft; double betaLeft = noLeft ? 0 : CounterClockwiseAngle(gammaLeft, vaLeft); if (!noRight) turnList.Add(new Turn((float)betaRight, this, 0)); if (!noLeft) turnList.Add(new Turn((float)betaLeft, this, 1)); } public override void TangentToPoint (List turnList) { bool noRight = false, noLeft = false; float rightMagn = (next-rightCircleCenter).magnitude; float leftMagn = (next-leftCircleCenter).magnitude; if (rightMagn < turningRadius) noRight = true; if (leftMagn < turningRadius) noLeft = true; if (!noRight) { double alfa = Atan2(next-rightCircleCenter); double delta = (ThreeSixtyRadians * 0.25) - Math.Asin(turningRadius / rightMagn); //Angle to the point where turning ends on the right circle gammaRight = alfa - delta; double betaRight = ClockwiseAngle(vaRight, gammaRight); turnList.Add(new Turn((float)betaRight, this, 6)); } if (!noLeft) { double alfaLeft = Atan2(next-leftCircleCenter); double deltaLeft = (ThreeSixtyRadians * 0.25) - Math.Asin(turningRadius / leftMagn); //Angle to the point where turning ends gammaLeft = alfaLeft + deltaLeft; double betaLeft = CounterClockwiseAngle(vaLeft, gammaLeft); turnList.Add(new Turn((float)betaLeft, this, 7)); } } public override void GetPath (Turn turn, List output) { switch (turn.id) { case 0: //Right - Point to tangent AddCircleSegment(gammaRight, vaRight, true, rightCircleCenter, output, turningRadius); break; case 1: //Left - Point to tangent AddCircleSegment(gammaLeft, vaLeft, false, leftCircleCenter, output, turningRadius); break; case 2: //Right Right - Tangent to tangent AddCircleSegment(preVaRight, alfaRightRight - ThreeSixtyRadians*0.25, true, preRightCircleCenter, output, turningRadius); AddCircleSegment(alfaRightRight - ThreeSixtyRadians*0.25, vaRight, true, rightCircleCenter, output, turningRadius); break; case 3: //Right Left - Tangent to tangent AddCircleSegment(preVaRight, alfaRightLeft - deltaRightLeft, true, preRightCircleCenter, output, turningRadius); AddCircleSegment(alfaRightLeft - deltaRightLeft + Math.PI, vaLeft, false, leftCircleCenter, output, turningRadius); break; case 4: //Left Right - Tangent to tangent AddCircleSegment(preVaLeft, alfaLeftRight + deltaLeftRight, false, preLeftCircleCenter, output, turningRadius); AddCircleSegment(alfaLeftRight + deltaLeftRight + Math.PI, vaRight, true, rightCircleCenter, output, turningRadius); break; case 5: //Left Left - Tangent to tangent AddCircleSegment(preVaLeft, alfaLeftLeft + ThreeSixtyRadians*0.25, false, preLeftCircleCenter, output, turningRadius); AddCircleSegment(alfaLeftLeft + ThreeSixtyRadians*0.25, vaLeft, false, leftCircleCenter, output, turningRadius); break; case 6: //Right - Tangent to point AddCircleSegment(vaRight, gammaRight, true, rightCircleCenter, output, turningRadius); break; case 7: //Left - Tangent to point AddCircleSegment(vaLeft, gammaLeft, false, leftCircleCenter, output, turningRadius); break; } } } [System.Serializable] /// Constant turning speed. public class ConstantTurn : TurnConstructor { Vector3 circleCenter; double gamma1; double gamma2; bool clockwise; public override void Prepare (int i, Vector3[] vectorPath) {} public override void TangentToTangent (List turnList) { Vector3 preNormal = Vector3.Cross(t1, Vector3.up); Vector3 dir = (current-prev); Vector3 pos = dir*0.5F + prev; dir = Vector3.Cross(dir, Vector3.up); bool didIntersect; circleCenter = VectorMath.LineDirIntersectionPointXZ(prev, preNormal, pos, dir, out didIntersect); if (!didIntersect) { return; } gamma1 = Atan2(prev-circleCenter); gamma2 = Atan2(current-circleCenter); clockwise = !VectorMath.RightOrColinearXZ(circleCenter, prev, prev+t1); double beta = clockwise ? ClockwiseAngle(gamma1, gamma2) : CounterClockwiseAngle(gamma1, gamma2); beta = GetLengthFromAngle(beta, (circleCenter - current).magnitude); turnList.Add(new Turn((float)beta, this, 0)); } public override void GetPath (Turn turn, List output) { AddCircleSegment(gamma1, gamma2, clockwise, circleCenter, output, (circleCenter - current).magnitude); normal = (current - circleCenter).normalized; t2 = Vector3.Cross(normal, Vector3.up).normalized; normal = -normal; if (!clockwise) { t2 = -t2; normal = -normal; } changedPreviousTangent = true; } } /// Abstract turn constructor. public abstract class TurnConstructor { /// /// Constant bias to add to the path lengths. /// This can be used to favor certain turn types before others. /// By for example setting this to -5, paths from this path constructor will be chosen /// if there are no other paths more than 5 world units shorter than this one (as opposed to just any shorter path) /// public float constantBias = 0; /// /// Bias to multiply the path lengths with. This can be used to favor certain turn types before others. /// See: /// public float factorBias = 1; public static float turningRadius = 1.0F; public const double ThreeSixtyRadians = Math.PI * 2; public static Vector3 prev, current, next; //The current points public static Vector3 t1, t2; //The current tangents - t2 is at 'current', t1 is at 'prev' public static Vector3 normal, prevNormal; //Normal at 'current' public static bool changedPreviousTangent = false; public abstract void Prepare(int i, Vector3[] vectorPath); public virtual void OnTangentUpdate () {} public virtual void PointToTangent (List turnList) {} public virtual void TangentToPoint (List turnList) {} public virtual void TangentToTangent (List turnList) {} public abstract void GetPath(Turn turn, List output); //abstract void Evaluate (Turn turn); public static void Setup (int i, Vector3[] vectorPath) { current = vectorPath[i]; prev = vectorPath[i-1]; next = vectorPath[i+1]; prev.y = current.y; next.y = current.y; t1 = t2; t2 = (next-current).normalized - (prev-current).normalized; t2 = t2.normalized; prevNormal = normal; normal = Vector3.Cross(t2, Vector3.up); normal = normal.normalized; } public static void PostPrepare () { changedPreviousTangent = false; } //Utilities public void AddCircleSegment (double startAngle, double endAngle, bool clockwise, Vector3 center, List output, float radius) { double step = ThreeSixtyRadians / 100; if (clockwise) { while (endAngle > startAngle+ThreeSixtyRadians) { endAngle -= ThreeSixtyRadians; } while (endAngle < startAngle) { endAngle += ThreeSixtyRadians; } } else { while (endAngle < startAngle-ThreeSixtyRadians) { endAngle += ThreeSixtyRadians; } while (endAngle > startAngle) { endAngle -= ThreeSixtyRadians; } } //Add curve if (clockwise) { for (double i = startAngle; i < endAngle; i += step) { output.Add(AngleToVector(i)*radius+center); } } else { for (double i = startAngle; i > endAngle; i -= step) { output.Add(AngleToVector(i)*radius+center); } } //Add last point output.Add(AngleToVector(endAngle)*radius+center); } public void DebugCircleSegment (Vector3 center, double startAngle, double endAngle, double radius, Color color) { double step = ThreeSixtyRadians / 100; while (endAngle < startAngle) { endAngle += ThreeSixtyRadians; } Vector3 prev = AngleToVector(startAngle)*(float)radius+center; for (double i = startAngle+step; i < endAngle; i += step) { Debug.DrawLine(prev, AngleToVector(i)*(float)radius+center); } Debug.DrawLine(prev, AngleToVector(endAngle)*(float)radius+center); } public void DebugCircle (Vector3 center, double radius, Color color) { double step = ThreeSixtyRadians / 100; Vector3 prePos = AngleToVector(-step)*(float)radius+center; for (double i = 0; i < ThreeSixtyRadians; i += step) { Vector3 pos = AngleToVector(i)*(float)radius+center; Debug.DrawLine(prePos, pos, color); prePos = pos; } } /// Returns the length of an circular arc with a radius and angle. Angle is specified in radians public double GetLengthFromAngle (double angle, double radius) { return radius * angle; } /// Returns the angle between from and to in a clockwise direction public double ClockwiseAngle (double from, double to) { return ClampAngle(to - from); } /// Returns the angle between from and to in a counter-clockwise direction public double CounterClockwiseAngle (double from, double to) { return ClampAngle(from - to); } public Vector3 AngleToVector (double a) { return new Vector3((float)Math.Cos(a), 0, (float)Math.Sin(a)); } public double ToDegrees (double rad) { return rad * Mathf.Rad2Deg; } public double ClampAngle (double a) { while (a < 0) { a += ThreeSixtyRadians; } while (a > ThreeSixtyRadians) { a -= ThreeSixtyRadians; } return a; } public double Atan2 (Vector3 v) { return Math.Atan2(v.z, v.x); } } //Turn class /// Represents a turn in a path. public struct Turn : IComparable { public float length; public int id; public TurnConstructor constructor; public float score { get { return length*constructor.factorBias+constructor.constantBias; } } public Turn (float length, TurnConstructor constructor, int id = 0) { this.length = length; this.id = id; this.constructor = constructor; } public void GetPath (List output) { constructor.GetPath(this, output); } public int CompareTo (Turn t) { return t.score > score ? -1 : (t.score < score ? 1 : 0); } public static bool operator < (Turn lhs, Turn rhs) { return lhs.score < rhs.score; } public static bool operator > (Turn lhs, Turn rhs) { return lhs.score > rhs.score; } } } }