From 2c68e1fb556d72661b69c225c3a30236b6e03730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Sat, 26 Sep 2026 17:40:52 +0200 Subject: [PATCH] Fix fighter visuals after landing snap --- src/camera/mod.rs | 14 +++++++----- src/fighter/mod.rs | 10 ++++++++- src/fighter/motion.rs | 44 +++++++++++++++++++++++++++++++++++-- src/fighter/state/air.rs | 1 + src/fighter/state/ground.rs | 4 ++++ src/fighter/state/mod.rs | 4 ++++ 6 files changed, 69 insertions(+), 8 deletions(-) diff --git a/src/camera/mod.rs b/src/camera/mod.rs index 40bdc2b..2f2c722 100644 --- a/src/camera/mod.rs +++ b/src/camera/mod.rs @@ -5,7 +5,7 @@ use bevy_ggrs::GgrsFrameTiming; use crate::{ debug_tools::DebugSettings, - fighter::{manifest::FighterManifest, motion::sample_presentation_translation}, + fighter::{manifest::FighterManifest, motion::{sample_presentation_translation_with_snap, FighterPresentationSnap}}, stage::manifest::StageCameraProfile, }; @@ -313,6 +313,7 @@ fn update_match_camera( &crate::fighter::motion::FighterPreviousTranslation, &crate::fighter::Fighter, &crate::fighter::FighterFacingDirection, + &FighterPresentationSnap, &mut FighterCameraExtents, ), With, @@ -329,16 +330,17 @@ fn update_match_camera( let subjects = fighters .iter_mut() .map( - |(translation, previous_translation, fighter, facing, mut extents)| { + |(translation, previous_translation, fighter, facing, presentation_snap, mut extents)| { let manifest = manifests .get(&fighter.manifest) .expect("Fighter manifest should be valid"); fighter_subject_bounds( - sample_presentation_translation( + sample_presentation_translation_with_snap( *previous_translation, *translation, timing.overstep_fraction(), debug_settings.motion_sampling, + *presentation_snap, ), manifest.camera, *facing, @@ -465,6 +467,7 @@ fn debug_draw_camera( &crate::fighter::motion::FighterPreviousTranslation, &crate::fighter::Fighter, &crate::fighter::FighterFacingDirection, + &FighterPresentationSnap, &FighterCameraExtents, ), With, @@ -486,15 +489,16 @@ fn debug_draw_camera( Color::srgb(1.0, 0.5, 0.0), ); draw_rectangle(&mut gizmos, camera.frame, Color::srgb(1.0, 1.0, 0.0)); - for (translation, previous_translation, fighter, _facing, extents) in &fighters { + for (translation, previous_translation, fighter, _facing, presentation_snap, extents) in &fighters { let manifest = manifests .get(&fighter.manifest) .expect("Fighter manifest should be valid"); - let center = sample_presentation_translation( + let center = sample_presentation_translation_with_snap( *previous_translation, *translation, timing.overstep_fraction(), debug_settings.motion_sampling, + *presentation_snap, ) + Vec2::Y * manifest.camera.vertical_origin_offset.to_num::(); draw_rectangle( &mut gizmos, diff --git a/src/fighter/mod.rs b/src/fighter/mod.rs index 4700ea4..fdde068 100644 --- a/src/fighter/mod.rs +++ b/src/fighter/mod.rs @@ -29,7 +29,8 @@ pub use attributes::FighterAttributes; pub use ecb::FighterECB; pub use manifest::FighterId; pub use motion::{ - FighterPreviousFacingDirection, FighterPreviousTranslation, FighterTranslation, FighterVelocity, + FighterPresentationSnap, FighterPreviousFacingDirection, FighterPreviousTranslation, + FighterTranslation, FighterVelocity, }; pub use surface::FighterSurfaceState; @@ -147,6 +148,10 @@ impl Plugin for FighterPlugin { fn build(&self, app: &mut App) { app.add_plugins(FighterAttackPlugin) // Every state gets a chance to interrupt itself, in priority order. + .add_systems( + GgrsSchedule, + motion::clear_fighter_presentation_snaps.in_set(GameplaySet::Input), + ) .add_systems( GgrsSchedule, ( @@ -202,6 +207,8 @@ impl Plugin for FighterPlugin { .checksum_component_with_hash::() .rollback_component_with_copy::() .checksum_component_with_hash::() + .rollback_component_with_copy::() + .checksum_component_with_hash::() .rollback_component_with_copy::() .checksum_component_with_hash::() .rollback_component_with_copy::() @@ -304,6 +311,7 @@ pub fn spawn_fighter( FighterVelocity::default(), FighterTranslation(spawn_position), FighterPreviousTranslation(spawn_position), + FighterPresentationSnap::default(), ), visual_root, animations, diff --git a/src/fighter/motion.rs b/src/fighter/motion.rs index 4f47a25..c2804a0 100644 --- a/src/fighter/motion.rs +++ b/src/fighter/motion.rs @@ -21,6 +21,11 @@ pub struct FighterVelocity(pub FGVec2); #[derive(Component, Deref, DerefMut, Default, Clone, Copy, Hash)] pub struct FighterPreviousTranslation(pub FGVec2); +/// Marks a simulation frame whose position changed discontinuously for +/// presentation purposes (for example, a landing correction). +#[derive(Component, Default, Clone, Copy, Hash, Debug, PartialEq, Eq)] +pub struct FighterPresentationSnap(pub bool); + #[derive(Component, Deref, DerefMut, Default, Clone, Copy, Hash)] #[require(FighterPreviousTranslation)] pub struct FighterTranslation(pub FGVec2); @@ -52,10 +57,31 @@ pub fn sample_presentation_translation( } } +pub fn sample_presentation_translation_with_snap( + previous: FighterPreviousTranslation, + current: FighterTranslation, + overstep_fraction: f32, + mode: PresentationMotionMode, + snapped: FighterPresentationSnap, +) -> Vec2 { + if snapped.0 { + Vec2::new(current.x.to_num(), current.y.to_num()) + } else { + sample_presentation_translation(previous, current, overstep_fraction, mode) + } +} + +pub fn clear_fighter_presentation_snaps(mut fighters: Query<&mut FighterPresentationSnap>) { + for mut snap in &mut fighters { + snap.0 = false; + } +} + pub fn apply_fighter_translation_to_visuals( query: Query<( &FighterTranslation, &FighterPreviousTranslation, + &FighterPresentationSnap, &FighterHitstopShake, &mut Transform, )>, @@ -64,12 +90,13 @@ pub fn apply_fighter_translation_to_visuals( game_settings: Res, ) { let overstep_fraction = timing.overstep_fraction(); - for (translation, previous_translation, hitstop_shake, mut transform) in query { - let translation = sample_presentation_translation( + for (translation, previous_translation, presentation_snap, hitstop_shake, mut transform) in query { + let translation = sample_presentation_translation_with_snap( *previous_translation, *translation, overstep_fraction, debug_settings.motion_sampling, + *presentation_snap, ); transform.translation = Vec3::new( translation.x, @@ -169,6 +196,19 @@ mod tests { assert_eq!(position, Vec2::new(7.0, 14.0)); } + #[test] + fn presentation_translation_uses_current_sample_after_snap() { + let position = sample_presentation_translation_with_snap( + FighterPreviousTranslation(FGVec2::new(FGi32::lit("2"), FGi32::lit("4"))), + FighterTranslation(FGVec2::new(FGi32::lit("6"), FGi32::lit("12"))), + 0.25, + PresentationMotionMode::Extrapolation, + FighterPresentationSnap(true), + ); + + assert_eq!(position, Vec2::new(6.0, 12.0)); + } + #[test] fn snapshot_fighter_motion_copies_current_world_state() { let mut world = World::new(); diff --git a/src/fighter/state/air.rs b/src/fighter/state/air.rs index ba86260..a948a25 100644 --- a/src/fighter/state/air.rs +++ b/src/fighter/state/air.rs @@ -123,6 +123,7 @@ pub fn collision_interrupt( ctx.ecb.left -= diff; ctx.ecb.right -= diff; ctx.translation.0 = res.hit_position; + ctx.presentation_snap.0 = true; ctx.surface.land(res.line_id); Some(FighterState::Land(LandingState::create(landing_type))) } diff --git a/src/fighter/state/ground.rs b/src/fighter/state/ground.rs index cf64667..024cbfe 100644 --- a/src/fighter/state/ground.rs +++ b/src/fighter/state/ground.rs @@ -376,6 +376,7 @@ mod tests { right: FGVec2::lit("0.5", "0.0"), }; let previous_ecb = FighterPreviousECB::from(ecb); + let mut presentation_snap = crate::fighter::FighterPresentationSnap::default(); let mut context = CollisionContext { translation: &mut translation, prev_translation: &previous_translation, @@ -386,6 +387,7 @@ mod tests { prev_ecb: &previous_ecb, stage_collision: &stage_collision, leave_ground_ecb_lock_duration: 10, + presentation_snap: &mut presentation_snap, downward_stick_threshold: FGi32::ONE, }; @@ -429,6 +431,7 @@ mod tests { right: FGVec2::lit("0.5", "0.0"), }; let previous_ecb = FighterPreviousECB::from(ecb); + let mut presentation_snap = crate::fighter::FighterPresentationSnap::default(); let mut context = CollisionContext { translation: &mut translation, prev_translation: &previous_translation, @@ -439,6 +442,7 @@ mod tests { prev_ecb: &previous_ecb, stage_collision: &stage_collision, leave_ground_ecb_lock_duration: 10, + presentation_snap: &mut presentation_snap, downward_stick_threshold: FGi32::ONE, }; diff --git a/src/fighter/state/mod.rs b/src/fighter/state/mod.rs index 85fca7f..67c39a4 100644 --- a/src/fighter/state/mod.rs +++ b/src/fighter/state/mod.rs @@ -20,6 +20,7 @@ use crate::fighter::{ Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop, FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole, FighterPreviousTranslation, FighterSurfaceState, FighterTranslation, FighterVelocity, + FighterPresentationSnap, }; use crate::fighter::surface::EcbLockPolicy; use crate::game_settings::GameSettings; @@ -200,6 +201,7 @@ pub struct CollisionContext<'a> { pub prev_ecb: &'a FighterPreviousECB, pub stage_collision: &'a StageCollision, pub leave_ground_ecb_lock_duration: u32, + pub presentation_snap: &'a mut FighterPresentationSnap, pub downward_stick_threshold: FGi32, } @@ -244,6 +246,7 @@ fighter_states! { pub struct FighterFrameQuery { pub translation: &'static mut FighterTranslation, pub prev_translation: &'static mut FighterPreviousTranslation, + pub presentation_snap: &'static mut FighterPresentationSnap, pub input: &'static mut FighterInput, pub hitboxes: &'static mut FighterHitboxes, pub velocity: &'static mut FighterVelocity, @@ -354,6 +357,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> { CollisionContext { translation: &mut self.translation, prev_translation: &self.prev_translation, + presentation_snap: &mut self.presentation_snap, input: &self.input, velocity: &mut self.velocity, surface: &mut self.surface,