using System; using UnityEngine; using Framework.Geometry.Lines; using Framework.Geometry.Lines.Intersection; namespace Framework.Navigation.Platformer { /// Calculates the jump-off and landing points between two grounds internal static class JumpCalculator { #region struct Range /// Stores an index range for an array private struct Range { /// Index where the range begins public int Start; /// Index where the range ends (inclusive) public int End; } #endregion // struct Range /// Determines the best jump to take from one ground to another /// Ground from which the agent wants to jump /// Ground onto which the agent wants to jump /// /// Factor of the agent's maximum jump distance that controls how far the jump /// should be. Positive values prefer the right side of the ground, negative ones /// prefer the left side. /// /// Limits like maximum jump height and distance /// /// The specifies the jump length the agent would /// prefer (a wide jump, even if not necessary, would be more suitable for /// a fast-moving agent, unless the required jump was opposite to the running direction). /// public static Segment2? GetJumpPath( Ground from, Ground to, float idealDistanceFactor, Constraints constraints ) { if(idealDistanceFactor < 0.0f) { Segment2? jumpPath = GetLeftJumpPath(from, to, -idealDistanceFactor, constraints); if(jumpPath.HasValue) { return jumpPath; } return GetRightJumpPath(from, to, -idealDistanceFactor, constraints); } else { Segment2? jumpPath = GetRightJumpPath(from, to, idealDistanceFactor, constraints); if(jumpPath.HasValue) { return jumpPath; } return GetLeftJumpPath(from, to, idealDistanceFactor, constraints); } } /// Calculates a jump path for an agent on the left side of the ground /// Ground the agent wants to jump off from /// Ground the agent wants to reach /// Jump distance the agent would prefer /// Limits like maximum jump height and distance /// The jump path to the other ground, if one exists public static Segment2? GetLeftJumpPath( Ground from, Ground to, float idealDistanceFactor, Constraints constraints ) { Vector2 fromLeft = from.Heights[0] + xyOnly(from.transform.position); Vector2 toLeft = to.Heights[0] + xyOnly(to.transform.position); // Is the origin ground further to the left? if(fromLeft.x < toLeft.x) { float y = from.GetGroundHeightAt(toLeft.x); if(y > toLeft.y) { // Destination ground starts under origin ground return null; // No path on this side, let getRightJumpPath() handle it } // See which jump off points can reach the edge of the target ground Range reachableSegments = getReachableSegments( from, toLeft.x - constraints.MaximumJumpDistance, toLeft.x ); float idealX = toLeft.x - (constraints.MaximumJumpDistance * idealDistanceFactor); // Now go over the segments and look the point closest to Vector2? jumpOffPoint = null; Vector2 fromOffset = xyOnly(from.transform.position); float jumpOffPointDistance = float.PositiveInfinity; for(int index = reachableSegments.Start; index < reachableSegments.End; ++index) { Segment2? union = constraints.GetLineSegmentInverseArchUnion( toLeft, new Segment2( from.Heights[index] + fromOffset, from.Heights[index + 1] + fromOffset ) ); if(union.HasValue) { if(union.Value.From.x < toLeft.x) { float closestX = Mathf.Clamp(idealX, union.Value.From.x, union.Value.To.x); float closestDistance = Mathf.Abs(idealX - closestX); if(closestDistance < jumpOffPointDistance) { jumpOffPoint = new Vector2( closestX, Intersector.GetLineSegmentYfromX(union.Value, closestX) ); jumpOffPointDistance = closestDistance; } } } } if(jumpOffPoint.HasValue) { return new Segment2(jumpOffPoint.Value, toLeft); } } else { // No, the destination ground is further to the left float y = to.GetGroundHeightAt(fromLeft.x); if(y > fromLeft.y) { // Jump to higher ground return null; // Could extend over this ground, let getRightJumpPath() handle it } // See which segments of the target ground we can reach from out jump off point Range reachableSegments = getReachableSegments( to, fromLeft.x - constraints.MaximumJumpDistance, fromLeft.x ); float idealX = fromLeft.x - (constraints.MaximumJumpDistance * idealDistanceFactor); // Now go over the segments and look the point closest to Vector2? landingPoint = null; Vector2 toOffset = xyOnly(to.transform.position); float landingPointDistance = float.PositiveInfinity; for(int index = reachableSegments.Start; index < reachableSegments.End; ++index) { Segment2? union = constraints.GetLineSegmentArchUnion( fromLeft, new Segment2(to.Heights[index] + toOffset, to.Heights[index + 1] + toOffset) ); if(union.HasValue) { if(union.Value.From.x < fromLeft.x) { float closestX = Mathf.Clamp(idealX, union.Value.From.x, union.Value.To.x); float closestDistance = Mathf.Abs(idealX - closestX); if(closestDistance < landingPointDistance) { landingPoint = new Vector2( closestX, Intersector.GetLineSegmentYfromX(union.Value, closestX) ); landingPointDistance = closestDistance; } } } } if(landingPoint.HasValue) { return new Segment2(fromLeft, landingPoint.Value); } } return null; } /// Calculates a jump path for an agent on the right side of the ground /// Ground the agent wants to jump off from /// Ground the agent wants to reach /// Jump distance the agent would prefer /// Limits like maximum jump height and distance /// The jump path to the other ground, if one exists public static Segment2? GetRightJumpPath( Ground from, Ground to, float idealDistanceFactor, Constraints constraints ) { Vector2 fromRight = from.Heights[from.Heights.Length - 1] + xyOnly(from.transform.position); Vector2 toRight = to.Heights[to.Heights.Length - 1] + xyOnly(to.transform.position); // Is the origin ground further to the left? if(fromRight.x > toRight.x) { float y = from.GetGroundHeightAt(toRight.x); if(y > toRight.y) { // Destination ground starts under origin ground return null; // No path on this side, let getRightJumpPath() handle it } // See which jump off points can reach the edge of the target ground Range reachableSegments = getReachableSegments( from, toRight.x, toRight.x + constraints.MaximumJumpDistance ); float idealX = toRight.x + (constraints.MaximumJumpDistance * idealDistanceFactor); // Now go over the segments and look the point closest to Vector2? jumpOffPoint = null; Vector2 fromOffset = xyOnly(from.transform.position); float jumpOffPointDistance = float.PositiveInfinity; for(int index = reachableSegments.Start; index < reachableSegments.End; ++index) { Segment2? union = constraints.GetLineSegmentInverseArchUnion( toRight, new Segment2( from.Heights[index] + fromOffset, from.Heights[index + 1] + fromOffset ) ); if(union.HasValue) { if(union.Value.To.x > toRight.x) { float closestX = Mathf.Clamp(idealX, union.Value.From.x, union.Value.To.x); float closestDistance = Mathf.Abs(idealX - closestX); if(closestDistance < jumpOffPointDistance) { jumpOffPoint = new Vector2( closestX, Intersector.GetLineSegmentYfromX(union.Value, closestX) ); jumpOffPointDistance = closestDistance; } } } } if(jumpOffPoint.HasValue) { return new Segment2(jumpOffPoint.Value, toRight); } } else { // No, the destination ground is further to the right float y = to.GetGroundHeightAt(fromRight.x); if(y > fromRight.y) { // Jump to higher ground return null; // Could extend over this ground, let getLeftJumpPath() handle it } // See which segments of the target ground we can reach from out jump off point Range reachableSegments = getReachableSegments( to, fromRight.x, fromRight.x + constraints.MaximumJumpDistance ); float idealX = fromRight.x + (constraints.MaximumJumpDistance * idealDistanceFactor); // Now go over the segments and look the point closest to Vector2? landingPoint = null; Vector2 toOffset = xyOnly(to.transform.position); float landingPointDistance = float.PositiveInfinity; for(int index = reachableSegments.Start; index < reachableSegments.End; ++index) { Segment2? union = constraints.GetLineSegmentArchUnion( fromRight, new Segment2(to.Heights[index] + toOffset, to.Heights[index + 1] + toOffset) ); if(union.HasValue) { if(union.Value.To.x > fromRight.x) { float closestX = Mathf.Clamp(idealX, union.Value.From.x, union.Value.To.x); float closestDistance = Mathf.Abs(idealX - closestX); if(closestDistance < landingPointDistance) { landingPoint = new Vector2( closestX, Intersector.GetLineSegmentYfromX(union.Value, closestX) ); landingPointDistance = closestDistance; } } } } if(landingPoint.HasValue) { return new Segment2(fromRight, landingPoint.Value); } } return null; } /// /// Determines the range of line segments within a specified coordinate range /// /// Ground that will be searched for reachable line segments /// Minimum reachable X coordinate /// Maximum reachable X coordinate /// The range of line segments within the specified coordinate range private static Range getReachableSegments(Ground ground, float minX, float maxX) { Vector3 position = ground.transform.position; var range = new Range(); // Look for the first index that is still outside of the coordinate range or, // if the ground begins within the coordinate range, just use index 0 while(range.Start < ground.Heights.Length - 1) { if(ground.Heights[range.Start + 1].x + position.x > minX) { break; } else { ++range.Start; } } // Now look for the first index where the ground leaves the coordinate range. range.End = range.Start + 1; while(range.End < ground.Heights.Length) { if(ground.Heights[range.End].x + position.x > maxX) { break; } else { ++range.End; } } // To simplify the previous search, we allowed it to potentially increment // the index one past the height array size. If this happened, go back by // one index. if(range.End >= ground.Heights.Length) { range.End = ground.Heights.Length - 1; } return range; } /// Returns only the X and Y coordinates of a 3D position /// Position from which the Z coordinate will be cut /// A 2D vector containing the input's X and Y coordinates private static Vector2 xyOnly(Vector3 position) { return new Vector2(position.x, position.y); } } } // namespace Framework.Navigation.Platformer