Make the baked animation system drive the character animations

This commit is contained in:
2026-09-01 21:07:53 +02:00
parent 89eeb6a868
commit b2da4a737d
11 changed files with 345 additions and 273 deletions
+1 -64
View File
@@ -1,23 +1,6 @@
use std::time::Duration;
use bevy::prelude::*; use bevy::prelude::*;
use crate::{ use crate::fighter::{FighterFacingDirection, FighterVisual};
fighter::{
FighterFacingDirection, FighterVisual,
visual::FighterAnimations,
},
player::Player,
};
#[derive(Component)]
pub struct FighterAnimationPlayerLink(Entity);
impl FighterAnimationPlayerLink {
pub fn player(&self) -> Entity {
self.0
}
}
/// The simulation-owned animation clock. Rendering may interpolate freely, but /// The simulation-owned animation clock. Rendering may interpolate freely, but
/// gameplay samples baked data only with this rollback-tracked value. /// 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<Entity, Added<AnimationPlayer>>,
parents: Query<&ChildOf>,
fighters: Query<Entity, With<Player>>,
) {
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( pub fn apply_animation(
children: Query< children: Query<
( (
@@ -102,28 +62,6 @@ pub fn apply_animation(
} }
} }
pub fn animation_init(
fighters: Query<
(&FighterAnimations, &FighterAnimationPlayerLink),
Added<FighterAnimationPlayerLink>,
>,
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( #[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize, Reflect, Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize, Reflect,
)] )]
@@ -142,7 +80,6 @@ pub enum AnimKind {
Turn, Turn,
// Attacks // Attacks
AttackJab1, AttackJab1,
AttackUpTilt, AttackUpTilt,
} }
+188 -18
View File
@@ -1,9 +1,10 @@
//! Deterministic, fighter-local skeletal animation samples for gameplay. //! Deterministic, fighter-local skeletal animation samples for gameplay and rendering.
//!
//! Rendering continues to use Bevy's floating-point animation player. Gameplay
//! reads this asset instead, so rollback never depends on render-time sampling.
use std::{collections::HashMap, io, path::Path}; use std::{
collections::{HashMap, HashSet},
io,
path::Path,
};
use bevy::{ use bevy::{
asset::{ asset::{
@@ -15,6 +16,7 @@ use bevy::{
}, },
prelude::*, prelude::*,
reflect::TypePath, reflect::TypePath,
world_serialization::{WorldInstanceReady, WorldInstanceSpawner},
}; };
use bevy_ggrs::prelude::GgrsSchedule; use bevy_ggrs::prelude::GgrsSchedule;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -64,6 +66,10 @@ impl FixedMat4 {
Ok(Self { cols }) 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 { pub fn mul(self, rhs: Self) -> Self {
let mut cols = [[FGi32::ZERO; 4]; 4]; let mut cols = [[FGi32::ZERO; 4]; 4];
for column in 0..4 { for column in 0..4 {
@@ -147,7 +153,7 @@ impl FixedMat4 {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BakedAnimationClip { 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<Vec<FixedMat4>>, pub frames: Vec<Vec<FixedMat4>>,
} }
@@ -156,6 +162,8 @@ pub struct BakedFighterAnimations {
pub frame_rate: u32, pub frame_rate: u32,
/// Indexed exactly like every frame in each clip. /// Indexed exactly like every frame in each clip.
pub bone_names: Vec<String>, pub bone_names: Vec<String>,
/// Parent node for each stored node, indexed like `bone_names`.
pub parents: Vec<Option<usize>>,
clips: Vec<BakedAnimationClip>, clips: Vec<BakedAnimationClip>,
clip_indices: HashMap<AnimKind, usize>, clip_indices: HashMap<AnimKind, usize>,
} }
@@ -190,8 +198,16 @@ impl BakedFighterAnimations {
#[derive(Component, Clone, Debug, Default)] #[derive(Component, Clone, Debug, Default)]
pub struct FighterBoneMatrices { pub struct FighterBoneMatrices {
bone_names: Vec<String>, bone_names: Vec<String>,
parents: Vec<Option<usize>>,
matrices: Vec<FixedMat4>, matrices: Vec<FixedMat4>,
previous_frame_matrices: Vec<FixedMat4>, previous_frame_matrices: Vec<FixedMat4>,
local_matrices: Vec<FixedMat4>,
}
#[derive(Component)]
struct BakedFighterBone {
fighter: Entity,
index: usize,
} }
impl FighterBoneMatrices { impl FighterBoneMatrices {
@@ -221,20 +237,40 @@ impl FighterBoneMatrices {
.zip(self.matrices.iter().copied()) .zip(self.matrices.iter().copied())
} }
fn set_pose(&mut self, bone_names: &[String], matrices: &[FixedMat4]) { pub fn local(&self, index: usize) -> Option<FixedMat4> {
self.local_matrices.get(index).copied()
}
fn set_pose(&mut self, bone_names: &[String], parents: &[Option<usize>], local: &[FixedMat4]) {
assert_eq!( assert_eq!(
bone_names.len(), bone_names.len(),
matrices.len(), local.len(),
"baked animation pose does not match its bone names" "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() { if self.bone_names.is_empty() {
self.bone_names.extend_from_slice(bone_names); self.bone_names.extend_from_slice(bone_names);
self.parents.extend_from_slice(parents);
} else { } else {
assert_eq!(self.bone_names, bone_names, "baked bone names changed"); 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); std::mem::swap(&mut self.matrices, &mut self.previous_frame_matrices);
self.matrices.clear(); self.local_matrices.clear();
self.matrices.extend_from_slice(matrices); 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 let pose = baked
.sample_pose(frame) .sample_pose(frame)
.expect("fighter animation frame should select a baked pose"); .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<WorldInstanceReady>,
mut commands: Commands,
spawner: Res<WorldInstanceSpawner>,
fighters: Query<&FighterAnimations>,
baked_assets: Res<Assets<BakedFighterAnimations>>,
nodes: Query<(&Name, Option<&ChildOf>)>,
animation_players: Query<(), With<AnimationPlayer>>,
) {
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::<HashSet<_>>();
for &entity in &entities {
if animation_players.contains(entity) {
commands.entity(entity).remove::<AnimationPlayer>();
}
}
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::<Vec<_>>();
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( .add_systems(
GgrsSchedule, GgrsSchedule,
update_fighter_bone_matrices.in_set(GameplaySet::Animation), 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() .iter()
.map(|node| local_transform(node.clone(), &channels, time)) .map(|node| local_transform(node.clone(), &channels, time))
.collect::<io::Result<Vec<_>>>()?; .collect::<io::Result<Vec<_>>>()?;
let mut model = vec![None; nodes.len()]; frames.push(local);
for index in 0..nodes.len() {
resolve_model_transform(index, &parents, &local, &mut model)?;
}
frames.push(model.into_iter().map(Option::unwrap).collect());
} }
let index = clips.len(); let index = clips.len();
clips.push(BakedAnimationClip { frames }); clips.push(BakedAnimationClip { frames });
@@ -496,6 +630,7 @@ fn bake_gltf(
Ok(BakedFighterAnimations { Ok(BakedFighterAnimations {
frame_rate: BAKED_ANIMATION_FPS, frame_rate: BAKED_ANIMATION_FPS,
bone_names: names, bone_names: names,
parents,
clips, clips,
clip_indices, clip_indices,
}) })
@@ -688,6 +823,7 @@ mod tests {
baked.clip(AnimKind::Wait).unwrap().frames[0].len(), baked.clip(AnimKind::Wait).unwrap().frames[0].len(),
baked.bone_names.len() baked.bone_names.len()
); );
assert_eq!(baked.parents.len(), baked.bone_names.len());
} }
#[test] #[test]
@@ -698,6 +834,7 @@ mod tests {
let baked = BakedFighterAnimations { let baked = BakedFighterAnimations {
frame_rate: BAKED_ANIMATION_FPS, frame_rate: BAKED_ANIMATION_FPS,
bone_names: vec!["root".into()], bone_names: vec!["root".into()],
parents: vec![None],
clips: vec![BakedAnimationClip { clips: vec![BakedAnimationClip {
frames: vec![vec![first], vec![second]], frames: vec![vec![first], vec![second]],
}], }],
@@ -725,7 +862,11 @@ mod tests {
let mut second = FixedMat4::IDENTITY; let mut second = FixedMat4::IDENTITY;
second.translate(FGVec3::lit("0", "3", "0")); second.translate(FGVec3::lit("0", "3", "0"));
let mut matrices = FighterBoneMatrices::default(); 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!(matrices.get_current("hand"), Some(second));
assert_eq!( 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] #[test]
fn fixed_matrix_transforms_points() { fn fixed_matrix_transforms_points() {
let transform = FixedMat4 { let transform = FixedMat4 {
+41 -14
View File
@@ -163,11 +163,13 @@ pub fn attack_debug(
&FighterBoneMatrices, &FighterBoneMatrices,
&FighterTranslation, &FighterTranslation,
&FighterFacingDirection, &FighterFacingDirection,
&FighterAnimationFrame,
&GlobalTransform
)>, )>,
attack_scripts: Res<Assets<FighterAttackScript>>, attack_scripts: Res<Assets<FighterAttackScript>>,
mut gizmos: Gizmos, 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 { if let Some(script) = &hitboxes.attack_script {
let script = attack_scripts let script = attack_scripts
.get(script) .get(script)
@@ -180,34 +182,59 @@ pub fn attack_debug(
let bone = &script.hitboxes[*i].bone; let bone = &script.hitboxes[*i].bone;
let trfs = matrices.get(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); if let Some((prev_trf, curr_trf)) = trfs {
let curr_trf = player_trf.mul(curr_trf);
let curr_pos = curr_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(
let curr_pos_draw = Vec3::new(curr_pos.x.to_num(), curr_pos.y.to_num(), curr_pos.z.to_num()); 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); let dist = Dir3::new_and_length(curr_pos_draw - prev_pos_draw);
if let Ok((aim_dir, dist)) = dist { if let Ok((aim_dir, dist)) = dist {
let attack_capsule = Capsule3d { let attack_capsule = Capsule3d {
half_length: dist * 0.5, half_length: dist * 0.5,
radius: radius.to_num() radius: radius.to_num(),
}; };
let mut capsule_draw_trf = Transform::IDENTITY; 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; 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.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);
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);
} else { } else {
warn!("Bone not found required by attack script: {}", bone); warn!("Bone not found required by attack script: {}", bone);
} }
+67 -71
View File
@@ -29,7 +29,6 @@ pub use motion::{FighterPreviousTranslation, FighterTranslation, FighterVelocity
use crate::{ use crate::{
fighter::{ fighter::{
animation::animation_init,
attack::FighterAttackScriptAssets, attack::FighterAttackScriptAssets,
manifest::FighterManifest, manifest::FighterManifest,
state::{FighterState, fall::FallState}, state::{FighterState, fall::FallState},
@@ -106,78 +105,75 @@ pub struct Fighter {
impl Plugin for FighterPlugin { impl Plugin for FighterPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems( app
Update, // Every state gets a chance to interrupt itself, in priority order.
(animation_init, animation::setup_fighter_animation_player), .add_systems(
) GgrsSchedule,
// Every state gets a chance to interrupt itself, in priority order. (
.add_systems( ecb::snapshot_fighter_ecb.before(state_interrupt_system),
GgrsSchedule, animation::advance_fighter_animation_frames.before(state_interrupt_system),
( state::state_interrupt_system,
ecb::snapshot_fighter_ecb.before(state_interrupt_system), )
animation::advance_fighter_animation_frames.before(state_interrupt_system), .in_set(GameplaySet::Interrupt),
state::state_interrupt_system,
) )
.in_set(GameplaySet::Interrupt), .add_systems(
) GgrsSchedule,
.add_systems( (state::state_update_system,).in_set(GameplaySet::StateUpdate),
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,
) )
.chain() /*.add_systems(
.run_if(in_state(crate::AppState::InMatch)), GgrsSchedule,
) (motion::integrate_gravity, motion::apply_grounded_motion)
// Rollback registration lives next to the systems that write these .chain()
// components; a component simulated here but missing from this list .in_set(GameplaySet::Physics),
// is a desync waiting to happen. )*/
.rollback_component_with_copy::<FighterECB>() .add_systems(
.rollback_component_with_copy::<ecb::FighterPreviousECB>() GgrsSchedule,
.rollback_component_with_copy::<FighterVelocity>() state::state_collision_interrupt_system.in_set(GameplaySet::Collision),
.rollback_component_with_copy::<FighterTranslation>() )
.rollback_component_with_copy::<FighterPreviousTranslation>() .add_systems(
.rollback_component_with_copy::<animation::FighterAnimationFrame>() Update,
.checksum_component_with_hash::<animation::FighterAnimationFrame>() (
.rollback_component_with_copy::<Grounded>() motion::apply_fighter_translation_to_visuals,
.rollback_component_with_copy::<FighterFacingDirection>() animation::apply_animation,
.rollback_resource_with_reflect::<StageCollision>() )
// Debug views. .chain()
.add_systems( .run_if(in_state(crate::AppState::InMatch)),
FixedPostUpdate, )
debug::debug_draw_ecb.run_if(crate::debug_tools::ecb_enabled), // Rollback registration lives next to the systems that write these
) // components; a component simulated here but missing from this list
.add_systems( // is a desync waiting to happen.
EguiPrimaryContextPass, .rollback_component_with_copy::<FighterECB>()
debug::fighter_debug.run_if(crate::debug_tools::fighter_info_enabled), .rollback_component_with_copy::<ecb::FighterPreviousECB>()
) .rollback_component_with_copy::<FighterVelocity>()
.add_systems( .rollback_component_with_copy::<FighterTranslation>()
FixedPostUpdate, .rollback_component_with_copy::<FighterPreviousTranslation>()
debug::animation_debug.run_if(crate::debug_tools::animation_bones_enabled), .rollback_component_with_copy::<animation::FighterAnimationFrame>()
) .checksum_component_with_hash::<animation::FighterAnimationFrame>()
.add_systems( .rollback_component_with_copy::<Grounded>()
FixedPostUpdate, .rollback_component_with_copy::<FighterFacingDirection>()
debug::attack_debug.run_if(crate::debug_tools::attack_hitboxes_enabled), .rollback_resource_with_reflect::<StageCollision>()
) // Debug views.
.rollback_component_with_clone::<FighterState>() .add_systems(
.rollback_component_with_clone::<FighterHitboxes>() FixedPostUpdate,
.checksum_component_with_hash::<FighterState>() debug::debug_draw_ecb.run_if(crate::debug_tools::ecb_enabled),
.checksum_component_with_hash::<FighterFacingDirection>() )
.register_type::<FighterFacingDirection>(); .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::<FighterState>()
.rollback_component_with_clone::<FighterHitboxes>()
.checksum_component_with_hash::<FighterState>()
.checksum_component_with_hash::<FighterFacingDirection>()
.register_type::<FighterFacingDirection>();
} }
} }
+1 -43
View File
@@ -5,7 +5,7 @@
use bevy::ecs::query::QueryData; use bevy::ecs::query::QueryData;
use bevy::prelude::*; use bevy::prelude::*;
use crate::fighter::animation::{AnimKind, FighterAnimationFrame, FighterAnimationPlayerLink}; use crate::fighter::animation::{AnimKind, FighterAnimationFrame};
use crate::fighter::attack::FighterAttackScriptAssets; use crate::fighter::attack::FighterAttackScriptAssets;
use crate::fighter::baked_animation::BakedFighterAnimations; use crate::fighter::baked_animation::BakedFighterAnimations;
use crate::fighter::ecb::FighterPreviousECB; use crate::fighter::ecb::FighterPreviousECB;
@@ -96,8 +96,6 @@ pub struct FighterStateContext<'a> {
pub game_settings: &'a GameSettings, pub game_settings: &'a GameSettings,
pub stage_collision: &'a StageCollision, pub stage_collision: &'a StageCollision,
pub animations: &'a FighterAnimations, pub animations: &'a FighterAnimations,
pub animation_player: &'a mut AnimationPlayer,
pub animation_transitions: &'a mut AnimationTransitions,
pub animation_frame: &'a mut FighterAnimationFrame, pub animation_frame: &'a mut FighterAnimationFrame,
pub baked_animations: &'a Assets<BakedFighterAnimations>, pub baked_animations: &'a Assets<BakedFighterAnimations>,
pub attack_scripts: &'a Assets<FighterAttackScript>, pub attack_scripts: &'a Assets<FighterAttackScript>,
@@ -107,14 +105,6 @@ pub struct FighterStateContext<'a> {
impl FighterStateContext<'_> { impl FighterStateContext<'_> {
pub fn play_animation(&mut self, kind: AnimKind, repeat: bool) { 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); self.animation_frame.reset(kind, repeat);
} }
@@ -169,8 +159,6 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
game_settings: &'a GameSettings, game_settings: &'a GameSettings,
stage_collision: &'a StageCollision, stage_collision: &'a StageCollision,
animations: &'a FighterAnimations, animations: &'a FighterAnimations,
animation_player: &'a mut AnimationPlayer,
animation_transitions: &'a mut AnimationTransitions,
baked_animations: &'a Assets<BakedFighterAnimations>, baked_animations: &'a Assets<BakedFighterAnimations>,
attack_scripts: &'a Assets<FighterAttackScript>, attack_scripts: &'a Assets<FighterAttackScript>,
attack_script_assets: &'a FighterAttackScriptAssets, attack_script_assets: &'a FighterAttackScriptAssets,
@@ -188,8 +176,6 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
game_settings, game_settings,
stage_collision, stage_collision,
animations, animations,
animation_player,
animation_transitions,
animation_frame: &mut self.animation_frame, animation_frame: &mut self.animation_frame,
baked_animations, baked_animations,
attack_scripts, attack_scripts,
@@ -206,7 +192,6 @@ pub struct StatefulFighterQuery {
pub state: &'static mut FighterState, pub state: &'static mut FighterState,
pub frame: FighterFrameQuery, pub frame: FighterFrameQuery,
pub animations: &'static FighterAnimations, pub animations: &'static FighterAnimations,
pub animation_player_link: &'static FighterAnimationPlayerLink,
pub fighter: &'static Fighter, pub fighter: &'static Fighter,
pub attack_script_assets: &'static FighterAttackScriptAssets, pub attack_script_assets: &'static FighterAttackScriptAssets,
} }
@@ -222,7 +207,6 @@ impl std::fmt::Display for StateNameDebug {
pub fn state_update_system( pub fn state_update_system(
mut query: Query<StatefulFighterQuery>, mut query: Query<StatefulFighterQuery>,
mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
stage_collision: Res<StageCollision>, stage_collision: Res<StageCollision>,
game_settings: Res<GameSettings>, game_settings: Res<GameSettings>,
baked_animations: Res<Assets<BakedFighterAnimations>>, baked_animations: Res<Assets<BakedFighterAnimations>>,
@@ -234,21 +218,13 @@ pub fn state_update_system(
mut state, mut state,
mut frame, mut frame,
animations, animations,
animation_player_link,
.. ..
} = fighter; } = 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( let mut update_ctx = frame.context(
&game_settings, &game_settings,
&stage_collision, &stage_collision,
animations, animations,
&mut animation_player,
&mut animation_transitions,
&baked_animations, &baked_animations,
&attack_scripts, &attack_scripts,
&fighter.attack_script_assets, &fighter.attack_script_assets,
@@ -262,7 +238,6 @@ pub fn state_update_system(
pub fn state_interrupt_system( pub fn state_interrupt_system(
mut query: Query<StatefulFighterQuery>, mut query: Query<StatefulFighterQuery>,
mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
stage_collision: Res<StageCollision>, stage_collision: Res<StageCollision>,
game_settings: Res<GameSettings>, game_settings: Res<GameSettings>,
baked_animations: Res<Assets<BakedFighterAnimations>>, baked_animations: Res<Assets<BakedFighterAnimations>>,
@@ -276,21 +251,13 @@ pub fn state_interrupt_system(
mut state, mut state,
mut frame, mut frame,
animations, animations,
animation_player_link,
fighter, fighter,
attack_script_assets, attack_script_assets,
} = fighter; } = 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( let mut state_context = frame.context(
&game_settings, &game_settings,
&stage_collision, &stage_collision,
animations, animations,
&mut animation_player,
&mut animation_transitions,
&baked_animations, &baked_animations,
&attack_scripts, &attack_scripts,
attack_script_assets, attack_script_assets,
@@ -314,7 +281,6 @@ pub fn state_interrupt_system(
pub fn state_collision_interrupt_system( pub fn state_collision_interrupt_system(
mut query: Query<StatefulFighterQuery>, mut query: Query<StatefulFighterQuery>,
mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
stage_collision: Res<StageCollision>, stage_collision: Res<StageCollision>,
game_settings: Res<GameSettings>, game_settings: Res<GameSettings>,
baked_animations: Res<Assets<BakedFighterAnimations>>, baked_animations: Res<Assets<BakedFighterAnimations>>,
@@ -328,21 +294,13 @@ pub fn state_collision_interrupt_system(
mut state, mut state,
mut frame, mut frame,
animations, animations,
animation_player_link,
fighter, fighter,
attack_script_assets, attack_script_assets,
} = fighter; } = 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( let mut state_context = frame.context(
&game_settings, &game_settings,
&stage_collision, &stage_collision,
animations, animations,
&mut animation_player,
&mut animation_transitions,
&baked_animations, &baked_animations,
&attack_scripts, &attack_scripts,
&attack_script_assets, &attack_script_assets,
+2 -7
View File
@@ -1,13 +1,8 @@
use bevy::prelude::*;
use std::collections::HashMap;
use crate::fighter::animation::AnimKind;
use crate::fighter::baked_animation::BakedFighterAnimations; use crate::fighter::baked_animation::BakedFighterAnimations;
use bevy::prelude::*;
#[derive(Component, Clone)] #[derive(Component, Clone)]
pub struct FighterAnimations { pub struct FighterAnimations {
pub graph: Handle<AnimationGraph>, /// The single animation source for gameplay and the visual skeleton.
pub clips: HashMap<AnimKind, AnimationNodeIndex>,
/// Gameplay samples this fixed-point asset; the graph remains visual-only.
pub baked: Handle<BakedFighterAnimations>, pub baked: Handle<BakedFighterAnimations>,
} }
-9
View File
@@ -47,12 +47,3 @@ impl FromWorld for GameSettings {
.clone() .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
}
+28 -35
View File
@@ -7,9 +7,21 @@ use bevy_asset_loader::prelude::*;
use bevy_ggrs::prelude::Session; use bevy_ggrs::prelude::Session;
use crate::{ use crate::{
AppState, args::Args, fighter::{ AppState,
FighterId, animation::AnimKind, attack::{AttackKind, FighterAttackScriptAssets}, baked_animation::BakedFighterAnimations, manifest::{FighterManifest, FighterManifestRegistry}, spawn_fighter, visual::FighterAnimations, args::Args,
}, math::{int::FGi32, vec::FGVec2}, netcode::{GGRSCfg, session::create_session}, scripting::FighterAttackScript, stage::{ 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, self,
manifest::{StageId, StageManifest, StageManifestRegistry}, manifest::{StageId, StageManifest, StageManifestRegistry},
}, },
@@ -31,7 +43,7 @@ pub struct SelectedFighterAssets {
pub fighter: FighterId, pub fighter: FighterId,
pub model: Handle<Gltf>, pub model: Handle<Gltf>,
pub baked_animations: Handle<BakedFighterAnimations>, pub baked_animations: Handle<BakedFighterAnimations>,
pub attack_scripts: HashMap<AttackKind, Handle<FighterAttackScript>> pub attack_scripts: HashMap<AttackKind, Handle<FighterAttackScript>>,
} }
#[derive(Resource)] #[derive(Resource)]
@@ -73,7 +85,12 @@ impl MatchAssets {
fighter: player.fighter, fighter: player.fighter,
model: asset_server.load(manifest.model_path.clone()), model: asset_server.load(manifest.model_path.clone()),
baked_animations: asset_server.load(manifest.baked_animation_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(), .collect(),
@@ -142,7 +159,6 @@ pub fn prepare_match(
stage_registry: Res<StageManifestRegistry>, stage_registry: Res<StageManifestRegistry>,
manifests: Res<Assets<FighterManifest>>, manifests: Res<Assets<FighterManifest>>,
stage_manifests: Res<Assets<StageManifest>>, stage_manifests: Res<Assets<StageManifest>>,
mut graphs: ResMut<Assets<AnimationGraph>>,
args: Res<Args>, args: Res<Args>,
mut next_state: ResMut<NextState<AppState>>, mut next_state: ResMut<NextState<AppState>>,
) { ) {
@@ -188,21 +204,7 @@ pub fn prepare_match(
return; return;
}; };
let mut graph = AnimationGraph::new(); for kind in manifest.animations.keys() {
let mut clips = HashMap::<AnimKind, AnimationNodeIndex>::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));
if baked.frame_count(*kind).is_none() { if baked.frame_count(*kind).is_none() {
fail_match( fail_match(
&mut next_state, &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"); fail_match(&mut next_state, "a character has no Wait animation");
return; return;
} }
let animations = FighterAnimations { let animations = FighterAnimations {
graph: graphs.add(graph),
clips,
baked: character.baked_animations.clone(), baked: character.baked_animations.clone(),
}; };
let attack_scripts = FighterAttackScriptAssets { let attack_scripts = FighterAttackScriptAssets {
scripts: character.attack_scripts.clone() scripts: character.attack_scripts.clone(),
}; };
prepared.push(( prepared.push((scene, animations, attack_scripts, manifest_handle));
scene,
animations,
attack_scripts,
manifest_handle,
));
} }
let session = match create_session(&args, request.players.len()) { let session = match create_session(&args, request.players.len()) {
@@ -265,10 +260,8 @@ pub fn prepare_match(
stage_manifest.camera.clone(), stage_manifest.camera.clone(),
); );
for ( for (index, (player, (scene, animations, attack_scripts, manifest_handle))) in
index, request.players.iter().zip(prepared).enumerate()
(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; let spawn_x = (index as i32 * 2 + 1 - request.players.len() as i32) * 5;
spawn_fighter( spawn_fighter(
-2
View File
@@ -1,4 +1,2 @@
pub type FGi32 = fixed::FixedI32<fixed::types::extra::U16>; pub type FGi32 = fixed::FixedI32<fixed::types::extra::U16>;
pub type FGWide = fixed::FixedI128<fixed::types::extra::U32>; pub type FGWide = fixed::FixedI128<fixed::types::extra::U32>;
+11 -4
View File
@@ -1,7 +1,15 @@
use std::io; 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}; use crate::scripting::{FighterAttackScript, move_compiler};
@@ -41,7 +49,7 @@ impl AssetTransformer for FighterAttackScriptTransformer {
type Settings = (); type Settings = ();
type Error = String; type Error = String;
async fn transform<'a>( async fn transform<'a>(
&'a self, &'a self,
source: TransformedAsset<Self::AssetInput>, source: TransformedAsset<Self::AssetInput>,
_: &'a Self::Settings, _: &'a Self::Settings,
@@ -51,7 +59,6 @@ impl AssetTransformer for FighterAttackScriptTransformer {
} }
} }
#[derive(Default, TypePath)] #[derive(Default, TypePath)]
struct FighterAttackScriptLoader; struct FighterAttackScriptLoader;
+3 -3
View File
@@ -1,14 +1,14 @@
use bevy::{prelude::*}; use bevy::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{fighter::attack::AttackHitbox, math::int::FGi32}; use crate::{fighter::attack::AttackHitbox, math::int::FGi32};
pub mod move_compiler;
pub mod importer; pub mod importer;
pub mod move_compiler;
pub enum MoveAngleKind { pub enum MoveAngleKind {
Normal(FGi32), Normal(FGi32),
Sakurai Sakurai,
} }
#[derive(Asset, Reflect, Serialize, Deserialize)] #[derive(Asset, Reflect, Serialize, Deserialize)]