using System; using System.Collections.Generic; using System.Linq; using System.Threading; using UnityEngine; using Framework.Geometry.Lines; namespace Framework.Navigation.Platformer { /// Calculates paths by which actors can get from one place to another public class Navigator { #region class VisitedGround /// Stores the navigation leading up to a specific area private class VisitedGround { /// Initializes a new area navigation record /// Parent area through which the area was reached /// Accumulated cost for reaching this area /// Point at which this area was entered public VisitedGround(Ground parent, float cost, float entryX) { this.Parent = parent; this.Cost = cost; this.EntryX = entryX; } /// Node from which the searcher came public Ground Parent; /// Cost with which the walkable area was reached /// /// This allows other path searchers to replace the node's parent if they /// found a shorter way to it. /// public float Cost; /// X coordinate where the agent entered the area public float EntryX; } #endregion // class VisitedGround; #region class PathSearch /// Searches for a path from the source to the target private class PathSearch { /// Initializes a new path search manager /// Constraints limiting the agent's movement public PathSearch(Constraints constraints) { this.openNodesLookup = new Dictionary(); this.closedNodesLookup = new Dictionary(); this.instructions = new List(); this.constraints = constraints; } /// Movement instructions that have resulted from the path search public IList Instructions { get { return this.instructions; } } /// Whether the path search has been cancelled public bool IsCancelled { get { return this.cancelRequested; } } /// Finds a path from the origin to the destination ground /// Ground on which the agent is starting /// Current X coordinate of the agent /// Ground the agent wants to reach /// Navigation instructions from the origin to the destination public IList FindPath(Ground from, float entryX, Ground to) { runExhaustiveSearch(from, entryX); if(this.cancelRequested) { return null; } generateNavigationInstructions(from, entryX, to); if(this.cancelRequested) { return null; } return this.instructions; } /// Cancels the path search public void Cancel() { this.cancelRequested = true; } /// /// Runs an exhaustive search (visiting all nodes) on the navigation graph /// /// Walkable area the search will start with /// Current position on the starting walking able /// The calculated costs for each node in the navigation graph private void runExhaustiveSearch(Ground from, float entryX) { this.openNodesLookup.Add(from, new VisitedGround(null, 0, entryX)); float averageJumpDistance = this.constraints.MaximumJumpDistance / 2.0f; while(this.openNodesLookup.Count > 0) { KeyValuePair node = this.openNodesLookup.First(); this.openNodesLookup.Remove(node.Key); this.closedNodesLookup.Add(node); // If the agent is no longer interested in the result, abort if(this.cancelRequested) { break; } // Check all other grounds that can be reached from this one and calculate // the cost of moving there for(int index = 0; index < node.Key.JumpTargets.Length; ++index) { Ground targetGround = node.Key.JumpTargets[index]; entryX = node.Value.EntryX; Ground sourceGround = node.Key; // Look for the optimal jump to reach the other platform from this one. Segment2? optimalJump; { Segment2? leftJump = JumpCalculator.GetLeftJumpPath( sourceGround, targetGround, averageJumpDistance, this.constraints ); Segment2? rightJump = JumpCalculator.GetRightJumpPath( sourceGround, targetGround, averageJumpDistance, this.constraints ); // Prefer the side on which the agent has the shorter way if(leftJump.HasValue) { float distanceToLeftJumpOff = Math.Abs(entryX - leftJump.Value.From.x); if(rightJump.HasValue) { float distanceToRightJumpOff = Math.Abs(entryX - rightJump.Value.From.x); if(distanceToRightJumpOff < distanceToLeftJumpOff) { optimalJump = rightJump; } } optimalJump = leftJump; } else { optimalJump = rightJump; } } // It may not be possible for the agent to make this particular jump, // so only process it if a valid jump solution was found. if(optimalJump.HasValue) { // Estimate the movement cost of getting to the other ground float cost = node.Value.Cost; cost += Math.Abs(optimalJump.Value.From.x - entryX); cost += optimalJump.Value.Length; // Check whether we processed the target ground already VisitedGround target; if(!this.closedNodesLookup.TryGetValue(targetGround, out target)) { this.openNodesLookup.TryGetValue(targetGround, out target); } // Create a new target ground or update the existing one in case we // found a shorter route to it. if(target == null) { target = new VisitedGround(node.Key, cost, entryX); this.openNodesLookup.Add(targetGround, target); } else { if(cost < target.Cost) { target.Parent = targetGround; target.Cost = cost; target.EntryX = entryX; } } } // if jump to jump target possible } // for each jump target in current open node } // while open nodes present } /// Generates the navigation instructions after the search finished /// Ground the agent starts on /// Position the agent starts at /// Ground the agent wants to reach private void generateNavigationInstructions(Ground from, float entryX, Ground to) { var path = new Stack(); // Follow the parent nodes back to the starting point, building the path that // the agent has to follow in reverse Ground current = to; while(current != from) { path.Push(current); VisitedGround visitedGround; if(this.closedNodesLookup.TryGetValue(current, out visitedGround)) { current = visitedGround.Parent; } else { return; // No path from source to target } } // Now convert the path into movement instructions while(path.Count > 0) { Ground intermediate = path.Pop(); if(this.cancelRequested) { return; } // Calculate a realistic jump trajectory for the left side Segment2? leftJump; { float leftBorder = Math.Max(from.Left, intermediate.Left); float leftJumpFloorX = Math.Min( Math.Max(entryX, leftBorder - this.constraints.MaximumJumpDistance), leftBorder ); float leftOptimalDistance = (leftBorder - leftJumpFloorX); leftJump = JumpCalculator.GetLeftJumpPath( from, intermediate, leftOptimalDistance / this.constraints.MaximumJumpDistance, this.constraints ); } // Calculate a realistic jump trajectory for the right side Segment2? rightJump; { float rightBorder = Math.Min(from.Right, intermediate.Right); float rightJumpFloorX = Math.Min( Math.Max(entryX, rightBorder), rightBorder + this.constraints.MaximumJumpDistance ); float rightOptimalDistance = (rightJumpFloorX - rightBorder); rightJump = JumpCalculator.GetRightJumpPath( from, intermediate, rightOptimalDistance / this.constraints.MaximumJumpDistance, this.constraints ); } // Prefer the side on which the agent has the shorter way Segment2? jump = null; if(leftJump.HasValue) { float distanceToLeftJumpOff = Math.Abs(entryX - leftJump.Value.From.x); if(rightJump.HasValue) { float distanceToRightJumpOff = Math.Abs(entryX - rightJump.Value.From.x); if(distanceToRightJumpOff < distanceToLeftJumpOff) { jump = rightJump; } } jump = leftJump; } if(rightJump.HasValue) { jump = rightJump; } // Add a movement instruction to the list if the jump off point requires moving float distanceToJumpOffPoint = Math.Abs(jump.Value.From.x - entryX); if(distanceToJumpOffPoint > 0.01f) { this.instructions.Add( new Instruction() { Ground = from, Action = Action.Walk, From = new Vector3(entryX, from.GetGroundHeightAt(entryX), 0.0f), To = new Vector3(jump.Value.From.x, jump.Value.From.y, 0.0f) } ); } // Add the jump itself this.instructions.Add( new Instruction() { Ground = from, Action = Action.Jump, From = new Vector3(jump.Value.From.x, jump.Value.From.y, 0.0f), To = new Vector3(jump.Value.To.x, jump.Value.To.y, 0.0f) } ); entryX = jump.Value.To.x; from = intermediate; } } /// Reachable nodes that have yet to be checked private IDictionary openNodesLookup; /// Reachable nodes that were already checked private IDictionary closedNodesLookup; /// Constraints by which the agent's movement is limited private Constraints constraints; /// Whether the search has been cancelled private volatile bool cancelRequested; /// Navigation instructions the agent has to follow private IList instructions; } #endregion // class PathSearch #region class PathSearchParameters /// Carries the parameters of a path search private class PathSearchParameters { /// Initializes a new path search parameter container /// Ground from which on the agent is starting /// Position the agent is starting at /// Ground to which the agent wants to move public PathSearchParameters(Ground from, float entryX, Ground to) { this.From = from; this.EntryX = entryX; this.To = to; } /// Ground from which on the agent is starting public Ground From; /// Position the agent is starting at public float EntryX; /// Ground to which the agent wants to move public Ground To; } #endregion // class PathSearchParameters #region class AsynchronousPathSearch /// Performs a path search asynchronously in a background thread private class AsynchronousPathSearch : PathSearch, IAsyncResult { /// Initializes a new asynchronous path search /// /// Constraints by which the movement of the agent is limited /// /// /// Callback that will be invoked when the path search has finished /// /// Custom object that will be passed to the callback public AsynchronousPathSearch( Constraints constraints, WaitCallback completionCallback, object state ) : base(constraints) { this.completionCallback = completionCallback; this.state = state; this.findPathInThreadDelegate = new WaitCallback(findPathInThread); } /// Starts a path search in a background thread /// Ground the agent is starting on /// Current position of the agent on the ground /// Ground the agent wants to get to public void Start(Ground from, float entryX, Ground to) { ThreadPool.QueueUserWorkItem( this.findPathInThreadDelegate, new PathSearchParameters(from, entryX, to) ); } /// Exception that has occurred in the background thread public Exception Exception { get { return this.exception; } } /// Searches a path from the origin to the destination /// /// Path search parameter container with informations about the path search /// private void findPathInThread(object state) { string oldName = Thread.CurrentThread.Name; Thread.CurrentThread.Name = "Navigator Path Searching Thread"; try { var parameters = (PathSearchParameters)state; FindPath(parameters.From, parameters.EntryX, parameters.To); // Let the caller know that the search is finished lock(this) { this.isCompleted = true; if(this.completionEvent != null) { this.completionEvent.Set(); } } // If the caller wanted to be notified upon completion, run the callback if(this.completionCallback != null) { this.completionCallback(this.state); } } catch(Exception exception) { this.exception = exception; } finally { Thread.CurrentThread.Name = oldName; } } /// Not supported. Always null. public object AsyncState { get { return this.state; } } /// /// Returns a wait handle that can be used to wait until the path search is finished /// public WaitHandle AsyncWaitHandle { get { if(this.completionEvent == null) { lock(this) { if(this.completionEvent == null) { this.completionEvent = new ManualResetEvent(this.isCompleted); } } } return this.completionEvent; } } /// /// Whether the path search has completed in the same thread that started it /// public bool CompletedSynchronously { get { return false; } } /// True if the path search has finished public bool IsCompleted { get { return this.isCompleted; } } /// Only created on demand; cleared when the path search finished private ManualResetEvent completionEvent; /// Whether the path search has finished private bool isCompleted; /// Delegate for the findPathInThread() method private WaitCallback findPathInThreadDelegate; /// Callback that will be invoked when the search has finished private WaitCallback completionCallback; /// User-defined state that will be passed to the callback private object state; /// Exception that occurred in the worker thread, if any private Exception exception; } #endregion // class AsynchronousPathSearch /// Calculates the best path for an agent to go to a target position /// Location the agent is starting from /// Location the agent wants to move to /// The agent's abilities - jump height, distance, etc. /// A list of instructions the agent can follow to get to the target public static IList FindPath( Locator from, Locator to, Constraints constraints ) { return new PathSearch(constraints).FindPath( from.CurrentGround, from.transform.position.x, to.CurrentGround ); } /// Calculates the best path for an agent to go to a target position /// Location the agent is starting from /// Location the agent wants to move to /// The agent's abilities - jump height, distance, etc. /// /// An asynchronous result handle that can be used to wait for /// completion of the path search /// public static IAsyncResult BeginFindPath( Locator from, Locator to, Constraints constraints ) { var pathSearch = new AsynchronousPathSearch(constraints, null, null); pathSearch.Start( from.CurrentGround, from.transform.position.x, to.CurrentGround ); return pathSearch; } /// /// Waits for an asynchronous path search to end and returns the movement instructions /// that have resulted from the path search /// /// Async result returned by the BeginFindPath() method /// A lsit of movement instructions that lead from start to finish public static IList EndFindPath(IAsyncResult asyncResult) { var pathSearch = asyncResult as AsynchronousPathSearch; if(pathSearch == null) { throw new ArgumentException( "The asyncResult does not belong to this class", "asyncResult" ); } // If the path search is not yet finished, wait until it is if(!pathSearch.IsCompleted) { pathSearch.AsyncWaitHandle.WaitOne(); } // If the path search was cancelled, end with the appropriate exception if(pathSearch.IsCancelled) { throw new OperationCanceledException("Path search was cancelled"); } // If an error occurred during the path search, rethrow it if(pathSearch.Exception != null) { throw pathSearch.Exception; } // Everything seems to have worked, return the movement instructions return pathSearch.Instructions; } /// Cancels the path search running in the background /// Async result returned by the BeginFindPath() method public static void CancelFindPath(IAsyncResult asyncResult) { var pathSearch = asyncResult as AsynchronousPathSearch; if(pathSearch == null) { throw new ArgumentException( "The asyncResult does not belong to this class", "asyncResult" ); } pathSearch.Cancel(); } } } // namespace Framework.Navigation.Platformer