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