using UnityEngine; using System.Collections.Generic; using Pathfinding.Pooling; using Unity.Mathematics; 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; /// The cost of the calculated path for each target. Will be 0 if a path was not found. public uint[] targetPathCosts; /// 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; /// 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; } public override bool endPointKnownBeforeCalculation => false; /// /// 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; 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; inverted = true; } protected override void OnEnterPool () { if (vectorPaths != null) for (int i = 0; i < vectorPaths.Length; i++) if (vectorPaths[i] != null) ListPool.Release(vectorPaths[i]); vectorPaths = null; vectorPath = null; if (nodePaths != null) for (int i = 0; i < nodePaths.Length; i++) if (nodePaths[i] != null) ListPool.Release(nodePaths[i]); nodePaths = null; path = null; callbacks = null; targetNodes = null; targetsFound = null; targetPathCosts = null; targetPoints = null; originalTargetPoints = null; base.OnEnterPool(); } /// Set chosenTarget to the index of the shortest path void ChooseShortestPath () { // When pathsForAll is false there will be at most one non-null path chosenTarget = -1; if (nodePaths != null) { uint bestG = uint.MaxValue; for (int i = 0; i < nodePaths.Length; i++) { if (nodePaths[i] != null) { var g = targetPathCosts[i]; if (g < bestG) { chosenTarget = i; bestG = g; } } } } } void SetPathParametersForReturn (int target) { path = nodePaths[target]; vectorPath = vectorPaths[target]; if (inverted) { startPoint = targetPoints[target]; originalStartPoint = originalTargetPoints[target]; } else { endPoint = targetPoints[target]; originalEndPoint = originalTargetPoints[target]; } cost = path != null ? targetPathCosts[target] : 0; } 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; 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 RebuildOpenList () { BinaryHeap heap = pathHandler.heap; for (int i = 0; i < heap.numberOfItems; i++) { var pathNodeIndex = heap.GetPathNodeIndex(i); Int3 pos; if (pathHandler.IsTemporaryNode(pathNodeIndex)) { pos = pathHandler.GetTemporaryNode(pathNodeIndex).position; } else { pos = pathHandler.GetNode(pathNodeIndex).DecodeVariantPosition(pathNodeIndex, pathHandler.pathNodes[pathNodeIndex].fractionAlongEdge); } // Note: node index can be 0 here because the multi target path never uses the euclidean embedding var hScore = (uint)heuristicObjective.Calculate((int3)pos, 0); heap.SetH(i, hScore); } pathHandler.heap.Rebuild(pathHandler.pathNodes); } protected override void Prepare () { var startNNInfo = GetNearest(startPoint); var startNode = startNNInfo.node; if (endingCondition != null) { FailWithError("Multi target paths cannot use custom ending conditions"); return; } 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 if (nnConstraint is PathNNConstraint pathNNConstraint) { pathNNConstraint.SetStart(startNNInfo.node); } pathHandler.AddTemporaryNode(new TemporaryNode { associatedNode = startNNInfo.node.NodeIndex, position = (Int3)startNNInfo.position, type = TemporaryNodeType.Start, }); vectorPaths = new List[targetPoints.Length]; nodePaths = new List[targetPoints.Length]; targetNodes = new GraphNode[targetPoints.Length]; targetsFound = new bool[targetPoints.Length]; targetPathCosts = new uint[targetPoints.Length]; targetNodeCount = 0; bool anyWalkable = false; bool anySameArea = false; bool anyNotNull = false; for (int i = 0; i < targetPoints.Length; i++) { var originalTarget = targetPoints[i]; var endNNInfo = GetNearest(originalTarget); targetNodes[i] = endNNInfo.node; targetPoints[i] = endNNInfo.position; if (targetNodes[i] != null) { anyNotNull = true; } 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; } else { targetNodeCount++; #if !ASTAR_NO_GRID_GRAPH // Potentially we want to special case grid graphs a bit // to better support some kinds of games if (!EndPointGridGraphSpecialCase(endNNInfo.node, originalTarget, i)) #endif { pathHandler.AddTemporaryNode(new TemporaryNode { associatedNode = endNNInfo.node.NodeIndex, position = (Int3)endNNInfo.position, targetIndex = i, type = TemporaryNodeType.End, }); } } } startPoint = startNNInfo.position; 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's no valid path to any of the given targets"); return; } MarkNodesAdjacentToTemporaryEndNodes(); AddStartNodesToHeap(); RecalculateHTarget(); } void RecalculateHTarget () { if (pathsForAll) { // Sequentially go through all targets. // First we will find the path to the first target (or at least aim for it, we might find another one along the way), // then we will change the heuristic objective to the second target and so on. // This does not guarantee that we find the targets in order of closest to furthest, // but that is not required since we want to find all paths anyway. var target = FirstTemporaryEndNode(); heuristicObjective = new HeuristicObjective(target, target, heuristic, heuristicScale, 0, null); } else { // Create a bounding box that contains all the end points, // and use that to calculate the heuristic. // This will ensure we find the closest target first. TemporaryEndNodesBoundingBox(out var mnTarget, out var mxTarget); heuristicObjective = new HeuristicObjective(mnTarget, mxTarget, heuristic, heuristicScale, 0, null); } // Rebuild the open list since all the H scores have changed RebuildOpenList(); } protected override void Cleanup () { // Make sure that the shortest path is set // after the path has been calculated ChooseShortestPath(); base.Cleanup(); } protected override void OnHeapExhausted () { CompleteState = PathCompleteState.Complete; } protected override void OnFoundEndNode (uint pathNode, uint hScore, uint gScore) { if (!pathHandler.IsTemporaryNode(pathNode)) { FailWithError("Expected the end node to be a temporary node. Cannot determine which path it belongs to. This could happen if you are using a custom ending condition for the path."); return; } var targetIndex = pathHandler.GetTemporaryNode(pathNode).targetIndex; if (targetsFound[targetIndex]) throw new System.ArgumentException("This target has already been found"); Trace(pathNode); vectorPaths[targetIndex] = vectorPath; nodePaths[targetIndex] = path; vectorPath = ListPool.Claim(); path = ListPool.Claim(); targetsFound[targetIndex] = true; targetPathCosts[targetIndex] = gScore; targetNodeCount--; // Mark all end nodes for this target as ignored to avoid including them // in the H-score calculation and to avoid calling OnFoundEndNode for this // target index again. for (uint i = 0; i < pathHandler.numTemporaryNodes; i++) { var nodeIndex = pathHandler.temporaryNodeStartIndex + i; ref var node = ref pathHandler.GetTemporaryNode(nodeIndex); if (node.type == TemporaryNodeType.End && node.targetIndex == targetIndex) { node.type = TemporaryNodeType.Ignore; } } // When we find the first target, we can stop because it will be the closest one. 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(); } protected override void Trace (uint pathNodeIndex) { base.Trace(pathNodeIndex, !inverted); } 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); } } 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.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(); } } }