use bevy::{ ecs::entity::Entities, prelude::*, tasks::{prelude::*, Task}, }; use bevy_rapier2d::{ na::{Isometry2, Vector2}, prelude::*, rapier::prelude::{ColliderHandle, ColliderSet, QueryPipeline, RigidBodySet}, }; use futures_lite::future; use leafwing_input_manager::{axislike::DualAxisData, plugin::InputManagerSystem, prelude::*}; use pathfinding::prelude::*; use crate::{ commands::RunIfExistsExt, core::PointLike, map::{Map, MapObstruction}, navigation::{NavigationAction, RotationSpeed}, }; #[derive(PartialEq, Eq, Clone, Copy, Hash, Debug)] pub struct NegotiatePathAction; impl Actionlike for NegotiatePathAction { const N_VARIANTS: usize = 1; fn get_at(index: usize) -> Option { if index == 0 { Some(Self) } else { None } } fn index(&self) -> usize { 0 } } #[derive(Component, Debug, Deref, DerefMut)] struct Calculating(Task>); #[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Eq, Hash, PartialEq, Reflect)] #[reflect(Component)] pub struct Destination(pub (i32, i32)); impl_pointlike_for_tuple_component!(Destination); impl_pointlike_for_tuple_component!(&Destination); #[derive(Component, Clone, Debug, Default, Reflect)] #[reflect(Component)] pub struct NoPath; #[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect)] #[reflect(Component)] pub struct Path(pub Vec<(i32, i32)>); #[derive(Component, Clone, Debug, Default, Reflect)] #[reflect(Component)] pub struct CostMap { width: usize, costs: Vec, } impl CostMap { pub fn new(width: usize, height: usize) -> Self { let count = width * height; Self { width, costs: vec![1.; count], } } pub fn modifier(&self, x: i32, y: i32) -> Option<&f32> { let idx = (y as usize) * self.width + (x as usize); self.costs.get(idx) } pub fn set_modifier(&mut self, x: usize, y: usize, cost: f32) { let idx = y * self.width + x; self.costs[idx] = cost; } } pub fn find_path( start: &dyn PointLike, destination: &dyn PointLike, map: &Map, cost_map: &Option, ) -> Option<(Vec<(i32, i32)>, u32)> { astar( &start.into(), |p| { let mut successors: Vec<((i32, i32), u32)> = vec![]; if let Some(tile) = map.at(p.0 as usize, p.1 as usize) { if tile.is_walkable() { for tile in map.get_available_exits(p.0 as usize, p.1 as usize) { let mut cost = tile.2 * 100.; if let Some(cost_map) = cost_map { if let Some(modifier) = cost_map.modifier(tile.0 as i32, tile.1 as i32) { cost *= modifier; } } successors.push(((tile.0 as i32, tile.1 as i32), cost as u32)); } } } successors }, |p| (p.distance_squared(destination) * 100.) as u32, |p| *p == destination.into(), ) } fn find_path_for_shape( initiator: ColliderHandle, start: Transform, destination: Destination, cost_map: &Option, query_pipeline: QueryPipeline, collider_set: ColliderSet, rigid_body_set: RigidBodySet, shape: Collider, ) -> Option { let path = astar( &start.i32(), |p| { let mut successors: Vec<((i32, i32), u32)> = vec![]; let x = p.0; let y = p.1; let exits = vec![ ((x - 1, y), 1.), ((x + 1, y), 1.), ((x, y - 1), 1.), ((x, y + 1), 1.), ((x - 1, y - 1), 1.5), ((x + 1, y - 1), 1.5), ((x - 1, y + 1), 1.5), ((x + 1, y + 1), 1.5), ]; for exit in &exits { let mut should_push = true; let shape_pos = Isometry2::new(Vector2::new(exit.0 .0 as f32, exit.0 .1 as f32), 0.); query_pipeline.intersections_with_shape( &rigid_body_set, &collider_set, &shape_pos, &*shape.raw, bevy_rapier2d::rapier::pipeline::QueryFilter::new() .predicate(&|h, _c| h != initiator), |_handle| { should_push = false; false }, ); if should_push { let mut cost = exit.1 * 100.; if let Some(cost_map) = cost_map { if let Some(modifier) = cost_map.modifier(exit.0 .0, exit.0 .1) { cost *= modifier; } } successors.push((exit.0, cost as u32)); } } successors }, |p| (p.distance_squared(&destination) * 100.) as u32, |p| *p == destination.i32(), ); if let Some(path) = path { Some(Path(path.0)) } else { None } } fn calculate_path( mut commands: Commands, rapier_context: Res, obstructions: Query<&RapierColliderHandle, With>, query: Query< ( Entity, &RapierColliderHandle, &Destination, &Transform, &Collider, Option<&CostMap>, ), Changed, >, ) { for (entity, handle, destination, coordinates, shape, cost_map) in &query { if coordinates.i32() == **destination { commands .entity(entity) .remove::() .remove::() .remove::() .remove::(); continue; } let coordinates_clone = *coordinates; let destination_clone = *destination; let query_pipeline = rapier_context.query_pipeline.clone(); let cost_map_clone = cost_map.cloned(); let handle_clone = *handle; let mut collider_set = rapier_context.colliders.clone(); let mut to_remove = vec![]; for handle in collider_set.iter() { if !obstructions.iter().map(|v| v.0).any(|x| x == handle.0) { to_remove.push(handle.0); } } let mut bodies = rapier_context.bodies.clone(); if !to_remove.is_empty() { let mut islands = rapier_context.islands.clone(); for handle in to_remove { collider_set.remove(handle, &mut islands, &mut bodies, false); } } let shape_clone = (*shape).clone(); trace!( "{:?}: path: calculating from {:?} to {:?}", entity, coordinates.i32(), destination ); let pool = AsyncComputeTaskPool::get(); let task = pool.spawn(async move { find_path_for_shape( handle_clone.0, coordinates_clone, destination_clone, &cost_map_clone, query_pipeline, collider_set, bodies, shape_clone, ) }); commands.run_if_exists(entity, |mut entity| { entity.insert(Calculating(task)); entity.remove::(); entity.remove::(); }); } } fn poll_tasks(mut commands: Commands, mut query: Query<(Entity, &mut Calculating)>) { for (entity, mut calculating) in query.iter_mut() { if let Some(result) = future::block_on(future::poll_once(&mut **calculating)) { if let Some(path) = result { trace!("{:?}: path: result: {:?}", entity, path); commands.entity(entity).insert(path); } else { trace!("{:?}: path: no path", entity); commands.entity(entity).insert(NoPath); } commands.entity(entity).remove::(); } } } fn remove_destination( mut commands: Commands, entities: &Entities, removed: RemovedComponents, ) { for entity in removed.iter() { if entities.contains(entity) { commands.entity(entity).remove::(); } } } fn negotiate_path( mut commands: Commands, mut query: Query<( Entity, &mut ActionState, &mut Path, &mut Transform, Option<&RotationSpeed>, )>, ) { for (entity, mut actions, mut path, mut transform, rotation_speed) in &mut query { let start_i32 = (transform.translation.x, transform.translation.y).i32(); trace!( "{entity:?}: start pathfinding from {start_i32:?} to {:?}: {:?}", path.last(), transform.translation.truncate() ); let mut cleanup = false; if let Some(last) = path.last() { if last == &start_i32 { trace!("{entity:?}: path: at destination"); cleanup = true; } } if !cleanup { if let Some(position) = path.iter().position(|v| v == &start_i32) { trace!("{entity:?}: Path contains start"); let (_, new_path) = path.split_at(position + 1); **path = new_path.to_vec(); } if let Some(next) = path.first() { trace!("{entity:?}: path: moving from {start_i32:?} to {next:?}"); let start = Vec2::new(start_i32.x(), start_i32.y()); let next = Vec2::new(next.0 as f32, next.1 as f32); let mut direction = next - start; direction = direction.normalize(); trace!("{entity:?}: path: direction: {direction:?}"); actions.press(NavigationAction::Move); actions.action_data_mut(NavigationAction::Move).axis_pair = Some(DualAxisData::from_xy(Vec2::new(-direction.y, direction.x))); trace!( "{entity:?}: path: move: {:?}", Vec2::new(direction.y, -direction.x) ); if rotation_speed.is_some() { let angle = direction.y.atan2(direction.x); transform.rotation = Quat::from_rotation_z(angle); } } else { trace!("{entity:?}: empty path, cleaning"); cleanup = true; } } if cleanup { commands .entity(entity) .remove::() .remove::() .remove::(); actions.release(NavigationAction::Move); trace!("{entity:?}: pathfinding: cleaned up"); } } } fn actions( mut commands: Commands, mut query: Query<( Entity, &ActionState, Option<&mut Destination>, )>, ) { for (entity, actions, destination) in &mut query { if actions.pressed(NegotiatePathAction) { if let Some(pair) = actions.axis_pair(NegotiatePathAction) { let dest = Destination((pair.x() as i32, pair.y() as i32)); if let Some(mut current_dest) = destination { if *current_dest != dest { *current_dest = dest; } } else { commands.entity(entity).insert(dest); } } else if destination.is_some() { commands.entity(entity).remove::(); } } } } pub struct PathfindingPlugin; impl Plugin for PathfindingPlugin { fn build(&self, app: &mut App) { app.add_plugin(InputManagerPlugin::::default()) .register_type::() .register_type::() .register_type::() .register_type::() .add_system(calculate_path) .add_system_to_stage(CoreStage::PostUpdate, remove_destination) .add_system(poll_tasks) .add_system_to_stage( CoreStage::PreUpdate, negotiate_path.after(InputManagerSystem::Tick), ) .add_system(actions); } }