diff --git a/src/fighter/baked_animation.rs b/src/fighter/baked_animation.rs index fd5556a..7d2b91b 100644 --- a/src/fighter/baked_animation.rs +++ b/src/fighter/baked_animation.rs @@ -16,14 +16,20 @@ use bevy::{ prelude::*, reflect::TypePath, }; +use bevy_ggrs::prelude::GgrsSchedule; use serde::{Deserialize, Serialize}; use crate::{ - fighter::{animation::AnimKind, manifest::FighterManifest}, + fighter::{ + animation::{AnimKind, FighterAnimationFrame}, + manifest::FighterManifest, + visual::FighterAnimations, + }, math::{ int::{FGWide, FGi32}, vec3::FGVec3, }, + schedule::GameplaySet, }; pub const BAKED_ANIMATION_FPS: u32 = 60; @@ -42,7 +48,7 @@ impl FixedMat4 { [FGi32::ZERO, FGi32::ONE, FGi32::ZERO, FGi32::ZERO], [FGi32::ZERO, FGi32::ZERO, FGi32::ONE, FGi32::ZERO], [FGi32::ZERO, FGi32::ZERO, FGi32::ZERO, FGi32::ONE], - ] + ], }; fn from_mat4(matrix: Mat4) -> io::Result { let values = matrix.to_cols_array(); @@ -151,11 +157,17 @@ pub struct BakedFighterAnimations { } impl BakedFighterAnimations { - pub fn sample(&self, kind: AnimKind, frame: u32, bone: &str) -> Option { - let bone_index = self.bone_names.iter().position(|name| name == bone)?; - let frames = &self.clip(kind)?.frames; - let pose = frames.get(frame as usize).or_else(|| frames.last())?; - pose.get(bone_index).copied() + pub fn sample_pose(&self, frame: &FighterAnimationFrame) -> Option<&[FixedMat4]> { + let frames = &self.clip(frame.kind)?.frames; + let frame_index = if frame.repeat { + frame.frame % frames.len() as u32 + } else { + frame.frame + }; + frames + .get(frame_index as usize) + .or_else(|| frames.last()) + .map(Vec::as_slice) } pub fn frame_count(&self, kind: AnimKind) -> Option { @@ -169,6 +181,62 @@ impl BakedFighterAnimations { } } +/// The current baked model-space pose for a fighter, recalculated every +/// rollback frame from its animation clock and immutable baked asset. +#[derive(Component, Clone, Debug, Default)] +pub struct FighterBoneMatrices { + bone_names: Vec, + matrices: Vec, +} + +impl FighterBoneMatrices { + pub fn get(&self, bone: &str) -> Option { + let index = self.bone_names.iter().position(|name| name == bone)?; + self.matrices.get(index).copied() + } + + pub fn iter(&self) -> impl Iterator { + self.bone_names + .iter() + .map(String::as_str) + .zip(self.matrices.iter().copied()) + } + + fn set_pose(&mut self, bone_names: &[String], matrices: &[FixedMat4]) { + assert_eq!( + bone_names.len(), + matrices.len(), + "baked animation pose does not match its bone names" + ); + if self.bone_names.is_empty() { + self.bone_names.extend_from_slice(bone_names); + } else { + assert_eq!(self.bone_names, bone_names, "baked bone names changed"); + } + self.matrices.clear(); + self.matrices.extend_from_slice(matrices); + } +} + +pub fn update_fighter_bone_matrices( + mut fighters: Query<( + &FighterAnimationFrame, + &FighterAnimations, + &mut FighterBoneMatrices, + )>, + baked_assets: Res>, +) { + for (frame, animations, mut matrices) in &mut fighters { + let baked = baked_assets + .get(&animations.baked) + .expect("fighter baked animation asset should be loaded before the match starts"); + let pose = baked + .sample_pose(frame) + .expect("fighter animation frame should select a baked pose"); + matrices.set_pose(&baked.bone_names, pose); + } +} + #[derive(Deserialize)] struct FighterBakeDeclaration { manifest_path: String, @@ -304,7 +372,11 @@ impl Plugin for BakedAnimationPlugin { FighterBakeTransformer, BakedFighterAnimationsSaver, )) - .set_default_asset_processor::("fighterbake"); + .set_default_asset_processor::("fighterbake") + .add_systems( + GgrsSchedule, + update_fighter_bone_matrices.in_set(GameplaySet::Animation), + ); } } @@ -598,6 +670,50 @@ mod tests { ); } + #[test] + fn pose_sampling_wraps_repeating_frames_and_clamps_finished_frames() { + let first = FixedMat4::IDENTITY; + let mut second = FixedMat4::IDENTITY; + second.translate(FGVec3::lit("2", "0", "0")); + let baked = BakedFighterAnimations { + frame_rate: BAKED_ANIMATION_FPS, + bone_names: vec!["root".into()], + clips: vec![BakedAnimationClip { + frames: vec![vec![first], vec![second]], + }], + clip_indices: HashMap::from([(AnimKind::Wait, 0)]), + }; + + let repeating = FighterAnimationFrame { + kind: AnimKind::Wait, + frame: 3, + repeat: true, + }; + let finished = FighterAnimationFrame { + kind: AnimKind::Wait, + frame: 3, + repeat: false, + }; + + assert_eq!(baked.sample_pose(&repeating), Some(&[second][..])); + assert_eq!(baked.sample_pose(&finished), Some(&[second][..])); + } + + #[test] + fn bone_matrix_component_keeps_names_aligned_with_the_current_pose() { + let first = FixedMat4::IDENTITY; + let mut second = FixedMat4::IDENTITY; + second.translate(FGVec3::lit("0", "3", "0")); + let mut matrices = FighterBoneMatrices::default(); + matrices.set_pose(&["root".into(), "hand".into()], &[first, second]); + + assert_eq!(matrices.get("hand"), Some(second)); + assert_eq!( + matrices.iter().collect::>(), + vec![("root", first), ("hand", second)] + ); + } + #[test] fn fixed_matrix_transforms_points() { let transform = FixedMat4 { diff --git a/src/fighter/debug.rs b/src/fighter/debug.rs index cec6956..05f3d38 100644 --- a/src/fighter/debug.rs +++ b/src/fighter/debug.rs @@ -4,19 +4,20 @@ use bevy::color::palettes::css::ORANGE; use bevy::{color::palettes::css::YELLOW, prelude::*}; use bevy_egui::{EguiContexts, egui}; +use crate::debug_tools::{DebugSettings, FighterDebugRowKind}; use crate::fighter::animation::FighterAnimationFrame; -use crate::fighter::baked_animation::BakedFighterAnimations; +use crate::fighter::baked_animation::{BakedFighterAnimations, FighterBoneMatrices}; use crate::fighter::ecb::FighterPreviousECB; use crate::fighter::state::StateNameDebug; use crate::fighter::visual::FighterAnimations; use crate::fighter::{ - FighterECB, FighterFacingDirection, FighterHitboxes, FighterPreviousTranslation, FighterTranslation, FighterVelocity, + FighterECB, FighterFacingDirection, FighterHitboxes, FighterPreviousTranslation, + FighterTranslation, FighterVelocity, }; use crate::input::FighterInput; use crate::math::vec3::FGVec3; use crate::player::Player; use crate::scripting::FighterAttackScript; -use crate::debug_tools::{DebugSettings, FighterDebugRowKind}; pub fn debug_draw_ecb( ecbs: Query<( @@ -57,7 +58,9 @@ pub fn fighter_debug( let mut fighters = query.iter().collect::>(); fighters.sort_unstable_by_key(|(player, ..)| player.handle); - for (player, velocity, input, animation_frame, state_name, animations, translation, facing) in fighters { + for (player, velocity, input, animation_frame, state_name, animations, translation, facing) in + fighters + { let Some(rows) = settings.player_rows(player.handle) else { continue; }; @@ -65,95 +68,109 @@ pub fn fighter_debug( continue; } - egui::Window::new(format!("Player {}", player.handle + 1)).show(contexts.ctx_mut()?, |ui| { - for row in rows { - match row { - FighterDebugRowKind::None => {} - FighterDebugRowKind::CurrentState => { - let frame_count = anims - .get(&animations.baked) - .and_then(|animation| animation.frame_count(animation_frame.kind)); - if let Some(frame_count) = frame_count { + egui::Window::new(format!("Player {}", player.handle + 1)).show( + contexts.ctx_mut()?, + |ui| { + for row in rows { + match row { + FighterDebugRowKind::None => {} + FighterDebugRowKind::CurrentState => { + let frame_count = anims + .get(&animations.baked) + .and_then(|animation| animation.frame_count(animation_frame.kind)); + if let Some(frame_count) = frame_count { + ui.label(format!( + "State: {:?} ({:?} {}/{})", + state_name.0, + animation_frame.kind, + animation_frame.frame, + frame_count + )); + } else { + ui.label(format!( + "State: {:?} ({:?})", + state_name.0, animation_frame.kind + )); + } + } + FighterDebugRowKind::Velocity => { ui.label(format!( - "State: {:?} ({:?} {}/{})", - state_name.0, animation_frame.kind, animation_frame.frame, frame_count + "Velocity: ({:.2}, {:.2})", + velocity.0.x, velocity.0.y )); - } else { - ui.label(format!("State: {:?} ({:?})", state_name.0, animation_frame.kind)); + } + FighterDebugRowKind::Input => { + const STICK_SIZE: f32 = 50.0; + const STICK_SIZE_FRACTION: f32 = 0.75; + let last_frame = input.get_last_frame(); + let movement = egui::Vec2::new( + last_frame.movement.x.to_num(), + -last_frame.movement.y.to_num::(), + ); + + ui.horizontal(|ui| { + let (_, rect) = ui.allocate_space(egui::Vec2::new( + STICK_SIZE * (1.0 + STICK_SIZE_FRACTION), + STICK_SIZE * (1.0 + STICK_SIZE_FRACTION), + )); + let painter = ui.painter(); + let button_bg = + egui::Rgba::WHITE * egui::Rgba::from_white_alpha(0.25); + painter.circle_filled(rect.center(), STICK_SIZE * 0.5, button_bg); + painter.circle_filled( + rect.center() + STICK_SIZE * 0.5 * movement, + STICK_SIZE * STICK_SIZE_FRACTION * 0.5, + button_bg, + ); + ui.label(format!( + "X: {:.2}\nY:{:.2}", + last_frame.movement.x, last_frame.movement.y + )); + }); + } + FighterDebugRowKind::Position => { + ui.label(format!("Position: {:?}", translation.0)); + } + FighterDebugRowKind::Facing => { + ui.label(format!("Facing: {:?}", facing)); } } - FighterDebugRowKind::Velocity => { - ui.label(format!("Velocity: ({:.2}, {:.2})", velocity.0.x, velocity.0.y)); - } - FighterDebugRowKind::Input => { - const STICK_SIZE: f32 = 50.0; - const STICK_SIZE_FRACTION: f32 = 0.75; - let last_frame = input.get_last_frame(); - let movement = egui::Vec2::new(last_frame.movement.x.to_num(), -last_frame.movement.y.to_num::()); - - ui.horizontal(|ui| { - let (_, rect) = ui.allocate_space(egui::Vec2::new(STICK_SIZE*(1.0 + STICK_SIZE_FRACTION), STICK_SIZE*(1.0 + STICK_SIZE_FRACTION))); - let painter = ui.painter(); - let button_bg = egui::Rgba::WHITE * egui::Rgba::from_white_alpha(0.25); - painter.circle_filled(rect.center(), STICK_SIZE * 0.5, button_bg); - painter.circle_filled( - rect.center() + STICK_SIZE * 0.5 * movement, - STICK_SIZE * STICK_SIZE_FRACTION * 0.5, - button_bg, - ); - ui.label(format!("X: {:.2}\nY:{:.2}", last_frame.movement.x, last_frame.movement.y)); - }); - } - FighterDebugRowKind::Position => { - ui.label(format!("Position: {:?}", translation.0)); - }, - FighterDebugRowKind::Facing => { - ui.label(format!("Facing: {:?}", facing)); - } } - } - }); + }, + ); } Ok(()) } -pub fn animation_debug( - query: Query<(&GlobalTransform, &FighterAnimationFrame, &FighterAnimations)>, - baked_anims: Res>, - mut gizmos: Gizmos, -) { - for (global_transform, frame, anims) in query { - let baked: &BakedFighterAnimations = baked_anims - .get(&anims.baked) - .expect("Baked anims should be in"); - for bone in &baked.bone_names { - if bone != "Bone_30" { - continue; - } - let out = anims.sample_bone(&baked_anims, frame, bone); - if let Some(out) = out { - let pos = global_transform.transform_point(Vec3::new( - out.cols[3][0].to_num(), - out.cols[3][1].to_num(), - out.cols[3][2].to_num(), - )); - //gizmos.text(pos, bone, 1.0, Vec2::new(0.0, 0.0), bevy::color::palettes::css::BLACK); - gizmos.cross(pos, 0.5, bevy::color::palettes::css::RED); - gizmos.axes(*global_transform, 1.0); - } +pub fn animation_debug(query: Query<(&GlobalTransform, &FighterBoneMatrices)>, mut gizmos: Gizmos) { + for (global_transform, matrices) in query { + for (_, matrix) in matrices.iter() { + let pos = global_transform.transform_point(Vec3::new( + matrix.cols[3][0].to_num(), + matrix.cols[3][1].to_num(), + matrix.cols[3][2].to_num(), + )); + gizmos.cross(pos, 0.5, bevy::color::palettes::css::RED); + gizmos.axes(*global_transform, 1.0); } } } pub fn attack_debug( - query: Query<(&FighterHitboxes, &FighterAnimationFrame, &FighterAnimations, &FighterTranslation, &FighterFacingDirection)>, - baked_anims: Res>, + query: Query<( + &FighterHitboxes, + &FighterBoneMatrices, + &FighterTranslation, + &FighterFacingDirection, + )>, attack_scripts: Res>, mut gizmos: Gizmos, ) { - for (hitboxes, frame, animations, translation, facing_direction) in query { + for (hitboxes, matrices, translation, facing_direction) in query { if let Some(script) = &hitboxes.attack_script { - let script = attack_scripts.get(script).expect("Script ref should be valid"); + let script = attack_scripts + .get(script) + .expect("Script ref should be valid"); let player_trf = translation.get_3d_transform(facing_direction); for i in hitboxes.active_hitboxes.iter() { @@ -161,16 +178,24 @@ pub fn attack_debug( let radius = script.hitboxes[*i].radius; let bone = &script.hitboxes[*i].bone; - let trf = animations.sample_bone(&baked_anims, frame, bone); + let trf = matrices.get(bone); if let Some(trf) = trf { let trf = player_trf.mul(trf); let start = player_trf.transform_point(FGVec3::lit("0.0", "1.0", "0.0")); let end = player_trf.transform_point(FGVec3::lit("0.0", "1.0", "1.0")); let pos = trf.transform_point(offset); let pos_draw = Vec3::new(pos.x.to_num(), pos.y.to_num(), pos.z.to_num()); + //Primitive + //gizmos.primitive_3d(, isometry, color) gizmos.sphere(pos_draw, radius.to_num(), bevy::color::palettes::css::RED); - - gizmos.arrow(start.to_vec3(), end.to_vec3(),bevy::color::palettes::css::RED); + + gizmos.arrow( + start.to_vec3(), + end.to_vec3(), + bevy::color::palettes::css::RED, + ); + } else { + warn!("Bone not found required by attack script: {}", bone); } } } diff --git a/src/fighter/mod.rs b/src/fighter/mod.rs index 1f67767..b5a071a 100644 --- a/src/fighter/mod.rs +++ b/src/fighter/mod.rs @@ -7,6 +7,7 @@ use bevy_ggrs::prelude::*; use serde::{Deserialize, Serialize}; pub mod animation; +pub mod attack; pub mod attributes; pub mod baked_animation; pub mod collision; @@ -16,7 +17,6 @@ pub mod manifest; pub mod motion; pub mod state; pub mod visual; -pub mod attack; // The fighter components are re-exported so the rest of the crate can say // `fighter::FighterVelocity` without caring which file it lives in. Named @@ -29,8 +29,18 @@ pub use motion::{FighterPreviousTranslation, FighterTranslation, FighterVelocity use crate::{ fighter::{ - animation::animation_init, attack::FighterAttackScriptAssets, manifest::FighterManifest, state::{FighterState, fall::FallState}, visual::FighterAnimations, - }, input::FighterInput, math::{int::FGi32, vec::FGVec2}, player::Player, schedule::GameplaySet, scripting::FighterAttackScript, stage::line::StageCollision, + animation::animation_init, + attack::FighterAttackScriptAssets, + manifest::FighterManifest, + state::{FighterState, fall::FallState}, + visual::FighterAnimations, + }, + input::FighterInput, + math::{int::FGi32, vec::FGVec2}, + player::Player, + schedule::GameplaySet, + scripting::FighterAttackScript, + stage::line::StageCollision, }; use state::state_interrupt_system; @@ -126,7 +136,10 @@ impl Plugin for FighterPlugin { ) .add_systems( Update, - (motion::apply_fighter_translation_to_visuals, animation::apply_animation) + ( + motion::apply_fighter_translation_to_visuals, + animation::apply_animation, + ) .chain() .run_if(in_state(crate::AppState::InMatch)), ) @@ -171,7 +184,7 @@ impl Plugin for FighterPlugin { #[derive(Component, Clone)] pub struct FighterHitboxes { pub attack_script: Option>, - pub active_hitboxes: Vec + pub active_hitboxes: Vec, } impl FighterHitboxes { @@ -195,18 +208,18 @@ pub fn spawn_fighter( Player { handle: player_handle, }, - Fighter { - manifest: manifest, - }, + Fighter { manifest: manifest }, FighterFacingDirection::Right, - (FighterECB { - vertical_half: FGi32::lit("5.0"), - horizontal_half: FGi32::lit("2.5"), - }, - FighterHitboxes { - attack_script: None, - active_hitboxes: vec![] - }), + ( + FighterECB { + vertical_half: FGi32::lit("5.0"), + horizontal_half: FGi32::lit("2.5"), + }, + FighterHitboxes { + attack_script: None, + active_hitboxes: vec![], + }, + ), ( FighterVelocity::default(), FighterTranslation(spawn_position), @@ -217,6 +230,7 @@ pub fn spawn_fighter( attack_scripts, FighterState::Fall(FallState), animation::FighterAnimationFrame::new(animation::AnimKind::Wait, true), + baked_animation::FighterBoneMatrices::default(), FighterVisual, FighterInput::default(), crate::camera::FighterCameraExtents::default(), diff --git a/src/fighter/state/mod.rs b/src/fighter/state/mod.rs index 9edd0f0..d00f307 100644 --- a/src/fighter/state/mod.rs +++ b/src/fighter/state/mod.rs @@ -7,12 +7,13 @@ use bevy::prelude::*; use crate::fighter::animation::{AnimKind, FighterAnimationFrame, FighterAnimationPlayerLink}; use crate::fighter::attack::FighterAttackScriptAssets; -use crate::fighter::baked_animation::{BakedFighterAnimations, FixedMat4}; +use crate::fighter::baked_animation::BakedFighterAnimations; use crate::fighter::ecb::FighterPreviousECB; use crate::fighter::manifest::FighterManifest; use crate::fighter::visual::FighterAnimations; use crate::fighter::{ - Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterPreviousTranslation, FighterTranslation, FighterVelocity, + Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterPreviousTranslation, + FighterTranslation, FighterVelocity, }; use crate::game_settings::GameSettings; use crate::input::FighterInput; @@ -24,13 +25,13 @@ pub mod air_dodge; pub mod dash; pub mod fall; pub mod ground; +pub mod ground_attack; pub mod jump; pub mod land; pub mod run; pub mod turn; pub mod wait; pub mod walk; -pub mod ground_attack; macro_rules! fighter_states { ($($variant:ident => $ty:ty),* $(,)?) => { @@ -131,11 +132,6 @@ impl FighterStateContext<'_> { .expect("Current animation should have baked frames") < self.animation_frame.frame } - - pub fn sample_bone(&self, bone: &str) -> Option { - self.animations - .sample_bone(self.baked_animations, self.animation_frame, bone) - } } fighter_states! { @@ -282,7 +278,7 @@ pub fn state_interrupt_system( animations, animation_player_link, fighter, - attack_script_assets + attack_script_assets, } = fighter; let Ok((mut animation_player, mut animation_transitions)) = animation_players.get_mut(animation_player_link.player()) diff --git a/src/lib.rs b/src/lib.rs index a1a2fe7..42baf68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,14 +21,19 @@ //! save, click Rebuild in jackdaw (or run `jd build`) and it //! appears in `Add Component`. No registration code is needed. -use bevy::{asset::processor::{AssetProcessor, ProcessorState}, prelude::*, tasks::block_on}; +use bevy::{ + asset::processor::{AssetProcessor, ProcessorState}, + prelude::*, + tasks::block_on, +}; use bevy_asset_loader::prelude::*; use bevy_wind_waker_shader::prelude::*; use clap::Parser; use jackdaw_runtime::EditorCategory; use crate::{ - camera::MatchCameraPlugin, fighter::manifest::FighterManifest, game_settings::GameSettings, schedule::GameplaySchedulePlugin, stage::manifest::StageManifest, + camera::MatchCameraPlugin, fighter::manifest::FighterManifest, game_settings::GameSettings, + schedule::GameplaySchedulePlugin, stage::manifest::StageManifest, }; mod args; @@ -42,8 +47,8 @@ mod math; pub mod netcode; pub mod player; pub mod schedule; -pub mod stage; pub mod scripting; +pub mod stage; #[derive(States, Debug, Clone, Copy, Eq, PartialEq, Hash, Default)] pub enum AppState { @@ -145,7 +150,8 @@ fn wait_for_asset_processor( processor: Option>, mut next_state: ResMut>, ) { - if processor.is_none_or(|processor| block_on(processor.get_state()) == ProcessorState::Finished) { + if processor.is_none_or(|processor| block_on(processor.get_state()) == ProcessorState::Finished) + { next_state.set(AppState::LoadCommonAssets); } } diff --git a/src/main.rs b/src/main.rs index 27a6996..cebb2e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,8 +6,6 @@ use bevy::asset::AssetMode::Processed; use bevy::camera_controller::free_camera::FreeCameraPlugin; use bevy::image::{ImageAddressMode, ImagePlugin, ImageSamplerDescriptor}; use bevy::prelude::*; -use bevy::remote::RemotePlugin; -use bevy::remote::http::RemoteHttpPlugin; use bevy_egui::EguiPlugin; use bevy_inspector_egui::quick::WorldInspectorPlugin; @@ -15,17 +13,19 @@ fn main() -> AppExit { // A `Repeat` sampler so the tileable materials authored in the // editor render the same way at runtime; Bevy's default // `ClampToEdge` smears texture edges on brush geometry. - let default_plugins = DefaultPlugins.set(ImagePlugin { - default_sampler: ImageSamplerDescriptor { - address_mode_u: ImageAddressMode::Repeat, - address_mode_v: ImageAddressMode::Repeat, - address_mode_w: ImageAddressMode::Repeat, - ..ImageSamplerDescriptor::linear() - }, - }).set(AssetPlugin { - mode: Processed, - ..default() - }); + let default_plugins = DefaultPlugins + .set(ImagePlugin { + default_sampler: ImageSamplerDescriptor { + address_mode_u: ImageAddressMode::Repeat, + address_mode_v: ImageAddressMode::Repeat, + address_mode_w: ImageAddressMode::Repeat, + ..ImageSamplerDescriptor::linear() + }, + }) + .set(AssetPlugin { + mode: Processed, + ..default() + }); App::new() .add_plugins(default_plugins) @@ -33,8 +33,6 @@ fn main() -> AppExit { .add_plugins(jackdaw_runtime::JackdawPlugin) .add_plugins(game_test::GamePlugin) .add_plugins(FreeCameraPlugin) - .add_plugins(RemotePlugin::default()) - .add_plugins(RemoteHttpPlugin::default()) .add_plugins(EguiPlugin::default()) .add_plugins(WorldInspectorPlugin::default()) // adds default options and `InspectorEguiImpl`s .insert_resource(Time::::from_hz(60.0))