2025-07-08 10:46:31 +00:00
using UnityEngine ;
using System.Collections.Generic ;
using Unity.Mathematics ;
using Pathfinding.Pooling ;
namespace Pathfinding {
/// <summary>
/// Basic path, finds the shortest path from A to B.
///
/// This is the most basic path object it will try to find the shortest path between two points.
/// 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)
/// </summary>
public class ABPath : Path {
/// <summary>Start node of the path</summary>
public GraphNode startNode = > path . Count > 0 ? path [ 0 ] : null ;
/// <summary>End node of the path</summary>
public GraphNode endNode = > path . Count > 0 ? path [ path . Count - 1 ] : null ;
/// <summary>Start Point exactly as in the path request</summary>
public Vector3 originalStartPoint ;
/// <summary>End Point exactly as in the path request</summary>
public Vector3 originalEndPoint ;
/// <summary>
/// Start point of the path.
/// This is the closest point on the <see cref="startNode"/> to <see cref="originalStartPoint"/>
/// </summary>
public Vector3 startPoint ;
/// <summary>
/// End point of the path.
/// This is the closest point on the <see cref="endNode"/> to <see cref="originalEndPoint"/>
/// </summary>
public Vector3 endPoint ;
/// <summary>
/// Total cost of this path as used by the pathfinding algorithm.
///
/// The cost is influenced by both the length of the path, as well as any tags or penalties on the nodes.
/// By default, the cost to move 1 world unit is <see cref="Int3.Precision"/>.
///
/// If the path failed, the cost will be set to zero.
///
/// See: tags (view in online documentation for working links)
/// </summary>
public uint cost ;
/// <summary>
/// Determines if a search for an end node should be done.
/// Set by different path types.
/// Since: Added in 3.0.8.3
/// </summary>
protected virtual bool hasEndPoint = > true ;
/// <summary>
/// True if this path type has a well defined end point, even before calculation starts.
///
/// This is for example true for the <see cref="ABPath"/> type, but false for the <see cref="RandomPath"/> type.
/// </summary>
public virtual bool endPointKnownBeforeCalculation = > true ;
/// <summary>
/// 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
///
/// The <see cref="endNode"/> and <see cref="endPoint"/> will be modified and be set to the node which ends up being closest to the target.
///
/// Warning: Using this may make path calculations significantly slower if you have a big graph. The reason is that
/// when the target node cannot be reached, the path must search through every single other node that it can reach in order
/// to determine which one is closest. This may be expensive, and is why this option is disabled by default.
/// </summary>
public bool calculatePartial ;
/// <summary>
/// Current best target for the partial path.
/// This is the node with the lowest H score.
/// </summary>
protected uint partialBestTargetPathNodeIndex = GraphNode . InvalidNodeIndex ;
protected uint partialBestTargetHScore = uint . MaxValue ;
protected uint partialBestTargetGScore = uint . MaxValue ;
/// <summary>
/// Optional ending condition for the path.
///
/// The ending condition determines when the path has been completed.
/// Can be used to for example mark a path as complete when it is within a specific distance from the target.
///
/// If ending conditions are used that are not centered around the endpoint of the path,
/// then you should also set the <see cref="heuristic"/> to None to ensure the path is still optimal.
/// The performance impact of setting the <see cref="heuristic"/> to None is quite large, so you might want to try to run it with the default
/// heuristic to see if the path is good enough for your use case anyway.
///
/// If null, no custom ending condition will be used. This means that the path will end when the target node has been reached.
///
/// Note: If the ending condition returns false for all nodes, the path will just keep searching until it has searched the whole graph. This can be slow.
///
/// See: <see cref="PathEndingCondition"/>
/// </summary>
public PathEndingCondition endingCondition ;
/// <summary>@{ @name Constructors</summary>
/// <summary>
/// Default constructor.
/// Do not use this. Instead use the static Construct method which can handle path pooling.
/// </summary>
public ABPath ( ) { }
/// <summary>
/// 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
/// </summary>
public static ABPath Construct ( Vector3 start , Vector3 end , OnPathDelegate callback = null ) {
var p = PathPool . GetPath < ABPath > ( ) ;
p . Setup ( start , end , callback ) ;
return p ;
}
protected void Setup ( Vector3 start , Vector3 end , OnPathDelegate callbackDelegate ) {
callback = callbackDelegate ;
UpdateStartEnd ( start , end ) ;
}
/// <summary>
/// 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.
///
/// <code>
/// var path = ABPath.FakePath(new List<Vector3> { new Vector3(1, 2, 3), new Vector3(4, 5, 6) });
///
/// ai.SetPath(path);
/// </code>
///
/// You can use it to combine existing paths like this:
///
/// <code>
/// 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);
/// </code>
/// </summary>
public static ABPath FakePath ( List < Vector3 > vectorPath , List < GraphNode > nodePath = null ) {
var path = PathPool . GetPath < ABPath > ( ) ;
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 ] ) ;
}
return path ;
}
/// <summary>@}</summary>
/// <summary>
/// Sets the start and end points.
/// Sets <see cref="originalStartPoint"/>, <see cref="originalEndPoint"/>, <see cref="startPoint"/>, <see cref="endPoint"/>
/// </summary>
protected void UpdateStartEnd ( Vector3 start , Vector3 end ) {
originalStartPoint = start ;
originalEndPoint = end ;
startPoint = start ;
endPoint = end ;
}
/// <summary>
/// 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 ();
/// </summary>
protected override void Reset ( ) {
base . Reset ( ) ;
originalStartPoint = Vector3 . zero ;
originalEndPoint = Vector3 . zero ;
startPoint = Vector3 . zero ;
endPoint = Vector3 . zero ;
calculatePartial = false ;
partialBestTargetPathNodeIndex = GraphNode . InvalidNodeIndex ;
partialBestTargetHScore = uint . MaxValue ;
partialBestTargetGScore = uint . MaxValue ;
cost = 0 ;
endingCondition = null ;
}
#if ! ASTAR_NO_GRID_GRAPH
/// <summary>Cached <see cref="Pathfinding.NNConstraint.None"/> to reduce allocations</summary>
static readonly NNConstraint NNConstraintNone = NNConstraint . None ;
/// <summary>
/// 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, new temporary endpoints will have been added
///
/// 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]
/// </summary>
protected virtual bool EndPointGridGraphSpecialCase ( GraphNode closestWalkableEndNode , Vector3 originalEndPoint , int targetIndex ) {
var gridNode = closestWalkableEndNode as GridNode ;
if ( gridNode ! = null ) {
var gridGraph = GridNode . GetGridGraph ( gridNode . GraphIndex ) ;
// Find the closest node, not neccessarily walkable
var endNNInfo2 = gridGraph . GetNearest ( originalEndPoint , NNConstraintNone ) ;
var gridNode2 = endNNInfo2 . node as GridNode ;
if ( gridNode ! = gridNode2 & & gridNode2 ! = null ) {
// 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
AddEndpointsForSurroundingGridNodes ( gridNode2 , originalEndPoint , targetIndex ) ;
// 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.
// TODO
return true ;
}
}
}
return false ;
}
/// <summary>Helper method to add endpoints around a specific unwalkable grid node</summary>
void AddEndpointsForSurroundingGridNodes ( GridNode gridNode , Vector3 desiredPoint , int targetIndex ) {
// 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 ;
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 ] ;
}
var adjacentNode = gridGraph . GetNode ( nx , nz ) ;
// Check if the position is still inside the grid
if ( adjacentNode ! = null ) {
pathHandler . AddTemporaryNode ( new TemporaryNode {
type = TemporaryNodeType . End ,
position = ( Int3 ) adjacentNode . ClosestPointOnNode ( desiredPoint ) ,
associatedNode = adjacentNode . NodeIndex ,
targetIndex = targetIndex ,
} ) ;
}
}
}
#endif
/// <summary>Prepares the path. Searches for start and end nodes and does some simple checking if a path is at all possible</summary>
protected override void Prepare ( ) {
var startNNInfo = GetNearest ( startPoint ) ;
//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 ) ;
}
startPoint = startNNInfo . position ;
if ( startNNInfo . node = = null ) {
FailWithError ( "Couldn't find a node close to the start point" ) ;
return ;
}
if ( ! CanTraverse ( startNNInfo . node ) ) {
FailWithError ( "The node closest to the start point could not be traversed" ) ;
return ;
}
pathHandler . AddTemporaryNode ( new TemporaryNode {
associatedNode = startNNInfo . node . NodeIndex ,
position = ( Int3 ) startNNInfo . position ,
type = TemporaryNodeType . Start ,
} ) ;
// 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
uint endNodeIndex = 0 ;
if ( hasEndPoint ) {
var endNNInfo = GetNearest ( originalEndPoint ) ;
endPoint = endNNInfo . position ;
if ( endNNInfo . node = = 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 ( endNNInfo . node ) ) {
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 ( startNNInfo . node . Area ! = endNNInfo . node . Area ) {
FailWithError ( "There is no valid path to the target" ) ;
return ;
}
endNodeIndex = endNNInfo . node . NodeIndex ;
#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 , originalEndPoint , 0 ) )
#endif
{
pathHandler . AddTemporaryNode ( new TemporaryNode {
associatedNode = endNNInfo . node . NodeIndex ,
position = ( Int3 ) endNNInfo . position ,
type = TemporaryNodeType . End ,
} ) ;
}
}
TemporaryEndNodesBoundingBox ( out var mnTarget , out var mxTarget ) ;
heuristicObjective = new HeuristicObjective ( mnTarget , mxTarget , heuristic , heuristicScale , endNodeIndex , AstarPath . active . euclideanEmbedding ) ;
MarkNodesAdjacentToTemporaryEndNodes ( ) ;
AddStartNodesToHeap ( ) ;
}
void CompletePartial ( ) {
CompleteState = PathCompleteState . Partial ;
// TODO: Add unit test for this
endPoint = pathHandler . GetNode ( partialBestTargetPathNodeIndex ) . ClosestPointOnNode ( originalEndPoint ) ;
cost = partialBestTargetGScore ;
Trace ( partialBestTargetPathNodeIndex ) ;
}
protected override void OnHeapExhausted ( ) {
if ( calculatePartial & & partialBestTargetPathNodeIndex ! = GraphNode . InvalidNodeIndex ) {
CompletePartial ( ) ;
} else {
FailWithError ( "Searched all reachable nodes, but could not find target. This can happen if you have nodes with a different tag blocking the way to the goal. You can enable path.calculatePartial to handle that case as a workaround (though this comes with a performance cost)." ) ;
}
}
protected override void OnFoundEndNode ( uint pathNode , uint hScore , uint gScore ) {
if ( pathHandler . IsTemporaryNode ( pathNode ) ) {
// Common case, a temporary node is used to represent the target.
// However, it may not be clamped to the closest point on the node.
// In particular the grid graph special case will not clamp it.
// So we clamp it here instead.
var tempNode = pathHandler . GetTemporaryNode ( pathNode ) ;
var associatedNode = pathHandler . GetNode ( tempNode . associatedNode ) ;
if ( endingCondition ! = null & & ! endingCondition . TargetFound ( associatedNode , partialBestTargetHScore , gScore ) ) {
// The ending condition is not fulfilled, so we should not stop here.
// This is a weird situation where the ending condition doesn't consider the closest node to the destination
// as a valid end node. It can be useful in rare cases, though.
return ;
}
endPoint = ( Vector3 ) tempNode . position ;
endPoint = associatedNode . ClosestPointOnNode ( endPoint ) ;
} else {
// The target node is a normal node. We use the center of the node as the end point.
// This can happen when using a custom ending condition.
var node = pathHandler . GetNode ( pathNode ) ;
endPoint = ( Vector3 ) node . position ;
}
cost = gScore ;
CompleteState = PathCompleteState . Complete ;
Trace ( pathNode ) ;
}
public override void OnVisitNode ( uint pathNode , uint hScore , uint gScore ) {
// This method may be called multiple times without checking if the path is complete yet.
if ( CompleteState ! = PathCompleteState . NotCalculated ) return ;
if ( endingCondition ! = null ) {
var node = pathHandler . GetNode ( pathNode ) ;
if ( endingCondition . TargetFound ( node , hScore , gScore ) ) {
OnFoundEndNode ( pathNode , hScore , gScore ) ;
if ( CompleteState = = PathCompleteState . Complete ) {
return ;
}
}
}
if ( hScore < partialBestTargetHScore ) {
partialBestTargetPathNodeIndex = pathNode ;
partialBestTargetHScore = hScore ;
partialBestTargetGScore = gScore ;
}
}
/// <summary>Returns a debug string for this path.</summary>
protected 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 ) {
text . Append ( " Path Cost: " ) ;
text . Append ( cost ) ;
}
if ( ! error & & logMode = = PathLog . Heavy ) {
if ( hasEndPoint & & endNode ! = null ) {
// 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 ( ) ;
}
}
}