From 123c628220926f5d8500070d76d2e17ae9ea18ad Mon Sep 17 00:00:00 2001 From: christopher Date: Thu, 12 Mar 2026 19:25:12 +0100 Subject: [PATCH 1/2] moved pathfinding plugin to pathfinding.rs --- crates/pathfinding/src/lib.rs | 100 +------------------------ crates/pathfinding/src/pathfinding.rs | 102 ++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 98 deletions(-) create mode 100644 crates/pathfinding/src/pathfinding.rs diff --git a/crates/pathfinding/src/lib.rs b/crates/pathfinding/src/lib.rs index 2747b5f..b82315e 100644 --- a/crates/pathfinding/src/lib.rs +++ b/crates/pathfinding/src/lib.rs @@ -1,56 +1,11 @@ use bevy::prelude::*; -use common::traits::AddNamedObserver; -use map_generation::world_map::WorldMap; use path::Path; -use pathfinder::{Pathfinder, PathfinderListener, PathfindingErrors, PathfindingState}; pub mod path; pub mod pathfinder; +mod pathfinding; +pub use pathfinding::plugin; -pub fn plugin(app: &mut App) { - app.register_type::() - .register_type::() - .add_systems(Update, calculate_path.run_if(resource_exists::)) - .add_systems( - Update, - (path::tick_path, path::follow_path, check_pathfinder).chain(), - ) - .add_named_observer(listen_for_path, "listen_for_path"); -} - -fn calculate_path( - world_map: Res, - mut query: Query<(Entity, &mut Pathfinder)>, - mut commands: Commands, -) { - for (entity, mut path) in &mut query { - match path.calculate_step(&world_map) { - PathfindingState::Calculating => (), - PathfindingState::Failed(err) => match err { - PathfindingErrors::NotEnoughChunks => { - debug!("not enough chunks"); - } - PathfindingErrors::Unreachable => { - debug!("pathfinding failed"); - commands.trigger(PathfindingCalculationEvent { - entity, - calculation: PathfindingCalculation::Failed, - }); - - commands.entity(entity).despawn(); - } - }, - PathfindingState::Complete(path) => { - debug!("pathfinder {} done", entity); - commands.trigger(PathfindingCalculationEvent { - entity, - calculation: PathfindingCalculation::Succeeded(path), - }); - commands.entity(entity).despawn(); - } - } - } -} #[derive(EntityEvent)] pub struct PathEvent { @@ -76,54 +31,3 @@ enum PathfindingCalculation { Failed, Succeeded(Path), } - -fn listen_for_path( - trigger: On, - listeners: Query<&PathfinderListener>, - mut commands: Commands, -) { - // if the event is triggered on a listener, we insert the path - if let PathfindingCalculation::Succeeded(path) = &trigger.calculation { - // if this is a successful path, we don't care about any other paths, so: - // add path to the listener entity and remove listener - if listeners.contains(trigger.entity) { - debug!( - "entity {} has found a successful path, removing all pathfinding children", - trigger.entity - ); - commands - .entity(trigger.entity) - .remove::() - .insert(path.clone()); - } - } -} - -fn check_pathfinder( - listeners: Query<(Entity, Option<&Children>), With>, - pathfinders: Query>, - mut commands: Commands, -) { - for (parent, children) in listeners { - if children.is_none_or(|x| { - pathfinders - .iter() - .filter(|element| x.contains(element)) - .count() - == 0 - }) { - // no children, so remove dis shit - debug!( - "entity {} has no more Pathfinder children, removing listener...", - parent - ); - commands - .entity(parent) - .remove::() - .trigger(|entity| PathEvent { - entity, - state: PathState::CalculationFailed, - }); - } - } -} diff --git a/crates/pathfinding/src/pathfinding.rs b/crates/pathfinding/src/pathfinding.rs new file mode 100644 index 0000000..6e8ee5d --- /dev/null +++ b/crates/pathfinding/src/pathfinding.rs @@ -0,0 +1,102 @@ +use bevy::prelude::*; +use common::traits::AddNamedObserver; +use map_generation::world_map::WorldMap; + +use crate::{ + PathEvent, PathState, PathfindingCalculation, PathfindingCalculationEvent, path::{self, Path}, pathfinder::{Pathfinder, PathfinderListener, PathfindingErrors, PathfindingState} +}; + +pub fn plugin(app: &mut App) { + app.register_type::() + .register_type::() + .add_systems(Update, calculate_path.run_if(resource_exists::)) + .add_systems( + Update, + (path::tick_path, path::follow_path, check_pathfinder).chain(), + ) + .add_named_observer(listen_for_path, "listen_for_path"); +} + +fn calculate_path( + world_map: Res, + mut query: Query<(Entity, &mut Pathfinder)>, + mut commands: Commands, +) { + for (entity, mut path) in &mut query { + match path.calculate_step(&world_map) { + PathfindingState::Calculating => (), + PathfindingState::Failed(err) => match err { + PathfindingErrors::NotEnoughChunks => { + debug!("not enough chunks"); + } + PathfindingErrors::Unreachable => { + debug!("pathfinding failed"); + commands.trigger(PathfindingCalculationEvent { + entity, + calculation: PathfindingCalculation::Failed, + }); + + commands.entity(entity).despawn(); + } + }, + PathfindingState::Complete(path) => { + debug!("pathfinder {} done", entity); + commands.trigger(PathfindingCalculationEvent { + entity, + calculation: PathfindingCalculation::Succeeded(path), + }); + commands.entity(entity).despawn(); + } + } + } +} +fn listen_for_path( + trigger: On, + listeners: Query<&PathfinderListener>, + mut commands: Commands, +) { + // if the event is triggered on a listener, we insert the path + if let PathfindingCalculation::Succeeded(path) = &trigger.calculation { + // if this is a successful path, we don't care about any other paths, so: + // add path to the listener entity and remove listener + if listeners.contains(trigger.entity) { + debug!( + "entity {} has found a successful path, removing all pathfinding children", + trigger.entity + ); + commands + .entity(trigger.entity) + .remove::() + .insert(path.clone()); + } + } +} + +fn check_pathfinder( + listeners: Query<(Entity, Option<&Children>), With>, + pathfinders: Query>, + mut commands: Commands, +) { + for (parent, children) in listeners { + if children.is_none_or(|x| { + pathfinders + .iter() + .filter(|element| x.contains(element)) + .count() + == 0 + }) { + // no children, so remove dis shit + debug!( + "entity {} has no more Pathfinder children, removing listener...", + parent + ); + commands + .entity(parent) + .remove::() + .trigger(|entity| PathEvent { + entity, + state: PathState::CalculationFailed, + }); + } + } +} From 9c971422da89f578904adcb5f9d4a18280facdd1 Mon Sep 17 00:00:00 2001 From: christopher Date: Thu, 12 Mar 2026 20:46:14 +0100 Subject: [PATCH 2/2] Removed world map dependency from pathfinder component pathfinder now is dependent on pathfinding_map trait that is implemented for world map. --- crates/common/src/traits/neighbors.rs | 6 +- crates/map_generation/src/block_type.rs | 5 ++ crates/map_generation/src/world_map.rs | 6 +- crates/pathfinding/src/lib.rs | 2 +- crates/pathfinding/src/pathfinder.rs | 78 ++++++----------------- crates/pathfinding/src/pathfinding.rs | 9 ++- crates/pathfinding/src/pathfinding_map.rs | 30 +++++++++ 7 files changed, 68 insertions(+), 68 deletions(-) create mode 100644 crates/pathfinding/src/pathfinding_map.rs diff --git a/crates/common/src/traits/neighbors.rs b/crates/common/src/traits/neighbors.rs index 820c286..8ce124d 100644 --- a/crates/common/src/traits/neighbors.rs +++ b/crates/common/src/traits/neighbors.rs @@ -16,9 +16,11 @@ pub trait Neighbors { fn all_neighbors(&self) -> Vec<(T, u32)>; } +pub type SquaredDistance = u32; + #[rustfmt::skip] impl Neighbors for IVec3 { - fn same_layer_neighbors(&self) -> Vec<(IVec3, u32)> { + fn same_layer_neighbors(&self) -> Vec<(IVec3, SquaredDistance)> { vec![ (self + IVec3::new(-1, 1, 0), 2), (self + IVec3::new( 0, 1, 0), 1), (self + IVec3::new( 1, 1, 0), 2), (self + IVec3::new(-1, 0, 0), 1), (self + IVec3::new( 1, 0, 0), 1), @@ -26,7 +28,7 @@ impl Neighbors for IVec3 { ] } - fn all_neighbors(&self) -> Vec<(IVec3, u32)> { + fn all_neighbors(&self) -> Vec<(IVec3, SquaredDistance)> { vec![ // layer above (self + IVec3::new(-1, 1, 1), 3), (self + IVec3::new( 0, 1, 1), 2), (self + IVec3::new( 1, 1, 1), 3), diff --git a/crates/map_generation/src/block_type.rs b/crates/map_generation/src/block_type.rs index 3805f70..ae3d644 100644 --- a/crates/map_generation/src/block_type.rs +++ b/crates/map_generation/src/block_type.rs @@ -39,6 +39,11 @@ impl SolidMaterial { SolidMaterial::Grass => TileTextureIndex(1), } } + + /// Returns the cost of an entity leaving this field. + pub const fn traversal_cost(&self) -> f32 { + 1.0 + } } impl BlockType { diff --git a/crates/map_generation/src/world_map.rs b/crates/map_generation/src/world_map.rs index 625c003..071610c 100644 --- a/crates/map_generation/src/world_map.rs +++ b/crates/map_generation/src/world_map.rs @@ -47,7 +47,8 @@ impl WorldMap { .or_insert(Chunk::new(coordinates, self.noise)) } - /// Tries to fetch a block from world. Will return None, if the chunk doesn't exist or the block is of type BlockType::None + /// Tries to fetch a block from world. Will return None, if the chunk + /// doesn't exist or the block is of type BlockType::None pub fn get_block(&self, coordinates: IWorldCoordinates) -> Option { let (chunk_coordinates, block_coordinates) = coordinates.to_chunk_and_block(); let index = to_index(block_coordinates); @@ -59,7 +60,8 @@ impl WorldMap { }) } - /// Returns a result of type BlockType, if the corresponding chunk has been found. Returns an empty error, when the chunk is not loaded. + /// Returns an option of type BlockType, if the corresponding chunk has been + /// found. Returns None when the chunk is not loaded. pub fn get_raw_block(&self, coordinates: IWorldCoordinates) -> Option { let (chunk_coordinate, block_coordinates) = coordinates.to_chunk_and_block(); let index = to_index(block_coordinates); diff --git a/crates/pathfinding/src/lib.rs b/crates/pathfinding/src/lib.rs index b82315e..0643d64 100644 --- a/crates/pathfinding/src/lib.rs +++ b/crates/pathfinding/src/lib.rs @@ -4,9 +4,9 @@ use path::Path; pub mod path; pub mod pathfinder; mod pathfinding; +pub mod pathfinding_map; pub use pathfinding::plugin; - #[derive(EntityEvent)] pub struct PathEvent { pub entity: Entity, diff --git a/crates/pathfinding/src/pathfinder.rs b/crates/pathfinding/src/pathfinder.rs index bb5d228..1297116 100644 --- a/crates/pathfinding/src/pathfinder.rs +++ b/crates/pathfinding/src/pathfinder.rs @@ -2,10 +2,9 @@ use std::cmp::Reverse; use bevy::{ecs::spawn::SpawnIter, platform::collections::HashMap, prelude::*}; use common::{traits::Neighbors, types::IWorldCoordinates}; -use map_generation::{block_type::BlockType, world_map::WorldMap}; use priority_queue::PriorityQueue; -use crate::path::Path; +use crate::{path::Path, pathfinding_map::PathfindingMap}; /// Attach this to calculate and ultimately follow a path. /// @@ -17,7 +16,7 @@ pub struct Pathfinder { #[reflect(ignore)] frontier: PriorityQueue>, came_from: HashMap>, - cost_so_far: HashMap, + cost_so_far: HashMap, steps: u32, allowed_failures: u8, current_failures: u8, @@ -50,7 +49,7 @@ impl Pathfinder { let mut came_from = HashMap::default(); came_from.insert(start.0, None); let mut cost_so_far = HashMap::default(); - cost_so_far.insert(start.0, 0); + cost_so_far.insert(start.0, 0.0); Pathfinder { target: target.0, frontier, @@ -93,39 +92,32 @@ impl Pathfinder { ) } - pub(crate) fn calculate_step(&mut self, world_map: &WorldMap) -> PathfindingState { - let Some((current_coordinates, current_priority)) = self.frontier.pop() else { - debug!("No frontier available"); + pub(crate) fn calculate_step( + &mut self, + pathfinding_map: &impl PathfindingMap, + ) -> PathfindingState { + let Some((current_coordinates, _current_priority)) = self.frontier.pop() else { + info!("No frontier available"); return PathfindingState::Failed(PathfindingErrors::Unreachable); }; if current_coordinates == self.target { - debug!("frontier is target"); + info!("frontier is target"); return PathfindingState::Complete(Path::new(self.to_path())); } - for (neighbor, neighbor_cost) in current_coordinates.all_neighbors() { - match self.is_floor_block(world_map, neighbor) { - Ok(true) => trace!("block is floor"), - Ok(false) => { - trace!("block is NOT floor"); - continue; - } - Err(e) => { - debug!("error, failure: {}", self.current_failures); - self.current_failures += 1; - self.frontier.push(current_coordinates, current_priority); - return PathfindingState::Failed(e); - } - } - + for (neighbor, neighbor_cost) in pathfinding_map.get_neighbors(current_coordinates) { + info!( + "current {:?} to neighbor {:?} would cost {}", + current_coordinates, neighbor, neighbor_cost + ); let new_cost = self.cost_so_far.get(¤t_coordinates).unwrap() + neighbor_cost; let current_cost = self.cost_so_far.get(&neighbor); if current_cost.is_none() || new_cost < *current_cost.unwrap() { self.cost_so_far.insert(neighbor, new_cost); - let priority = - new_cost + heuristic(world_map) + neighbor.distance_squared(self.target) as u32; - self.frontier.push(neighbor, Reverse(priority)); + let priority = new_cost + heuristic(neighbor, self.target); + self.frontier + .push(neighbor, Reverse(priority.round() as u32)); self.came_from.insert(neighbor, Some(current_coordinates)); } } @@ -133,35 +125,6 @@ impl Pathfinder { PathfindingState::Calculating } - fn is_floor_block( - &self, - world_map: &WorldMap, - neighbor: IVec3, - ) -> Result { - let neighbor_block = world_map - .get_raw_block(IWorldCoordinates(neighbor)) - .ok_or({ - if self.current_failures >= self.allowed_failures { - PathfindingErrors::Unreachable - } else { - PathfindingErrors::NotEnoughChunks - } - })?; - - trace!("checking {}, is {:?}", neighbor, neighbor_block); - let block_below = world_map - .get_raw_block(IWorldCoordinates(neighbor - IVec3::Z)) - .ok_or({ - if self.current_failures >= self.allowed_failures { - PathfindingErrors::Unreachable - } else { - PathfindingErrors::NotEnoughChunks - } - })?; - trace!("below {}, is {:?}", neighbor - IVec3::Z, block_below); - Ok(neighbor_block == BlockType::None && matches!(block_below, BlockType::Solid(_))) - } - fn to_path(&self) -> Vec { let mut points = vec![]; let mut next = self.target; @@ -179,8 +142,8 @@ impl Pathfinder { } } -fn heuristic(_world_map: &WorldMap) -> u32 { - 1 +fn heuristic(from: IVec3, to: IVec3) -> f32 { + from.distance_squared(to) as f32 } pub(crate) enum PathfindingState { @@ -190,6 +153,5 @@ pub(crate) enum PathfindingState { } pub(crate) enum PathfindingErrors { - NotEnoughChunks, Unreachable, } diff --git a/crates/pathfinding/src/pathfinding.rs b/crates/pathfinding/src/pathfinding.rs index 6e8ee5d..69d696d 100644 --- a/crates/pathfinding/src/pathfinding.rs +++ b/crates/pathfinding/src/pathfinding.rs @@ -3,7 +3,9 @@ use common::traits::AddNamedObserver; use map_generation::world_map::WorldMap; use crate::{ - PathEvent, PathState, PathfindingCalculation, PathfindingCalculationEvent, path::{self, Path}, pathfinder::{Pathfinder, PathfinderListener, PathfindingErrors, PathfindingState} + PathEvent, PathState, PathfindingCalculation, PathfindingCalculationEvent, + path::{self, Path}, + pathfinder::{Pathfinder, PathfinderListener, PathfindingErrors, PathfindingState}, }; pub fn plugin(app: &mut App) { @@ -23,12 +25,9 @@ fn calculate_path( mut commands: Commands, ) { for (entity, mut path) in &mut query { - match path.calculate_step(&world_map) { + match path.calculate_step(world_map.as_ref()) { PathfindingState::Calculating => (), PathfindingState::Failed(err) => match err { - PathfindingErrors::NotEnoughChunks => { - debug!("not enough chunks"); - } PathfindingErrors::Unreachable => { debug!("pathfinding failed"); commands.trigger(PathfindingCalculationEvent { diff --git a/crates/pathfinding/src/pathfinding_map.rs b/crates/pathfinding/src/pathfinding_map.rs new file mode 100644 index 0000000..774f2ff --- /dev/null +++ b/crates/pathfinding/src/pathfinding_map.rs @@ -0,0 +1,30 @@ +use bevy::prelude::*; +use common::{traits::Neighbors, types::IWorldCoordinates}; +use map_generation::{block_type::BlockType, world_map::WorldMap}; + +pub(crate) trait PathfindingMap { + fn get_neighbors(&self, coordinates: IVec3) -> impl Iterator; +} + +impl PathfindingMap for WorldMap { + fn get_neighbors(&self, coordinates: IVec3) -> impl Iterator { + coordinates + .all_neighbors() + .into_iter() + .filter_map(|(neighbor, squared_distance)| { + let next_block = self.get_raw_block(IWorldCoordinates(neighbor))?; + let block_below = self.get_raw_block(IWorldCoordinates(neighbor - IVec3::Z))?; + if next_block != BlockType::None { + return None; + } + let BlockType::Solid(material) = block_below else { + return None; + }; + + Some(( + neighbor, + material.traversal_cost() * squared_distance as f32, + )) + }) + } +}