use bevy::log::LogPlugin; use bevy::prelude::*; use bevy::utils::HashMap; use ryot_core::prelude::Navigable; use ryot_pathfinder::components::{Path, PathFindingQuery}; use ryot_pathfinder::pathable::Pathable; use ryot_pathfinder::prelude::PathableApp; use ryot_utils::cache::Cache; use std::fmt::Debug; use std::time::{Duration, Instant}; #[cfg(feature = "ryot_tiled")] use ryot_tiled::tile_size; #[cfg(not(feature = "ryot_tiled"))] /// tile_size is a concept of ryot_tiled, here we mock it i f ryot_tiled is not enabled. fn tile_size() -> UVec2 { UVec2::new(32, 32) } /// A component that represents an on-going pathfinding execution. #[derive(Component, Copy, Clone)] pub(crate) struct Pathing(P); /// A marker component that represents the visual element of an obstacle. #[derive(Component, Copy, Clone)] pub(crate) struct Obstacle; /// This is a builder for creating examples for pathfinding. /// This makes it easier to create examples with different configurations. #[derive(Copy, Clone)] pub struct ExampleBuilder< P: Pathable + Component + Debug + Into, N: Navigable + Copy + Default, > { pub grid_size: i32, pub n_entities: usize, pub n_obstacles: usize, pub max_distance: i32, pub sleep: u64, pub navigable: N, pub query_builder: fn(P) -> PathFindingQuery

, pub _phantom: std::marker::PhantomData

