Pathfinding With Built-In Systems
Pathfind and AgentPos Components
To use the NorthstarPlugin
pathfinding systems, insert a Pathfind
and AgentPos
on the entity you want to pathfind.
You will need to maintain the grid position in AgentPos
.
#![allow(unused)] fn main() { commands .spawn(( Name::new("Player"), // Request a path to 8,8 Pathfind::new(UVec3::new(8, 8, 0)), // The entities current position in the grid AgentPos(UVec3::new(4, 4, 0)) Blocking, // Insert the Blocking component if using collision and this entity should block others. )); }
There are also shorthand constructors for creating Pathfind
.
#![allow(unused)] fn main() { // If you're on a 2d grid you can use `new_2d()` without having to create a new UVec3. Pathfind::new_2d(8, 8) // Same with 3d Pathfind::new_3d(8, 8, 4) }
Pathfind Configuration
Pathfind has configuration options you can set by chaining.
mode(mode)
Default: PathfindMode::Refined
Use this to set the desired algorithm to find the goal. Ex: Pathfind::new_2d(8, 8).mode(PathfindMode::AStar)
.
See below for a list of PathfindMode
s and their description.
partial()
Default: Not enabled
Apply .partial()
to request an incomplete path if the goal is not reachable. Ex: Pathfind::new_2d(4, 4).mode(PathfindMode::Astar).partial()
.
PathfindMode
The pathfinding algorithm enum. Current options are:
PathfindMode::Refined
This is the default algorithm
Gets a high level path to the goal at the chunk level. If a path is found, the path is iterated over with a line of sight / tracing algorithm to attempt to create the shortest path to the goal. The refinement is more expensive than the HPA* algorithm but not nearly as expensive as using A*.
PathfindMode::Coarse
Returns the unrefined HPA* path pulled from the cached entrance paths. This will not return the path with the least steps to the goal but is extremely fast to generate. It's great for natural paths NPCs might use to move around a building for example.
PathfindMode::AStar
This is standard A* pathfinding. It's very expensive for long distance goals on large maps but is still useful for very short distances or when you're concerned with the absolute shortest path. A good use would be movement where action points are subtracted based on number of moves.
NextPos
The pathfind system detects entities with a changed Pathfind
component. It then runs the pathfinding algorithm and, if a valid path is found, inserts the next step as a NextPos
component.
You should consume the NextPos
component by moving the entity accordingly and then removing the component afterward. In a subsequent frame, the next_position system will insert the next NextPos
if the path is not yet complete.
If collision avoidance is enabled, the next_position system will also handle local avoidance. It may adjust the path if another entity is blocking the current path within the configured avoidance_distance (as set in GridSettingsBuilder
).
See Grid Settings for more information on enabling and configuring collision.
Example movement system:
#![allow(unused)] fn main() { fn movement( mut commands: Commands, mut query: Query<(Entity, &mut AgentPos, &NextPos)>, ) { for (entity, mut agent_pos, next_pos) in &mut query { // Set the entities GridPos to the NextPos UVec3. agent_pos.0 = next_pos.0; // Update the entities translation let translation = Vec3::new( next.0.x as f32 * 32.0, // Assuming tiles are 32x32 next.0.y as f32 * 32.0, 0.0 ); commands.entity(entity) .insert(Transform::from_translation(translation)) .remove::<NextPos>(); } } }
Pathfinding/Collision Marker Components
PathfindingFailed
This component is inserted into an entity if a path to the desired goal cannot be found. You will want to create a system that determines how to handle the failure in a way unique to your game.
AvoidanceFailed
This component is inserted when collision avoidance is enabled and the entity cannot find a path around a local Blocking
entity.
The reroute_path
system will automatically attempt to compute a new full HPA* path to resolve the issue in the next frame. You may also choose to handle this yourself in a custom system.
RerouteFailed
This component is added when all attempts to resolve a collision-related pathing issue have failed, meaning no viable path to the goal exists at the moment or the entity is stuck.
You must handle this case in your own system — for example, by:
- Selecting a new goal
- Waiting and retrying after a delay
- Alerting the player/user
#![allow(unused)] fn main() { fn handle_pathfind_failed( mut commands: Commands, mut query: Query<(Entity, &Name, &Pathfind), With<PathfindingFailed>>, ) { for (entity, name, path) in &mut query { log::warn!("{} cannot find a route to {}!", name, path.goal); let new_goal = locate_new_cheese(); commands .entity(entity) .insert(Pathfind::new(new_goal)) .remove::<PathfindingFailed>(); } } fn handle_reroute_failed( mut commands: Commands, mut query: Query<Entity, With<RerouteFailed>>, ) { for entity in &mut query { let some_new_goal = UVec3::new(3, 30, 0); // Just some random new goal commands .entity(entity) .insert(Pathfind::new(some_new_goal)) .remove::<RerouteFailed>(); } } }
PathingSet
The NorthstarPlugin
pathfinding systems run in their own system set named PathingSet
.
You can use the set to ensure that your systems dealing with pathfinding and entity movement happen before or after the pathing systems.
#![allow(unused)] fn main() { app.add_systems(Update, move_pathfinders.before(PathingSet)); }
Staggering Pathfinding in the NorthstarPlugin
Systems
The systems provided by NorthstarPlugin
are designed to stagger how many agents can process pathfinding and collision avoidance in a single frame.
By default, both limits are set to 128
agents per frame. This may be too high if you actually have that many active agents. On the other hand, setting it too low can cause noticeable delays, with agents appearing to sit idle. You’ll want to tune these values based on your game’s performance needs.
Currently the reroute_path
system that attempts to reroute agents that have a failed path that local collision avoidance is unable to resolve can still cause stutter. In a future update it will be moved into any async call.
To override the default settings, insert the NorthstarPluginSettings
resource into your app:
#![allow(unused)] fn main() { App::new() .insert_resource(NorthstarPluginSettings { max_pathfinding_agents_per_frame: 16, max_collision_avoidance_agents_per_frame: 16, }) }