Searching for paths
Tutorial on how to calculate paths.
- Introduction
- Using a built-in movement script
- Using the Seeker component
- Using the AstarPath class directly
- Path pooling
Introduction
Pathfinding is all about calculating paths. But there are many variations on this. The most basic path goes from a point A to another point B, but there are other path types that for example calculate paths to the closest point of a list of candidate points, and path types that calculate a path in a random direction.
Furthermore, you may be using a built-in movement script, in which case it will want to handle pathfinding requests, or you may want to write a custom movement script, in which case you will have to handle pathfinding requests yourself. You may even want to calculate a path without a movement script at all, for example to show a preview path to the player, before any movement takes place.
This page will cover how to calculate paths in a number of different ways.
Using a built-in movement script
If you are using one of the built-in movement scripts, the agent periodically recalculates its path completely automatically. The only thing you need to do is to set the ai.destination property to the target point, or use the AIDestinationSetter to make it move towards an existing GameObject. You can control how often the agent recalculates its path using the properties on the AutoRepathPolicy, which is part of the movement script.
void PointAndClick (IAstarAI ai) {
If you need to, you can force the agent to recalculate its path immediately using ai.SearchPath.
// Check if the mouse button is pressed
if (Input.GetMouseButton(0)) {
var cam = Camera.main;
// Make the raycast hit all colliders
LayerMask mask = -1;
// Shoot a ray from the cursor, to see where in the world it hits
if (Physics.Raycast(cam.ScreenPointToRay(Input.mousePosition), out var hit, Mathf.Infinity, mask)) {
// Set the destination of the AI to the point where the ray hit
ai.destination = hit.point;
}
}
}
In some cases you may want to take more direct control over the pathfinding requests. In that case you can use the ai.SetPath method to assign a pathfinding request, or an already calculated path, to the agent. However, you should take care to disable the agent's automatic path recalculations if you do this, otherwise the agent will overwrite your path with its own path shortly after you set it.
// Disable the automatic path recalculation
ai.canSearch = false;
var pointToAvoid = enemy.position;
// Make the AI flee from the enemy.
// The path will be about 20 world units long (the default cost of moving 1 world unit is 1000).
var path = FleePath.Construct(ai.position, pointToAvoid, 1000 * 20);
ai.SetPath(path);
Using the Seeker component
If you are writing your own movement script, or you have some other reason to calculate paths yourself, you can use the Seeker component to calculate paths. The Seeker component can be attached to any GameObject and it will handle pathfinding requests for that GameObject. It has a convenient collection of methods that make it easy to request paths, it also contains various pathfinding settings, and it can handle path modifiers.
Most built-in movement scripts use the Seeker component internally to calculate paths. The exception is the FollowerEntity component, since it uses Unity's Entity Component System behind the scenes, and that doesn't play well with traditional Unity components.
A Seeker will only process one pathfinding request at a time. If you request a new path before the previous one has been completed, the previous path request will be canceled. If you want to calculate multiple paths simultaneously you should skip the Seeker and use the AstarPath class directly.
void Start () {
The above code will request a path from the current position of the GameObject to a position 10 units forward. When the path has been calculated, the OnPathComplete method will be called with the path as an argument. If you attach any path modifiers to the same GameObject as the Seeker, they will be applied to the path before the OnPathComplete method is called.
// Get the seeker component attached to this GameObject
var seeker = GetComponent<Seeker>();
// Schedule a new path request from the current position to a position 10 units forward.
// When the path has been calculated, the OnPathComplete method will be called, unless it was canceled by another path request
seeker.StartPath(transform.position, transform.position + Vector3.forward * 10, OnPathComplete);
// Note that the path is NOT calculated at this point
// It has just been queued for calculation
}
void OnPathComplete (Path path) {
// The path is now calculated!
if (path.error) {
Debug.LogError("Path failed: " + path.errorLog);
return;
}
// Cast the path to the path type we were using
var abPath = path as ABPath;
// Draw the path in the scene view for 10 seconds
for (int i = 0; i < abPath.vectorPath.Count - 1; i++) {
Debug.DrawLine(abPath.vectorPath[i], abPath.vectorPath[i+1], Color.red, 10);
}
}
Try it out in your own scene. Attach a Seeker component to a GameObject, and then attach the above script to the same GameObject. It may take a frame or two for the path to be calculated, depending on the complexity of the path, and how many other paths are scheduled at the same time.
If your path requests are failing, then you may want to take a look at the page Error messages for a list of common error messages and how to solve them.
A common mistake is to assume that the path is already calculated right after the StartPath call. This is, however, incorrect, as the StartPath call will only put the path in a queue. This is done because when many units are calculating their paths at the same time, it is desirable to spread out the path calculations over several frames to avoid FPS drops. If you have enabled multithreading, all paths will be calculated asynchronously on separate threads.
There are, of course, cases when you need to calculate the path immediately. Then you can use the Path.BlockUntilCalculated method.
var path = seeker.StartPath(transform.position, transform.position + Vector3.forward * 10, OnPathComplete);
You can also wait for the path to be calculated in a coroutine using Path.WaitForPath.
path.BlockUntilCalculated();
// The path is calculated now, and the OnPathComplete callback has been called
IEnumerator Start () {
You can also create your own path objects, instead of using the Seeker's methods. This will allow you to change settings on the path object before calculating it.
// Get the seeker component attached to this GameObject
var seeker = GetComponent<Seeker>();
var path = seeker.StartPath(transform.position, transform.position + Vector3.forward * 10, null);
// Wait... This may take a frame or two depending on how complex the path is
// The rest of the game will continue to run while we wait
yield return StartCoroutine(path.WaitForPath());
// The path is calculated now
// Draw the path in the scene view for 10 seconds
for (int i = 0; i < path.vectorPath.Count - 1; i++) {
Debug.DrawLine(path.vectorPath[i], path.vectorPath[i+1], Color.red, 10);
}
}
// Create a new path object, the last parameter is a callback function
Observant readers may notice a GC allocation incurring every time a delegate to OnPathComplete is created. It's not much, but it adds up. You can avoid this allocation by caching a delegate in a local field.
// but it will be used internally by the seeker, so we will set it to null here
// Paths are created using the static Construct call because then it can use
// pooled paths, instead of creating a new path object all the time.
// This is is a nice way to avoid frequent GC spikes.
var p = ABPath.Construct(transform.position, transform.position+transform.forward*10, null);
// By default, a search for the closest walkable nodes to the start and end nodes will be carried out
// but for example in a turn based game, you might not want it to search for the closest walkable node, but instead
// return an error if the target point was at an unwalkable node.
// Setting the NNConstraint to None will disable the nearest walkable node search
p.nnConstraint = NNConstraint.None;
// Schedule the path request by sending it to the Seeker
seeker.StartPath(p, OnPathComplete);
protected OnPathDelegate onPathComplete;
The Seeker component is intended to handle pathfinding requests for a single character in the game. For this reason it will only process a single path request at a time. If you call StartPath when another path is already being calculated, it will log a warning saying that the previous path calculation was aborted. If you specifically wanted to cancel the previous path request you can do this without logging a warning by calling Seeker.CancelCurrentPathRequest.
void OnEnable () {
onPathComplete = OnPathComplete;
}
public void SearchPath () {
// Now use the onPathComplete field, instead of OnPathComplete directly, to avoid a GC allocation
GetComponent<Seeker>().StartPath(transform.position, transform.position+transform.forward*10, onPathComplete);
}
void OnPathComplete (Path p) {
// Just in case this component was destroyed while a path was being calculated
if (!this) return;
}
If you want to calculate multiple paths simultaneously, you should skip the Seeker. Take a look at the Using the AstarPath class directly section instead.
Other types of paths
There are other types of paths than the standard one, for example the MultiTargetPath (Pro feature). These can be scheduled easily as well, especially the MultiTargetPath since the Seeker has a special function for it
var endPoints = new Vector3[] {
transform.position + Vector3.forward * 5,
transform.position + Vector3.right * 10,
transform.position + Vector3.back * 15
};
// Start a multi target path, where endPoints is a Vector3[] array.
// The pathsForAll parameter specifies if a path to every end point should be searched for
// or if it should only try to find the shortest path to any end point.
var path = seeker.StartMultiTargetPath(transform.position, endPoints, pathsForAll: true, callback: null);
path.BlockUntilCalculated();
if (path.error) {
Debug.LogError("Error calculating path: " + path.errorLog);
return;
}
Debug.Log("The closest target was index " + path.chosenTarget);
// Draw the path to all targets
foreach (var subPath in path.vectorPaths) {
for (int i = 0; i < subPath.Count - 1; i++) {
Debug.DrawLine(subPath[i], subPath[i+1], Color.green, 10);
}
}
// Draw the path to the closest target
for (int i = 0; i < path.vectorPath.Count - 1; i++) {
Debug.DrawLine(path.vectorPath[i], path.vectorPath[i+1], Color.red, 10);
}
The MultiTargetPath is an A* Pathfinding Project Pro Feature, so you will get an error if you try the above code using the Free version of the project.
The generic way to schedule any type of path is Path p = MyPathType.Construct (...); // Where MyPathType is for example MultiTargetPath
seeker.StartPath(p, OnPathComplete);
The Construct method is used instead of a constructor so that path pooling can be done more easily.
Using the AstarPath class directly
If you want even more control of each path. Then you can use the AstarPath component directly. The main function you then use is AstarPath.StartPath. This is useful if you want to calculate a lot of paths at the same time. The Seeker is for agents that only have one active path at a time, and if you try to request multiple paths at the same time it will only calculate the last one and cancel the rest.
The Seeker interally uses the AstarPath.StartPath method to calculate the paths.
The paths calculated using AstarPath.StartPath will not be post-processed. However, you can call Seeker.PostProcess after a path has been calculated, to post-process it using the modifiers attached to a particular Seeker.
// There must be an AstarPath instance in the scene
if (AstarPath.active == null) return;
// We can calculate multiple paths asynchronously
for (int i = 0; i < 10; i++) {
var path = ABPath.Construct(transform.position, transform.position+transform.forward*i*10, OnPathComplete);
// Calculate the path by using the AstarPath component directly
AstarPath.StartPath(path);
}
Path pooling
To avoid allocations when creating new path objects, path pooling can be used. This has been skipped in the above examples, but if you care about minimizing garbage collection, it might be a good idea to use it. The built-in movement scripts already use path pooling internally.
You can read more about path pooling in: Pooling.