diff --git a/Cargo.lock b/Cargo.lock index 892d0f86..5950c203 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2610,6 +2610,8 @@ dependencies = [ "criterion", "de_core", "de_map", + "de_messages", + "de_multiplayer", "de_objects", "de_types", "futures-lite", diff --git a/crates/messages/src/players/mod.rs b/crates/messages/src/players/mod.rs index d66719e8..ad452e13 100644 --- a/crates/messages/src/players/mod.rs +++ b/crates/messages/src/players/mod.rs @@ -70,7 +70,7 @@ pub enum ToPlayers { /// replaced by this one. SetPath { entity: EntityNet, - waypoints: PathNet, + waypoints: Option, }, /// Instantaneously transform an object. /// diff --git a/crates/multiplayer/src/lib.rs b/crates/multiplayer/src/lib.rs index 2d6b989d..3ca6eb6c 100644 --- a/crates/multiplayer/src/lib.rs +++ b/crates/multiplayer/src/lib.rs @@ -25,7 +25,7 @@ pub use crate::{ netstate::NetState, playermsg::{ GameNetSet, NetEntities, NetEntityCommands, NetRecvDespawnActiveEvent, NetRecvHealthEvent, - NetRecvSpawnActiveEvent, NetRecvTransformEvent, + NetRecvSetPathEvent, NetRecvSpawnActiveEvent, NetRecvTransformEvent, }, }; use crate::{netstate::NetStatePlugin, network::NetworkPlugin}; diff --git a/crates/multiplayer/src/playermsg.rs b/crates/multiplayer/src/playermsg.rs index 967e633b..5d9dd18b 100644 --- a/crates/multiplayer/src/playermsg.rs +++ b/crates/multiplayer/src/playermsg.rs @@ -5,7 +5,7 @@ use bevy::{ }; use de_core::{gconfig::GameConfig, schedule::PreMovement, state::AppState}; use de_messages::{EntityNet, NetEntityIndex, ToPlayers}; -use de_types::{objects::ActiveObjectType, player::Player}; +use de_types::{objects::ActiveObjectType, path::Path, player::Player}; use crate::messages::{FromPlayersEvent, MessagesSet}; @@ -18,6 +18,7 @@ impl Plugin for PlayerMsgPlugin { .add_event::() .add_event::() .add_event::() + .add_event::() .add_systems(OnEnter(AppState::InGame), setup) .add_systems(OnExit(AppState::InGame), cleanup) .add_systems( @@ -145,6 +146,26 @@ impl NetRecvTransformEvent { } } +#[derive(Event)] +pub struct NetRecvSetPathEvent { + entity: Entity, + path: Option, +} + +impl NetRecvSetPathEvent { + fn new(entity: Entity, path: Option) -> Self { + Self { entity, path } + } + + pub fn entity(&self) -> Entity { + self.entity + } + + pub fn path(&self) -> Option<&Path> { + self.path.as_ref() + } +} + #[derive(SystemParam)] pub struct NetEntities<'w> { config: Res<'w, GameConfig>, @@ -329,6 +350,7 @@ fn recv_messages( mut inputs: EventReader, mut spawn_events: EventWriter, mut despawn_events: EventWriter, + mut path_events: EventWriter, mut transform_events: EventWriter, mut health_events: EventWriter, ) { @@ -354,6 +376,17 @@ fn recv_messages( let local = net_commands.deregister(*entity); despawn_events.send(NetRecvDespawnActiveEvent::new(local)); } + ToPlayers::SetPath { entity, waypoints } => { + let Some(local) = net_commands.remote_local_id(*entity) else { + warn!("Received net path update of unrecognized entity: {entity:?}"); + continue; + }; + + path_events.send(NetRecvSetPathEvent::new( + local, + waypoints.as_ref().map(|p| p.into()), + )); + } ToPlayers::Transform { entity, transform } => { if let Some(local) = net_commands.remote_local_id(*entity) { transform_events.send(NetRecvTransformEvent::new(local, transform.into())); diff --git a/crates/pathing/Cargo.toml b/crates/pathing/Cargo.toml index 1c311735..9c8130bb 100644 --- a/crates/pathing/Cargo.toml +++ b/crates/pathing/Cargo.toml @@ -15,6 +15,8 @@ categories.workspace = true # DE de_core.workspace = true de_map.workspace = true +de_messages.workspace = true +de_multiplayer.workspace = true de_objects.workspace = true de_types.workspace = true diff --git a/crates/pathing/src/lib.rs b/crates/pathing/src/lib.rs index 4fff8164..7a03814a 100644 --- a/crates/pathing/src/lib.rs +++ b/crates/pathing/src/lib.rs @@ -13,6 +13,7 @@ mod graph; mod path; mod pplugin; mod query; +mod syncing; mod triangulation; mod utils; @@ -24,6 +25,7 @@ pub use path::ScheduledPath; use pplugin::PathingPlugin; pub use pplugin::UpdateEntityPathEvent; pub use query::{PathQueryProps, PathTarget}; +use syncing::SyncingPlugin; pub struct PathingPluginGroup; @@ -32,5 +34,6 @@ impl PluginGroup for PathingPluginGroup { PluginGroupBuilder::start::() .add(FinderPlugin) .add(PathingPlugin) + .add(SyncingPlugin) } } diff --git a/crates/pathing/src/pplugin.rs b/crates/pathing/src/pplugin.rs index f70fbaa6..08304480 100644 --- a/crates/pathing/src/pplugin.rs +++ b/crates/pathing/src/pplugin.rs @@ -38,6 +38,7 @@ pub struct PathingPlugin; impl Plugin for PathingPlugin { fn build(&self, app: &mut App) { app.add_event::() + .add_event::() .add_systems(OnEnter(AppState::InGame), setup) .add_systems(OnExit(AppState::InGame), cleanup) .add_systems( @@ -51,10 +52,7 @@ impl Plugin for PathingPlugin { .in_set(PathingSet::UpdateRequestedPaths) .after(PathingSet::UpdateExistingPaths), check_path_results - // This is needed to avoid race condition in PathTarget - // removal which would happen if path was not-found before - // this system is run. - .before(PathingSet::UpdateRequestedPaths) + .in_set(PathingSet::PathResults) // This system removes finished tasks from UpdatePathsState // and inserts Scheduledpath components. When this happen, // the tasks is no longer available however the component @@ -64,6 +62,12 @@ impl Plugin for PathingPlugin { // that a path is either already scheduled or being // computed. Thus this system must run after it. .after(PathingSet::UpdateExistingPaths), + update_path_components + .after(PathingSet::PathResults) + // This is needed to avoid race condition in PathTarget + // removal which would happen if path was not-found before + // this system is run. + .before(PathingSet::UpdateRequestedPaths), ) .run_if(in_state(GameState::Playing)), ) @@ -75,9 +79,10 @@ impl Plugin for PathingPlugin { } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, SystemSet)] -enum PathingSet { +pub(crate) enum PathingSet { UpdateRequestedPaths, UpdateExistingPaths, + PathResults, } /// This event triggers computation of shortest path to a target and @@ -91,7 +96,8 @@ pub struct UpdateEntityPathEvent { impl UpdateEntityPathEvent { /// # Arguments /// - /// * `entity` - entity whose path should be updated / inserted. + /// * `entity` - entity whose path should be updated / inserted. This must + /// be a locally simulated entity. /// /// * `target` - desired path target & path searching query configuration. pub fn new(entity: Entity, target: PathTarget) -> Self { @@ -107,6 +113,27 @@ impl UpdateEntityPathEvent { } } +/// This event is sent when a new path is found for a locally simulated entity. +#[derive(Event)] +pub(crate) struct PathFoundEvent { + entity: Entity, + path: Option, +} + +impl PathFoundEvent { + fn new(entity: Entity, path: Option) -> Self { + Self { entity, path } + } + + pub(crate) fn entity(&self) -> Entity { + self.entity + } + + pub(crate) fn path(&self) -> Option<&Path> { + self.path.as_ref() + } +} + #[derive(Default, Resource)] struct UpdatePathsState { tasks: AHashMap, @@ -214,15 +241,24 @@ fn update_requested_paths( } fn check_path_results( - mut commands: Commands, mut state: ResMut, - targets: Query<&PathTarget>, + mut events: EventWriter, ) { for (entity, path) in state.check_results() { - let mut entity_commands = commands.entity(entity); - match path { + events.send(PathFoundEvent::new(entity, path)); + } +} + +fn update_path_components( + mut commands: Commands, + targets: Query<&PathTarget>, + mut events: EventReader, +) { + for event in events.iter() { + let mut entity_commands = commands.entity(event.entity()); + match event.path() { Some(path) => { - entity_commands.insert(ScheduledPath::new(path)); + entity_commands.insert(ScheduledPath::new(path.clone())); } None => { entity_commands.remove::(); @@ -230,7 +266,7 @@ fn check_path_results( // This must be here on top of target removal in // remove_path_targets due to the possibility that // `ScheduledPath` was never found. - if let Ok(target) = targets.get(entity) { + if let Ok(target) = targets.get(event.entity()) { if !target.permanent() { entity_commands.remove::(); } diff --git a/crates/pathing/src/syncing.rs b/crates/pathing/src/syncing.rs new file mode 100644 index 00000000..27775016 --- /dev/null +++ b/crates/pathing/src/syncing.rs @@ -0,0 +1,62 @@ +use bevy::prelude::*; +use de_core::{ + gconfig::is_multiplayer, + schedule::{Movement, PreMovement}, + state::AppState, +}; +use de_messages::ToPlayers; +use de_multiplayer::{GameNetSet, NetEntities, NetRecvSetPathEvent, ToPlayersEvent}; + +use crate::{ + pplugin::{PathFoundEvent, PathingSet}, + ScheduledPath, +}; + +pub struct SyncingPlugin; + +impl Plugin for SyncingPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + PreMovement, + receive_paths + .run_if(on_event::()) + .run_if(in_state(AppState::InGame)) + .after(GameNetSet::Messages), + ) + .add_systems( + Movement, + send_new_paths + .run_if(is_multiplayer) + .run_if(in_state(AppState::InGame)) + .after(PathingSet::PathResults), + ); + } +} + +fn receive_paths(mut commands: Commands, mut events: EventReader) { + for event in events.iter() { + let mut entity_commands = commands.entity(event.entity()); + + match event.path() { + Some(path) => { + entity_commands.insert(ScheduledPath::new(path.clone())); + } + None => { + entity_commands.remove::(); + } + } + } +} + +fn send_new_paths( + net_entities: NetEntities, + mut path_events: EventReader, + mut net_events: EventWriter, +) { + for event in path_events.iter() { + net_events.send(ToPlayersEvent::new(ToPlayers::SetPath { + entity: net_entities.local_net_id(event.entity()), + waypoints: event.path().map(|p| p.try_into().unwrap()), + })); + } +} diff --git a/crates/types/src/path.rs b/crates/types/src/path.rs index 51d80e9a..94f528c1 100644 --- a/crates/types/src/path.rs +++ b/crates/types/src/path.rs @@ -2,6 +2,7 @@ use glam::Vec2; /// A path on the map defined by a sequence of way points. Start and target /// position are included. +#[derive(Clone)] pub struct Path { length: f32, waypoints: Vec,