Implement root motion into grounded attacks and implement dash attack
CI / build-and-test (pull_request) Successful in 1m33s
CI / production-build (pull_request) Successful in 10m41s

This commit is contained in:
2026-10-05 21:51:07 +02:00
parent 256485ce44
commit 3816e16432
11 changed files with 213 additions and 25 deletions
+32
View File
@@ -364,6 +364,38 @@ mod tests {
assert_eq!(matrices.root_motion_delta(), FGVec3::lit("4", "0", "0"));
}
#[test]
fn root_motion_translation_is_removed_from_published_pose() {
let names = ["root".into(), "hand".into()];
let parents = [None, Some(0)];
let mut root = FixedMat4::IDENTITY;
root.translate(FGVec3::lit("3", "4", "5"));
let mut hand = FixedMat4::IDENTITY;
hand.translate(FGVec3::lit("1", "2", "0"));
let mut matrices = FighterBoneMatrices::default();
let frame = crate::fighter::animation::FighterAnimationFrame::new(AnimKind::Wait, false);
matrices.set_pose_with_frame(
frame,
"root",
FGVec3::ZERO,
None,
AnimKind::Wait,
&names,
&parents,
&[root, hand],
FGi32::ONE,
);
assert_eq!(matrices.root_motion_delta(), FGVec3::ZERO);
assert_eq!(matrices.local(0).unwrap().get_translation(), FGVec3::ZERO);
assert_eq!(matrices.get_current("root").unwrap().get_translation(), FGVec3::ZERO);
assert_eq!(
matrices.get_current("hand").unwrap().get_translation(),
FGVec3::lit("1", "2", "0")
);
}
#[test]
fn repeating_root_motion_preserves_cycle_displacement_at_wrap() {
let names = ["root".into()];
+41 -8
View File
@@ -122,6 +122,10 @@ pub struct FighterBoneMatrices {
model_matrices: Vec<Option<FixedMat4>>,
presentation_kind: Option<AnimKind>,
previous_animation_frame: Option<FighterAnimationFrame>,
/// The raw, scaled model-space root translation used as the baseline for
/// root-motion extraction. This is kept separately because the published
/// pose has the root translation removed.
previous_root_motion_translation: Option<FGVec3>,
root_motion_delta: crate::math::vec3::FGVec3,
}
@@ -351,6 +355,17 @@ impl FighterBoneMatrices {
}
self.local_matrices.clear();
self.local_matrices.extend_from_slice(local);
let root_index = if root_motion_bone.is_empty() {
None
} else {
Some(
self.bone_names
.iter()
.position(|name| name == root_motion_bone)
.expect("configured root motion bone should exist"),
)
};
self.model_matrices.resize(local.len(), None);
self.model_matrices.fill(None);
for index in 0..local.len() {
@@ -365,6 +380,26 @@ impl FighterBoneMatrices {
[FGi32::ZERO, FGi32::ZERO, FGi32::ZERO, FGi32::ONE],
],
};
let raw_root_motion_translation = root_index.map(|root_index| {
model_scale
.mul(
self.model_matrices[root_index]
.expect("baked bone hierarchy should be valid"),
)
.get_translation()
});
// Gameplay owns the root's displacement. Keep rotation and scale in
// the animation, but remove its translation before publishing the pose
// used by visuals and gameplay volumes.
if let Some(root_index) = root_index {
self.local_matrices[root_index].translate(FGVec3::ZERO);
self.model_matrices.fill(None);
for index in 0..local.len() {
resolve_model_transform(index, parents, &self.local_matrices, &mut self.model_matrices)
.expect("baked bone hierarchy should be valid");
}
}
self.matrices.clear();
self.matrices.extend(
self.model_matrices.iter().map(|matrix| {
@@ -374,15 +409,12 @@ impl FighterBoneMatrices {
self.root_motion_delta = if advance_history
&& !animation_changed
&& self.previous_animation_frame.is_some()
&& !root_motion_bone.is_empty()
&& let (Some(current), Some(previous)) = (
raw_root_motion_translation,
self.previous_root_motion_translation,
)
{
let root_index = self
.bone_names
.iter()
.position(|name| name == root_motion_bone)
.expect("configured root motion bone should exist");
let mut delta = self.matrices[root_index].get_translation()
- self.previous_frame_matrices[root_index].get_translation();
let mut delta = current - previous;
if animation_frame.repeat
&& self.previous_animation_frame.is_some_and(|previous| {
previous.frame.saturating_add(1) == animation_frame.frame
@@ -395,6 +427,7 @@ impl FighterBoneMatrices {
} else {
FGVec3::ZERO
};
self.previous_root_motion_translation = raw_root_motion_translation;
if advance_history {
self.previous_animation_frame = Some(animation_frame);
}