, } impl, N: Navigable + Copy + Default> Default for ExampleBuilder { fn default() -> Self { Self { grid_size: 10, n_entities: 1, n_obstacles: 0, max_distance: 10, sleep: 100, navigable: N::default(), // This is the default query builder, it will create a query that will try to find a // path to the same position. Check the `PathFindingQuery` documentation for more // information about the available options. query_builder: |pos| PathFindingQuery::new(pos).with_success_distance(0.), _phantom: std::marker::PhantomData, } } } #[allow(dead_code)] impl, N: Navigable + Copy + Default> ExampleBuilder { pub fn with_grid_size(mut self, grid_size: i32) -> Self { self.grid_size = grid_size; self } pub fn with_n_entities(mut self, n_entities: usize) -> Self { self.n_entities = n_entities; self } pub fn with_n_obstacles(mut self, n_obstacles: usize) -> Self { self.n_obstacles = n_obstacles; self } pub fn with_max_distance(mut self, max_distance: i32) -> Self { self.max_distance = max_distance; self } pub fn with_sleep(mut self, sleep: u64) -> Self { self.sleep = sleep; self } pub fn with_navigable(mut self, navigable: N) -> Self { self.navigable = navigable; self } pub fn with_query_builder(mut self, query_builder: fn(P) -> PathFindingQuery

) -> Self { self.query_builder = query_builder; self } /// This method creates a Bevy App with the necessary systems to run the pathfinding example /// with visual feedback. pub fn drawing_app(&self) -> App { let mut app = App::new(); app.add_plugins(DefaultPlugins) .add_systems(Startup, basic_setup) .add_systems(First, self.draw_grid()) .add_systems(Last, draw_actors::

) .add_systems(Update, draw_target::

) .add_systems(Update, draw_obstacles::

); // This is the relevant part of the app, the rest is just visual feedback. app.add_pathable::() .add_systems(Startup, (self.spawn_many(), self.spawn_obstacles(true))) .add_systems(Update, (self.start_path(), self.process_path())); app } /// This method creates a Bevy App with the necessary systems to run the pathfinding example /// without visual feedback. pub fn minimum_app(&self) -> App { let mut app = App::new(); app.add_plugins(MinimalPlugins) .add_plugins(LogPlugin::default()); // This is the relevant part of the example, the rest is just boilerplate. app.add_pathable::() .add_systems(Startup, (self.spawn_many(), self.spawn_obstacles(false))) .add_systems(Update, (self.start_path(), self.process_path())); app } /// This exemplifies how do you trigger a pathfinding execution. pub fn start_path( &self, ) -> impl FnMut( Commands, Res>, Query<(Entity, &P), (Without>, Without)>, ) { let builder = *self; move |mut commands: Commands, cache: Res>, q_pos: Query<(Entity, &P), (Without>, Without)>| { // Here we have a system that goes through all entities with a P component that // doesn't have a Pathing component (no pathing being executed) and is not an Obstacle. for (entity, current_pos) in q_pos.iter() { // Random position generator, can be ignored. let mut pos = builder.random_from_pos(current_pos); // This loop is just a way to avoid spawning an entity on top of an obstacle. // Can be ignored for the sake of understanding the pathfinding system. while !cache .read() .unwrap() .get(&pos) .copied() .unwrap_or_default() .is_walkable() { pos = builder.random_from_pos(current_pos); } debug!("Starting path from {:?} to {:?}", current_pos, pos); // This is the relevant part of the system, where we insert a PathFindingQuery. // Adding this component will trigger the pathfinding system to initiate the // async task that will calculate the path. commands .entity(entity) .insert(((builder.query_builder)(pos), Pathing(pos))); } } } // Just a spawner for the test, can be ignored. pub fn spawn_many(&self) -> impl FnMut(Commands) { let builder = *self; move |mut commands: Commands| { for _ in 0..builder.n_entities { commands.spawn(builder.random_pos()); } } } // Just a spawner for the test, can be ignored. pub fn spawn_obstacles(&self, draw: bool) -> impl FnMut(Commands, ResMut>) { let builder = *self; move |mut commands: Commands, cache: ResMut>| { let Ok(mut write_guard) = cache.write() else { return; }; for _ in 0..builder.n_obstacles { let pos = builder.random_pos(); write_guard.insert(pos, builder.navigable); if draw { commands.spawn((pos, Obstacle)); } } } } /// This exemplifies how do you process the result of a pathfinding execution. /// The result is stored in a Path

component. pub fn process_path( &self, ) -> impl FnMut( Commands, Query<(Entity, &mut P, &mut Path

, Option<&PathFindingQuery

>)>, Local>, ) { let builder = *self; move |mut commands: Commands, mut q_paths: Query<(Entity, &mut P, &mut Path

, Option<&PathFindingQuery

>)>, mut last_executed: Local>| { for (entity, mut pos, mut path, path_query) in q_paths.iter_mut() { // Just a simple delay on the consumption, to simulate a thinking (or walking) time. // We will consume one position every {sleep} milliseconds. if last_executed .entry(entity) .or_insert(Instant::now()) .elapsed() < Duration::from_millis(builder.sleep) { return; } // Here we update the last execution time, to keep track of the delay. last_executed.insert(entity, Instant::now()); // If the path is empty, and there is no new query being executed, // we remove the Pathing component, so the entity stops. if path.is_empty() && path_query.is_none() { commands.entity(entity).remove::>(); continue; } let Some(next_pos) = path.first().copied() else { return; }; // If there is a next position, we remove it from the path and update the entity's // position to the next position. path.remove(0); *pos = next_pos; } } } // Just a beautifier for the test, can be ignored. pub fn draw_grid(&self) -> impl FnMut(Gizmos) { let builder = *self; move |mut gizmos: Gizmos| { for x in -builder.grid_size..=builder.grid_size { for y in -builder.grid_size..=builder.grid_size { gizmos.rect_2d( P::generate(x, y, 0).into(), 0., tile_size().as_vec2(), Color::WHITE, ); } } } } // Just a spawner for the test, can be ignored. pub fn random_pos(&self) -> P { P::generate( rand::random::() % self.grid_size, rand::random::() % self.grid_size, 0, ) } // Just a spawner for the test, can be ignored. pub fn random_from_pos(&self, pos: &P) -> P { let (x, y, z) = pos.coordinates(); P::generate( (x + rand::random::() % self.max_distance).clamp(-self.grid_size, self.grid_size), (y + rand::random::() % self.max_distance).clamp(-self.grid_size, self.grid_size), z, ) } } // Just a spawner for the test, can be ignored. pub fn basic_setup(mut commands: Commands) { commands.spawn(Camera2dBundle::default()); } // Just a beautifier for the test, can be ignored. pub fn draw_actors>( mut gizmos: Gizmos, mut q_paths: Query<&P, Without>, ) { for pos in q_paths.iter_mut() { gizmos.circle_2d((*pos).into(), (tile_size().x / 2) as f32, Color::BLUE); } } // Just a beautifier for the test, can be ignored. pub fn draw_target>( mut gizmos: Gizmos, mut q_targets: Query<&Pathing

>, ) { for Pathing(pos) in q_targets.iter_mut() { gizmos.circle_2d((*pos).into(), (tile_size().x / 2) as f32, Color::GREEN); } } // Just a beautifier for the test, can be ignored. pub fn draw_obstacles>( mut gizmos: Gizmos, mut q_obstacles: Query<&P, With>, ) { for pos in q_obstacles.iter_mut() { gizmos.rect_2d((*pos).into(), 0., tile_size().as_vec2() / 2., Color::RED); } }