using System; using UnityEngine; using Framework.Geometry.Lines; using Framework.Geometry.Lines.Intersection; namespace Framework.Navigation.Platformer { /// Constraints that can be put on a navigation agent's path search /// /// /// The limits on jump distance and height are interpreted in a shape like /// a window arch: /// /// /// /// ####### This would be the places reachable from a platform with /// ############# a limit of 5 units maximum jump distance, jump height /// ############### and drop height. /// ############### /// ################# An agent can jump 5 units straight up or bridge 5 units /// #####_______##### horizontally with a jump, but it can not go to a platform /// ##### ##### 5 units up and 5 units away because that's not within the /// ##### ##### ellipse formed by maximum distance and maximum jump height. /// ##### ##### /// ##### ##### Downwards jumps use a rectangular region since a far jump /// ##### ##### is always possible and gravity will do the rest. /// /// /// [Serializable] public class Constraints { /// Constraints that allow an actor to move virtually anywhere public static readonly Constraints Infinite = new Constraints() { MaximumJumpHeight = 1000.0f, MaximumDropHeight = 1000.0f, MaximumJumpDistance = 1000.0f }; /// Maximum height the agent can climb in a jump /// /// The actual jump trajectory may curve slightly higher than this, /// as the value specifies the height different that the agent can overcome. /// public float MaximumJumpHeight; /// Maximum height the agent can drop down from without injury public float MaximumDropHeight; /// Maximum distance the agent can bridge with a jump /// /// For jumps to higher ground, this is combined with the maximum jump height /// into an ellipse that defines which other places the agent can reach /// This is combined with the maximum jump height or drop height to form /// an ellipse that defines which other places the agent can reach by jumps. /// public float MaximumJumpDistance; /// /// Tests whether a point lies within the arch described by the constraints /// /// Center coordinate of the arch /// Point that will be tested for being within the arch /// True if the point is within the arch, false otherwise public bool IsPointInsideArch(Vector2 archCenter, Vector2 point) { if(point.y < archCenter.y) { return Intersector.IsPointInRectangle( point, new Vector2( archCenter.x - this.MaximumJumpDistance, archCenter.y - this.MaximumDropHeight ), new Vector2( archCenter.x + this.MaximumJumpDistance, archCenter.y ) ); } else { return Intersector.IsPointInEllipse( point, archCenter, new Vector2(this.MaximumJumpDistance, this.MaximumJumpHeight) ); } } /// /// Tests whether a point lies within an inverse arch described by the constraints /// /// Center coordinate of the arch /// Point that will be tested for being within the arch /// True if the point is within the arch, false otherwise public bool IsPointInsideInverseArch(Vector2 archCenter, Vector2 point) { if(point.y > archCenter.y) { return Intersector.IsPointInRectangle( point, new Vector2( archCenter.x - this.MaximumJumpDistance, archCenter.y ), new Vector2( archCenter.x + this.MaximumJumpDistance, archCenter.y + this.MaximumJumpHeight ) ); } else { return Intersector.IsPointInEllipse( point, archCenter, new Vector2(this.MaximumJumpDistance, this.MaximumJumpHeight) ); } } /// /// Forms the union between a line segment and the arch described by the constraints /// /// Center coordinate of the arch /// /// Line segment that will be restricted to the area within the arch /// /// /// The part of the line segment that was within the arch or null if it was outside entirely /// public Segment2? GetLineSegmentArchUnion(Vector2 archCenter, Segment2 segment) { bool startsBelow = (segment.From.y < archCenter.y); bool endsBelow = (segment.To.y < archCenter.y); // If it crosses over the center of the arch, we need to do both queries and // join the resulting line segments if(startsBelow ^ endsBelow) { Segment2? bottom = getArchUnionForBottom(ref archCenter, ref segment); Segment2? top = getArchUnionForTopOrInverseBottom(ref archCenter, ref segment); // If one of the two queries didn't return a line segment, we can just // return the other (possible when line enters arch at exact center) if(!bottom.HasValue) { return top; } else if(!top.HasValue) { return bottom; } // If both queries returned valid line segments, join them if(startsBelow) { return new Segment2(bottom.Value.From, top.Value.To); } else { return new Segment2(top.Value.From, bottom.Value.To); } } else if(startsBelow) { // Both sides below, only do lower query return getArchUnionForBottom(ref archCenter, ref segment); } else { // Both sides above, only do upper query return getArchUnionForTopOrInverseBottom(ref archCenter, ref segment); } } /// /// Forms the union between a line segment and the arch described by the constraints /// /// Center coordinate of the arch /// /// Line segment that will be restricted to the area within the arch /// /// /// The part of the line segment that was within the arch or null if it was outside entirely /// public Segment2? GetLineSegmentInverseArchUnion( Vector2 archCenter, Segment2 segment ) { bool startsAbove = (segment.From.y > archCenter.y); bool endsAbove = (segment.To.y > archCenter.y); // If it crosses over the center of the arch, we need to do both queries and // join the resulting line segments if(startsAbove ^ endsAbove) { Segment2? bottom = getArchUnionForTopOrInverseBottom(ref archCenter, ref segment); Segment2? top = getArchUnionForInverseTop(ref archCenter, ref segment); // If one of the two queries didn't return a line segment, we can just // return the other (possible when line enters arch at exact center) if(!bottom.HasValue) { return top; } else if(!top.HasValue) { return bottom; } // If both queries returned valid line segments, join them if(startsAbove) { return new Segment2(top.Value.From, bottom.Value.To); } else { return new Segment2(bottom.Value.From, top.Value.To); } } else if(startsAbove) { // Both sides below, only do lower query return getArchUnionForInverseTop(ref archCenter, ref segment); } else { // Both sides above, only do upper query return getArchUnionForTopOrInverseBottom(ref archCenter, ref segment); } } /// Builds the union of a line segment and the top of the arch /// Center coordinate of the arch /// /// Line segment of which a union with the arch will be built /// /// The union between the line segment and the arch's top private Segment2? getArchUnionForTopOrInverseBottom( ref Vector2 archCenter, ref Segment2 segment ) { return Intersector.GetLineSegmentEllipseUnion( segment, archCenter, new Vector2(this.MaximumJumpDistance, this.MaximumJumpHeight) ); } /// Builds the union of a line segment and the bottom of the arch /// Center coordinate of the arch /// /// Line segment of which a union with the arch will be built /// /// The union between the line segment and the arch's bottom private Segment2? getArchUnionForBottom( ref Vector2 archCenter, ref Segment2 segment ) { return Intersector.GetLineSegmentRectangleUnion( segment, new Vector2( archCenter.x - this.MaximumJumpDistance, archCenter.y - this.MaximumDropHeight ), new Vector2( archCenter.x + this.MaximumJumpDistance, archCenter.y ) ); } /// Builds the union of a line segment and the top of an inverse arch /// Center coordinate of the arch /// /// Line segment of which a union with the arch will be built /// /// The union between the line segment and the inverse arch's top private Segment2? getArchUnionForInverseTop( ref Vector2 archCenter, ref Segment2 segment ) { return Intersector.GetLineSegmentRectangleUnion( segment, new Vector2( archCenter.x - this.MaximumJumpDistance, archCenter.y ), new Vector2( archCenter.x + this.MaximumJumpDistance, archCenter.y + this.MaximumDropHeight ) ); } } } // namespace Framework.Navigation.Platformer