using UnityEngine; using System.Collections.Generic; namespace Pathfinding { /// /// Basic path, finds the shortest path from A to B. /// \ingroup paths /// This is the most basic path object it will try to find the shortest path between two points.\n /// Many other path types inherit from this type. /// See: Seeker.StartPath /// See: calling-pathfinding (view in online documentation for working links) /// See: getstarted (view in online documentation for working links) /// public class ABPath : Path { /// Start node of the path public GraphNode startNode; /// End node of the path public GraphNode endNode; /// Start Point exactly as in the path request public Vector3 originalStartPoint; /// End Point exactly as in the path request public Vector3 originalEndPoint; /// /// Start point of the path. /// This is the closest point on the to /// public Vector3 startPoint; /// /// End point of the path. /// This is the closest point on the to /// public Vector3 endPoint; /// /// Determines if a search for an end node should be done. /// Set by different path types. /// \since Added in 3.0.8.3 /// protected virtual bool hasEndPoint { get { return true; } } public Int3 startIntPoint; /// < Start point in integer coordinates /// /// Calculate partial path if the target node cannot be reached. /// If the target node cannot be reached, the node which was closest (given by heuristic) will be chosen as target node /// and a partial path will be returned. /// This only works if a heuristic is used (which is the default). /// If a partial path is found, CompleteState is set to Partial. /// Note: It is not required by other path types to respect this setting /// /// Warning: This feature is currently a work in progress and may not work in the current version /// public bool calculatePartial; /// /// Current best target for the partial path. /// This is the node with the lowest H score. /// Warning: This feature is currently a work in progress and may not work in the current version /// protected PathNode partialBestTarget; /// Saved original costs for the end node. See: ResetCosts protected int[] endNodeCosts; #if !ASTAR_NO_GRID_GRAPH /// Used in EndPointGridGraphSpecialCase GridNode gridSpecialCaseNode; #endif /// @{ @name Constructors /// /// Default constructor. /// Do not use this. Instead use the static Construct method which can handle path pooling. /// public ABPath () {} /// /// Construct a path with a start and end point. /// The delegate will be called when the path has been calculated. /// Do not confuse it with the Seeker callback as they are sent at different times. /// If you are using a Seeker to start the path you can set callback to null. /// /// Returns: The constructed path object /// public static ABPath Construct (Vector3 start, Vector3 end, OnPathDelegate callback = null) { var p = PathPool.GetPath(); p.Setup(start, end, callback); return p; } protected void Setup (Vector3 start, Vector3 end, OnPathDelegate callbackDelegate) { callback = callbackDelegate; UpdateStartEnd(start, end); } /// /// Creates a fake path. /// Creates a path that looks almost exactly like it would if the pathfinding system had calculated it. /// /// This is useful if you want your agents to follow some known path that cannot be calculated using the pathfinding system for some reason. /// /// /// var path = ABPath.FakePath(new List { new Vector3(1, 2, 3), new Vector3(4, 5, 6) }); /// /// ai.SetPath(path); /// /// /// You can use it to combine existing paths like this: /// /// /// var a = Vector3.zero; /// var b = new Vector3(1, 2, 3); /// var c = new Vector3(2, 3, 4); /// var path1 = ABPath.Construct(a, b); /// var path2 = ABPath.Construct(b, c); /// /// AstarPath.StartPath(path1); /// AstarPath.StartPath(path2); /// path1.BlockUntilCalculated(); /// path2.BlockUntilCalculated(); /// /// // Combine the paths /// // Note: Skip the first element in the second path as that will likely be the last element in the first path /// var newVectorPath = path1.vectorPath.Concat(path2.vectorPath.Skip(1)).ToList(); /// var newNodePath = path1.path.Concat(path2.path.Skip(1)).ToList(); /// var combinedPath = ABPath.FakePath(newVectorPath, newNodePath); /// /// public static ABPath FakePath (List vectorPath, List nodePath = null) { var path = PathPool.GetPath(); for (int i = 0; i < vectorPath.Count; i++) path.vectorPath.Add(vectorPath[i]); path.completeState = PathCompleteState.Complete; ((IPathInternals)path).AdvanceState(PathState.Returned); if (vectorPath.Count > 0) { path.UpdateStartEnd(vectorPath[0], vectorPath[vectorPath.Count - 1]); } if (nodePath != null) { for (int i = 0; i < nodePath.Count; i++) path.path.Add(nodePath[i]); if (nodePath.Count > 0) { path.startNode = nodePath[0]; path.endNode = nodePath[nodePath.Count - 1]; } } return path; } /// @} /// /// Sets the start and end points. /// Sets , , , , and (to end ) /// protected void UpdateStartEnd (Vector3 start, Vector3 end) { originalStartPoint = start; originalEndPoint = end; startPoint = start; endPoint = end; startIntPoint = (Int3)start; hTarget = (Int3)end; } internal override uint GetConnectionSpecialCost (GraphNode a, GraphNode b, uint currentCost) { if (startNode != null && endNode != null) { if (a == startNode) { return (uint)((startIntPoint - (b == endNode ? hTarget : b.position)).costMagnitude * (currentCost*1.0/(a.position-b.position).costMagnitude)); } if (b == startNode) { return (uint)((startIntPoint - (a == endNode ? hTarget : a.position)).costMagnitude * (currentCost*1.0/(a.position-b.position).costMagnitude)); } if (a == endNode) { return (uint)((hTarget - b.position).costMagnitude * (currentCost*1.0/(a.position-b.position).costMagnitude)); } if (b == endNode) { return (uint)((hTarget - a.position).costMagnitude * (currentCost*1.0/(a.position-b.position).costMagnitude)); } } else { // endNode is null, startNode should never be null for an ABPath if (a == startNode) { return (uint)((startIntPoint - b.position).costMagnitude * (currentCost*1.0/(a.position-b.position).costMagnitude)); } if (b == startNode) { return (uint)((startIntPoint - a.position).costMagnitude * (currentCost*1.0/(a.position-b.position).costMagnitude)); } } return currentCost; } /// /// Reset all values to their default values. /// All inheriting path types must implement this function, resetting ALL their variables to enable recycling of paths. /// Call this base function in inheriting types with base.Reset (); /// protected override void Reset () { base.Reset(); startNode = null; endNode = null; originalStartPoint = Vector3.zero; originalEndPoint = Vector3.zero; startPoint = Vector3.zero; endPoint = Vector3.zero; calculatePartial = false; partialBestTarget = null; startIntPoint = new Int3(); hTarget = new Int3(); endNodeCosts = null; #if !ASTAR_NO_GRID_GRAPH gridSpecialCaseNode = null; #endif } #if !ASTAR_NO_GRID_GRAPH /// Cached to reduce allocations static readonly NNConstraint NNConstraintNone = NNConstraint.None; /// /// Applies a special case for grid nodes. /// /// Assume the closest walkable node is a grid node. /// We will now apply a special case only for grid graphs. /// In tile based games, an obstacle often occupies a whole /// node. When a path is requested to the position of an obstacle /// (single unwalkable node) the closest walkable node will be /// one of the 8 nodes surrounding that unwalkable node /// but that node is not neccessarily the one that is most /// optimal to walk to so in this special case /// we mark all nodes around the unwalkable node as targets /// and when we search and find any one of them we simply exit /// and set that first node we found to be the 'real' end node /// because that will be the optimal node (this does not apply /// in general unless the heuristic is set to None, but /// for a single unwalkable node it does). /// This also applies if the nearest node cannot be traversed for /// some other reason like restricted tags. /// /// Returns: True if the workaround was applied. If this happens the /// endPoint, endNode, hTarget and hTargetNode fields will be modified. /// /// Image below shows paths when this special case is applied. The path goes from the white sphere to the orange box. /// [Open online documentation to see images] /// /// Image below shows paths when this special case has been disabled /// [Open online documentation to see images] /// protected virtual bool EndPointGridGraphSpecialCase (GraphNode closestWalkableEndNode) { var gridNode = closestWalkableEndNode as GridNode; if (gridNode != null) { var gridGraph = GridNode.GetGridGraph(gridNode.GraphIndex); // Find the closest node, not neccessarily walkable var endNNInfo2 = AstarPath.active.GetNearest(originalEndPoint, NNConstraintNone); var gridNode2 = endNNInfo2.node as GridNode; if (gridNode != gridNode2 && gridNode2 != null && gridNode.GraphIndex == gridNode2.GraphIndex) { // Calculate the coordinates of the nodes var x1 = gridNode.NodeInGridIndex % gridGraph.width; var z1 = gridNode.NodeInGridIndex / gridGraph.width; var x2 = gridNode2.NodeInGridIndex % gridGraph.width; var z2 = gridNode2.NodeInGridIndex / gridGraph.width; bool wasClose = false; switch (gridGraph.neighbours) { case NumNeighbours.Four: if ((x1 == x2 && System.Math.Abs(z1-z2) == 1) || (z1 == z2 && System.Math.Abs(x1-x2) == 1)) { // If 'O' is gridNode2, then gridNode is one of the nodes marked with an 'x' // x // x O x // x wasClose = true; } break; case NumNeighbours.Eight: if (System.Math.Abs(x1-x2) <= 1 && System.Math.Abs(z1-z2) <= 1) { // If 'O' is gridNode2, then gridNode is one of the nodes marked with an 'x' // x x x // x O x // x x x wasClose = true; } break; case NumNeighbours.Six: // Hexagon graph for (int i = 0; i < 6; i++) { var nx = x2 + gridGraph.neighbourXOffsets[GridGraph.hexagonNeighbourIndices[i]]; var nz = z2 + gridGraph.neighbourZOffsets[GridGraph.hexagonNeighbourIndices[i]]; if (x1 == nx && z1 == nz) { // If 'O' is gridNode2, then gridNode is one of the nodes marked with an 'x' // x x // x O x // x x wasClose = true; break; } } break; default: // Should not happen unless NumNeighbours is modified in the future throw new System.Exception("Unhandled NumNeighbours"); } if (wasClose) { // We now need to find all nodes marked with an x to be able to mark them as targets SetFlagOnSurroundingGridNodes(gridNode2, 1, true); // Note, other methods assume hTarget is (Int3)endPoint endPoint = (Vector3)gridNode2.position; hTarget = gridNode2.position; endNode = gridNode2; // hTargetNode is used for heuristic optimizations // (also known as euclidean embedding). // Even though the endNode is not walkable // we can use it for better heuristics since // there is a workaround added (EuclideanEmbedding.ApplyGridGraphEndpointSpecialCase) // which is there to support this case. hTargetNode = endNode; // We need to save this node // so that we can reset flag1 on all nodes later gridSpecialCaseNode = gridNode2; return true; } } } return false; } /// Helper method to set PathNode.flag1 to a specific value for all nodes adjacent to a grid node void SetFlagOnSurroundingGridNodes (GridNode gridNode, int flag, bool flagState) { // Loop through all adjacent grid nodes var gridGraph = GridNode.GetGridGraph(gridNode.GraphIndex); // Number of neighbours as an int int mxnum = gridGraph.neighbours == NumNeighbours.Four ? 4 : (gridGraph.neighbours == NumNeighbours.Eight ? 8 : 6); // Calculate the coordinates of the node var x = gridNode.NodeInGridIndex % gridGraph.width; var z = gridNode.NodeInGridIndex / gridGraph.width; if (flag != 1 && flag != 2) throw new System.ArgumentOutOfRangeException("flag"); for (int i = 0; i < mxnum; i++) { int nx, nz; if (gridGraph.neighbours == NumNeighbours.Six) { // Hexagon graph nx = x + gridGraph.neighbourXOffsets[GridGraph.hexagonNeighbourIndices[i]]; nz = z + gridGraph.neighbourZOffsets[GridGraph.hexagonNeighbourIndices[i]]; } else { nx = x + gridGraph.neighbourXOffsets[i]; nz = z + gridGraph.neighbourZOffsets[i]; } // Check if the position is still inside the grid if (nx >= 0 && nz >= 0 && nx < gridGraph.width && nz < gridGraph.depth) { var adjacentNode = gridGraph.nodes[nz*gridGraph.width + nx]; var pathNode = pathHandler.GetPathNode(adjacentNode); if (flag == 1) pathNode.flag1 = flagState; else pathNode.flag2 = flagState; } } } #endif /// Prepares the path. Searches for start and end nodes and does some simple checking if a path is at all possible protected override void Prepare () { AstarProfiler.StartProfile("Get Nearest"); //Initialize the NNConstraint nnConstraint.tags = enabledTags; var startNNInfo = AstarPath.active.GetNearest(startPoint, nnConstraint); //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); } startPoint = startNNInfo.position; startIntPoint = (Int3)startPoint; startNode = startNNInfo.node; if (startNode == null) { FailWithError("Couldn't find a node close to the start point"); return; } if (!CanTraverse(startNode)) { FailWithError("The node closest to the start point could not be traversed"); return; } // If it is declared that this path type has an end point // Some path types might want to use most of the ABPath code, but will not have an explicit end point at this stage if (hasEndPoint) { var endNNInfo = AstarPath.active.GetNearest(endPoint, nnConstraint); endPoint = endNNInfo.position; endNode = endNNInfo.node; if (endNode == null) { FailWithError("Couldn't find a node close to the end point"); return; } // This should not trigger unless the user has modified the NNConstraint if (!CanTraverse(endNode)) { FailWithError("The node closest to the end point could not be traversed"); return; } // This should not trigger unless the user has modified the NNConstraint if (startNode.Area != endNode.Area) { FailWithError("There is no valid path to the target"); return; } #if !ASTAR_NO_GRID_GRAPH // Potentially we want to special case grid graphs a bit // to better support some kinds of games // If this returns true it will overwrite the // endNode, endPoint, hTarget and hTargetNode fields if (!EndPointGridGraphSpecialCase(endNNInfo.node)) #endif { // Note, other methods assume hTarget is (Int3)endPoint hTarget = (Int3)endPoint; hTargetNode = endNode; // Mark end node with flag1 to mark it as a target point pathHandler.GetPathNode(endNode).flag1 = true; } } AstarProfiler.EndProfile(); } /// /// Checks if the start node is the target and complete the path if that is the case. /// This is necessary so that subclasses (e.g XPath) can override this behaviour. /// /// If the start node is a valid target point, this method should set CompleteState to Complete /// and trace the path. /// protected virtual void CompletePathIfStartIsValidTarget () { // flag1 specifies if a node is a target node for the path if (hasEndPoint && pathHandler.GetPathNode(startNode).flag1) { CompleteWith(startNode); Trace(pathHandler.GetPathNode(startNode)); } } protected override void Initialize () { // Mark nodes to enable special connection costs for start and end nodes // See GetConnectionSpecialCost if (startNode != null) pathHandler.GetPathNode(startNode).flag2 = true; if (endNode != null) pathHandler.GetPathNode(endNode).flag2 = true; // Zero out the properties on the start node PathNode startRNode = pathHandler.GetPathNode(startNode); startRNode.node = startNode; startRNode.pathID = pathHandler.PathID; startRNode.parent = null; startRNode.cost = 0; startRNode.G = GetTraversalCost(startNode); startRNode.H = CalculateHScore(startNode); // Check if the start node is the target and complete the path if that is the case CompletePathIfStartIsValidTarget(); if (CompleteState == PathCompleteState.Complete) return; // Open the start node and puts its neighbours in the open list startNode.Open(this, startRNode, pathHandler); searchedNodes++; partialBestTarget = startRNode; // Any nodes left to search? if (pathHandler.heap.isEmpty) { if (calculatePartial) { CompleteState = PathCompleteState.Partial; Trace(partialBestTarget); } else { FailWithError("No open points, the start node didn't open any nodes"); } return; } // Pop the first node off the open list currentR = pathHandler.heap.Remove(); } protected override void Cleanup () { // TODO: Set flag1 = false as well? if (startNode != null) { var pathStartNode = pathHandler.GetPathNode(startNode); pathStartNode.flag1 = false; pathStartNode.flag2 = false; } if (endNode != null) { var pathEndNode = pathHandler.GetPathNode(endNode); pathEndNode.flag1 = false; pathEndNode.flag2 = false; } #if !ASTAR_NO_GRID_GRAPH // Set flag1 and flag2 to false on all nodes we set it to true on // at the start of the path call. Otherwise this state // will leak to other path calculations and cause all // kinds of havoc. // flag2 is also set because the end node could have changed // and thus the flag2 which is set to false above might not set // it on the correct node if (gridSpecialCaseNode != null) { var pathNode = pathHandler.GetPathNode(gridSpecialCaseNode); pathNode.flag1 = false; pathNode.flag2 = false; SetFlagOnSurroundingGridNodes(gridSpecialCaseNode, 1, false); SetFlagOnSurroundingGridNodes(gridSpecialCaseNode, 2, false); } #endif } /// /// Completes the path using the specified target node. /// This method assumes that the node is a target node of the path /// not just any random node. /// void CompleteWith (GraphNode node) { #if !ASTAR_NO_GRID_GRAPH if (endNode == node) { // Common case, no grid graph special case has been applied // Nothing to do } else { // See EndPointGridGraphSpecialCase() var gridNode = node as GridNode; if (gridNode == null) { throw new System.Exception("Some path is not cleaning up the flag1 field. This is a bug."); } // The grid graph special case has been applied // The closest point on the node is not yet known // so we need to calculate it endPoint = gridNode.ClosestPointOnNode(originalEndPoint); // This is now our end node // We didn't know it before, but apparently it was optimal // to move to this node endNode = node; } #else // This should always be true unless // the grid graph special case has been applied // which can only happen if grid graphs have not // been stripped out with ASTAR_NO_GRID_GRAPH node.MustBeEqual(endNode); #endif // Mark the path as completed CompleteState = PathCompleteState.Complete; } /// /// Calculates the path until completed or until the time has passed targetTick. /// Usually a check is only done every 500 nodes if the time has passed targetTick. /// Time/Ticks are got from System.DateTime.UtcNow.Ticks. /// /// Basic outline of what the function does for the standard path (Pathfinding.ABPath). /// /// while the end has not been found and no error has occurred /// check if we have reached the end /// if so, exit and return the path /// /// open the current node, i.e loop through its neighbours, mark them as visited and put them on a heap /// /// check if there are still nodes left to process (or have we searched the whole graph) /// if there are none, flag error and exit /// /// pop the next node of the heap and set it as current /// /// check if the function has exceeded the time limit /// if so, return and wait for the function to get called again /// /// 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) { searchedNodes++; // Close the current node, if the current node is the target node then the path is finished if (currentR.flag1) { // We found a target point // Mark that node as the end point CompleteWith(currentR.node); break; } if (currentR.H < partialBestTarget.H) { partialBestTarget = currentR; } AstarProfiler.StartFastProfile(4); // Loop through all walkable neighbours of the node and add them to the open list. currentR.node.Open(this, currentR, pathHandler); AstarProfiler.EndFastProfile(4); // Any nodes left to search? if (pathHandler.heap.isEmpty) { if (calculatePartial && partialBestTarget != null) { CompleteState = PathCompleteState.Partial; Trace(partialBestTarget); } else { FailWithError("Searched whole area but could not find target"); } return; } // 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; // Mostly for development if (searchedNodes > 1000000) { throw new System.Exception("Probable infinite loop. Over 1,000,000 nodes searched"); } } counter++; } AstarProfiler.StartProfile("Trace"); if (CompleteState == PathCompleteState.Complete) { Trace(currentR); } else if (calculatePartial && partialBestTarget != null) { CompleteState = PathCompleteState.Partial; Trace(partialBestTarget); } AstarProfiler.EndProfile(); } /// Returns a debug string for this path. internal override string DebugString (PathLog logMode) { if (logMode == PathLog.None || (!error && logMode == PathLog.OnlyErrors)) { return ""; } var text = new System.Text.StringBuilder(); DebugStringPrefix(logMode, text); if (!error && logMode == PathLog.Heavy) { if (hasEndPoint && endNode != null) { PathNode nodeR = pathHandler.GetPathNode(endNode); text.Append("\nEnd Node\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); } text.Append("\nStart Node"); text.Append("\n Point: "); text.Append(((Vector3)startPoint).ToString()); text.Append("\n Graph: "); if (startNode != null) text.Append(startNode.GraphIndex); else text.Append("< null startNode >"); } DebugStringSuffix(logMode, text); return text.ToString(); } /// \cond INTERNAL /// /// Returns in which direction to move from a point on the path. /// A simple and quite slow (well, compared to more optimized algorithms) algorithm first finds the closest path segment (from and then returns /// the direction to the next point from there. The direction is not normalized. /// Returns: Direction to move from a point, returns Vector3.zero if is null or has a length of 0 /// Deprecated: /// [System.Obsolete()] public Vector3 GetMovementVector (Vector3 point) { if (vectorPath == null || vectorPath.Count == 0) { return Vector3.zero; } if (vectorPath.Count == 1) { return vectorPath[0]-point; } float minDist = float.PositiveInfinity;//Mathf.Infinity; int minSegment = 0; for (int i = 0; i < vectorPath.Count-1; i++) { Vector3 closest = VectorMath.ClosestPointOnSegment(vectorPath[i], vectorPath[i+1], point); float dist = (closest-point).sqrMagnitude; if (dist < minDist) { minDist = dist; minSegment = i; } } return vectorPath[minSegment+1]-point; } /// \endcond } }