1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
|
using UnityEngine;
using System.Collections.Generic;
using Unity.Mathematics;
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 () {
//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;
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 = AstarPath.active.GetNearest(originalEndPoint, nnConstraint);
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();
}
}
}
|