Fix input drops/repetitions (#69)

This commit is contained in:
PROMETHIA-27
2025-10-07 15:31:17 -04:00
committed by GitHub
parent adaa9cab30
commit 8f24f4e03a
8 changed files with 286 additions and 110 deletions

View File

@@ -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()
},
)?,

View File

@@ -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);

View File

@@ -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,
});
}
}

View File

@@ -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::<TriggerStateRes>();
@@ -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<TriggerStateRes>,
@@ -136,6 +201,7 @@ fn on_trigger_state(
}
}
#[cfg(feature = "server")]
fn update(
mut res: ResMut<TriggerStateRes>,
mut commands: Commands,
@@ -201,6 +267,7 @@ fn update(
}
}
#[cfg(feature = "server")]
fn update_heal_ability(
res: Res<TriggerStateRes>,
mut commands: Commands,

View File

@@ -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,
});
}
}
}

View File

@@ -1,27 +1,37 @@
use super::{ControlState, Controls};
use crate::{
GameState,
control::{CharacterInputEnabled, ControllerSet},
};
use bevy::{
input::{
#[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},
},
prelude::*,
};
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::<Controls>();
app.init_resource::<InputStateCache<KeyCode>>();
app.register_required_components::<Gamepad, InputStateCache<GamepadButton>>();
app.register_type::<ControllerSettings>();
app.add_systems(PreUpdate, (cache_keyboard_state, cache_gamepad_state));
#[cfg(feature = "client")]
{
app.add_systems(
FixedUpdate,
FixedPreUpdate,
(
gamepad_controls,
keyboard_controls,
@@ -29,19 +39,18 @@ pub fn plugin(app: &mut App) {
mouse_click,
gamepad_connections.run_if(on_event::<GamepadEvent>),
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)),
),
);
#[cfg(feature = "client")]
{
use lightyear::prelude::client::input::InputSet;
app.add_systems(
)
.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<Button> {
map: HashMap<Button, InputState>,
}
impl<Button> Default for InputStateCache<Button> {
fn default() -> Self {
Self { map: default() }
}
}
#[cfg(feature = "client")]
impl<Button: Hash + Eq> InputStateCache<Button> {
fn clear(&mut self) {
for state in self.map.values_mut() {
*state = InputState::default();
}
}
fn pressed(&self, button: Button) -> bool {
self.map
.get(&button)
.map(|state| state.pressed)
.unwrap_or_default()
}
fn just_pressed(&self, button: Button) -> bool {
self.map
.get(&button)
.map(|state| state.just_pressed)
.unwrap_or_default()
}
}
#[derive(Default)]
struct InputState {
pressed: bool,
just_pressed: bool,
}
fn cache_keyboard_state(
mut cache: ResMut<InputStateCache<KeyCode>>,
keyboard: Res<ButtonInput<KeyCode>>,
) {
let mut cache_key = |key| {
cache.map.entry(key).or_default().pressed |= keyboard.pressed(key);
cache.map.entry(key).or_default().just_pressed |= keyboard.just_pressed(key);
};
cache_key(KeyCode::Space);
cache_key(KeyCode::Tab);
cache_key(KeyCode::KeyB);
cache_key(KeyCode::Enter);
cache_key(KeyCode::Comma);
cache_key(KeyCode::Period);
cache_key(KeyCode::KeyQ);
cache_key(KeyCode::KeyE);
}
#[cfg(feature = "client")]
fn clear_keyboard_state(mut cache: ResMut<InputStateCache<KeyCode>>) {
cache.clear();
}
fn cache_gamepad_state(mut gamepads: Query<(&Gamepad, &mut InputStateCache<GamepadButton>)>) {
for (gamepad, mut cache) in gamepads.iter_mut() {
let mut cache_button = |button| {
cache.map.entry(button).or_default().pressed |= gamepad.pressed(button);
cache.map.entry(button).or_default().just_pressed |= gamepad.just_pressed(button);
};
cache_button(GamepadButton::North);
cache_button(GamepadButton::East);
cache_button(GamepadButton::South);
cache_button(GamepadButton::West);
cache_button(GamepadButton::DPadUp);
cache_button(GamepadButton::DPadRight);
cache_button(GamepadButton::DPadDown);
cache_button(GamepadButton::DPadLeft);
cache_button(GamepadButton::LeftTrigger);
cache_button(GamepadButton::LeftTrigger2);
cache_button(GamepadButton::RightTrigger);
cache_button(GamepadButton::RightTrigger2);
cache_button(GamepadButton::Select);
cache_button(GamepadButton::Start);
cache_button(GamepadButton::LeftThumb);
cache_button(GamepadButton::RightThumb);
}
}
#[cfg(feature = "client")]
fn clear_gamepad_state(mut caches: Query<&mut InputStateCache<GamepadButton>>) {
for mut cache in caches.iter_mut() {
cache.clear();
}
}
#[cfg(feature = "client")]
/// Take keyboard and gamepad state and combine them into unified input state
fn combine_controls(controls: Res<Controls>, mut combined_controls: ResMut<ControlState>) {
let keyboard = controls.keyboard_state;
@@ -102,6 +210,7 @@ fn combine_controls(controls: Res<Controls>, mut combined_controls: ResMut<Contr
combined_controls.cash_heal = gamepad.cash_heal | keyboard.cash_heal;
}
#[cfg(feature = "client")]
/// Applies a square deadzone to a Vec2
fn deadzone_square(v: Vec2, min: f32) -> Vec2 {
Vec2::new(
@@ -110,9 +219,13 @@ fn deadzone_square(v: Vec2, min: f32) -> Vec2 {
)
}
#[cfg(feature = "client")]
/// Collect gamepad inputs
fn gamepad_controls(gamepads: Query<(Entity, &Gamepad)>, mut controls: ResMut<Controls>) {
let Some((_e, gamepad)) = gamepads.iter().next() else {
fn gamepad_controls(
gamepads: Query<(Entity, &Gamepad, &InputStateCache<GamepadButton>)>,
mut controls: ResMut<Controls>,
) {
let Some((_e, gamepad, cache)) = gamepads.iter().next() else {
if controls.gamepad_state.is_some() {
controls.gamepad_state = None;
}
@@ -146,17 +259,17 @@ fn gamepad_controls(gamepads: Query<(Entity, &Gamepad)>, mut controls: ResMut<Co
let state = ControlState {
move_dir: deadzone_square(gamepad.left_stick(), deadzone_left_stick),
look_dir,
jump: gamepad.pressed(GamepadButton::South),
view_mode: gamepad.pressed(GamepadButton::LeftTrigger2),
trigger: gamepad.pressed(GamepadButton::RightTrigger2),
just_triggered: gamepad.just_pressed(GamepadButton::RightTrigger2),
select_left: gamepad.just_pressed(GamepadButton::LeftTrigger),
select_right: gamepad.just_pressed(GamepadButton::RightTrigger),
backpack_left: gamepad.just_pressed(GamepadButton::DPadLeft),
backpack_right: gamepad.just_pressed(GamepadButton::DPadRight),
backpack_swap: gamepad.just_pressed(GamepadButton::DPadDown),
backpack_toggle: gamepad.just_pressed(GamepadButton::DPadUp),
cash_heal: gamepad.just_pressed(GamepadButton::East),
jump: cache.pressed(GamepadButton::South),
view_mode: cache.pressed(GamepadButton::LeftTrigger2),
trigger: cache.pressed(GamepadButton::RightTrigger2),
just_triggered: cache.just_pressed(GamepadButton::RightTrigger2),
select_left: cache.just_pressed(GamepadButton::LeftTrigger),
select_right: cache.just_pressed(GamepadButton::RightTrigger),
backpack_left: cache.just_pressed(GamepadButton::DPadLeft),
backpack_right: cache.just_pressed(GamepadButton::DPadRight),
backpack_swap: cache.just_pressed(GamepadButton::DPadDown),
backpack_toggle: cache.just_pressed(GamepadButton::DPadUp),
cash_heal: cache.just_pressed(GamepadButton::East),
};
if controls
@@ -169,6 +282,7 @@ fn gamepad_controls(gamepads: Query<(Entity, &Gamepad)>, mut controls: ResMut<Co
}
}
#[cfg(feature = "client")]
/// Collect mouse movement input
fn mouse_rotate(mut mouse: EventReader<MouseMotion>, mut controls: ResMut<Controls>) {
controls.keyboard_state.look_dir = Vec2::ZERO;
@@ -178,8 +292,13 @@ fn mouse_rotate(mut mouse: EventReader<MouseMotion>, mut controls: ResMut<Contro
}
}
#[cfg(feature = "client")]
/// Collect keyboard input
fn keyboard_controls(keyboard: Res<ButtonInput<KeyCode>>, mut controls: ResMut<Controls>) {
fn keyboard_controls(
keyboard: Res<ButtonInput<KeyCode>>,
cache: Res<InputStateCache<KeyCode>>,
mut controls: ResMut<Controls>,
) {
let up_binds = [KeyCode::KeyW, KeyCode::ArrowUp];
let down_binds = [KeyCode::KeyS, KeyCode::ArrowDown];
let left_binds = [KeyCode::KeyA, KeyCode::ArrowLeft];
@@ -195,17 +314,18 @@ fn keyboard_controls(keyboard: Res<ButtonInput<KeyCode>>, mut controls: ResMut<C
let direction = Vec2::new(horizontal as f32, vertical as f32).clamp_length_max(1.0);
controls.keyboard_state.move_dir = direction;
controls.keyboard_state.jump = keyboard.pressed(KeyCode::Space);
controls.keyboard_state.view_mode = keyboard.pressed(KeyCode::Tab);
controls.keyboard_state.backpack_toggle = keyboard.just_pressed(KeyCode::KeyB);
controls.keyboard_state.backpack_swap = keyboard.just_pressed(KeyCode::Enter);
controls.keyboard_state.backpack_left = keyboard.just_pressed(KeyCode::Comma);
controls.keyboard_state.backpack_right = keyboard.just_pressed(KeyCode::Period);
controls.keyboard_state.select_left = keyboard.just_pressed(KeyCode::KeyQ);
controls.keyboard_state.select_right = keyboard.just_pressed(KeyCode::KeyE);
controls.keyboard_state.cash_heal = keyboard.just_pressed(KeyCode::Enter);
controls.keyboard_state.jump = cache.pressed(KeyCode::Space);
controls.keyboard_state.view_mode = cache.pressed(KeyCode::Tab);
controls.keyboard_state.backpack_toggle = cache.just_pressed(KeyCode::KeyB);
controls.keyboard_state.backpack_swap = cache.just_pressed(KeyCode::Enter);
controls.keyboard_state.backpack_left = cache.just_pressed(KeyCode::Comma);
controls.keyboard_state.backpack_right = cache.just_pressed(KeyCode::Period);
controls.keyboard_state.select_left = cache.just_pressed(KeyCode::KeyQ);
controls.keyboard_state.select_right = cache.just_pressed(KeyCode::KeyE);
controls.keyboard_state.cash_heal = cache.just_pressed(KeyCode::Enter);
}
#[cfg(feature = "client")]
/// Collect mouse button input when pressed
fn mouse_click(mut events: EventReader<MouseButtonInput>, mut controls: ResMut<Controls>) {
controls.keyboard_state.just_triggered = false;
@@ -226,13 +346,13 @@ fn mouse_click(mut events: EventReader<MouseButtonInput>, mut controls: ResMut<C
..
} => {
controls.keyboard_state.trigger = false;
controls.keyboard_state.just_triggered = false;
}
_ => {}
}
}
}
#[cfg(feature = "client")]
/// Receive gamepad connections and disconnections
fn gamepad_connections(mut evr_gamepad: EventReader<GamepadEvent>) {
for ev in evr_gamepad.read() {

View File

@@ -45,7 +45,9 @@ impl MapEntities for ControlState {
#[derive(Resource, Debug, Default)]
struct Controls {
#[cfg(feature = "client")]
keyboard_state: ControlState,
#[cfg(feature = "client")]
gamepad_state: Option<ControlState>,
}
@@ -74,9 +76,12 @@ pub fn plugin(app: &mut App) {
app.add_event::<ControllerSwitchEvent>();
app.configure_sets(
FixedPreUpdate,
ControllerSet::CollectInputs.run_if(in_state(GameState::Playing)),
)
.configure_sets(
FixedUpdate,
(
ControllerSet::CollectInputs,
ControllerSet::ApplyControlsFly.run_if(resource_equals(SelectedController(
ControllerSet::ApplyControlsFly,
))),

View File

@@ -107,8 +107,6 @@ fn on_spawn_check(
.insert_if(Ai, || !spawn.disable_ai)
.with_child((Name::from("body-rig"), AnimatedCharacter::new(id)))
.observe(on_kill);
#[cfg(feature = "server")]
ecommands.insert(Replicate::to_clients(NetworkTarget::All));
commands.trigger(SpawnCharacter(transform.translation));
commands.trigger(PlaySound::Beaming);