From 8f24f4e03abe5a86ca646f41ea4b8e70f6de4dd4 Mon Sep 17 00:00:00 2001 From: PROMETHIA-27 <42193387+PROMETHIA-27@users.noreply.github.com> Date: Tue, 7 Oct 2025 15:31:17 -0400 Subject: [PATCH] Fix input drops/repetitions (#69) --- crates/client/src/client.rs | 2 +- crates/server/src/server.rs | 5 +- crates/shared/src/abilities/missile.rs | 27 ++- crates/shared/src/abilities/mod.rs | 89 ++++++++-- crates/shared/src/abilities/thrown.rs | 38 ++--- crates/shared/src/control/controls.rs | 226 +++++++++++++++++++------ crates/shared/src/control/mod.rs | 7 +- crates/shared/src/npc.rs | 2 - 8 files changed, 286 insertions(+), 110 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 7051569..dc63426 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -109,7 +109,7 @@ fn attempt_connection(mut commands: Commands) -> Result { client::NetcodeClient::new( auth, NetcodeConfig { - client_timeout_secs: 1, + client_timeout_secs: 3, ..default() }, )?, diff --git a/crates/server/src/server.rs b/crates/server/src/server.rs index 3a13d4a..5d1bcd1 100644 --- a/crates/server/src/server.rs +++ b/crates/server/src/server.rs @@ -117,7 +117,10 @@ fn start_server(mut commands: Commands) -> Result { Name::from("Server"), LocalAddr(server_addr), ServerUdpIo::default(), - NetcodeServer::new(NetcodeConfig::default()), + NetcodeServer::new(NetcodeConfig { + client_timeout_secs: 3, + ..Default::default() + }), Link::new(Some(conditioner)), )); commands.trigger(server::Start); diff --git a/crates/shared/src/abilities/missile.rs b/crates/shared/src/abilities/missile.rs index 6879d5e..79fd6fc 100644 --- a/crates/shared/src/abilities/missile.rs +++ b/crates/shared/src/abilities/missile.rs @@ -1,11 +1,11 @@ use super::TriggerMissile; use crate::{ GameState, - abilities::BuildExplosionSprite, + abilities::{ExplodingProjectile, ExplodingProjectileSet}, heads_database::HeadsDatabase, physics_layers::GameLayer, protocol::{GltfSceneRoot, PlaySound}, - utils::{commands::CommandExt, explosions::Explosion, global_observer, trail::Trail}, + utils::{global_observer, trail::Trail}, }; use avian3d::prelude::*; use bevy::prelude::*; @@ -23,7 +23,10 @@ struct MissileProjectile { } pub fn plugin(app: &mut App) { - app.add_systems(Update, shot_collision.run_if(in_state(GameState::Playing))); + app.add_systems( + FixedUpdate, + shot_collision.in_set(ExplodingProjectileSet::Mark), + ); app.add_systems( FixedUpdate, (update, timeout).run_if(in_state(GameState::Playing)), @@ -141,20 +144,14 @@ fn shot_collision( continue; }; - commands.trigger(PlaySound::MissileExplosion); - - commands.entity(shot_entity).despawn(); - - commands.trigger(Explosion { + commands.entity(shot_entity).insert(ExplodingProjectile { + sound: PlaySound::MissileExplosion, damage, position: shot_pos, - radius: 6., - }); - - commands.trigger_server(BuildExplosionSprite { - pos: shot_pos, - pixels_per_meter: 16., - time: 0.01, + radius: 6.0, + animation: true, + anim_pixels_per_meter: 16.0, + anim_time: 0.01, }); } } diff --git a/crates/shared/src/abilities/mod.rs b/crates/shared/src/abilities/mod.rs index ff90daf..d1ccab2 100644 --- a/crates/shared/src/abilities/mod.rs +++ b/crates/shared/src/abilities/mod.rs @@ -6,23 +6,27 @@ pub mod missile; pub mod thrown; use crate::{ - GameState, - aim::AimTarget, - character::CharacterHierarchy, - global_observer, - heads::ActiveHeads, - heads_database::HeadsDatabase, + GameState, global_observer, loading_assets::GameAssets, physics_layers::GameLayer, - player::{Player, PlayerBodyMesh}, protocol::PlaySound, utils::{billboards::Billboard, sprite_3d_animation::AnimationTimer}, }; #[cfg(feature = "server")] -use crate::{control::ControlState, head::ActiveHead}; +use crate::{ + aim::AimTarget, + character::CharacterHierarchy, + control::ControlState, + head::ActiveHead, + heads::ActiveHeads, + heads_database::HeadsDatabase, + player::{Player, PlayerBodyMesh}, + utils::explosions::Explosion, +}; use bevy::{pbr::NotShadowCaster, prelude::*}; use bevy_sprite3d::{Sprite3dBuilder, Sprite3dParams}; pub use healing::Healing; +#[cfg(feature = "server")] use healing::HealingStateChanged; #[cfg(feature = "server")] use lightyear::prelude::input::native::ActionState; @@ -86,6 +90,7 @@ pub struct TriggerCurver(pub TriggerData); #[derive(Resource, Default)] pub struct TriggerStateRes { + #[cfg(feature = "server")] next_trigger_timestamp: f32, active: bool, } @@ -96,6 +101,25 @@ impl TriggerStateRes { } } +#[derive(Component)] +#[component(storage = "SparseSet")] +#[allow(dead_code)] +pub struct ExplodingProjectile { + sound: PlaySound, + damage: u32, + position: Vec3, + radius: f32, + animation: bool, + anim_pixels_per_meter: f32, + anim_time: f32, +} + +#[derive(SystemSet, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum ExplodingProjectileSet { + Mark, + Explode, +} + pub fn plugin(app: &mut App) { app.init_resource::(); @@ -106,20 +130,61 @@ pub fn plugin(app: &mut App) { app.add_plugins(healing::plugin); app.add_plugins(curver::plugin); + app.configure_sets( + FixedUpdate, + ExplodingProjectileSet::Explode + .after(ExplodingProjectileSet::Mark) + .run_if(in_state(GameState::Playing)), + ); + app.add_systems(OnEnter(GameState::Playing), setup); + #[cfg(feature = "server")] app.add_systems( - Update, - (update, update_heal_ability).run_if(in_state(GameState::Playing)), + FixedUpdate, + (on_trigger_state, update, update_heal_ability) + .chain() + .run_if(in_state(GameState::Playing)), ); #[cfg(feature = "server")] app.add_systems( FixedUpdate, - on_trigger_state.run_if(in_state(GameState::Playing)), + explode_projectiles.in_set(ExplodingProjectileSet::Explode), ); global_observer!(app, build_explosion_sprite); } +#[cfg(feature = "server")] +fn explode_projectiles(mut commands: Commands, query: Query<(Entity, &ExplodingProjectile)>) { + for (shot_entity, projectile) in query.iter() { + if let Ok(mut entity) = commands.get_entity(shot_entity) { + entity.try_despawn(); + } else { + continue; + } + + commands.trigger(projectile.sound.clone()); + + commands.trigger(Explosion { + damage: projectile.damage, + position: projectile.position, + //TODO: should be around 1 grid in distance + radius: projectile.radius, + }); + + //TODO: support different impact animations + if projectile.animation { + use crate::utils::commands::CommandExt; + + commands.trigger_server(BuildExplosionSprite { + pos: projectile.position, + pixels_per_meter: projectile.anim_pixels_per_meter, + time: projectile.anim_time, + }); + } + } +} + #[cfg(feature = "server")] fn on_trigger_state( mut res: ResMut, @@ -136,6 +201,7 @@ fn on_trigger_state( } } +#[cfg(feature = "server")] fn update( mut res: ResMut, mut commands: Commands, @@ -201,6 +267,7 @@ fn update( } } +#[cfg(feature = "server")] fn update_heal_ability( res: Res, mut commands: Commands, diff --git a/crates/shared/src/abilities/thrown.rs b/crates/shared/src/abilities/thrown.rs index a9f281e..62fc45e 100644 --- a/crates/shared/src/abilities/thrown.rs +++ b/crates/shared/src/abilities/thrown.rs @@ -1,13 +1,10 @@ use super::TriggerThrow; use crate::{ - GameState, - abilities::BuildExplosionSprite, + abilities::{ExplodingProjectile, ExplodingProjectileSet}, heads_database::HeadsDatabase, physics_layers::GameLayer, protocol::{GltfSceneRoot, PlaySound}, - utils::{ - auto_rotate::AutoRotation, commands::CommandExt, explosions::Explosion, global_observer, - }, + utils::{auto_rotate::AutoRotation, global_observer}, }; use avian3d::prelude::*; use bevy::prelude::*; @@ -24,7 +21,10 @@ pub struct ThrownProjectile { } pub fn plugin(app: &mut App) { - app.add_systems(Update, shot_collision.run_if(in_state(GameState::Playing))); + app.add_systems( + FixedUpdate, + shot_collision.in_set(ExplodingProjectileSet::Mark), + ); global_observer!(app, on_trigger_thrown); } @@ -116,28 +116,14 @@ fn shot_collision( continue; }; - if let Ok(mut entity) = commands.get_entity(shot_entity) { - entity.try_despawn(); - } else { - continue; - } - - commands.trigger(PlaySound::ThrowHit); - - commands.trigger(Explosion { + commands.entity(shot_entity).insert(ExplodingProjectile { + sound: PlaySound::ThrowHit, damage, position: shot_pos, - //TODO: should be around 1 grid in distance - radius: 5., + radius: 5.0, + animation, + anim_pixels_per_meter: 32.0, + anim_time: 0.02, }); - - //TODO: support different impact animations - if animation { - commands.trigger_server(BuildExplosionSprite { - pos: shot_pos, - pixels_per_meter: 32., - time: 0.02, - }); - } } } diff --git a/crates/shared/src/control/controls.rs b/crates/shared/src/control/controls.rs index 9a9a9ed..f2f45dd 100644 --- a/crates/shared/src/control/controls.rs +++ b/crates/shared/src/control/controls.rs @@ -1,47 +1,56 @@ use super::{ControlState, Controls}; -use crate::{ - GameState, - control::{CharacterInputEnabled, ControllerSet}, -}; -use bevy::{ - input::{ - ButtonState, - gamepad::{GamepadConnection, GamepadEvent}, - mouse::{MouseButtonInput, MouseMotion}, - }, - prelude::*, +#[cfg(feature = "client")] +use crate::control::ControllerSet; +use crate::{GameState, control::CharacterInputEnabled}; +#[cfg(feature = "client")] +use bevy::input::{ + ButtonState, + gamepad::{GamepadConnection, GamepadEvent}, + mouse::{MouseButtonInput, MouseMotion}, }; +use bevy::prelude::*; +#[cfg(feature = "client")] +use lightyear::input::client::InputSet; #[cfg(feature = "client")] use lightyear::prelude::input::native::{ActionState, InputMarker}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +#[cfg(feature = "client")] +use std::hash::Hash; pub fn plugin(app: &mut App) { app.init_resource::(); + app.init_resource::>(); + + app.register_required_components::>(); app.register_type::(); - app.add_systems( - FixedUpdate, - ( - gamepad_controls, - keyboard_controls, - mouse_rotate, - mouse_click, - gamepad_connections.run_if(on_event::), - combine_controls, - ) - .chain() - .in_set(ControllerSet::CollectInputs) - .run_if( - in_state(GameState::Playing) - .and(resource_exists_and_equals(CharacterInputEnabled::On)), - ), - ); + app.add_systems(PreUpdate, (cache_keyboard_state, cache_gamepad_state)); #[cfg(feature = "client")] { - use lightyear::prelude::client::input::InputSet; app.add_systems( + FixedPreUpdate, + ( + gamepad_controls, + keyboard_controls, + mouse_rotate, + mouse_click, + gamepad_connections.run_if(on_event::), + combine_controls, + clear_keyboard_state, + clear_gamepad_state, + ) + .chain() + .in_set(ControllerSet::CollectInputs) + .before(InputSet::WriteClientInputs) + .run_if( + in_state(GameState::Playing) + .and(resource_exists_and_equals(CharacterInputEnabled::On)), + ), + ) + .add_systems( FixedPreUpdate, buffer_inputs.in_set(InputSet::WriteClientInputs), ); @@ -82,6 +91,105 @@ fn reset_control_state_on_disable( } } +/// Caches information that depends on the Update schedule so that it can be read safely from the Fixed schedule +/// without losing/duplicating info +#[derive(Component, Resource)] +struct InputStateCache