Improve hitbox determinisim and performance.

This commit is contained in:
2026-09-12 16:48:16 +02:00
parent 6e01147125
commit 50f0371b54
7 changed files with 348 additions and 57 deletions
+70 -16
View File
@@ -6,7 +6,8 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
fighter::{ fighter::{
Fighter, FighterAttributes, FighterFacingDirection, FighterHitboxes, FighterTranslation, Fighter, FighterAttributes, FighterFacingDirection, FighterHitboxes,
FighterPreviousFacingDirection, FighterPreviousTranslation, FighterTranslation,
animation::{AnimKind, FighterAnimationFrame}, animation::{AnimKind, FighterAnimationFrame},
baked_animation::{FighterBoneMatrices, FixedMat4}, baked_animation::{FighterBoneMatrices, FixedMat4},
hurtbox::{FixedAffineCapsule, FixedCapsule}, hurtbox::{FixedAffineCapsule, FixedCapsule},
@@ -16,6 +17,7 @@ use crate::{
int::{FGi32, FGi32Ext}, int::{FGi32, FGi32Ext},
vec3::FGVec3, vec3::FGVec3,
}, },
player::Player,
schedule::GameplaySet, schedule::GameplaySet,
scripting::FighterAttackScript, scripting::FighterAttackScript,
}; };
@@ -212,19 +214,32 @@ pub fn solve_hitboxes(
mut query: Query<( mut query: Query<(
&FighterBoneMatrices, &FighterBoneMatrices,
&FighterTranslation, &FighterTranslation,
&FighterPreviousTranslation,
&FighterFacingDirection, &FighterFacingDirection,
&FighterPreviousFacingDirection,
&mut FighterHitboxes, &mut FighterHitboxes,
&FighterAnimationFrame, &FighterAnimationFrame,
)>, )>,
attack_scripts: Res<Assets<FighterAttackScript>>, attack_scripts: Res<Assets<FighterAttackScript>>,
) { ) {
for (matrices, translation, facing_direction, mut hitboxes, animation_frame) in &mut query { for (
matrices,
translation,
previous_translation,
facing_direction,
previous_facing_direction,
mut hitboxes,
animation_frame,
) in &mut query
{
hitboxes.active_hitboxes_solved.clear(); hitboxes.active_hitboxes_solved.clear();
if let Some(ref attack_script) = hitboxes.attack_script { if let Some(ref attack_script) = hitboxes.attack_script {
let attack_script = attack_scripts let attack_script = attack_scripts
.get(attack_script) .get(attack_script)
.expect("Attack script should be valid"); .expect("Attack script should be valid");
let fighter_trf = translation.get_3d_transform(facing_direction); let fighter_trf = translation.get_3d_transform(facing_direction);
let previous_fighter_trf = FighterTranslation(previous_translation.0)
.get_3d_transform(&previous_facing_direction.0);
let active_hitboxes = hitboxes.active_hitboxes.clone(); let active_hitboxes = hitboxes.active_hitboxes.clone();
for hitbox_idx in active_hitboxes { for hitbox_idx in active_hitboxes {
let hitbox = attack_script let hitbox = attack_script
@@ -245,18 +260,24 @@ pub fn solve_hitboxes(
if hitbox.start_frame == animation_frame.frame { if hitbox.start_frame == animation_frame.frame {
// Sphere only // Sphere only
hitboxes.active_hitboxes_solved.push(FixedCapsule { hitboxes.active_hitboxes_solved.push(FighterSolvedHitbox {
hitbox_idx,
capsule: FixedCapsule {
start: curr_hbox_pos, start: curr_hbox_pos,
end: curr_hbox_pos, end: curr_hbox_pos,
radius: hitbox.radius, radius: hitbox.radius,
},
}) })
} else { } else {
let prev_hbox_matrix = fighter_trf.mul(prev_bone_matrix); let prev_hbox_matrix = previous_fighter_trf.mul(prev_bone_matrix);
let prev_hbox_pos = prev_hbox_matrix.transform_point(hitbox.offset); let prev_hbox_pos = prev_hbox_matrix.transform_point(hitbox.offset);
hitboxes.active_hitboxes_solved.push(FixedCapsule { hitboxes.active_hitboxes_solved.push(FighterSolvedHitbox {
hitbox_idx,
capsule: FixedCapsule {
start: prev_hbox_pos, start: prev_hbox_pos,
end: curr_hbox_pos, end: curr_hbox_pos,
radius: hitbox.radius, radius: hitbox.radius,
},
}); });
} }
} }
@@ -312,10 +333,18 @@ pub fn solve_hurtboxes(
} }
pub fn intersect_attacks_with_hurtboxes( pub fn intersect_attacks_with_hurtboxes(
query: Query<(Entity, &FighterSolvedHurtboxes, &FighterHitboxes)>, mut query_attacker: Query<(Entity, &mut FighterHitboxes, &Fighter, &Player)>,
query_attacked: Query<(Entity, &FighterSolvedHurtboxes, &Player)>,
attack_scripts: Res<Assets<FighterAttackScript>>, attack_scripts: Res<Assets<FighterAttackScript>>,
) { ) {
/*for (entity_hurting, _, hitboxes) in query { let mut attackers = query_attacker.iter_mut().collect::<Vec<_>>();
attackers
.sort_by(|(_, _, _, player_a), (_, _, _, player_b)| player_a.handle.cmp(&player_b.handle));
let mut receivers = query_attacked.iter().collect::<Vec<_>>();
receivers.sort_by(|(_, _, player_a), (_, _, player_b)| player_a.handle.cmp(&player_b.handle));
for (entity_hurting, mut hitboxes, _, _) in attackers {
if hitboxes.attack_script.is_none() { if hitboxes.attack_script.is_none() {
continue; continue;
} }
@@ -329,18 +358,39 @@ pub fn intersect_attacks_with_hurtboxes(
let attack_script = attack_script.unwrap(); let attack_script = attack_script.unwrap();
let capsules: Vec<(&AttackHitbox, FixedCapsule)> = hitboxes.active_hitboxes.iter().map(|idx| { for (entity_receiving, hurtboxes, _) in receivers.iter() {
(attack_script.hitboxes.get(*idx).unwrap(), ) if entity_hurting == *entity_receiving {
}).collect();
for (entity_to_hurt, hurtboxes, _) in query {
// Self collisions not allowed
if entity_to_hurt == entity_hurting {
continue; continue;
} }
if hitboxes.hit_fighters.contains(entity_receiving) {
continue;
}
let mut hitting_hitboxes = vec![];
for hurtbox in hurtboxes.0.iter() {
for solved_hitbox in hitboxes.active_hitboxes_solved.iter() {
if hurtbox.capsule.intersects(solved_hitbox.capsule) {
hitting_hitboxes.push(solved_hitbox.hitbox_idx);
}
}
}
hitting_hitboxes.sort_by(|a, b| {
let hitbox_id_a = attack_script.hitboxes[*a].id;
let hitbox_id_b = attack_script.hitboxes[*b].id;
hitbox_id_a.cmp(&hitbox_id_b)
});
if hitting_hitboxes.is_empty() {
continue;
}
let hitting_hitbox_idx = hitting_hitboxes[0];
hitboxes.hit_fighters.push(*entity_receiving);
info!("Hit by hitbox {}!!!!", hitting_hitbox_idx);
}
} }
} }
}*/
} }
pub struct FighterAttackPlugin; pub struct FighterAttackPlugin;
@@ -349,7 +399,11 @@ impl Plugin for FighterAttackPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems( app.add_systems(
GgrsSchedule, GgrsSchedule,
(solve_hurtboxes, solve_hitboxes) (
solve_hurtboxes,
solve_hitboxes,
intersect_attacks_with_hurtboxes,
)
.chain() .chain()
.after(crate::fighter::baked_animation::update_fighter_bone_matrices) .after(crate::fighter::baked_animation::update_fighter_bone_matrices)
.in_set(GameplaySet::Animation), .in_set(GameplaySet::Animation),
+156 -24
View File
@@ -3,7 +3,6 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
io, io,
path::Path,
}; };
use bevy::{ use bevy::{
@@ -18,11 +17,12 @@ use bevy::{
reflect::TypePath, reflect::TypePath,
world_serialization::{WorldInstanceReady, WorldInstanceSpawner}, world_serialization::{WorldInstanceReady, WorldInstanceSpawner},
}; };
use bevy_ggrs::prelude::GgrsSchedule; use bevy_ggrs::{GgrsFrameTiming, prelude::GgrsSchedule};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
fighter::{ fighter::{
Fighter,
animation::{AnimKind, FighterAnimationFrame}, animation::{AnimKind, FighterAnimationFrame},
manifest::FighterManifest, manifest::FighterManifest,
visual::FighterAnimations, visual::FighterAnimations,
@@ -194,7 +194,10 @@ impl BakedFighterAnimations {
pub fn sample_pose(&self, frame: &FighterAnimationFrame) -> Option<&[FixedMat4]> { pub fn sample_pose(&self, frame: &FighterAnimationFrame) -> Option<&[FixedMat4]> {
let frames = &self.clip(frame.kind)?.frames; let frames = &self.clip(frame.kind)?.frames;
let frame_index = if frame.repeat { let frame_index = if frame.repeat {
frame.frame % frames.len() as u32 // The terminal sample is retained for one-shot animations, but is
// the same point in time as the next loop's first sample.
let loop_frame_count = frames.len().saturating_sub(1).max(1) as u32;
frame.frame % loop_frame_count
} else { } else {
frame.frame frame.frame
}; };
@@ -224,6 +227,7 @@ pub struct FighterBoneMatrices {
matrices: Vec<FixedMat4>, matrices: Vec<FixedMat4>,
previous_frame_matrices: Vec<FixedMat4>, previous_frame_matrices: Vec<FixedMat4>,
local_matrices: Vec<FixedMat4>, local_matrices: Vec<FixedMat4>,
previous_local_matrices: Vec<FixedMat4>,
} }
#[derive(Component)] #[derive(Component)]
@@ -263,7 +267,23 @@ impl FighterBoneMatrices {
self.local_matrices.get(index).copied() self.local_matrices.get(index).copied()
} }
fn set_pose(&mut self, bone_names: &[String], parents: &[Option<usize>], local: &[FixedMat4]) { fn presentation_local(&self, index: usize, overstep_fraction: f32) -> Option<Transform> {
let current = self.local_matrices.get(index).copied()?;
let previous = self
.previous_local_matrices
.get(index)
.copied()
.unwrap_or(current);
Some(interpolate_transform(previous, current, overstep_fraction))
}
fn set_pose(
&mut self,
bone_names: &[String],
parents: &[Option<usize>],
local: &[FixedMat4],
model_scale: FGi32,
) {
assert_eq!( assert_eq!(
bone_names.len(), bone_names.len(),
local.len(), local.len(),
@@ -282,6 +302,7 @@ impl FighterBoneMatrices {
assert_eq!(self.parents, parents, "baked bone parents 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);
std::mem::swap(&mut self.local_matrices, &mut self.previous_local_matrices);
self.local_matrices.clear(); self.local_matrices.clear();
self.local_matrices.extend_from_slice(local); self.local_matrices.extend_from_slice(local);
let mut model = vec![None; local.len()]; let mut model = vec![None; local.len()];
@@ -289,9 +310,17 @@ impl FighterBoneMatrices {
resolve_model_transform(index, parents, local, &mut model) resolve_model_transform(index, parents, local, &mut model)
.expect("baked bone hierarchy should be valid"); .expect("baked bone hierarchy should be valid");
} }
let model_scale = FixedMat4 {
cols: [
[model_scale, FGi32::ZERO, FGi32::ZERO, FGi32::ZERO],
[FGi32::ZERO, model_scale, FGi32::ZERO, FGi32::ZERO],
[FGi32::ZERO, FGi32::ZERO, model_scale, FGi32::ZERO],
[FGi32::ZERO, FGi32::ZERO, FGi32::ZERO, FGi32::ONE],
],
};
self.matrices = model self.matrices = model
.into_iter() .into_iter()
.map(|matrix| matrix.expect("baked bone hierarchy should be valid")) .map(|matrix| model_scale.mul(matrix.expect("baked bone hierarchy should be valid")))
.collect(); .collect();
} }
} }
@@ -300,18 +329,32 @@ pub fn update_fighter_bone_matrices(
mut fighters: Query<( mut fighters: Query<(
&FighterAnimationFrame, &FighterAnimationFrame,
&FighterAnimations, &FighterAnimations,
&Fighter,
&mut FighterBoneMatrices, &mut FighterBoneMatrices,
)>, )>,
baked_assets: Res<Assets<BakedFighterAnimations>>, baked_assets: Res<Assets<BakedFighterAnimations>>,
manifests: Res<Assets<FighterManifest>>,
) { ) {
for (frame, animations, mut matrices) in &mut fighters { for (frame, animations, fighter, mut matrices) in &mut fighters {
let baked = baked_assets let baked = baked_assets
.get(&animations.baked) .get(&animations.baked)
.expect("fighter baked animation asset should be loaded before the match starts"); .expect("fighter baked animation asset should be loaded before the match starts");
assert_eq!(
baked.frame_rate, BAKED_ANIMATION_FPS,
"fighter baked animation rate does not match the simulation rate"
);
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, &baked.parents, pose); let manifest = manifests
.get(&fighter.manifest)
.expect("fighter manifest should be loaded before the match starts");
matrices.set_pose(
&baked.bone_names,
&baked.parents,
pose,
manifest.model_scale,
);
} }
} }
@@ -403,15 +446,30 @@ fn bind_fighter_visual_bones(
fn apply_baked_fighter_visual_pose( fn apply_baked_fighter_visual_pose(
mut nodes: Query<(&mut Transform, &BakedFighterBone)>, mut nodes: Query<(&mut Transform, &BakedFighterBone)>,
fighters: Query<&FighterBoneMatrices>, fighters: Query<&FighterBoneMatrices>,
timing: Res<GgrsFrameTiming>,
) { ) {
let overstep_fraction = timing.overstep_fraction();
for (mut transform, bone) in &mut nodes { for (mut transform, bone) in &mut nodes {
let Ok(matrices) = fighters.get(bone.fighter) else { let Ok(matrices) = fighters.get(bone.fighter) else {
continue; continue;
}; };
let Some(local) = matrices.local(bone.index) else { let Some(local) = matrices.presentation_local(bone.index, overstep_fraction) else {
continue; continue;
}; };
*transform = Transform::from_matrix(local.to_mat4()); *transform = local;
}
}
fn interpolate_transform(previous: FixedMat4, current: FixedMat4, factor: f32) -> Transform {
let (previous_scale, previous_rotation, previous_translation) =
previous.to_mat4().to_scale_rotation_translation();
let (current_scale, current_rotation, current_translation) =
current.to_mat4().to_scale_rotation_translation();
let factor = factor.clamp(0.0, 1.0);
Transform {
translation: previous_translation.lerp(current_translation, factor),
rotation: previous_rotation.slerp(current_rotation, factor),
scale: previous_scale.lerp(current_scale, factor),
} }
} }
@@ -452,10 +510,10 @@ impl AssetLoader for FighterBakeSourceLoader {
let manifest: FighterManifest = ron::de::from_bytes(&manifest_bytes) let manifest: FighterManifest = ron::de::from_bytes(&manifest_bytes)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?; .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
// The processor's AssetServer reads processed assets. Reading this let model_bytes = load_context
// source dependency through it would wait for a processed GLB while .read_asset_bytes(&manifest.model_path)
// this processor is still producing the first processed asset. .await
let model_bytes = std::fs::read(Path::new("assets").join(&manifest.model_path))?; .map_err(|error| io::Error::other(error.to_string()))?;
Ok(FighterBakeSource { Ok(FighterBakeSource {
model_bytes, model_bytes,
animations: manifest.animations, animations: manifest.animations,
@@ -616,10 +674,10 @@ fn bake_gltf(
.iter() .iter()
.filter_map(Channel::duration) .filter_map(Channel::duration)
.fold(0.0_f32, f32::max); .fold(0.0_f32, f32::max);
let frame_count = (duration * BAKED_ANIMATION_FPS as f32).ceil() as usize + 1; let frame_count = baked_frame_count(duration, BAKED_ANIMATION_FPS);
let mut frames = Vec::with_capacity(frame_count); let mut frames = Vec::with_capacity(frame_count);
for frame in 0..frame_count { for frame in 0..frame_count {
let time = frame as f32 / BAKED_ANIMATION_FPS as f32; let time = (frame as f32 / BAKED_ANIMATION_FPS as f32).min(duration);
let local = nodes let local = nodes
.iter() .iter()
.map(|node| local_transform(node.clone(), &channels, time)) .map(|node| local_transform(node.clone(), &channels, time))
@@ -658,6 +716,17 @@ fn bake_gltf(
}) })
} }
fn baked_frame_count(duration: f32, frame_rate: u32) -> usize {
let frame = duration as f64 * frame_rate as f64;
let nearest_frame = frame.round();
let last_frame = if (frame - nearest_frame).abs() < 0.0001 {
nearest_frame
} else {
frame.ceil()
};
last_frame as usize + 1
}
fn resolve_model_transform( fn resolve_model_transform(
index: usize, index: usize,
parents: &[Option<usize>], parents: &[Option<usize>],
@@ -829,18 +898,19 @@ mod tests {
let manifest: FighterManifest = let manifest: FighterManifest =
ron::from_str(include_str!("../../assets/fighters/test/test.fighter.ron")).unwrap(); ron::from_str(include_str!("../../assets/fighters/test/test.fighter.ron")).unwrap();
let baked = bake_gltf( let baked = bake_gltf(
include_bytes!("../../assets/fighters/test/test-fighter-normal.glb"), include_bytes!("../../assets/fighters/test/test-fighter-2.glb"),
&manifest.animations, &manifest.animations,
) )
.unwrap(); .unwrap();
assert_eq!(baked.frame_rate, BAKED_ANIMATION_FPS); assert_eq!(baked.frame_rate, BAKED_ANIMATION_FPS);
assert!(baked.frame_count(AnimKind::Wait).unwrap() > 1); assert_eq!(baked.frame_count(AnimKind::Wait), Some(121));
assert_eq!(baked.frame_count(AnimKind::Walk), Some(33));
assert_eq!(baked.frame_count(AnimKind::Run), Some(33));
assert_eq!(baked.frame_count(AnimKind::Dash), Some(21));
assert_eq!(baked.frame_count(AnimKind::Landing), Some(9));
assert_eq!(baked.frame_count(AnimKind::JumpBack), Some(57));
assert_eq!(baked.clip_indices.len(), manifest.animations.len()); assert_eq!(baked.clip_indices.len(), manifest.animations.len());
assert_eq!(
baked.clip_indices[&AnimKind::JumpSquat],
baked.clip_indices[&AnimKind::Landing]
);
assert_eq!( assert_eq!(
baked.clip(AnimKind::Wait).unwrap().frames[0].len(), baked.clip(AnimKind::Wait).unwrap().frames[0].len(),
baked.bone_names.len() baked.bone_names.len()
@@ -853,12 +923,14 @@ mod tests {
let first = FixedMat4::IDENTITY; let first = FixedMat4::IDENTITY;
let mut second = FixedMat4::IDENTITY; let mut second = FixedMat4::IDENTITY;
second.translate(FGVec3::lit("2", "0", "0")); second.translate(FGVec3::lit("2", "0", "0"));
let mut terminal = FixedMat4::IDENTITY;
terminal.translate(FGVec3::lit("4", "0", "0"));
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], parents: vec![None],
clips: vec![BakedAnimationClip { clips: vec![BakedAnimationClip {
frames: vec![vec![first], vec![second]], frames: vec![vec![first], vec![second], vec![terminal]],
}], }],
clip_indices: HashMap::from([(AnimKind::Wait, 0)]), clip_indices: HashMap::from([(AnimKind::Wait, 0)]),
}; };
@@ -870,12 +942,48 @@ mod tests {
}; };
let finished = FighterAnimationFrame { let finished = FighterAnimationFrame {
kind: AnimKind::Wait, kind: AnimKind::Wait,
frame: 3, frame: 4,
repeat: false, repeat: false,
}; };
assert_eq!(baked.sample_pose(&repeating), Some(&[second][..])); assert_eq!(baked.sample_pose(&repeating), Some(&[second][..]));
assert_eq!(baked.sample_pose(&finished), Some(&[second][..])); assert_eq!(baked.sample_pose(&finished), Some(&[terminal][..]));
}
#[test]
fn frame_count_snaps_source_frame_rounding_error_to_the_bake_grid() {
assert_eq!(baked_frame_count(0.533333361, 60), 33);
assert_eq!(baked_frame_count(0.333333343, 60), 21);
assert_eq!(baked_frame_count(0.133333340, 60), 9);
assert_eq!(baked_frame_count(2.0, 60), 121);
}
#[test]
fn linear_source_channel_is_resampled_between_thirty_hz_keys() {
let channel = Channel {
node: 0,
property: gltf::animation::Property::Translation,
interpolation: gltf::animation::Interpolation::Linear,
times: vec![0.0, 1.0 / 30.0],
values: ChannelValues::Vec3(vec![[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]]),
};
let ChannelValues::Vec3(values) = &channel.values else {
unreachable!()
};
assert_eq!(sample_vec3(&channel, values, 1.0 / 60.0), [1.0, 0.0, 0.0]);
}
#[test]
fn presentation_pose_interpolates_previous_and_current_local_transforms() {
let mut previous = FixedMat4::IDENTITY;
previous.translate(FGVec3::lit("2", "0", "0"));
let mut current = FixedMat4::IDENTITY;
current.translate(FGVec3::lit("6", "0", "0"));
let transform = interpolate_transform(previous, current, 0.25);
assert_eq!(transform.translation, Vec3::new(3.0, 0.0, 0.0));
} }
#[test] #[test]
@@ -888,6 +996,7 @@ mod tests {
&["root".into(), "hand".into()], &["root".into(), "hand".into()],
&[None, Some(0)], &[None, Some(0)],
&[first, second], &[first, second],
FGi32::ONE,
); );
assert_eq!(matrices.get_current("hand"), Some(second)); assert_eq!(matrices.get_current("hand"), Some(second));
@@ -909,6 +1018,7 @@ mod tests {
&["root".into(), "hand".into()], &["root".into(), "hand".into()],
&[None, Some(0)], &[None, Some(0)],
&[root, hand], &[root, hand],
FGi32::ONE,
); );
assert_eq!(matrices.local(1), Some(hand)); assert_eq!(matrices.local(1), Some(hand));
@@ -926,6 +1036,28 @@ mod tests {
); );
} }
#[test]
fn model_scale_affects_gameplay_bones_without_changing_visual_locals() {
let mut root = FixedMat4::IDENTITY;
root.translate(FGVec3::lit("1", "0", "0"));
let mut hand = FixedMat4::IDENTITY;
hand.translate(FGVec3::lit("0", "2", "0"));
let mut matrices = FighterBoneMatrices::default();
matrices.set_pose(
&["root".into(), "hand".into()],
&[None, Some(0)],
&[root, hand],
FGi32::lit("10"),
);
assert_eq!(matrices.local(1), Some(hand));
assert_eq!(
matrices.get_current("hand").unwrap().get_translation(),
FGVec3::lit("10", "20", "0")
);
}
#[test] #[test]
fn fixed_matrix_transforms_points() { fn fixed_matrix_transforms_points() {
let transform = FixedMat4 { let transform = FixedMat4 {
+5 -4
View File
@@ -341,14 +341,15 @@ pub fn attack_debug(
for (hitboxes, matrices, translation, facing_direction, frame) in query { for (hitboxes, matrices, translation, facing_direction, frame) in query {
if let Some(script) = &hitboxes.attack_script { if let Some(script) = &hitboxes.attack_script {
for solved_hitbox in hitboxes.active_hitboxes_solved.iter() { for solved_hitbox in hitboxes.active_hitboxes_solved.iter() {
let start = solved_hitbox.start.to_vec3(); let start = solved_hitbox.capsule.start.to_vec3();
let end = solved_hitbox.end.to_vec3(); let end = solved_hitbox.capsule.end.to_vec3();
let center = (solved_hitbox.start + solved_hitbox.end) * FGi32::lit("0.5"); let center =
(solved_hitbox.capsule.start + solved_hitbox.capsule.end) * FGi32::lit("0.5");
let length = (start - end).length(); let length = (start - end).length();
let attack_capsule = Capsule3d { let attack_capsule = Capsule3d {
half_length: length * 0.5, half_length: length * 0.5,
radius: solved_hitbox.radius.to_num(), radius: solved_hitbox.capsule.radius.to_num(),
}; };
let aim_dir = (end - start).normalize_or(Vec3::Y); let aim_dir = (end - start).normalize_or(Vec3::Y);
+20 -5
View File
@@ -26,13 +26,18 @@ pub mod visual;
pub use attributes::FighterAttributes; pub use attributes::FighterAttributes;
pub use ecb::FighterECB; pub use ecb::FighterECB;
pub use manifest::FighterId; pub use manifest::FighterId;
pub use motion::{FighterPreviousTranslation, FighterTranslation, FighterVelocity, Grounded}; pub use motion::{
FighterPreviousFacingDirection, FighterPreviousTranslation, FighterTranslation,
FighterVelocity, Grounded,
};
use crate::{ use crate::{
fighter::{ fighter::{
attack::{FighterAttackPlugin, FighterAttackScriptAssets, FighterSolvedHurtboxes}, attack::{
FighterAttackPlugin, FighterAttackScriptAssets, FighterSolvedHitboxes,
FighterSolvedHurtboxes,
},
baked_animation::FighterBoneMatrices, baked_animation::FighterBoneMatrices,
hurtbox::FixedCapsule,
manifest::FighterManifest, manifest::FighterManifest,
state::{FighterState, fall::FallState}, state::{FighterState, fall::FallState},
visual::FighterAnimations, visual::FighterAnimations,
@@ -113,7 +118,8 @@ impl Plugin for FighterPlugin {
.add_systems( .add_systems(
GgrsSchedule, GgrsSchedule,
( (
ecb::snapshot_fighter_ecb.before(state_interrupt_system), (ecb::snapshot_fighter_ecb, motion::snapshot_fighter_motion)
.before(state_interrupt_system),
animation::advance_fighter_animation_frames.before(state_interrupt_system), animation::advance_fighter_animation_frames.before(state_interrupt_system),
state::state_interrupt_system, state::state_interrupt_system,
) )
@@ -155,6 +161,8 @@ impl Plugin for FighterPlugin {
.checksum_component_with_hash::<FighterTranslation>() .checksum_component_with_hash::<FighterTranslation>()
.rollback_component_with_copy::<FighterPreviousTranslation>() .rollback_component_with_copy::<FighterPreviousTranslation>()
.checksum_component_with_hash::<FighterPreviousTranslation>() .checksum_component_with_hash::<FighterPreviousTranslation>()
.rollback_component_with_copy::<FighterPreviousFacingDirection>()
.checksum_component_with_hash::<FighterPreviousFacingDirection>()
.rollback_component_with_copy::<animation::FighterAnimationFrame>() .rollback_component_with_copy::<animation::FighterAnimationFrame>()
.checksum_component_with_hash::<animation::FighterAnimationFrame>() .checksum_component_with_hash::<animation::FighterAnimationFrame>()
.rollback_component_with_copy::<Grounded>() .rollback_component_with_copy::<Grounded>()
@@ -198,13 +206,15 @@ impl Plugin for FighterPlugin {
pub struct FighterHitboxes { pub struct FighterHitboxes {
pub attack_script: Option<Handle<FighterAttackScript>>, pub attack_script: Option<Handle<FighterAttackScript>>,
pub active_hitboxes: Vec<usize>, pub active_hitboxes: Vec<usize>,
pub active_hitboxes_solved: Vec<FixedCapsule>, pub active_hitboxes_solved: Vec<attack::FighterSolvedHitbox>,
pub hit_fighters: Vec<Entity>,
} }
impl FighterHitboxes { impl FighterHitboxes {
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.attack_script = None; self.attack_script = None;
self.active_hitboxes.clear(); self.active_hitboxes.clear();
self.hit_fighters.clear();
} }
} }
@@ -213,6 +223,7 @@ pub fn spawn_fighter(
player_handle: usize, player_handle: usize,
spawn_position: FGVec2, spawn_position: FGVec2,
manifest: Handle<FighterManifest>, manifest: Handle<FighterManifest>,
model_scale: FGi32,
animations: FighterAnimations, animations: FighterAnimations,
attack_scripts: FighterAttackScriptAssets, attack_scripts: FighterAttackScriptAssets,
visual_root: WorldAssetRoot, visual_root: WorldAssetRoot,
@@ -224,6 +235,8 @@ pub fn spawn_fighter(
}, },
Fighter { manifest: manifest }, Fighter { manifest: manifest },
FighterFacingDirection::Right, FighterFacingDirection::Right,
FighterPreviousFacingDirection(FighterFacingDirection::Right),
Transform::from_scale(Vec3::splat(model_scale.to_num())),
( (
FighterECB { FighterECB {
vertical_half: FGi32::lit("5.0"), vertical_half: FGi32::lit("5.0"),
@@ -233,6 +246,7 @@ pub fn spawn_fighter(
attack_script: None, attack_script: None,
active_hitboxes: vec![], active_hitboxes: vec![],
active_hitboxes_solved: vec![], active_hitboxes_solved: vec![],
hit_fighters: vec![],
}, },
), ),
( (
@@ -248,6 +262,7 @@ pub fn spawn_fighter(
( (
baked_animation::FighterBoneMatrices::default(), baked_animation::FighterBoneMatrices::default(),
FighterSolvedHurtboxes::default(), FighterSolvedHurtboxes::default(),
FighterSolvedHitboxes::default(),
FighterVisual, FighterVisual,
FighterInput::default(), FighterInput::default(),
crate::camera::FighterCameraExtents::default(), crate::camera::FighterCameraExtents::default(),
+47
View File
@@ -10,6 +10,9 @@ use crate::{
math::{int::FGi32, vec::FGVec2, vec3::FGVec3}, math::{int::FGi32, vec::FGVec2, vec3::FGVec3},
}; };
#[derive(Component, Clone, Copy, Hash)]
pub struct FighterPreviousFacingDirection(pub FighterFacingDirection);
#[derive(Component, Clone, Copy, Hash)] #[derive(Component, Clone, Copy, Hash)]
pub struct Grounded; pub struct Grounded;
@@ -100,6 +103,20 @@ pub fn copy_fighter_transform_to_visuals(fighters: Query<(&FighterTranslation, &
pub fn fighter_movement() {} pub fn fighter_movement() {}
pub fn snapshot_fighter_motion(
query: Query<(
&FighterTranslation,
&FighterFacingDirection,
&mut FighterPreviousTranslation,
&mut FighterPreviousFacingDirection,
)>,
) {
for (translation, facing, mut previous_translation, mut previous_facing) in query {
previous_translation.0 = translation.0;
previous_facing.0 = *facing;
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -127,4 +144,34 @@ mod tests {
assert_eq!(position, Vec2::new(7.0, 14.0)); assert_eq!(position, Vec2::new(7.0, 14.0));
} }
#[test]
fn snapshot_fighter_motion_copies_current_world_state() {
let mut world = World::new();
world.spawn((
FighterTranslation(FGVec2::new(FGi32::lit("3"), FGi32::lit("4"))),
FighterFacingDirection::Left,
FighterPreviousTranslation::default(),
FighterPreviousFacingDirection(FighterFacingDirection::Right),
));
let mut schedule = Schedule::default();
schedule.add_systems(snapshot_fighter_motion);
schedule.run(&mut world);
let (_, _, previous_translation, previous_facing) = world
.query::<(
&FighterTranslation,
&FighterFacingDirection,
&FighterPreviousTranslation,
&FighterPreviousFacingDirection,
)>()
.single(&world)
.unwrap();
assert_eq!(
previous_translation.0,
FGVec2::new(FGi32::lit("3"), FGi32::lit("4"))
);
assert_eq!(previous_facing.0, FighterFacingDirection::Left);
}
} }
+4 -1
View File
@@ -43,7 +43,10 @@ impl LocalInputAssignments {
} }
pub fn get_from_slot(&self, slot: usize) -> Option<&LocalInputSource> { pub fn get_from_slot(&self, slot: usize) -> Option<&LocalInputSource> {
self.0.iter().find(|(c_slot, _)| slot == *c_slot).map(|(_, source)| source) self.0
.iter()
.find(|(c_slot, _)| slot == *c_slot)
.map(|(_, source)| source)
} }
} }
+40 -1
View File
@@ -87,11 +87,12 @@ impl MoveCompiler {
fn into_attack_script(mut self) -> Result<FighterAttackScript, String> { fn into_attack_script(mut self) -> Result<FighterAttackScript, String> {
self.remove_all_hitboxes().map_err(|err| err.to_string())?; self.remove_all_hitboxes().map_err(|err| err.to_string())?;
let hitboxes = self let mut hitboxes: Vec<_> = self
.hitboxes .hitboxes
.into_iter() .into_iter()
.map(|(_, hitbox)| hitbox) .map(|(_, hitbox)| hitbox)
.collect(); .collect();
hitboxes.sort_unstable_by_key(|hitbox| hitbox.id);
let iasa_frame = self.iasa_frame.unwrap_or(100000); let iasa_frame = self.iasa_frame.unwrap_or(100000);
let wont_autocancel_window = self.wont_autocancel_window.unwrap_or((0, 100000)); let wont_autocancel_window = self.wont_autocancel_window.unwrap_or((0, 100000));
Ok(FighterAttackScript { Ok(FighterAttackScript {
@@ -242,3 +243,41 @@ pub fn compile_script(text: &str) -> Result<FighterAttackScript, String> {
}) })
.map_err(|err| err.to_string())? .map_err(|err| err.to_string())?
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compiled_hitboxes_are_sorted_by_authored_id() {
let script = compile_script(
r#"
frame(1);
hitbox(20, "hand_l", damage("1"), vec3("0", "0", "0"), fg32("1"), angle(fg32("0")), knockback_normal(), knockback("1"), fg32("1"));
frame(2);
hitbox(3, "hand_l", damage("2"), vec3("0", "0", "0"), fg32("1"), angle(fg32("0")), knockback_normal(), knockback("1"), fg32("1"));
frame(3);
hitbox(10, "hand_l", damage("3"), vec3("0", "0", "0"), fg32("1"), angle(fg32("0")), knockback_normal(), knockback("1"), fg32("1"));
frame(4);
"#,
)
.unwrap();
assert_eq!(
script
.hitboxes
.iter()
.map(|hitbox| hitbox.id)
.collect::<Vec<_>>(),
vec![3, 10, 20]
);
assert_eq!(
script
.hitboxes
.iter()
.map(|hitbox| hitbox.start_frame)
.collect::<Vec<_>>(),
vec![2, 3, 1]
);
}
}