From b2da4a737ddafd6617042ec5ae852029f248c8cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=80lex=20Rom=C3=A0n?= Date: Tue, 1 Sep 2026 21:07:53 +0200 Subject: [PATCH] Make the baked animation system drive the character animations --- src/fighter/animation.rs | 65 +---------- src/fighter/baked_animation.rs | 206 ++++++++++++++++++++++++++++++--- src/fighter/debug.rs | 55 ++++++--- src/fighter/mod.rs | 138 +++++++++++----------- src/fighter/state/mod.rs | 44 +------ src/fighter/visual.rs | 9 +- src/game_settings.rs | 9 -- src/match_loading.rs | 63 +++++----- src/math/int.rs | 2 - src/scripting/importer.rs | 19 ++- src/scripting/mod.rs | 8 +- 11 files changed, 345 insertions(+), 273 deletions(-) diff --git a/src/fighter/animation.rs b/src/fighter/animation.rs index e950a3c..71ba4d7 100644 --- a/src/fighter/animation.rs +++ b/src/fighter/animation.rs @@ -1,23 +1,6 @@ -use std::time::Duration; - use bevy::prelude::*; -use crate::{ - fighter::{ - FighterFacingDirection, FighterVisual, - visual::FighterAnimations, - }, - player::Player, -}; - -#[derive(Component)] -pub struct FighterAnimationPlayerLink(Entity); - -impl FighterAnimationPlayerLink { - pub fn player(&self) -> Entity { - self.0 - } -} +use crate::fighter::{FighterFacingDirection, FighterVisual}; /// The simulation-owned animation clock. Rendering may interpolate freely, but /// gameplay samples baked data only with this rollback-tracked value. @@ -54,29 +37,6 @@ pub fn advance_fighter_animation_frames(mut fighters: Query<&mut FighterAnimatio } } -pub fn setup_fighter_animation_player( - mut commands: Commands, - animated: Query>, - parents: Query<&ChildOf>, - fighters: Query>, -) { - for child in &animated { - let Some(parent) = parents - .iter_ancestors(child) - .filter_map(|parent| fighters.get(parent).ok()) - .next() - else { - continue; - }; - - commands.entity(child).insert(AnimationTransitions::new()); - info!("LINK"); - commands - .entity(parent) - .insert(FighterAnimationPlayerLink(child)); - } -} - pub fn apply_animation( children: Query< ( @@ -102,28 +62,6 @@ pub fn apply_animation( } } -pub fn animation_init( - fighters: Query< - (&FighterAnimations, &FighterAnimationPlayerLink), - Added, - >, - mut commands: Commands, - mut anim_player_query: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>, -) { - for (anim, link) in fighters { - commands - .entity(link.0) - .insert(AnimationGraphHandle(anim.graph.clone())); - info!("BEGIN! {:?}", fighters); - if let Ok((mut player, mut transitions)) = anim_player_query.get_mut(link.0) { - info!("DOS"); - transitions - .play(&mut player, anim.clips[&AnimKind::Wait], Duration::ZERO) - .repeat(); - } - } -} - #[derive( Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize, Reflect, )] @@ -142,7 +80,6 @@ pub enum AnimKind { Turn, // Attacks - AttackJab1, AttackUpTilt, } diff --git a/src/fighter/baked_animation.rs b/src/fighter/baked_animation.rs index 1dd79ff..b447326 100644 --- a/src/fighter/baked_animation.rs +++ b/src/fighter/baked_animation.rs @@ -1,9 +1,10 @@ -//! Deterministic, fighter-local skeletal animation samples for gameplay. -//! -//! Rendering continues to use Bevy's floating-point animation player. Gameplay -//! reads this asset instead, so rollback never depends on render-time sampling. +//! Deterministic, fighter-local skeletal animation samples for gameplay and rendering. -use std::{collections::HashMap, io, path::Path}; +use std::{ + collections::{HashMap, HashSet}, + io, + path::Path, +}; use bevy::{ asset::{ @@ -15,6 +16,7 @@ use bevy::{ }, prelude::*, reflect::TypePath, + world_serialization::{WorldInstanceReady, WorldInstanceSpawner}, }; use bevy_ggrs::prelude::GgrsSchedule; use serde::{Deserialize, Serialize}; @@ -64,6 +66,10 @@ impl FixedMat4 { Ok(Self { cols }) } + pub fn to_mat4(self) -> Mat4 { + Mat4::from_cols_array_2d(&self.cols.map(|column| column.map(|value| value.to_num()))) + } + pub fn mul(self, rhs: Self) -> Self { let mut cols = [[FGi32::ZERO; 4]; 4]; for column in 0..4 { @@ -147,7 +153,7 @@ impl FixedMat4 { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct BakedAnimationClip { - /// One model-space matrix per stored node, for each 60 Hz simulation frame. + /// One parent-relative matrix per stored node, for each 60 Hz simulation frame. pub frames: Vec>, } @@ -156,6 +162,8 @@ pub struct BakedFighterAnimations { pub frame_rate: u32, /// Indexed exactly like every frame in each clip. pub bone_names: Vec, + /// Parent node for each stored node, indexed like `bone_names`. + pub parents: Vec>, clips: Vec, clip_indices: HashMap, } @@ -190,8 +198,16 @@ impl BakedFighterAnimations { #[derive(Component, Clone, Debug, Default)] pub struct FighterBoneMatrices { bone_names: Vec, + parents: Vec>, matrices: Vec, previous_frame_matrices: Vec, + local_matrices: Vec, +} + +#[derive(Component)] +struct BakedFighterBone { + fighter: Entity, + index: usize, } impl FighterBoneMatrices { @@ -221,20 +237,40 @@ impl FighterBoneMatrices { .zip(self.matrices.iter().copied()) } - fn set_pose(&mut self, bone_names: &[String], matrices: &[FixedMat4]) { + pub fn local(&self, index: usize) -> Option { + self.local_matrices.get(index).copied() + } + + fn set_pose(&mut self, bone_names: &[String], parents: &[Option], local: &[FixedMat4]) { assert_eq!( bone_names.len(), - matrices.len(), + local.len(), "baked animation pose does not match its bone names" ); + assert_eq!( + bone_names.len(), + parents.len(), + "baked bone parents do not match names" + ); if self.bone_names.is_empty() { self.bone_names.extend_from_slice(bone_names); + self.parents.extend_from_slice(parents); } else { assert_eq!(self.bone_names, bone_names, "baked bone names changed"); + assert_eq!(self.parents, parents, "baked bone parents changed"); } std::mem::swap(&mut self.matrices, &mut self.previous_frame_matrices); - self.matrices.clear(); - self.matrices.extend_from_slice(matrices); + self.local_matrices.clear(); + self.local_matrices.extend_from_slice(local); + let mut model = vec![None; local.len()]; + for index in 0..local.len() { + resolve_model_transform(index, parents, local, &mut model) + .expect("baked bone hierarchy should be valid"); + } + self.matrices = model + .into_iter() + .map(|matrix| matrix.expect("baked bone hierarchy should be valid")) + .collect(); } } @@ -253,7 +289,107 @@ pub fn update_fighter_bone_matrices( let pose = baked .sample_pose(frame) .expect("fighter animation frame should select a baked pose"); - matrices.set_pose(&baked.bone_names, pose); + matrices.set_pose(&baked.bone_names, &baked.parents, pose); + } +} + +fn bind_fighter_visual_bones( + ready: On, + mut commands: Commands, + spawner: Res, + fighters: Query<&FighterAnimations>, + baked_assets: Res>, + nodes: Query<(&Name, Option<&ChildOf>)>, + animation_players: Query<(), With>, +) { + let fighter = ready.entity; + let Ok(animations) = fighters.get(fighter) else { + return; + }; + let baked = baked_assets + .get(&animations.baked) + .expect("fighter baked animation asset should be loaded before its visual scene"); + let entities = spawner + .iter_instance_entities(ready.instance_id) + .collect::>(); + for &entity in &entities { + if animation_players.contains(entity) { + commands.entity(entity).remove::(); + } + } + let mut indices = HashMap::with_capacity(baked.bone_names.len()); + for (index, name) in baked.bone_names.iter().enumerate() { + let key = (baked.parents[index], name.as_str()); + assert!( + indices.insert(key, index).is_none(), + "fighter GLTF has duplicate node names under the same parent" + ); + } + + let mut assigned = HashMap::new(); + let mut remaining = entities.iter().copied().collect::>(); + while !remaining.is_empty() { + let mut next = Vec::new(); + let mut progress = false; + for entity in remaining { + let Ok((name, parent)) = nodes.get(entity) else { + continue; + }; + let parent = parent.map(ChildOf::parent); + let parent_index = match parent { + Some(parent) if entities.contains(&parent) => { + if let Some(index) = assigned.get(&parent) { + Some(*index) + } else if nodes.get(parent).ok().is_some_and(|(name, _)| { + baked.bone_names.iter().any(|bone| bone == name.as_str()) + }) { + next.push(entity); + continue; + } else { + // WorldAsset adds an unnamed instance root above the GLTF scene roots. + None + } + } + _ => None, + }; + if let Some(&index) = indices.get(&(parent_index, name.as_str())) { + assigned.insert(entity, index); + progress = true; + } + } + if !progress { + break; + } + remaining = next; + } + + if assigned.len() != baked.bone_names.len() { + warn!( + fighter = ?fighter, + bound = assigned.len(), + expected = baked.bone_names.len(), + "could not bind every baked fighter node to its visual scene" + ); + } + for (entity, index) in assigned { + commands + .entity(entity) + .insert(BakedFighterBone { fighter, index }); + } +} + +fn apply_baked_fighter_visual_pose( + mut nodes: Query<(&mut Transform, &BakedFighterBone)>, + fighters: Query<&FighterBoneMatrices>, +) { + for (mut transform, bone) in &mut nodes { + let Ok(matrices) = fighters.get(bone.fighter) else { + continue; + }; + let Some(local) = matrices.local(bone.index) else { + continue; + }; + *transform = Transform::from_matrix(local.to_mat4()); } } @@ -396,7 +532,9 @@ impl Plugin for BakedAnimationPlugin { .add_systems( GgrsSchedule, update_fighter_bone_matrices.in_set(GameplaySet::Animation), - ); + ) + .add_systems(Update, apply_baked_fighter_visual_pose) + .add_observer(bind_fighter_visual_bones); } } @@ -464,11 +602,7 @@ fn bake_gltf( .iter() .map(|node| local_transform(node.clone(), &channels, time)) .collect::>>()?; - let mut model = vec![None; nodes.len()]; - for index in 0..nodes.len() { - resolve_model_transform(index, &parents, &local, &mut model)?; - } - frames.push(model.into_iter().map(Option::unwrap).collect()); + frames.push(local); } let index = clips.len(); clips.push(BakedAnimationClip { frames }); @@ -496,6 +630,7 @@ fn bake_gltf( Ok(BakedFighterAnimations { frame_rate: BAKED_ANIMATION_FPS, bone_names: names, + parents, clips, clip_indices, }) @@ -688,6 +823,7 @@ mod tests { baked.clip(AnimKind::Wait).unwrap().frames[0].len(), baked.bone_names.len() ); + assert_eq!(baked.parents.len(), baked.bone_names.len()); } #[test] @@ -698,6 +834,7 @@ mod tests { let baked = BakedFighterAnimations { frame_rate: BAKED_ANIMATION_FPS, bone_names: vec!["root".into()], + parents: vec![None], clips: vec![BakedAnimationClip { frames: vec![vec![first], vec![second]], }], @@ -725,7 +862,11 @@ mod tests { 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]); + matrices.set_pose( + &["root".into(), "hand".into()], + &[None, Some(0)], + &[first, second], + ); assert_eq!(matrices.get_current("hand"), Some(second)); assert_eq!( @@ -734,6 +875,35 @@ mod tests { ); } + #[test] + fn local_pose_reconstructs_model_space_for_gameplay() { + let mut root = FixedMat4::IDENTITY; + root.translate(FGVec3::lit("10", "0", "0")); + let mut hand = FixedMat4::IDENTITY; + hand.translate(FGVec3::lit("0", "3", "0")); + let mut matrices = FighterBoneMatrices::default(); + + matrices.set_pose( + &["root".into(), "hand".into()], + &[None, Some(0)], + &[root, hand], + ); + + assert_eq!(matrices.local(1), Some(hand)); + assert_eq!( + matrices.get_current("hand").unwrap().get_translation(), + FGVec3::lit("10", "3", "0") + ); + assert_eq!( + matrices + .local(1) + .unwrap() + .to_mat4() + .transform_point3(Vec3::ZERO), + Vec3::new(0.0, 3.0, 0.0) + ); + } + #[test] fn fixed_matrix_transforms_points() { let transform = FixedMat4 { diff --git a/src/fighter/debug.rs b/src/fighter/debug.rs index 7fd57b9..48f4f5c 100644 --- a/src/fighter/debug.rs +++ b/src/fighter/debug.rs @@ -163,11 +163,13 @@ pub fn attack_debug( &FighterBoneMatrices, &FighterTranslation, &FighterFacingDirection, + &FighterAnimationFrame, + &GlobalTransform )>, attack_scripts: Res>, mut gizmos: Gizmos, ) { - for (hitboxes, matrices, translation, facing_direction) in query { + for (hitboxes, matrices, translation, facing_direction, frame, global_trf) in query { if let Some(script) = &hitboxes.attack_script { let script = attack_scripts .get(script) @@ -180,34 +182,59 @@ pub fn attack_debug( let bone = &script.hitboxes[*i].bone; let trfs = matrices.get(bone); + if let Some((prev_trf, curr_trf)) = trfs { - let prev_trf = player_trf.mul(prev_trf); let curr_trf = player_trf.mul(curr_trf); - - let prev_pos = prev_trf.transform_point(offset); let curr_pos = curr_trf.transform_point(offset); - let prev_pos_draw = Vec3::new(prev_pos.x.to_num(), prev_pos.y.to_num(), prev_pos.z.to_num()); - let curr_pos_draw = Vec3::new(curr_pos.x.to_num(), curr_pos.y.to_num(), curr_pos.z.to_num()); + let curr_pos_draw = Vec3::new( + curr_pos.x.to_num(), + curr_pos.y.to_num(), + curr_pos.z.to_num(), + ); + + if script.hitboxes[*i].start_frame == frame.frame { + gizmos.sphere(curr_pos_draw, radius.to_num(), bevy::color::palettes::css::RED); + continue; + } + + let prev_trf = player_trf.mul(prev_trf); + + let prev_pos = prev_trf.transform_point(offset); + + let prev_pos_draw = Vec3::new( + prev_pos.x.to_num(), + prev_pos.y.to_num(), + prev_pos.z.to_num(), + ); + let dist = Dir3::new_and_length(curr_pos_draw - prev_pos_draw); if let Ok((aim_dir, dist)) = dist { let attack_capsule = Capsule3d { half_length: dist * 0.5, - radius: radius.to_num() + radius: radius.to_num(), }; let mut capsule_draw_trf = Transform::IDENTITY; - capsule_draw_trf.align(Dir3::Y, aim_dir, Dir3::X, aim_dir.any_orthonormal_vector()); + capsule_draw_trf.align( + Dir3::Y, + aim_dir, + Dir3::X, + aim_dir.any_orthonormal_vector(), + ); capsule_draw_trf.translation = (prev_pos_draw + curr_pos_draw) * 0.5; - gizmos.primitive_3d(&attack_capsule, capsule_draw_trf.to_isometry(), bevy::color::palettes::css::RED); + gizmos.primitive_3d( + &attack_capsule, + capsule_draw_trf.to_isometry(), + bevy::color::palettes::css::RED, + ); } - - - gizmos.sphere(prev_pos_draw, radius.to_num(), bevy::color::palettes::css::RED.darker(0.5)); - gizmos.sphere(curr_pos_draw, radius.to_num(), bevy::color::palettes::css::RED); - + gizmos.cross(curr_trf.transform_point(FGVec3::lit("0.0", "0.0", "0.0")).to_vec3(), 1.0, bevy::color::palettes::css::BLUE); + gizmos.cross(curr_trf.to_mat4().transform_point(Vec3::ZERO), 1.0, bevy::color::palettes::css::HOT_PINK); + + gizmos.axes(curr_trf.to_mat4(), 1.0); } else { warn!("Bone not found required by attack script: {}", bone); } diff --git a/src/fighter/mod.rs b/src/fighter/mod.rs index b5a071a..0ac81b1 100644 --- a/src/fighter/mod.rs +++ b/src/fighter/mod.rs @@ -29,7 +29,6 @@ pub use motion::{FighterPreviousTranslation, FighterTranslation, FighterVelocity use crate::{ fighter::{ - animation::animation_init, attack::FighterAttackScriptAssets, manifest::FighterManifest, state::{FighterState, fall::FallState}, @@ -106,78 +105,75 @@ pub struct Fighter { impl Plugin for FighterPlugin { fn build(&self, app: &mut App) { - app.add_systems( - Update, - (animation_init, animation::setup_fighter_animation_player), - ) - // Every state gets a chance to interrupt itself, in priority order. - .add_systems( - GgrsSchedule, - ( - ecb::snapshot_fighter_ecb.before(state_interrupt_system), - animation::advance_fighter_animation_frames.before(state_interrupt_system), - state::state_interrupt_system, + app + // Every state gets a chance to interrupt itself, in priority order. + .add_systems( + GgrsSchedule, + ( + ecb::snapshot_fighter_ecb.before(state_interrupt_system), + animation::advance_fighter_animation_frames.before(state_interrupt_system), + state::state_interrupt_system, + ) + .in_set(GameplaySet::Interrupt), ) - .in_set(GameplaySet::Interrupt), - ) - .add_systems( - GgrsSchedule, - (state::state_update_system,).in_set(GameplaySet::StateUpdate), - ) - /*.add_systems( - GgrsSchedule, - (motion::integrate_gravity, motion::apply_grounded_motion) - .chain() - .in_set(GameplaySet::Physics), - )*/ - .add_systems( - GgrsSchedule, - state::state_collision_interrupt_system.in_set(GameplaySet::Collision), - ) - .add_systems( - Update, - ( - motion::apply_fighter_translation_to_visuals, - animation::apply_animation, + .add_systems( + GgrsSchedule, + (state::state_update_system,).in_set(GameplaySet::StateUpdate), ) - .chain() - .run_if(in_state(crate::AppState::InMatch)), - ) - // Rollback registration lives next to the systems that write these - // components; a component simulated here but missing from this list - // is a desync waiting to happen. - .rollback_component_with_copy::() - .rollback_component_with_copy::() - .rollback_component_with_copy::() - .rollback_component_with_copy::() - .rollback_component_with_copy::() - .rollback_component_with_copy::() - .checksum_component_with_hash::() - .rollback_component_with_copy::() - .rollback_component_with_copy::() - .rollback_resource_with_reflect::() - // Debug views. - .add_systems( - FixedPostUpdate, - debug::debug_draw_ecb.run_if(crate::debug_tools::ecb_enabled), - ) - .add_systems( - EguiPrimaryContextPass, - debug::fighter_debug.run_if(crate::debug_tools::fighter_info_enabled), - ) - .add_systems( - FixedPostUpdate, - debug::animation_debug.run_if(crate::debug_tools::animation_bones_enabled), - ) - .add_systems( - FixedPostUpdate, - debug::attack_debug.run_if(crate::debug_tools::attack_hitboxes_enabled), - ) - .rollback_component_with_clone::() - .rollback_component_with_clone::() - .checksum_component_with_hash::() - .checksum_component_with_hash::() - .register_type::(); + /*.add_systems( + GgrsSchedule, + (motion::integrate_gravity, motion::apply_grounded_motion) + .chain() + .in_set(GameplaySet::Physics), + )*/ + .add_systems( + GgrsSchedule, + state::state_collision_interrupt_system.in_set(GameplaySet::Collision), + ) + .add_systems( + Update, + ( + motion::apply_fighter_translation_to_visuals, + animation::apply_animation, + ) + .chain() + .run_if(in_state(crate::AppState::InMatch)), + ) + // Rollback registration lives next to the systems that write these + // components; a component simulated here but missing from this list + // is a desync waiting to happen. + .rollback_component_with_copy::() + .rollback_component_with_copy::() + .rollback_component_with_copy::() + .rollback_component_with_copy::() + .rollback_component_with_copy::() + .rollback_component_with_copy::() + .checksum_component_with_hash::() + .rollback_component_with_copy::() + .rollback_component_with_copy::() + .rollback_resource_with_reflect::() + // Debug views. + .add_systems( + FixedPostUpdate, + debug::debug_draw_ecb.run_if(crate::debug_tools::ecb_enabled), + ) + .add_systems( + EguiPrimaryContextPass, + debug::fighter_debug.run_if(crate::debug_tools::fighter_info_enabled), + ) + .add_systems( + FixedPostUpdate, + debug::animation_debug.run_if(crate::debug_tools::animation_bones_enabled), + ) + .add_systems( + FixedPostUpdate, + debug::attack_debug.run_if(crate::debug_tools::attack_hitboxes_enabled), + ) + .rollback_component_with_clone::() + .rollback_component_with_clone::() + .checksum_component_with_hash::() + .checksum_component_with_hash::() + .register_type::(); } } diff --git a/src/fighter/state/mod.rs b/src/fighter/state/mod.rs index d00f307..544410b 100644 --- a/src/fighter/state/mod.rs +++ b/src/fighter/state/mod.rs @@ -5,7 +5,7 @@ use bevy::ecs::query::QueryData; use bevy::prelude::*; -use crate::fighter::animation::{AnimKind, FighterAnimationFrame, FighterAnimationPlayerLink}; +use crate::fighter::animation::{AnimKind, FighterAnimationFrame}; use crate::fighter::attack::FighterAttackScriptAssets; use crate::fighter::baked_animation::BakedFighterAnimations; use crate::fighter::ecb::FighterPreviousECB; @@ -96,8 +96,6 @@ pub struct FighterStateContext<'a> { pub game_settings: &'a GameSettings, pub stage_collision: &'a StageCollision, pub animations: &'a FighterAnimations, - pub animation_player: &'a mut AnimationPlayer, - pub animation_transitions: &'a mut AnimationTransitions, pub animation_frame: &'a mut FighterAnimationFrame, pub baked_animations: &'a Assets, pub attack_scripts: &'a Assets, @@ -107,14 +105,6 @@ pub struct FighterStateContext<'a> { impl FighterStateContext<'_> { pub fn play_animation(&mut self, kind: AnimKind, repeat: bool) { - let active = self.animation_transitions.play( - self.animation_player, - self.animations.clips[&kind], - std::time::Duration::ZERO, - ); - if repeat { - active.repeat(); - } self.animation_frame.reset(kind, repeat); } @@ -169,8 +159,6 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> { game_settings: &'a GameSettings, stage_collision: &'a StageCollision, animations: &'a FighterAnimations, - animation_player: &'a mut AnimationPlayer, - animation_transitions: &'a mut AnimationTransitions, baked_animations: &'a Assets, attack_scripts: &'a Assets, attack_script_assets: &'a FighterAttackScriptAssets, @@ -188,8 +176,6 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> { game_settings, stage_collision, animations, - animation_player, - animation_transitions, animation_frame: &mut self.animation_frame, baked_animations, attack_scripts, @@ -206,7 +192,6 @@ pub struct StatefulFighterQuery { pub state: &'static mut FighterState, pub frame: FighterFrameQuery, pub animations: &'static FighterAnimations, - pub animation_player_link: &'static FighterAnimationPlayerLink, pub fighter: &'static Fighter, pub attack_script_assets: &'static FighterAttackScriptAssets, } @@ -222,7 +207,6 @@ impl std::fmt::Display for StateNameDebug { pub fn state_update_system( mut query: Query, - mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>, stage_collision: Res, game_settings: Res, baked_animations: Res>, @@ -234,21 +218,13 @@ pub fn state_update_system( mut state, mut frame, animations, - animation_player_link, .. } = fighter; - let Ok((mut animation_player, mut animation_transitions)) = - animation_players.get_mut(animation_player_link.player()) - else { - continue; - }; let mut update_ctx = frame.context( &game_settings, &stage_collision, animations, - &mut animation_player, - &mut animation_transitions, &baked_animations, &attack_scripts, &fighter.attack_script_assets, @@ -262,7 +238,6 @@ pub fn state_update_system( pub fn state_interrupt_system( mut query: Query, - mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>, stage_collision: Res, game_settings: Res, baked_animations: Res>, @@ -276,21 +251,13 @@ pub fn state_interrupt_system( mut state, mut frame, animations, - animation_player_link, fighter, attack_script_assets, } = fighter; - let Ok((mut animation_player, mut animation_transitions)) = - animation_players.get_mut(animation_player_link.player()) - else { - continue; - }; let mut state_context = frame.context( &game_settings, &stage_collision, animations, - &mut animation_player, - &mut animation_transitions, &baked_animations, &attack_scripts, attack_script_assets, @@ -314,7 +281,6 @@ pub fn state_interrupt_system( pub fn state_collision_interrupt_system( mut query: Query, - mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>, stage_collision: Res, game_settings: Res, baked_animations: Res>, @@ -328,21 +294,13 @@ pub fn state_collision_interrupt_system( mut state, mut frame, animations, - animation_player_link, fighter, attack_script_assets, } = fighter; - let Ok((mut animation_player, mut animation_transitions)) = - animation_players.get_mut(animation_player_link.player()) - else { - continue; - }; let mut state_context = frame.context( &game_settings, &stage_collision, animations, - &mut animation_player, - &mut animation_transitions, &baked_animations, &attack_scripts, &attack_script_assets, diff --git a/src/fighter/visual.rs b/src/fighter/visual.rs index 244b08e..96f36fb 100644 --- a/src/fighter/visual.rs +++ b/src/fighter/visual.rs @@ -1,13 +1,8 @@ -use bevy::prelude::*; -use std::collections::HashMap; - -use crate::fighter::animation::AnimKind; use crate::fighter::baked_animation::BakedFighterAnimations; +use bevy::prelude::*; #[derive(Component, Clone)] pub struct FighterAnimations { - pub graph: Handle, - pub clips: HashMap, - /// Gameplay samples this fixed-point asset; the graph remains visual-only. + /// The single animation source for gameplay and the visual skeleton. pub baked: Handle, } diff --git a/src/game_settings.rs b/src/game_settings.rs index f7bd025..5850f4b 100644 --- a/src/game_settings.rs +++ b/src/game_settings.rs @@ -47,12 +47,3 @@ impl FromWorld for GameSettings { .clone() } } - -#[derive(Reflect, Resource, Default)] -#[reflect(Resource)] -pub struct DebugSettings { - pub show_attack_hitboxes: bool, - pub show_stage_lines: bool, - pub show_ecb: bool, - pub debug_gizmos_draw_in_front: bool -} \ No newline at end of file diff --git a/src/match_loading.rs b/src/match_loading.rs index b13b6ca..f2fb5f0 100644 --- a/src/match_loading.rs +++ b/src/match_loading.rs @@ -7,9 +7,21 @@ use bevy_asset_loader::prelude::*; use bevy_ggrs::prelude::Session; use crate::{ - AppState, args::Args, fighter::{ - FighterId, animation::AnimKind, attack::{AttackKind, FighterAttackScriptAssets}, baked_animation::BakedFighterAnimations, manifest::{FighterManifest, FighterManifestRegistry}, spawn_fighter, visual::FighterAnimations, - }, math::{int::FGi32, vec::FGVec2}, netcode::{GGRSCfg, session::create_session}, scripting::FighterAttackScript, stage::{ + AppState, + args::Args, + fighter::{ + FighterId, + animation::AnimKind, + attack::{AttackKind, FighterAttackScriptAssets}, + baked_animation::BakedFighterAnimations, + manifest::{FighterManifest, FighterManifestRegistry}, + spawn_fighter, + visual::FighterAnimations, + }, + math::{int::FGi32, vec::FGVec2}, + netcode::{GGRSCfg, session::create_session}, + scripting::FighterAttackScript, + stage::{ self, manifest::{StageId, StageManifest, StageManifestRegistry}, }, @@ -31,7 +43,7 @@ pub struct SelectedFighterAssets { pub fighter: FighterId, pub model: Handle, pub baked_animations: Handle, - pub attack_scripts: HashMap> + pub attack_scripts: HashMap>, } #[derive(Resource)] @@ -73,7 +85,12 @@ impl MatchAssets { fighter: player.fighter, model: asset_server.load(manifest.model_path.clone()), baked_animations: asset_server.load(manifest.baked_animation_path.clone()), - attack_scripts: manifest.attack_scripts.clone().into_iter().map(|(kind, path)| (kind, asset_server.load(path))).collect() + attack_scripts: manifest + .attack_scripts + .clone() + .into_iter() + .map(|(kind, path)| (kind, asset_server.load(path))) + .collect(), } }) .collect(), @@ -142,7 +159,6 @@ pub fn prepare_match( stage_registry: Res, manifests: Res>, stage_manifests: Res>, - mut graphs: ResMut>, args: Res, mut next_state: ResMut>, ) { @@ -188,21 +204,7 @@ pub fn prepare_match( return; }; - let mut graph = AnimationGraph::new(); - let mut clips = HashMap::::new(); - - for (kind, clip_name) in &manifest.animations { - let Some(clip) = gltf.named_animations.get(clip_name.as_str()) else { - fail_match( - &mut next_state, - &format!( - "{:?} animation manifest references missing clip '{clip_name}'", - character.fighter - ), - ); - return; - }; - clips.insert(*kind, graph.add_clip(clip.clone(), 1.0, graph.root)); + for kind in manifest.animations.keys() { if baked.frame_count(*kind).is_none() { fail_match( &mut next_state, @@ -215,27 +217,20 @@ pub fn prepare_match( } } - if !clips.contains_key(&AnimKind::Wait) { + if baked.frame_count(AnimKind::Wait).is_none() { fail_match(&mut next_state, "a character has no Wait animation"); return; } let animations = FighterAnimations { - graph: graphs.add(graph), - clips, baked: character.baked_animations.clone(), }; let attack_scripts = FighterAttackScriptAssets { - scripts: character.attack_scripts.clone() + scripts: character.attack_scripts.clone(), }; - prepared.push(( - scene, - animations, - attack_scripts, - manifest_handle, - )); + prepared.push((scene, animations, attack_scripts, manifest_handle)); } let session = match create_session(&args, request.players.len()) { @@ -265,10 +260,8 @@ pub fn prepare_match( stage_manifest.camera.clone(), ); - for ( - index, - (player, (scene, animations, attack_scripts, manifest_handle)), - ) in request.players.iter().zip(prepared).enumerate() + for (index, (player, (scene, animations, attack_scripts, manifest_handle))) in + request.players.iter().zip(prepared).enumerate() { let spawn_x = (index as i32 * 2 + 1 - request.players.len() as i32) * 5; spawn_fighter( diff --git a/src/math/int.rs b/src/math/int.rs index a54ebb7..f763d63 100644 --- a/src/math/int.rs +++ b/src/math/int.rs @@ -1,4 +1,2 @@ - - pub type FGi32 = fixed::FixedI32; pub type FGWide = fixed::FixedI128; diff --git a/src/scripting/importer.rs b/src/scripting/importer.rs index 2221e90..cad4578 100644 --- a/src/scripting/importer.rs +++ b/src/scripting/importer.rs @@ -1,7 +1,15 @@ - use std::io; -use bevy::{asset::{AssetLoader, AssetPath, AsyncReadExt, AsyncWriteExt, LoadContext, io::{Reader, Writer}, processor::LoadTransformAndSave, saver::{AssetSaver, SavedAsset}, transformer::{AssetTransformer, TransformedAsset}}, prelude::*}; +use bevy::{ + asset::{ + AssetLoader, AssetPath, AsyncReadExt, AsyncWriteExt, LoadContext, + io::{Reader, Writer}, + processor::LoadTransformAndSave, + saver::{AssetSaver, SavedAsset}, + transformer::{AssetTransformer, TransformedAsset}, + }, + prelude::*, +}; use crate::scripting::{FighterAttackScript, move_compiler}; @@ -26,7 +34,7 @@ impl AssetLoader for FighterAttackScriptSourceLoader { reader.read_to_string(&mut contents).await?; Ok(FighterAttackScriptContents(contents)) } - + fn extensions(&self) -> &[&str] { &["attack.rhai"] } @@ -41,7 +49,7 @@ impl AssetTransformer for FighterAttackScriptTransformer { type Settings = (); type Error = String; - async fn transform<'a>( + async fn transform<'a>( &'a self, source: TransformedAsset, _: &'a Self::Settings, @@ -51,7 +59,6 @@ impl AssetTransformer for FighterAttackScriptTransformer { } } - #[derive(Default, TypePath)] struct FighterAttackScriptLoader; @@ -118,4 +125,4 @@ impl Plugin for FighterScriptImportPlugin { )) .set_default_asset_processor::("attack.rhai"); } -} \ No newline at end of file +} diff --git a/src/scripting/mod.rs b/src/scripting/mod.rs index 3cef616..7ff8fa8 100644 --- a/src/scripting/mod.rs +++ b/src/scripting/mod.rs @@ -1,14 +1,14 @@ -use bevy::{prelude::*}; +use bevy::prelude::*; use serde::{Deserialize, Serialize}; use crate::{fighter::attack::AttackHitbox, math::int::FGi32}; -pub mod move_compiler; pub mod importer; +pub mod move_compiler; pub enum MoveAngleKind { Normal(FGi32), - Sakurai + Sakurai, } #[derive(Asset, Reflect, Serialize, Deserialize)] @@ -16,4 +16,4 @@ pub struct FighterAttackScript { pub hitboxes: Vec, pub iasa_frame: u32, pub wont_autocancel_window: (u32, u32), -} \ No newline at end of file +}