Add physics interpolation and fix lingering ground attack hitboxes
This commit is contained in:
+33
-8
@@ -1,8 +1,13 @@
|
|||||||
//! Presentation-only state and tuning for the match camera.
|
//! Presentation-only state and tuning for the match camera.
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
use bevy_ggrs::GgrsFrameTiming;
|
||||||
|
|
||||||
use crate::{fighter::manifest::FighterManifest, stage::manifest::StageCameraProfile};
|
use crate::{
|
||||||
|
debug_tools::DebugSettings,
|
||||||
|
fighter::{manifest::FighterManifest, motion::sample_presentation_translation},
|
||||||
|
stage::manifest::StageCameraProfile,
|
||||||
|
};
|
||||||
|
|
||||||
const EMPTY_FRAME_HALF_SIZE: f32 = 40.0;
|
const EMPTY_FRAME_HALF_SIZE: f32 = 40.0;
|
||||||
const SUBJECT_COUNT_SCALES: [f32; 4] = [1.5, 1.32, 1.16, 1.0];
|
const SUBJECT_COUNT_SCALES: [f32; 4] = [1.5, 1.32, 1.16, 1.0];
|
||||||
@@ -110,7 +115,10 @@ impl Plugin for MatchCameraPlugin {
|
|||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
Update,
|
Update,
|
||||||
(update_match_camera, debug_draw_camera)
|
(
|
||||||
|
update_match_camera,
|
||||||
|
debug_draw_camera.run_if(crate::debug_tools::camera_debug_enabled),
|
||||||
|
)
|
||||||
.chain()
|
.chain()
|
||||||
.run_if(in_state(crate::AppState::InMatch)),
|
.run_if(in_state(crate::AppState::InMatch)),
|
||||||
);
|
);
|
||||||
@@ -302,6 +310,7 @@ fn update_match_camera(
|
|||||||
mut fighters: Query<
|
mut fighters: Query<
|
||||||
(
|
(
|
||||||
&crate::fighter::motion::FighterTranslation,
|
&crate::fighter::motion::FighterTranslation,
|
||||||
|
&crate::fighter::motion::FighterPreviousTranslation,
|
||||||
&crate::fighter::Fighter,
|
&crate::fighter::Fighter,
|
||||||
&crate::fighter::FighterFacingDirection,
|
&crate::fighter::FighterFacingDirection,
|
||||||
&mut FighterCameraExtents,
|
&mut FighterCameraExtents,
|
||||||
@@ -311,25 +320,34 @@ fn update_match_camera(
|
|||||||
camera_profiles: Query<&StageCameraProfile, With<MatchCamera>>,
|
camera_profiles: Query<&StageCameraProfile, With<MatchCamera>>,
|
||||||
mut cameras: Query<(&mut Transform, &mut Projection, &mut MatchCamera), With<MatchCamera>>,
|
mut cameras: Query<(&mut Transform, &mut Projection, &mut MatchCamera), With<MatchCamera>>,
|
||||||
manifests: Res<Assets<FighterManifest>>,
|
manifests: Res<Assets<FighterManifest>>,
|
||||||
|
timing: Res<GgrsFrameTiming>,
|
||||||
|
debug_settings: Res<DebugSettings>,
|
||||||
) {
|
) {
|
||||||
let Ok(profile) = camera_profiles.single() else {
|
let Ok(profile) = camera_profiles.single() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let subjects = fighters
|
let subjects = fighters
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.map(|(translation, fighter, facing, mut extents)| {
|
.map(
|
||||||
|
|(translation, previous_translation, fighter, facing, mut extents)| {
|
||||||
let manifest = manifests
|
let manifest = manifests
|
||||||
.get(&fighter.manifest)
|
.get(&fighter.manifest)
|
||||||
.expect("Fighter manifest should be valid");
|
.expect("Fighter manifest should be valid");
|
||||||
fighter_subject_bounds(
|
fighter_subject_bounds(
|
||||||
Vec2::new(translation.x.to_num(), translation.y.to_num()),
|
sample_presentation_translation(
|
||||||
|
*previous_translation,
|
||||||
|
*translation,
|
||||||
|
timing.overstep_fraction(),
|
||||||
|
debug_settings.motion_sampling,
|
||||||
|
),
|
||||||
manifest.camera,
|
manifest.camera,
|
||||||
*facing,
|
*facing,
|
||||||
profile,
|
profile,
|
||||||
&mut extents,
|
&mut extents,
|
||||||
time.delta_secs(),
|
time.delta_secs(),
|
||||||
)
|
)
|
||||||
})
|
},
|
||||||
|
)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for (mut transform, mut projection, mut camera) in &mut cameras {
|
for (mut transform, mut projection, mut camera) in &mut cameras {
|
||||||
@@ -444,6 +462,7 @@ fn debug_draw_camera(
|
|||||||
fighters: Query<
|
fighters: Query<
|
||||||
(
|
(
|
||||||
&crate::fighter::motion::FighterTranslation,
|
&crate::fighter::motion::FighterTranslation,
|
||||||
|
&crate::fighter::motion::FighterPreviousTranslation,
|
||||||
&crate::fighter::Fighter,
|
&crate::fighter::Fighter,
|
||||||
&crate::fighter::FighterFacingDirection,
|
&crate::fighter::FighterFacingDirection,
|
||||||
&FighterCameraExtents,
|
&FighterCameraExtents,
|
||||||
@@ -452,6 +471,8 @@ fn debug_draw_camera(
|
|||||||
>,
|
>,
|
||||||
cameras: Query<(&MatchCamera, &StageCameraProfile)>,
|
cameras: Query<(&MatchCamera, &StageCameraProfile)>,
|
||||||
manifests: Res<Assets<FighterManifest>>,
|
manifests: Res<Assets<FighterManifest>>,
|
||||||
|
timing: Res<GgrsFrameTiming>,
|
||||||
|
debug_settings: Res<DebugSettings>,
|
||||||
) {
|
) {
|
||||||
for (camera, profile) in &cameras {
|
for (camera, profile) in &cameras {
|
||||||
draw_rectangle(
|
draw_rectangle(
|
||||||
@@ -465,12 +486,16 @@ fn debug_draw_camera(
|
|||||||
Color::srgb(1.0, 0.5, 0.0),
|
Color::srgb(1.0, 0.5, 0.0),
|
||||||
);
|
);
|
||||||
draw_rectangle(&mut gizmos, camera.frame, Color::srgb(1.0, 1.0, 0.0));
|
draw_rectangle(&mut gizmos, camera.frame, Color::srgb(1.0, 1.0, 0.0));
|
||||||
for (translation, fighter, _facing, extents) in &fighters {
|
for (translation, previous_translation, fighter, _facing, extents) in &fighters {
|
||||||
let manifest = manifests
|
let manifest = manifests
|
||||||
.get(&fighter.manifest)
|
.get(&fighter.manifest)
|
||||||
.expect("Fighter manifest should be valid");
|
.expect("Fighter manifest should be valid");
|
||||||
let center = Vec2::new(translation.x.to_num(), translation.y.to_num())
|
let center = sample_presentation_translation(
|
||||||
+ Vec2::Y * manifest.camera.vertical_origin_offset.to_num::<f32>();
|
*previous_translation,
|
||||||
|
*translation,
|
||||||
|
timing.overstep_fraction(),
|
||||||
|
debug_settings.motion_sampling,
|
||||||
|
) + Vec2::Y * manifest.camera.vertical_origin_offset.to_num::<f32>();
|
||||||
draw_rectangle(
|
draw_rectangle(
|
||||||
&mut gizmos,
|
&mut gizmos,
|
||||||
CameraFrameBounds {
|
CameraFrameBounds {
|
||||||
|
|||||||
+80
-3
@@ -19,6 +19,29 @@ pub enum FighterDebugRowKind {
|
|||||||
Facing,
|
Facing,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub enum PresentationMotionMode {
|
||||||
|
#[default]
|
||||||
|
Interpolation,
|
||||||
|
Extrapolation,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PresentationMotionMode {
|
||||||
|
pub const fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Interpolation => "Interpolation",
|
||||||
|
Self::Extrapolation => "Extrapolation",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cycle(&mut self) {
|
||||||
|
*self = match self {
|
||||||
|
Self::Interpolation => Self::Extrapolation,
|
||||||
|
Self::Extrapolation => Self::Interpolation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FighterDebugRowKind {
|
impl FighterDebugRowKind {
|
||||||
const ALL: [Self; 6] = [
|
const ALL: [Self; 6] = [
|
||||||
Self::None,
|
Self::None,
|
||||||
@@ -57,6 +80,7 @@ enum DebugFlag {
|
|||||||
StageCollision,
|
StageCollision,
|
||||||
AnimationBones,
|
AnimationBones,
|
||||||
AttackHitboxes,
|
AttackHitboxes,
|
||||||
|
Camera,
|
||||||
DrawThroughGeometry,
|
DrawThroughGeometry,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +97,9 @@ enum DebugMenuEntry {
|
|||||||
PlayerSlots {
|
PlayerSlots {
|
||||||
label: &'static str,
|
label: &'static str,
|
||||||
},
|
},
|
||||||
|
MotionSampling {
|
||||||
|
label: &'static str,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const FIGHTER_MENU: &[DebugMenuEntry] = &[
|
const FIGHTER_MENU: &[DebugMenuEntry] = &[
|
||||||
@@ -87,6 +114,10 @@ const FIGHTER_MENU: &[DebugMenuEntry] = &[
|
|||||||
label: "Animation bones",
|
label: "Animation bones",
|
||||||
flag: DebugFlag::AnimationBones,
|
flag: DebugFlag::AnimationBones,
|
||||||
},
|
},
|
||||||
|
DebugMenuEntry::Toggle {
|
||||||
|
label: "Camera",
|
||||||
|
flag: DebugFlag::Camera,
|
||||||
|
},
|
||||||
DebugMenuEntry::Toggle {
|
DebugMenuEntry::Toggle {
|
||||||
label: "Attack hitboxes",
|
label: "Attack hitboxes",
|
||||||
flag: DebugFlag::AttackHitboxes,
|
flag: DebugFlag::AttackHitboxes,
|
||||||
@@ -103,10 +134,15 @@ const DIAGNOSTICS_MENU: &[DebugMenuEntry] = &[DebugMenuEntry::Toggle {
|
|||||||
flag: DebugFlag::Network,
|
flag: DebugFlag::Network,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
const RENDERING_MENU: &[DebugMenuEntry] = &[DebugMenuEntry::Toggle {
|
const RENDERING_MENU: &[DebugMenuEntry] = &[
|
||||||
|
DebugMenuEntry::MotionSampling {
|
||||||
|
label: "Motion sampling",
|
||||||
|
},
|
||||||
|
DebugMenuEntry::Toggle {
|
||||||
label: "Draw through geometry",
|
label: "Draw through geometry",
|
||||||
flag: DebugFlag::DrawThroughGeometry,
|
flag: DebugFlag::DrawThroughGeometry,
|
||||||
}];
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const ROOT_MENU: &[DebugMenuEntry] = &[
|
const ROOT_MENU: &[DebugMenuEntry] = &[
|
||||||
DebugMenuEntry::Submenu {
|
DebugMenuEntry::Submenu {
|
||||||
@@ -146,7 +182,9 @@ pub struct DebugSettings {
|
|||||||
pub stage_collision: bool,
|
pub stage_collision: bool,
|
||||||
pub animation_bones: bool,
|
pub animation_bones: bool,
|
||||||
pub attack_hitboxes: bool,
|
pub attack_hitboxes: bool,
|
||||||
|
pub camera: bool,
|
||||||
pub draw_through_geometry: bool,
|
pub draw_through_geometry: bool,
|
||||||
|
pub motion_sampling: PresentationMotionMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DebugSettings {
|
impl Default for DebugSettings {
|
||||||
@@ -162,7 +200,9 @@ impl Default for DebugSettings {
|
|||||||
stage_collision: false,
|
stage_collision: false,
|
||||||
animation_bones: false,
|
animation_bones: false,
|
||||||
attack_hitboxes: false,
|
attack_hitboxes: false,
|
||||||
|
camera: false,
|
||||||
draw_through_geometry: false,
|
draw_through_geometry: false,
|
||||||
|
motion_sampling: PresentationMotionMode::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,6 +263,7 @@ impl DebugSettings {
|
|||||||
DebugFlag::AnimationBones => self.animation_bones,
|
DebugFlag::AnimationBones => self.animation_bones,
|
||||||
DebugFlag::AttackHitboxes => self.attack_hitboxes,
|
DebugFlag::AttackHitboxes => self.attack_hitboxes,
|
||||||
DebugFlag::DrawThroughGeometry => self.draw_through_geometry,
|
DebugFlag::DrawThroughGeometry => self.draw_through_geometry,
|
||||||
|
DebugFlag::Camera => self.camera,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,9 +277,14 @@ impl DebugSettings {
|
|||||||
DebugFlag::DrawThroughGeometry => {
|
DebugFlag::DrawThroughGeometry => {
|
||||||
self.draw_through_geometry = !self.draw_through_geometry
|
self.draw_through_geometry = !self.draw_through_geometry
|
||||||
}
|
}
|
||||||
|
DebugFlag::Camera => self.camera = !self.camera,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cycle_motion_sampling(&mut self) {
|
||||||
|
self.motion_sampling.cycle();
|
||||||
|
}
|
||||||
|
|
||||||
fn rows_mut(&mut self, handle: usize) -> &mut [FighterDebugRowKind; PLAYER_DEBUG_ROW_COUNT] {
|
fn rows_mut(&mut self, handle: usize) -> &mut [FighterDebugRowKind; PLAYER_DEBUG_ROW_COUNT] {
|
||||||
self.player_rows
|
self.player_rows
|
||||||
.entry(handle)
|
.entry(handle)
|
||||||
@@ -342,6 +388,16 @@ fn handle_palette_input(
|
|||||||
rows[selected] = rows[selected].cycle(direction);
|
rows[selected] = rows[selected].cycle(direction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let DebugMenuPage::Static(entries) = page
|
||||||
|
&& matches!(
|
||||||
|
entries.get(settings.selected()),
|
||||||
|
Some(DebugMenuEntry::MotionSampling { .. })
|
||||||
|
)
|
||||||
|
&& (gamepad.just_pressed(GamepadButton::DPadLeft)
|
||||||
|
|| gamepad.just_pressed(GamepadButton::DPadRight))
|
||||||
|
{
|
||||||
|
settings.cycle_motion_sampling();
|
||||||
|
}
|
||||||
if gamepad.just_pressed(GamepadButton::South) {
|
if gamepad.just_pressed(GamepadButton::South) {
|
||||||
match page {
|
match page {
|
||||||
DebugMenuPage::Static(entries) if page_len != 0 => match entries[settings.selected()] {
|
DebugMenuPage::Static(entries) if page_len != 0 => match entries[settings.selected()] {
|
||||||
@@ -350,6 +406,7 @@ fn handle_palette_input(
|
|||||||
settings.push_page(DebugMenuPage::Static(entries));
|
settings.push_page(DebugMenuPage::Static(entries));
|
||||||
}
|
}
|
||||||
DebugMenuEntry::PlayerSlots { .. } => settings.push_page(DebugMenuPage::PlayerList),
|
DebugMenuEntry::PlayerSlots { .. } => settings.push_page(DebugMenuPage::PlayerList),
|
||||||
|
DebugMenuEntry::MotionSampling { .. } => settings.cycle_motion_sampling(),
|
||||||
},
|
},
|
||||||
DebugMenuPage::PlayerList if page_len != 0 => {
|
DebugMenuPage::PlayerList if page_len != 0 => {
|
||||||
let handle = player_handles[settings.selected()];
|
let handle = player_handles[settings.selected()];
|
||||||
@@ -409,6 +466,12 @@ fn draw_palette(
|
|||||||
| DebugMenuEntry::PlayerSlots { label } => {
|
| DebugMenuEntry::PlayerSlots { label } => {
|
||||||
ui.label(format!("{marker} {label} >"));
|
ui.label(format!("{marker} {label} >"));
|
||||||
}
|
}
|
||||||
|
DebugMenuEntry::MotionSampling { label } => {
|
||||||
|
ui.label(format!(
|
||||||
|
"{marker} {label}: {}",
|
||||||
|
settings.motion_sampling.label()
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -437,7 +500,7 @@ fn draw_palette(
|
|||||||
let controls = if matches!(page, DebugMenuPage::PlayerRows(_)) {
|
let controls = if matches!(page, DebugMenuPage::PlayerRows(_)) {
|
||||||
"D-pad: select/change B: back"
|
"D-pad: select/change B: back"
|
||||||
} else {
|
} else {
|
||||||
"D-pad: select A: enter/toggle B: back"
|
"D-pad: select A: enter/toggle Left/right: change B: back"
|
||||||
};
|
};
|
||||||
ui.label(controls);
|
ui.label(controls);
|
||||||
});
|
});
|
||||||
@@ -471,6 +534,10 @@ pub fn attack_hitboxes_enabled(settings: Res<DebugSettings>) -> bool {
|
|||||||
settings.attack_hitboxes
|
settings.attack_hitboxes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn camera_debug_enabled(settings: Res<DebugSettings>) -> bool {
|
||||||
|
settings.camera
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -509,4 +576,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert!(settings.player_rows(1).is_none());
|
assert!(settings.player_rows(1).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn presentation_motion_mode_cycles_between_choices() {
|
||||||
|
let mut mode = PresentationMotionMode::Interpolation;
|
||||||
|
|
||||||
|
mode.cycle();
|
||||||
|
assert_eq!(mode, PresentationMotionMode::Extrapolation);
|
||||||
|
mode.cycle();
|
||||||
|
assert_eq!(mode, PresentationMotionMode::Interpolation);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use bevy::prelude::*;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
fighter::{
|
fighter::{
|
||||||
FighterECB, FighterFacingDirection, FighterTranslation, FighterVisual,
|
FighterFacingDirection, FighterVisual,
|
||||||
visual::FighterAnimations,
|
visual::FighterAnimations,
|
||||||
},
|
},
|
||||||
player::Player,
|
player::Player,
|
||||||
@@ -54,15 +54,6 @@ pub fn advance_fighter_animation_frames(mut fighters: Query<&mut FighterAnimatio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_fighter_translation_to_visuals(
|
|
||||||
query: Query<(&FighterTranslation, &FighterECB, &mut Transform)>,
|
|
||||||
) {
|
|
||||||
for (translation, ecb, mut transform) in query {
|
|
||||||
let translation = translation.0 + ecb.get_bottom_point();
|
|
||||||
transform.translation = Vec3::new(translation.x.to_num(), translation.y.to_num(), 0.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setup_fighter_animation_player(
|
pub fn setup_fighter_animation_player(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
animated: Query<Entity, Added<AnimationPlayer>>,
|
animated: Query<Entity, Added<AnimationPlayer>>,
|
||||||
@@ -153,4 +144,5 @@ pub enum AnimKind {
|
|||||||
// Attacks
|
// Attacks
|
||||||
|
|
||||||
AttackJab1,
|
AttackJab1,
|
||||||
|
AttackUpTilt,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ impl AttackAngle {
|
|||||||
#[derive(Serialize, Deserialize, Reflect, PartialEq, Eq, Hash, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Reflect, PartialEq, Eq, Hash, Clone, Debug)]
|
||||||
pub enum AttackKind {
|
pub enum AttackKind {
|
||||||
NAir,
|
NAir,
|
||||||
Jab
|
Jab,
|
||||||
|
UpTilt,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AttackKind {
|
impl AttackKind {
|
||||||
@@ -72,6 +73,7 @@ impl AttackKind {
|
|||||||
match self {
|
match self {
|
||||||
AttackKind::NAir => AnimKind::AttackJab1,
|
AttackKind::NAir => AnimKind::AttackJab1,
|
||||||
AttackKind::Jab => AnimKind::AttackJab1,
|
AttackKind::Jab => AnimKind::AttackJab1,
|
||||||
|
AttackKind::UpTilt => AnimKind::AttackUpTilt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-6
@@ -125,13 +125,10 @@ impl Plugin for FighterPlugin {
|
|||||||
state::state_collision_interrupt_system.in_set(GameplaySet::Collision),
|
state::state_collision_interrupt_system.in_set(GameplaySet::Collision),
|
||||||
)
|
)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
GgrsSchedule,
|
Update,
|
||||||
(
|
(motion::apply_fighter_translation_to_visuals, animation::apply_animation)
|
||||||
animation::apply_fighter_translation_to_visuals,
|
|
||||||
animation::apply_animation,
|
|
||||||
)
|
|
||||||
.chain()
|
.chain()
|
||||||
.in_set(GameplaySet::Animation),
|
.run_if(in_state(crate::AppState::InMatch)),
|
||||||
)
|
)
|
||||||
// Rollback registration lives next to the systems that write these
|
// Rollback registration lives next to the systems that write these
|
||||||
// components; a component simulated here but missing from this list
|
// components; a component simulated here but missing from this list
|
||||||
@@ -177,6 +174,13 @@ pub struct FighterHitboxes {
|
|||||||
pub active_hitboxes: Vec<usize>
|
pub active_hitboxes: Vec<usize>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FighterHitboxes {
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.attack_script = None;
|
||||||
|
self.active_hitboxes.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn spawn_fighter(
|
pub fn spawn_fighter(
|
||||||
commands: &mut Commands,
|
commands: &mut Commands,
|
||||||
player_handle: usize,
|
player_handle: usize,
|
||||||
@@ -203,8 +207,11 @@ pub fn spawn_fighter(
|
|||||||
attack_script: None,
|
attack_script: None,
|
||||||
active_hitboxes: vec![]
|
active_hitboxes: vec![]
|
||||||
}),
|
}),
|
||||||
|
(
|
||||||
FighterVelocity::default(),
|
FighterVelocity::default(),
|
||||||
FighterTranslation(spawn_position),
|
FighterTranslation(spawn_position),
|
||||||
|
FighterPreviousTranslation(spawn_position),
|
||||||
|
),
|
||||||
visual_root,
|
visual_root,
|
||||||
animations,
|
animations,
|
||||||
attack_scripts,
|
attack_scripts,
|
||||||
|
|||||||
+78
-1
@@ -2,8 +2,13 @@
|
|||||||
//! this module is what turns that into a position.
|
//! this module is what turns that into a position.
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
use bevy_ggrs::GgrsFrameTiming;
|
||||||
|
|
||||||
use crate::{fighter::{FighterFacingDirection, baked_animation::FixedMat4}, math::{int::FGi32, vec::FGVec2, vec3::FGVec3}};
|
use crate::debug_tools::PresentationMotionMode;
|
||||||
|
use crate::{
|
||||||
|
fighter::{FighterECB, FighterFacingDirection, baked_animation::FixedMat4},
|
||||||
|
math::{int::FGi32, vec::FGVec2, vec3::FGVec3},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Component, Clone, Copy)]
|
#[derive(Component, Clone, Copy)]
|
||||||
pub struct Grounded;
|
pub struct Grounded;
|
||||||
@@ -29,6 +34,49 @@ impl FighterTranslation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Samples deterministic fighter positions for presentation without changing
|
||||||
|
/// gameplay state or rollback timing.
|
||||||
|
pub fn sample_presentation_translation(
|
||||||
|
previous: FighterPreviousTranslation,
|
||||||
|
current: FighterTranslation,
|
||||||
|
overstep_fraction: f32,
|
||||||
|
mode: PresentationMotionMode,
|
||||||
|
) -> Vec2 {
|
||||||
|
let previous = Vec2::new(previous.x.to_num(), previous.y.to_num());
|
||||||
|
let current = Vec2::new(current.x.to_num(), current.y.to_num());
|
||||||
|
match mode {
|
||||||
|
PresentationMotionMode::Interpolation => previous.lerp(current, overstep_fraction),
|
||||||
|
PresentationMotionMode::Extrapolation => current + (current - previous) * overstep_fraction,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_fighter_translation_to_visuals(
|
||||||
|
query: Query<(
|
||||||
|
&FighterTranslation,
|
||||||
|
&FighterPreviousTranslation,
|
||||||
|
&FighterECB,
|
||||||
|
&mut Transform,
|
||||||
|
)>,
|
||||||
|
timing: Res<GgrsFrameTiming>,
|
||||||
|
debug_settings: Res<crate::debug_tools::DebugSettings>,
|
||||||
|
) {
|
||||||
|
let overstep_fraction = timing.overstep_fraction();
|
||||||
|
for (translation, previous_translation, ecb, mut transform) in query {
|
||||||
|
let translation = sample_presentation_translation(
|
||||||
|
*previous_translation,
|
||||||
|
*translation,
|
||||||
|
overstep_fraction,
|
||||||
|
debug_settings.motion_sampling,
|
||||||
|
);
|
||||||
|
let bottom = ecb.get_bottom_point();
|
||||||
|
transform.translation = Vec3::new(
|
||||||
|
translation.x + bottom.x.to_num::<f32>(),
|
||||||
|
translation.y + bottom.y.to_num::<f32>(),
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn apply_air_motion(
|
pub fn apply_air_motion(
|
||||||
fighters: Query<(
|
fighters: Query<(
|
||||||
&FighterVelocity,
|
&FighterVelocity,
|
||||||
@@ -51,3 +99,32 @@ pub fn copy_fighter_transform_to_visuals(fighters: Query<(&FighterTranslation, &
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn fighter_movement() {}
|
pub fn fighter_movement() {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn presentation_translation_interpolates_between_fixed_samples() {
|
||||||
|
let position = sample_presentation_translation(
|
||||||
|
FighterPreviousTranslation(FGVec2::new(FGi32::lit("2"), FGi32::lit("4"))),
|
||||||
|
FighterTranslation(FGVec2::new(FGi32::lit("6"), FGi32::lit("12"))),
|
||||||
|
0.25,
|
||||||
|
PresentationMotionMode::Interpolation,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(position, Vec2::new(3.0, 6.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn presentation_translation_extrapolates_from_current_sample() {
|
||||||
|
let position = sample_presentation_translation(
|
||||||
|
FighterPreviousTranslation(FGVec2::new(FGi32::lit("2"), FGi32::lit("4"))),
|
||||||
|
FighterTranslation(FGVec2::new(FGi32::lit("6"), FGi32::lit("12"))),
|
||||||
|
0.25,
|
||||||
|
PresentationMotionMode::Extrapolation,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(position, Vec2::new(7.0, 14.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,6 +32,17 @@ impl FighterStateImpl for GroundAttackState {
|
|||||||
state_context.hitboxes.attack_script = Some(state_context.attack_script_assets.scripts[&self.attack_kind].clone());
|
state_context.hitboxes.attack_script = Some(state_context.attack_script_assets.scripts[&self.attack_kind].clone());
|
||||||
state_context.hitboxes.active_hitboxes.clear();
|
state_context.hitboxes.active_hitboxes.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_exit(&mut self, state_context: &mut super::FighterStateContext) {
|
||||||
|
state_context.hitboxes.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_collision_interrupt(
|
||||||
|
&mut self,
|
||||||
|
_state_context: &mut super::FighterStateContext,
|
||||||
|
) -> Option<FighterState> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_interrupt(state_context: &super::FighterStateContext, grounded_common: &GroundedStateCommon) -> Option<FighterState> {
|
pub fn check_interrupt(state_context: &super::FighterStateContext, grounded_common: &GroundedStateCommon) -> Option<FighterState> {
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ macro_rules! fighter_states {
|
|||||||
match self { $(Self::$variant(s) => s.on_enter(ctx)),* }
|
match self { $(Self::$variant(s) => s.on_enter(ctx)),* }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_exit(&mut self, ctx: &mut FighterStateContext) {
|
||||||
|
match self { $(Self::$variant(s) => s.on_exit(ctx)),* }
|
||||||
|
}
|
||||||
|
|
||||||
pub fn check_collision_interrupt(&mut self, ctx: &mut FighterStateContext) -> Option<FighterState> {
|
pub fn check_collision_interrupt(&mut self, ctx: &mut FighterStateContext) -> Option<FighterState> {
|
||||||
match self { $(Self::$variant(s) => s.check_collision_interrupt(ctx)),* }
|
match self { $(Self::$variant(s) => s.check_collision_interrupt(ctx)),* }
|
||||||
}
|
}
|
||||||
@@ -68,6 +72,7 @@ pub trait FighterStateImpl: Sized {
|
|||||||
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState>;
|
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState>;
|
||||||
|
|
||||||
fn on_enter(&mut self, _state_context: &mut FighterStateContext) {}
|
fn on_enter(&mut self, _state_context: &mut FighterStateContext) {}
|
||||||
|
fn on_exit(&mut self, _state_context: &mut FighterStateContext) {}
|
||||||
fn update(&mut self, _state_context: &mut FighterStateContext);
|
fn update(&mut self, _state_context: &mut FighterStateContext);
|
||||||
|
|
||||||
fn check_collision_interrupt(
|
fn check_collision_interrupt(
|
||||||
@@ -272,7 +277,7 @@ pub fn state_interrupt_system(
|
|||||||
for fighter in &mut query {
|
for fighter in &mut query {
|
||||||
let StatefulFighterQueryItem {
|
let StatefulFighterQueryItem {
|
||||||
entity,
|
entity,
|
||||||
state,
|
mut state,
|
||||||
mut frame,
|
mut frame,
|
||||||
animations,
|
animations,
|
||||||
animation_player_link,
|
animation_player_link,
|
||||||
@@ -301,6 +306,7 @@ pub fn state_interrupt_system(
|
|||||||
let mut new_state = new_state;
|
let mut new_state = new_state;
|
||||||
let old_state_name = state.name();
|
let old_state_name = state.name();
|
||||||
let new_state_name = new_state.name();
|
let new_state_name = new_state.name();
|
||||||
|
state.on_exit(&mut state_context);
|
||||||
new_state.on_enter(&mut state_context);
|
new_state.on_enter(&mut state_context);
|
||||||
commands
|
commands
|
||||||
.entity(entity)
|
.entity(entity)
|
||||||
@@ -352,6 +358,7 @@ pub fn state_collision_interrupt_system(
|
|||||||
let mut new_state = new_state;
|
let mut new_state = new_state;
|
||||||
let old_state_name = state.name();
|
let old_state_name = state.name();
|
||||||
let new_state_name = new_state.name();
|
let new_state_name = new_state.name();
|
||||||
|
state.on_exit(&mut state_context);
|
||||||
new_state.on_enter(&mut state_context);
|
new_state.on_enter(&mut state_context);
|
||||||
commands
|
commands
|
||||||
.entity(entity)
|
.entity(entity)
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ impl Plugin for FighterNetcodePlugin {
|
|||||||
app.add_plugins(plugin)
|
app.add_plugins(plugin)
|
||||||
// Engine-level components that every rolled-back entity may carry.
|
// Engine-level components that every rolled-back entity may carry.
|
||||||
// Gameplay components are registered by the plugin that owns them.
|
// Gameplay components are registered by the plugin that owns them.
|
||||||
.rollback_component_with_clone::<Transform>()
|
|
||||||
.rollback_component_with_clone::<Name>()
|
.rollback_component_with_clone::<Name>()
|
||||||
.add_systems(
|
.add_systems(
|
||||||
EguiPrimaryContextPass,
|
EguiPrimaryContextPass,
|
||||||
|
|||||||
Reference in New Issue
Block a user