Make the baked animation system drive the character animations
This commit is contained in:
@@ -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<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(
|
||||
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(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize, Reflect,
|
||||
)]
|
||||
@@ -142,7 +80,6 @@ pub enum AnimKind {
|
||||
Turn,
|
||||
|
||||
// Attacks
|
||||
|
||||
AttackJab1,
|
||||
AttackUpTilt,
|
||||
}
|
||||
|
||||
+188
-18
@@ -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<Vec<FixedMat4>>,
|
||||
}
|
||||
|
||||
@@ -156,6 +162,8 @@ pub struct BakedFighterAnimations {
|
||||
pub frame_rate: u32,
|
||||
/// Indexed exactly like every frame in each clip.
|
||||
pub bone_names: Vec<String>,
|
||||
/// Parent node for each stored node, indexed like `bone_names`.
|
||||
pub parents: Vec<Option<usize>>,
|
||||
clips: Vec<BakedAnimationClip>,
|
||||
clip_indices: HashMap<AnimKind, usize>,
|
||||
}
|
||||
@@ -190,8 +198,16 @@ impl BakedFighterAnimations {
|
||||
#[derive(Component, Clone, Debug, Default)]
|
||||
pub struct FighterBoneMatrices {
|
||||
bone_names: Vec<String>,
|
||||
parents: Vec<Option<usize>>,
|
||||
matrices: Vec<FixedMat4>,
|
||||
previous_frame_matrices: Vec<FixedMat4>,
|
||||
local_matrices: Vec<FixedMat4>,
|
||||
}
|
||||
|
||||
#[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<FixedMat4> {
|
||||
self.local_matrices.get(index).copied()
|
||||
}
|
||||
|
||||
fn set_pose(&mut self, bone_names: &[String], parents: &[Option<usize>], 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<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(
|
||||
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::<io::Result<Vec<_>>>()?;
|
||||
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 {
|
||||
|
||||
+41
-14
@@ -163,11 +163,13 @@ pub fn attack_debug(
|
||||
&FighterBoneMatrices,
|
||||
&FighterTranslation,
|
||||
&FighterFacingDirection,
|
||||
&FighterAnimationFrame,
|
||||
&GlobalTransform
|
||||
)>,
|
||||
attack_scripts: Res<Assets<FighterAttackScript>>,
|
||||
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);
|
||||
}
|
||||
|
||||
+67
-71
@@ -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::<FighterECB>()
|
||||
.rollback_component_with_copy::<ecb::FighterPreviousECB>()
|
||||
.rollback_component_with_copy::<FighterVelocity>()
|
||||
.rollback_component_with_copy::<FighterTranslation>()
|
||||
.rollback_component_with_copy::<FighterPreviousTranslation>()
|
||||
.rollback_component_with_copy::<animation::FighterAnimationFrame>()
|
||||
.checksum_component_with_hash::<animation::FighterAnimationFrame>()
|
||||
.rollback_component_with_copy::<Grounded>()
|
||||
.rollback_component_with_copy::<FighterFacingDirection>()
|
||||
.rollback_resource_with_reflect::<StageCollision>()
|
||||
// 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::<FighterState>()
|
||||
.rollback_component_with_clone::<FighterHitboxes>()
|
||||
.checksum_component_with_hash::<FighterState>()
|
||||
.checksum_component_with_hash::<FighterFacingDirection>()
|
||||
.register_type::<FighterFacingDirection>();
|
||||
/*.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::<FighterECB>()
|
||||
.rollback_component_with_copy::<ecb::FighterPreviousECB>()
|
||||
.rollback_component_with_copy::<FighterVelocity>()
|
||||
.rollback_component_with_copy::<FighterTranslation>()
|
||||
.rollback_component_with_copy::<FighterPreviousTranslation>()
|
||||
.rollback_component_with_copy::<animation::FighterAnimationFrame>()
|
||||
.checksum_component_with_hash::<animation::FighterAnimationFrame>()
|
||||
.rollback_component_with_copy::<Grounded>()
|
||||
.rollback_component_with_copy::<FighterFacingDirection>()
|
||||
.rollback_resource_with_reflect::<StageCollision>()
|
||||
// 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::<FighterState>()
|
||||
.rollback_component_with_clone::<FighterHitboxes>()
|
||||
.checksum_component_with_hash::<FighterState>()
|
||||
.checksum_component_with_hash::<FighterFacingDirection>()
|
||||
.register_type::<FighterFacingDirection>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<BakedFighterAnimations>,
|
||||
pub attack_scripts: &'a Assets<FighterAttackScript>,
|
||||
@@ -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<BakedFighterAnimations>,
|
||||
attack_scripts: &'a Assets<FighterAttackScript>,
|
||||
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<StatefulFighterQuery>,
|
||||
mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
|
||||
stage_collision: Res<StageCollision>,
|
||||
game_settings: Res<GameSettings>,
|
||||
baked_animations: Res<Assets<BakedFighterAnimations>>,
|
||||
@@ -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<StatefulFighterQuery>,
|
||||
mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
|
||||
stage_collision: Res<StageCollision>,
|
||||
game_settings: Res<GameSettings>,
|
||||
baked_animations: Res<Assets<BakedFighterAnimations>>,
|
||||
@@ -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<StatefulFighterQuery>,
|
||||
mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
|
||||
stage_collision: Res<StageCollision>,
|
||||
game_settings: Res<GameSettings>,
|
||||
baked_animations: Res<Assets<BakedFighterAnimations>>,
|
||||
@@ -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,
|
||||
|
||||
@@ -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<AnimationGraph>,
|
||||
pub clips: HashMap<AnimKind, AnimationNodeIndex>,
|
||||
/// Gameplay samples this fixed-point asset; the graph remains visual-only.
|
||||
/// The single animation source for gameplay and the visual skeleton.
|
||||
pub baked: Handle<BakedFighterAnimations>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user