2023-08-01 06:49:57 +00:00
using Pathfinding.Util ;
using System.Collections.Generic ;
using UnityEngine ;
namespace Pathfinding {
/// <summary>
/// Contains useful functions for working with paths and nodes.
/// This class works a lot with the <see cref="Pathfinding.GraphNode"/> class, a useful function to get nodes is AstarPath.GetNearest.
/// See: <see cref="AstarPath.GetNearest"/>
/// See: <see cref="Pathfinding.GraphUpdateUtilities"/>
/// See: <see cref="Pathfinding.GraphUtilities"/>
/// </summary>
public static class PathUtilities {
/// <summary>
/// Returns if there is a walkable path from node1 to node2.
/// This method is extremely fast because it only uses precalculated information.
///
/// <code>
2024-02-20 18:34:40 +00:00
/// GraphNode node1 = AstarPath.active.GetNearest(point1, NNConstraint.Walkable).node;
/// GraphNode node2 = AstarPath.active.GetNearest(point2, NNConstraint.Walkable).node;
2023-08-01 06:49:57 +00:00
///
/// if (PathUtilities.IsPathPossible(node1, node2)) {
/// // Yay, there is a path between those two nodes
/// }
/// </code>
///
/// Equivalent to calling <see cref="IsPathPossible(List<GraphNode>)"/> with a list containing node1 and node2.
///
/// See: graph-updates (view in online documentation for working links)
/// See: <see cref="AstarPath.GetNearest"/>
/// </summary>
public static bool IsPathPossible ( GraphNode node1 , GraphNode node2 ) {
return node1 . Walkable & & node2 . Walkable & & node1 . Area = = node2 . Area ;
}
/// <summary>
/// Returns if there are walkable paths between all nodes.
///
/// See: graph-updates (view in online documentation for working links)
///
/// Returns true for empty lists.
///
/// See: <see cref="AstarPath.GetNearest"/>
/// </summary>
public static bool IsPathPossible ( List < GraphNode > nodes ) {
if ( nodes . Count = = 0 ) return true ;
uint area = nodes [ 0 ] . Area ;
for ( int i = 0 ; i < nodes . Count ; i + + ) if ( ! nodes [ i ] . Walkable | | nodes [ i ] . Area ! = area ) return false ;
return true ;
}
/// <summary>
/// Returns if there are walkable paths between all nodes.
/// See: graph-updates (view in online documentation for working links)
///
/// This method will actually only check if the first node can reach all other nodes. However this is
/// equivalent in 99% of the cases since almost always the graph connections are bidirectional.
/// If you are not aware of any cases where you explicitly create unidirectional connections
/// this method can be used without worries.
///
/// Returns true for empty lists
///
/// Warning: This method is significantly slower than the IsPathPossible method which does not take a tagMask
///
/// See: <see cref="AstarPath.GetNearest"/>
/// </summary>
public static bool IsPathPossible ( List < GraphNode > nodes , int tagMask ) {
if ( nodes . Count = = 0 ) return true ;
// Make sure that the first node has a valid tag
if ( ( ( tagMask > > ( int ) nodes [ 0 ] . Tag ) & 1 ) = = 0 ) return false ;
// Fast check first
if ( ! IsPathPossible ( nodes ) ) return false ;
// Make sure that the first node can reach all other nodes
var reachable = GetReachableNodes ( nodes [ 0 ] , tagMask ) ;
bool result = true ;
// Make sure that the first node can reach all other nodes
for ( int i = 1 ; i < nodes . Count ; i + + ) {
if ( ! reachable . Contains ( nodes [ i ] ) ) {
result = false ;
break ;
}
}
// Pool the temporary list
ListPool < GraphNode > . Release ( ref reachable ) ;
return result ;
}
/// <summary>
/// Returns all nodes reachable from the seed node.
/// This function performs a DFS (depth-first-search) or flood fill of the graph and returns all nodes which can be reached from
/// the seed node. In almost all cases this will be identical to returning all nodes which have the same area as the seed node.
/// In the editor areas are displayed as different colors of the nodes.
/// The only case where it will not be so is when there is a one way path from some part of the area to the seed node
/// but no path from the seed node to that part of the graph.
///
/// The returned list is not sorted in any particular way.
///
/// Depending on the number of reachable nodes, this function can take quite some time to calculate
/// so don't use it too often or it might affect the framerate of your game.
///
/// See: bitmasks (view in online documentation for working links).
///
/// Returns: A List<Node> containing all nodes reachable from the seed node.
/// For better memory management the returned list should be pooled, see Pathfinding.Util.ListPool.
/// </summary>
/// <param name="seed">The node to start the search from.</param>
/// <param name="tagMask">Optional mask for tags. This is a bitmask.</param>
/// <param name="filter">Optional filter for which nodes to search. You can combine this with tagMask = -1 to make the filter determine everything.
/// Only walkable nodes are searched regardless of the filter. If the filter function returns false the node will be treated as unwalkable.</param>
public static List < GraphNode > GetReachableNodes ( GraphNode seed , int tagMask = - 1 , System . Func < GraphNode , bool > filter = null ) {
Stack < GraphNode > dfsStack = StackPool < GraphNode > . Claim ( ) ;
List < GraphNode > reachable = ListPool < GraphNode > . Claim ( ) ;
/// <summary>TODO: Pool</summary>
var map = new HashSet < GraphNode > ( ) ;
System . Action < GraphNode > callback ;
// Check if we can use the fast path
if ( tagMask = = - 1 & & filter = = null ) {
callback = ( GraphNode node ) = > {
if ( node . Walkable & & map . Add ( node ) ) {
reachable . Add ( node ) ;
dfsStack . Push ( node ) ;
}
} ;
} else {
callback = ( GraphNode node ) = > {
if ( node . Walkable & & ( ( tagMask > > ( int ) node . Tag ) & 0x1 ) ! = 0 & & map . Add ( node ) ) {
if ( filter ! = null & & ! filter ( node ) ) return ;
reachable . Add ( node ) ;
dfsStack . Push ( node ) ;
}
} ;
}
callback ( seed ) ;
while ( dfsStack . Count > 0 ) {
dfsStack . Pop ( ) . GetConnections ( callback ) ;
}
StackPool < GraphNode > . Release ( dfsStack ) ;
return reachable ;
}
static Queue < GraphNode > BFSQueue ;
static Dictionary < GraphNode , int > BFSMap ;
/// <summary>
/// Returns all nodes up to a given node-distance from the seed node.
/// This function performs a BFS (<a href="https://en.wikipedia.org/wiki/Breadth-first_search">breadth-first search</a>) or flood fill of the graph and returns all nodes within a specified node distance which can be reached from
/// the seed node. In almost all cases when depth is large enough this will be identical to returning all nodes which have the same area as the seed node.
/// In the editor areas are displayed as different colors of the nodes.
/// The only case where it will not be so is when there is a one way path from some part of the area to the seed node
/// but no path from the seed node to that part of the graph.
///
/// The returned list is sorted by node distance from the seed node
/// i.e distance is measured in the number of nodes the shortest path from seed to that node would pass through.
/// Note that the distance measurement does not take heuristics, penalties or tag penalties.
///
/// Depending on the number of nodes, this function can take quite some time to calculate
/// so don't use it too often or it might affect the framerate of your game.
///
/// Returns: A List<GraphNode> containing all nodes reachable up to a specified node distance from the seed node.
/// For better memory management the returned list should be pooled, see Pathfinding.Util.ListPool
///
/// Warning: This method is not thread safe. Only use it from the Unity thread (i.e normal game code).
///
/// The video below shows the BFS result with varying values of depth. Points are sampled on the nodes using <see cref="GetPointsOnNodes"/>.
/// [Open online documentation to see videos]
///
/// <code>
2024-02-20 18:34:40 +00:00
/// var seed = AstarPath.active.GetNearest(transform.position, NNConstraint.Walkable).node;
2023-08-01 06:49:57 +00:00
/// var nodes = PathUtilities.BFS(seed, 10);
/// foreach (var node in nodes) {
/// Debug.DrawRay((Vector3)node.position, Vector3.up, Color.red, 10);
/// }
/// </code>
/// </summary>
/// <param name="seed">The node to start the search from.</param>
/// <param name="depth">The maximum node-distance from the seed node.</param>
/// <param name="tagMask">Optional mask for tags. This is a bitmask.</param>
/// <param name="filter">Optional filter for which nodes to search. You can combine this with depth = int.MaxValue and tagMask = -1 to make the filter determine everything.
/// Only walkable nodes are searched regardless of the filter. If the filter function returns false the node will be treated as unwalkable.</param>
public static List < GraphNode > BFS ( GraphNode seed , int depth , int tagMask = - 1 , System . Func < GraphNode , bool > filter = null ) {
#if ASTAR_PROFILE
System . Diagnostics . Stopwatch watch = new System . Diagnostics . Stopwatch ( ) ;
watch . Start ( ) ;
#endif
BFSQueue = BFSQueue ? ? new Queue < GraphNode > ( ) ;
var que = BFSQueue ;
BFSMap = BFSMap ? ? new Dictionary < GraphNode , int > ( ) ;
var map = BFSMap ;
// Even though we clear at the end of this function, it is good to
// do it here as well in case the previous invocation of the method
// threw an exception for some reason
// and didn't clear the que and map
que . Clear ( ) ;
map . Clear ( ) ;
List < GraphNode > result = ListPool < GraphNode > . Claim ( ) ;
int currentDist = - 1 ;
System . Action < GraphNode > callback ;
if ( tagMask = = - 1 ) {
callback = node = > {
if ( node . Walkable & & ! map . ContainsKey ( node ) ) {
if ( filter ! = null & & ! filter ( node ) ) return ;
map . Add ( node , currentDist + 1 ) ;
result . Add ( node ) ;
que . Enqueue ( node ) ;
}
} ;
} else {
callback = node = > {
if ( node . Walkable & & ( ( tagMask > > ( int ) node . Tag ) & 0x1 ) ! = 0 & & ! map . ContainsKey ( node ) ) {
if ( filter ! = null & & ! filter ( node ) ) return ;
map . Add ( node , currentDist + 1 ) ;
result . Add ( node ) ;
que . Enqueue ( node ) ;
}
} ;
}
callback ( seed ) ;
while ( que . Count > 0 ) {
GraphNode n = que . Dequeue ( ) ;
currentDist = map [ n ] ;
if ( currentDist > = depth ) break ;
n . GetConnections ( callback ) ;
}
que . Clear ( ) ;
map . Clear ( ) ;
#if ASTAR_PROFILE
watch . Stop ( ) ;
Debug . Log ( ( 1000 * watch . Elapsed . TotalSeconds ) . ToString ( "0.0 ms" ) ) ;
#endif
return result ;
}
/// <summary>
/// Returns points in a spiral centered around the origin with a minimum clearance from other points.
/// The points are laid out on the involute of a circle
/// See: http://en.wikipedia.org/wiki/Involute
/// Which has some nice properties.
/// All points are separated by clearance world units.
/// This method is O(n), yes if you read the code you will see a binary search, but that binary search
/// has an upper bound on the number of steps, so it does not yield a log factor.
///
/// Note: Consider recycling the list after usage to reduce allocations.
/// See: Pathfinding.Util.ListPool
/// </summary>
public static List < Vector3 > GetSpiralPoints ( int count , float clearance ) {
List < Vector3 > pts = ListPool < Vector3 > . Claim ( count ) ;
// The radius of the smaller circle used for generating the involute of a circle
// Calculated from the separation distance between the turns
float a = clearance / ( 2 * Mathf . PI ) ;
float t = 0 ;
pts . Add ( InvoluteOfCircle ( a , t ) ) ;
for ( int i = 0 ; i < count ; i + + ) {
Vector3 prev = pts [ pts . Count - 1 ] ;
// d = -t0/2 + sqrt( t0^2/4 + 2d/a )
// Minimum angle (radians) which would create an arc distance greater than clearance
float d = - t / 2 + Mathf . Sqrt ( t * t / 4 + 2 * clearance / a ) ;
// Binary search for separating this point and the previous one
float mn = t + d ;
float mx = t + 2 * d ;
while ( mx - mn > 0.01f ) {
float mid = ( mn + mx ) / 2 ;
Vector3 p = InvoluteOfCircle ( a , mid ) ;
if ( ( p - prev ) . sqrMagnitude < clearance * clearance ) {
mn = mid ;
} else {
mx = mid ;
}
}
pts . Add ( InvoluteOfCircle ( a , mx ) ) ;
t = mx ;
}
return pts ;
}
/// <summary>
/// Returns the XZ coordinate of the involute of circle.
/// See: http://en.wikipedia.org/wiki/Involute
/// </summary>
private static Vector3 InvoluteOfCircle ( float a , float t ) {
return new Vector3 ( a * ( Mathf . Cos ( t ) + t * Mathf . Sin ( t ) ) , 0 , a * ( Mathf . Sin ( t ) - t * Mathf . Cos ( t ) ) ) ;
}
/// <summary>
/// Will calculate a number of points around p which are on the graph and are separated by clearance from each other.
/// This is like GetPointsAroundPoint except that previousPoints are treated as being in world space.
/// The average of the points will be found and then that will be treated as the group center.
/// </summary>
/// <param name="p">The point to generate points around</param>
/// <param name="g">The graph to use for linecasting. If you are only using one graph, you can get this by AstarPath.active.graphs[0] as IRaycastableGraph.
/// Note that not all graphs are raycastable, recast, navmesh and grid graphs are raycastable. On recast and navmesh it works the best.</param>
/// <param name="previousPoints">The points to use for reference. Note that these are in world space.
/// The new points will overwrite the existing points in the list. The result will be in world space.</param>
/// <param name="radius">The final points will be at most this distance from p.</param>
/// <param name="clearanceRadius">The points will if possible be at least this distance from each other.</param>
public static void GetPointsAroundPointWorld ( Vector3 p , IRaycastableGraph g , List < Vector3 > previousPoints , float radius , float clearanceRadius ) {
if ( previousPoints . Count = = 0 ) return ;
Vector3 avg = Vector3 . zero ;
for ( int i = 0 ; i < previousPoints . Count ; i + + ) avg + = previousPoints [ i ] ;
avg / = previousPoints . Count ;
for ( int i = 0 ; i < previousPoints . Count ; i + + ) previousPoints [ i ] - = avg ;
GetPointsAroundPoint ( p , g , previousPoints , radius , clearanceRadius ) ;
}
/// <summary>
/// Will calculate a number of points around center which are on the graph and are separated by clearance from each other.
/// The maximum distance from center to any point will be radius.
/// Points will first be tried to be laid out as previousPoints and if that fails, random points will be selected.
/// This is great if you want to pick a number of target points for group movement. If you pass all current agent points from e.g the group's average position
/// this method will return target points so that the units move very little within the group, this is often aesthetically pleasing and reduces jitter if using
/// some kind of local avoidance.
///
/// TODO: Write unit tests
/// </summary>
/// <param name="center">The point to generate points around</param>
/// <param name="g">The graph to use for linecasting. If you are only using one graph, you can get this by AstarPath.active.graphs[0] as IRaycastableGraph.
/// Note that not all graphs are raycastable, recast, navmesh and grid graphs are raycastable. On recast and navmesh it works the best.</param>
/// <param name="previousPoints">The points to use for reference. Note that these should not be in world space. They are treated as relative to center.
/// The new points will overwrite the existing points in the list. The result will be in world space, not relative to center.</param>
/// <param name="radius">The final points will be at most this distance from center.</param>
/// <param name="clearanceRadius">The points will if possible be at least this distance from each other.</param>
public static void GetPointsAroundPoint ( Vector3 center , IRaycastableGraph g , List < Vector3 > previousPoints , float radius , float clearanceRadius ) {
if ( g = = null ) throw new System . ArgumentNullException ( "g" ) ;
var graph = g as NavGraph ;
if ( graph = = null ) throw new System . ArgumentException ( "g is not a NavGraph" ) ;
2024-02-20 18:34:40 +00:00
NNInfoInternal nn = graph . GetNearestForce ( center , PathNNConstraint . Walkable ) ;
2023-08-01 06:49:57 +00:00
center = nn . clampedPosition ;
if ( nn . node = = null ) {
// No valid point to start from
return ;
}
// Make sure the enclosing circle has a radius which can pack circles with packing density 0.5
radius = Mathf . Max ( radius , 1.4142f * clearanceRadius * Mathf . Sqrt ( previousPoints . Count ) ) ; //Mathf.Sqrt(previousPoints.Count*clearanceRadius*2));
clearanceRadius * = clearanceRadius ;
for ( int i = 0 ; i < previousPoints . Count ; i + + ) {
Vector3 dir = previousPoints [ i ] ;
float magn = dir . magnitude ;
if ( magn > 0 ) dir / = magn ;
float newMagn = radius ; //magn > radius ? radius : magn;
dir * = newMagn ;
GraphHitInfo hit ;
int tests = 0 ;
while ( true ) {
Vector3 pt = center + dir ;
if ( g . Linecast ( center , pt , out hit ) ) {
if ( hit . point = = Vector3 . zero ) {
// Oops, linecast actually failed completely
// try again unless we have tried lots of times
// then we just continue anyway
tests + + ;
if ( tests > 8 ) {
previousPoints [ i ] = pt ;
break ;
}
} else {
pt = hit . point ;
}
}
bool worked = false ;
for ( float q = 0.1f ; q < = 1.0f ; q + = 0.05f ) {
Vector3 qt = Vector3 . Lerp ( center , pt , q ) ;
worked = true ;
for ( int j = 0 ; j < i ; j + + ) {
if ( ( previousPoints [ j ] - qt ) . sqrMagnitude < clearanceRadius ) {
worked = false ;
break ;
}
}
// Abort after 8 tests or when we have found a valid point
if ( worked | | tests > 8 ) {
worked = true ;
previousPoints [ i ] = qt ;
break ;
}
}
// Break out of nested loop
if ( worked ) {
break ;
}
// If we could not find a valid point, reduce the clearance radius slightly to improve
// the chances next time
clearanceRadius * = 0.9f ;
// This will pick points in 2D closer to the edge of the circle with a higher probability
dir = Random . onUnitSphere * Mathf . Lerp ( newMagn , radius , tests / 5 ) ;
dir . y = 0 ;
tests + + ;
}
}
}
/// <summary>
/// Returns randomly selected points on the specified nodes with each point being separated by clearanceRadius from each other.
/// Selecting points ON the nodes only works for TriangleMeshNode (used by Recast Graph and Navmesh Graph) and GridNode (used by GridGraph).
/// For other node types, only the positions of the nodes will be used.
///
/// clearanceRadius will be reduced if no valid points can be found.
///
/// Note: This method assumes that the nodes in the list have the same type for some special cases.
/// More specifically if the first node is not a TriangleMeshNode or a GridNode, it will use a fast path
/// which assumes that all nodes in the list have the same surface area (which usually is a surface area of zero and the
/// nodes are all PointNodes).
/// </summary>
public static List < Vector3 > GetPointsOnNodes ( List < GraphNode > nodes , int count , float clearanceRadius = 0 ) {
if ( nodes = = null ) throw new System . ArgumentNullException ( "nodes" ) ;
if ( nodes . Count = = 0 ) throw new System . ArgumentException ( "no nodes passed" ) ;
List < Vector3 > pts = ListPool < Vector3 > . Claim ( count ) ;
// Square
clearanceRadius * = clearanceRadius ;
if ( clearanceRadius > 0 | | nodes [ 0 ] is TriangleMeshNode
#if ! ASTAR_NO_GRID_GRAPH
| | nodes [ 0 ] is GridNode
#endif
) {
// Accumulated area of all nodes
List < float > accs = ListPool < float > . Claim ( nodes . Count ) ;
// Total area of all nodes so far
float tot = 0 ;
for ( int i = 0 ; i < nodes . Count ; i + + ) {
var surfaceArea = nodes [ i ] . SurfaceArea ( ) ;
// Ensures that even if the nodes have a surface area of 0, a random one will still be picked
// instead of e.g always picking the first or the last one.
surfaceArea + = 0.001f ;
tot + = surfaceArea ;
accs . Add ( tot ) ;
}
for ( int i = 0 ; i < count ; i + + ) {
//Pick point
int testCount = 0 ;
int testLimit = 10 ;
bool worked = false ;
while ( ! worked ) {
worked = true ;
// If no valid points could be found, progressively lower the clearance radius until such a point is found
if ( testCount > = testLimit ) {
// Note that clearanceRadius is a squared radius
clearanceRadius * = 0.9f * 0.9f ;
testLimit + = 10 ;
if ( testLimit > 100 ) clearanceRadius = 0 ;
}
// Pick a random node among the ones in the list weighted by their area
float tg = Random . value * tot ;
int v = accs . BinarySearch ( tg ) ;
if ( v < 0 ) v = ~ v ;
if ( v > = nodes . Count ) {
// Cover edge cases
worked = false ;
continue ;
}
var node = nodes [ v ] ;
var p = node . RandomPointOnSurface ( ) ;
// Test if it is some distance away from the other points
if ( clearanceRadius > 0 ) {
for ( int j = 0 ; j < pts . Count ; j + + ) {
if ( ( pts [ j ] - p ) . sqrMagnitude < clearanceRadius ) {
worked = false ;
break ;
}
}
}
if ( worked ) {
pts . Add ( p ) ;
break ;
}
testCount + + ;
}
}
ListPool < float > . Release ( ref accs ) ;
} else {
// Fast path, assumes all nodes have the same area (usually zero)
for ( int i = 0 ; i < count ; i + + ) {
pts . Add ( ( Vector3 ) nodes [ Random . Range ( 0 , nodes . Count ) ] . RandomPointOnSurface ( ) ) ;
}
}
return pts ;
}
}
}