using UnityEngine;
using System.Collections.Generic;
namespace Pathfinding {
///
/// A path which searches from one point to a number of different targets in one search or from a number of different start points to a single target.
///
/// This is faster than searching with an ABPath for each target if pathsForAll is true.
/// This path type can be used for example when you want an agent to find the closest target of a few different options.
///
/// When pathsForAll is true, it will calculate a path to each target point, but it can share a lot of calculations for the different paths so
/// it is faster than requesting them separately.
///
/// When pathsForAll is false, it will perform a search using the heuristic set to None and stop as soon as it finds the first target.
/// This may be faster or slower than requesting each path separately.
/// It will run a Dijkstra search where it searches all nodes around the start point until the closest target is found.
/// Note that this is usually faster if some target points are very close to the start point and some are very far away, but
/// it can be slower if all target points are relatively far away because then it will have to search a much larger
/// region since it will not use any heuristics.
///
/// See: Seeker.StartMultiTargetPath
/// See: MultiTargetPathExample.cs (view in online documentation for working links) "Example of how to use multi-target-paths"
///
/// Version: Since 3.7.1 the vectorPath and path fields are always set to the shortest path even when pathsForAll is true.
///
public class MultiTargetPath : ABPath {
/// Callbacks to call for each individual path
public OnPathDelegate[] callbacks;
/// Nearest nodes to the
public GraphNode[] targetNodes;
/// Number of target nodes left to find
protected int targetNodeCount;
/// Indicates if the target has been found. Also true if the target cannot be reached (is in another area)
public bool[] targetsFound;
/// Target points specified when creating the path. These are snapped to the nearest nodes
public Vector3[] targetPoints;
/// Target points specified when creating the path. These are not snapped to the nearest nodes
public Vector3[] originalTargetPoints;
/// Stores all vector paths to the targets. Elements are null if no path was found
public List[] vectorPaths;
/// Stores all paths to the targets. Elements are null if no path was found
public List[] nodePaths;
/// If true, a path to all targets will be returned, otherwise just the one to the closest one.
public bool pathsForAll = true;
/// The closest target index (if any target was found)
public int chosenTarget = -1;
///
/// Current target for Sequential .
/// Refers to an item in the targetPoints array
///
int sequentialTarget;
///
/// How to calculate the heuristic.
/// The heuristic target"/> can be calculated in different ways,
/// by taking the Average position of all targets, or taking the mid point of them (i.e center of the AABB encapsulating all targets).
///
/// The one which works best seems to be Sequential, it sets to the target furthest away, and when that target is found, it moves on to the next one.
/// Some modes have the option to be 'moving' (e.g 'MovingAverage'), that means that it is updated every time a target is found.
/// The H score is calculated according to AstarPath.heuristic
///
/// Note: If pathsForAll is false then this option is ignored and it is always treated as being set to None
///
public HeuristicMode heuristicMode = HeuristicMode.Sequential;
public enum HeuristicMode {
None,
Average,
MovingAverage,
Midpoint,
MovingMidpoint,
Sequential
}
/// False if the path goes from one point to multiple targets. True if it goes from multiple start points to one target point
public bool inverted { get; protected set; }
///
/// Default constructor.
/// Do not use this. Instead use the static Construct method which can handle path pooling.
///
public MultiTargetPath () {}
public static MultiTargetPath Construct (Vector3[] startPoints, Vector3 target, OnPathDelegate[] callbackDelegates, OnPathDelegate callback = null) {
MultiTargetPath p = Construct(target, startPoints, callbackDelegates, callback);
p.inverted = true;
return p;
}
public static MultiTargetPath Construct (Vector3 start, Vector3[] targets, OnPathDelegate[] callbackDelegates, OnPathDelegate callback = null) {
var p = PathPool.GetPath();
p.Setup(start, targets, callbackDelegates, callback);
return p;
}
protected void Setup (Vector3 start, Vector3[] targets, OnPathDelegate[] callbackDelegates, OnPathDelegate callback) {
inverted = false;
this.callback = callback;
callbacks = callbackDelegates;
if (callbacks != null && callbacks.Length != targets.Length) throw new System.ArgumentException("The targets array must have the same length as the callbackDelegates array");
targetPoints = targets;
originalStartPoint = start;
startPoint = start;
startIntPoint = (Int3)start;
if (targets.Length == 0) {
FailWithError("No targets were assigned to the MultiTargetPath");
return;
}
endPoint = targets[0];
originalTargetPoints = new Vector3[targetPoints.Length];
for (int i = 0; i < targetPoints.Length; i++) {
originalTargetPoints[i] = targetPoints[i];
}
}
protected override void Reset () {
base.Reset();
pathsForAll = true;
chosenTarget = -1;
sequentialTarget = 0;
inverted = true;
heuristicMode = HeuristicMode.Sequential;
}
protected override void OnEnterPool () {
if (vectorPaths != null)
for (int i = 0; i < vectorPaths.Length; i++)
if (vectorPaths[i] != null) Util.ListPool.Release(vectorPaths[i]);
vectorPaths = null;
vectorPath = null;
if (nodePaths != null)
for (int i = 0; i < nodePaths.Length; i++)
if (nodePaths[i] != null) Util.ListPool.Release(nodePaths[i]);
nodePaths = null;
path = null;
callbacks = null;
targetNodes = null;
targetsFound = null;
targetPoints = null;
originalTargetPoints = null;
base.OnEnterPool();
}
/// Set chosenTarget to the index of the shortest path
void ChooseShortestPath () {
//
// When pathsForAll is false there will only be one non-null path
chosenTarget = -1;
if (nodePaths != null) {
uint bestG = int.MaxValue;
for (int i = 0; i < nodePaths.Length; i++) {
var currentPath = nodePaths[i];
if (currentPath != null) {
// Get the G score of the first or the last node in the path
// depending on if the paths are reversed or not
var g = pathHandler.GetPathNode(currentPath[inverted ? 0 : currentPath.Count-1]).G;
if (chosenTarget == -1 || g < bestG) {
chosenTarget = i;
bestG = g;
}
}
}
}
}
void SetPathParametersForReturn (int target) {
path = nodePaths[target];
vectorPath = vectorPaths[target];
if (inverted) {
startNode = targetNodes[target];
startPoint = targetPoints[target];
originalStartPoint = originalTargetPoints[target];
} else {
endNode = targetNodes[target];
endPoint = targetPoints[target];
originalEndPoint = originalTargetPoints[target];
}
}
protected override void ReturnPath () {
if (error) {
// Call all callbacks
if (callbacks != null) {
for (int i = 0; i < callbacks.Length; i++)
if (callbacks[i] != null) callbacks[i] (this);
}
if (callback != null) callback(this);
return;
}
bool anySucceded = false;
// Set the end point to the start point
// since the path is reversed
// (the start point will be set individually for each path)
if (inverted) {
endPoint = startPoint;
endNode = startNode;
originalEndPoint = originalStartPoint;
}
for (int i = 0; i < nodePaths.Length; i++) {
if (nodePaths[i] != null) {
// Note that we use the lowercase 'completeState' here.
// The property (CompleteState) will ensure that the complete state is never
// changed away from the error state but in this case we don't want that behaviour.
completeState = PathCompleteState.Complete;
anySucceded = true;
} else {
completeState = PathCompleteState.Error;
}
if (callbacks != null && callbacks[i] != null) {
SetPathParametersForReturn(i);
callbacks[i] (this);
// In case a modifier changed the vectorPath, update the array of all vectorPaths
vectorPaths[i] = vectorPath;
}
}
if (anySucceded) {
completeState = PathCompleteState.Complete;
SetPathParametersForReturn(chosenTarget);
} else {
completeState = PathCompleteState.Error;
}
if (callback != null) {
callback(this);
}
}
protected void FoundTarget (PathNode nodeR, int i) {
nodeR.flag1 = false; // Reset bit 8
Trace(nodeR);
vectorPaths[i] = vectorPath;
nodePaths[i] = path;
vectorPath = Util.ListPool.Claim();
path = Util.ListPool.Claim();
targetsFound[i] = true;
targetNodeCount--;
// Since we have found one target
// and the heuristic is always set to None when
// pathsForAll is false, we will have found the shortest path
if (!pathsForAll) {
CompleteState = PathCompleteState.Complete;
targetNodeCount = 0;
return;
}
// If there are no more targets to find, return here and avoid calculating a new hTarget
if (targetNodeCount <= 0) {
CompleteState = PathCompleteState.Complete;
return;
}
RecalculateHTarget(false);
}
protected void RebuildOpenList () {
BinaryHeap heap = pathHandler.heap;
for (int j = 0; j < heap.numberOfItems; j++) {
PathNode nodeR = heap.GetNode(j);
nodeR.H = CalculateHScore(nodeR.node);
heap.SetF(j, nodeR.F);
}
pathHandler.heap.Rebuild();
}
protected override void Prepare () {
nnConstraint.tags = enabledTags;
var startNNInfo = AstarPath.active.GetNearest(startPoint, nnConstraint);
startNode = startNNInfo.node;
if (startNode == null) {
FailWithError("Could not find start node for multi target path");
return;
}
if (!CanTraverse(startNode)) {
FailWithError("The node closest to the start point could not be traversed");
return;
}
// Tell the NNConstraint which node was found as the start node if it is a PathNNConstraint and not a normal NNConstraint
var pathNNConstraint = nnConstraint as PathNNConstraint;
if (pathNNConstraint != null) {
pathNNConstraint.SetStart(startNNInfo.node);
}
vectorPaths = new List[targetPoints.Length];
nodePaths = new List[targetPoints.Length];
targetNodes = new GraphNode[targetPoints.Length];
targetsFound = new bool[targetPoints.Length];
targetNodeCount = targetPoints.Length;
bool anyWalkable = false;
bool anySameArea = false;
bool anyNotNull = false;
for (int i = 0; i < targetPoints.Length; i++) {
var endNNInfo = AstarPath.active.GetNearest(targetPoints[i], nnConstraint);
targetNodes[i] = endNNInfo.node;
targetPoints[i] = endNNInfo.position;
if (targetNodes[i] != null) {
anyNotNull = true;
endNode = targetNodes[i];
}
bool notReachable = false;
if (endNNInfo.node != null && CanTraverse(endNNInfo.node)) {
anyWalkable = true;
} else {
notReachable = true;
}
if (endNNInfo.node != null && endNNInfo.node.Area == startNode.Area) {
anySameArea = true;
} else {
notReachable = true;
}
if (notReachable) {
// Signal that the pathfinder should not look for this node because we have already found it
targetsFound[i] = true;
targetNodeCount--;
}
}
startPoint = startNNInfo.position;
startIntPoint = (Int3)startPoint;
if (!anyNotNull) {
FailWithError("Couldn't find a valid node close to the any of the end points");
return;
}
if (!anyWalkable) {
FailWithError("No target nodes could be traversed");
return;
}
if (!anySameArea) {
FailWithError("There are no valid paths to the targets");
return;
}
RecalculateHTarget(true);
}
void RecalculateHTarget (bool firstTime) {
// When pathsForAll is false
// then no heuristic should be used
if (!pathsForAll) {
heuristic = Heuristic.None;
heuristicScale = 0.0F;
return;
}
// Calculate a new hTarget and rebuild the open list if necessary
// Rebuilding the open list is necessary when the H score for nodes changes
switch (heuristicMode) {
case HeuristicMode.None:
heuristic = Heuristic.None;
heuristicScale = 0F;
break;
case HeuristicMode.Average:
if (!firstTime) return;
// No break
// The first time the implementation
// for Average and MovingAverage is identical
// so we just use fallthrough
goto case HeuristicMode.MovingAverage;
case HeuristicMode.MovingAverage:
// Pick the average position of all nodes that have not been found yet
var avg = Vector3.zero;
int count = 0;
for (int j = 0; j < targetPoints.Length; j++) {
if (!targetsFound[j]) {
avg += (Vector3)targetNodes[j].position;
count++;
}
}
// Should use asserts, but they were first added in Unity 5.1
// so I cannot use them because I want to keep compatibility with 4.6
// (as of 2015)
if (count == 0) throw new System.Exception("Should not happen");
avg /= count;
hTarget = (Int3)avg;
break;
case HeuristicMode.Midpoint:
if (!firstTime) return;
// No break
// The first time the implementation
// for Midpoint and MovingMidpoint is identical
// so we just use fallthrough
goto case HeuristicMode.MovingMidpoint;
case HeuristicMode.MovingMidpoint:
Vector3 min = Vector3.zero;
Vector3 max = Vector3.zero;
bool set = false;
// Pick the median of all points that have
// not been found yet
for (int j = 0; j < targetPoints.Length; j++) {
if (!targetsFound[j]) {
if (!set) {
min = (Vector3)targetNodes[j].position;
max = (Vector3)targetNodes[j].position;
set = true;
} else {
min = Vector3.Min((Vector3)targetNodes[j].position, min);
max = Vector3.Max((Vector3)targetNodes[j].position, max);
}
}
}
var midpoint = (Int3)((min+max)*0.5F);
hTarget = midpoint;
break;
case HeuristicMode.Sequential:
// The first time the hTarget should always be recalculated
// But other times we can skip it if we have not yet found the current target
// since then the hTarget would just be set to the same value again
if (!firstTime && !targetsFound[sequentialTarget]) {
return;
}
float dist = 0;
// Pick the target which is furthest away and has not been found yet
for (int j = 0; j < targetPoints.Length; j++) {
if (!targetsFound[j]) {
float d = (targetNodes[j].position-startNode.position).sqrMagnitude;
if (d > dist) {
dist = d;
hTarget = (Int3)targetPoints[j];
sequentialTarget = j;
}
}
}
break;
}
// Rebuild the open list since all the H scores have changed
// However the first time we can skip this since
// no nodes are added to the heap yet
if (!firstTime) {
RebuildOpenList();
}
}
protected override void Initialize () {
// Reset the start node to prevent
// old info from previous paths to be used
PathNode startRNode = pathHandler.GetPathNode(startNode);
startRNode.node = startNode;
startRNode.pathID = pathID;
startRNode.parent = null;
startRNode.cost = 0;
startRNode.G = GetTraversalCost(startNode);
startRNode.H = CalculateHScore(startNode);
for (int j = 0; j < targetNodes.Length; j++) {
if (startNode == targetNodes[j]) {
// The start node is equal to the target node
// so we can immediately mark the path as calculated
FoundTarget(startRNode, j);
} else if (targetNodes[j] != null) {
// Mark the node with a flag so that we can quickly check if we have found a target node
pathHandler.GetPathNode(targetNodes[j]).flag1 = true;
}
}
// If all paths have either been invalidated or found already because they were at the same node as the start node
if (targetNodeCount <= 0) {
CompleteState = PathCompleteState.Complete;
return;
}
//if (recalcStartEndCosts) {
// startNode.InitialOpen (open,hTarget,startIntPoint,this,true);
//} else {
startNode.Open(this, startRNode, pathHandler);
//}
searchedNodes++;
//any nodes left to search?
if (pathHandler.heap.isEmpty) {
FailWithError("No open points, the start node didn't open any nodes");
return;
}
// Take the first node off the heap
currentR = pathHandler.heap.Remove();
}
protected override void Cleanup () {
// Make sure that the shortest path is set
// after the path has been calculated
ChooseShortestPath();
ResetFlags();
}
/// Reset flag1 on all nodes after the pathfinding has completed (no matter if an error occurs or if the path is canceled)
void ResetFlags () {
// Reset all flags
if (targetNodes != null) {
for (int i = 0; i < targetNodes.Length; i++) {
if (targetNodes[i] != null) pathHandler.GetPathNode(targetNodes[i]).flag1 = false;
}
}
}
protected override void CalculateStep (long targetTick) {
int counter = 0;
// Continue to search as long as we haven't encountered an error and we haven't found the target
while (CompleteState == PathCompleteState.NotCalculated) {
// @Performance Just for debug info
searchedNodes++;
// The node might be the target node for one of the paths
if (currentR.flag1) {
// Close the current node, if the current node is the target node then the path is finnished
for (int i = 0; i < targetNodes.Length; i++) {
if (!targetsFound[i] && currentR.node == targetNodes[i]) {
FoundTarget(currentR, i);
if (CompleteState != PathCompleteState.NotCalculated) {
break;
}
}
}
if (targetNodeCount <= 0) {
CompleteState = PathCompleteState.Complete;
break;
}
}
// Loop through all walkable neighbours of the node and add them to the open list.
currentR.node.Open(this, currentR, pathHandler);
// Any nodes left to search?
if (pathHandler.heap.isEmpty) {
CompleteState = PathCompleteState.Complete;
break;
}
// Select the node with the lowest F score and remove it from the open list
AstarProfiler.StartFastProfile(7);
currentR = pathHandler.heap.Remove();
AstarProfiler.EndFastProfile(7);
// Check for time every 500 nodes, roughly every 0.5 ms usually
if (counter > 500) {
// Have we exceded the maxFrameTime, if so we should wait one frame before continuing the search since we don't want the game to lag
if (System.DateTime.UtcNow.Ticks >= targetTick) {
// Return instead of yield'ing, a separate function handles the yield (CalculatePaths)
return;
}
counter = 0;
}
counter++;
}
}
protected override void Trace (PathNode node) {
base.Trace(node);
if (inverted) {
// Reverse the paths
int half = path.Count/2;
for (int i = 0; i < half; i++) {
GraphNode tmp = path[i];
path[i] = path[path.Count-i-1];
path[path.Count-i-1] = tmp;
}
for (int i = 0; i < half; i++) {
Vector3 tmp = vectorPath[i];
vectorPath[i] = vectorPath[vectorPath.Count-i-1];
vectorPath[vectorPath.Count-i-1] = tmp;
}
}
}
protected override string DebugString (PathLog logMode) {
if (logMode == PathLog.None || (!error && logMode == PathLog.OnlyErrors)) {
return "";
}
System.Text.StringBuilder text = pathHandler.DebugStringBuilder;
text.Length = 0;
DebugStringPrefix(logMode, text);
if (!error) {
text.Append("\nShortest path was ");
text.Append(chosenTarget == -1 ? "undefined" : nodePaths[chosenTarget].Count.ToString());
text.Append(" nodes long");
if (logMode == PathLog.Heavy) {
text.Append("\nPaths (").Append(targetsFound.Length).Append("):");
for (int i = 0; i < targetsFound.Length; i++) {
text.Append("\n\n Path ").Append(i).Append(" Found: ").Append(targetsFound[i]);
if (nodePaths[i] != null) {
text.Append("\n Length: ");
text.Append(nodePaths[i].Count);
GraphNode node = nodePaths[i][nodePaths[i].Count-1];
if (node != null) {
PathNode nodeR = pathHandler.GetPathNode(endNode);
if (nodeR != null) {
text.Append("\n End Node");
text.Append("\n G: ");
text.Append(nodeR.G);
text.Append("\n H: ");
text.Append(nodeR.H);
text.Append("\n F: ");
text.Append(nodeR.F);
text.Append("\n Point: ");
text.Append(((Vector3)endPoint).ToString());
text.Append("\n Graph: ");
text.Append(endNode.GraphIndex);
} else {
text.Append("\n End Node: Null");
}
}
}
}
text.Append("\nStart Node");
text.Append("\n Point: ");
text.Append(((Vector3)endPoint).ToString());
text.Append("\n Graph: ");
text.Append(startNode.GraphIndex);
text.Append("\nBinary Heap size at completion: ");
text.AppendLine(pathHandler.heap == null ? "Null" : (pathHandler.heap.numberOfItems-2).ToString()); // -2 because numberOfItems includes the next item to be added and item zero is not used
}
}
DebugStringSuffix(logMode, text);
return text.ToString();
}
}
}