Improve hitbox determinisim and performance.
This commit is contained in:
+70
-16
@@ -6,7 +6,8 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
fighter::{
|
||||
Fighter, FighterAttributes, FighterFacingDirection, FighterHitboxes, FighterTranslation,
|
||||
Fighter, FighterAttributes, FighterFacingDirection, FighterHitboxes,
|
||||
FighterPreviousFacingDirection, FighterPreviousTranslation, FighterTranslation,
|
||||
animation::{AnimKind, FighterAnimationFrame},
|
||||
baked_animation::{FighterBoneMatrices, FixedMat4},
|
||||
hurtbox::{FixedAffineCapsule, FixedCapsule},
|
||||
@@ -16,6 +17,7 @@ use crate::{
|
||||
int::{FGi32, FGi32Ext},
|
||||
vec3::FGVec3,
|
||||
},
|
||||
player::Player,
|
||||
schedule::GameplaySet,
|
||||
scripting::FighterAttackScript,
|
||||
};
|
||||
@@ -212,19 +214,32 @@ pub fn solve_hitboxes(
|
||||
mut query: Query<(
|
||||
&FighterBoneMatrices,
|
||||
&FighterTranslation,
|
||||
&FighterPreviousTranslation,
|
||||
&FighterFacingDirection,
|
||||
&FighterPreviousFacingDirection,
|
||||
&mut FighterHitboxes,
|
||||
&FighterAnimationFrame,
|
||||
)>,
|
||||
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();
|
||||
if let Some(ref attack_script) = hitboxes.attack_script {
|
||||
let attack_script = attack_scripts
|
||||
.get(attack_script)
|
||||
.expect("Attack script should be valid");
|
||||
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();
|
||||
for hitbox_idx in active_hitboxes {
|
||||
let hitbox = attack_script
|
||||
@@ -245,18 +260,24 @@ pub fn solve_hitboxes(
|
||||
|
||||
if hitbox.start_frame == animation_frame.frame {
|
||||
// Sphere only
|
||||
hitboxes.active_hitboxes_solved.push(FixedCapsule {
|
||||
hitboxes.active_hitboxes_solved.push(FighterSolvedHitbox {
|
||||
hitbox_idx,
|
||||
capsule: FixedCapsule {
|
||||
start: curr_hbox_pos,
|
||||
end: curr_hbox_pos,
|
||||
radius: hitbox.radius,
|
||||
},
|
||||
})
|
||||
} 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);
|
||||
hitboxes.active_hitboxes_solved.push(FixedCapsule {
|
||||
hitboxes.active_hitboxes_solved.push(FighterSolvedHitbox {
|
||||
hitbox_idx,
|
||||
capsule: FixedCapsule {
|
||||
start: prev_hbox_pos,
|
||||
end: curr_hbox_pos,
|
||||
radius: hitbox.radius,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -312,10 +333,18 @@ pub fn solve_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>>,
|
||||
) {
|
||||
/*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() {
|
||||
continue;
|
||||
}
|
||||
@@ -329,18 +358,39 @@ pub fn intersect_attacks_with_hurtboxes(
|
||||
|
||||
let attack_script = attack_script.unwrap();
|
||||
|
||||
let capsules: Vec<(&AttackHitbox, FixedCapsule)> = hitboxes.active_hitboxes.iter().map(|idx| {
|
||||
(attack_script.hitboxes.get(*idx).unwrap(), )
|
||||
}).collect();
|
||||
|
||||
for (entity_to_hurt, hurtboxes, _) in query {
|
||||
// Self collisions not allowed
|
||||
if entity_to_hurt == entity_hurting {
|
||||
for (entity_receiving, hurtboxes, _) in receivers.iter() {
|
||||
if entity_hurting == *entity_receiving {
|
||||
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;
|
||||
@@ -349,7 +399,11 @@ impl Plugin for FighterAttackPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(
|
||||
GgrsSchedule,
|
||||
(solve_hurtboxes, solve_hitboxes)
|
||||
(
|
||||
solve_hurtboxes,
|
||||
solve_hitboxes,
|
||||
intersect_attacks_with_hurtboxes,
|
||||
)
|
||||
.chain()
|
||||
.after(crate::fighter::baked_animation::update_fighter_bone_matrices)
|
||||
.in_set(GameplaySet::Animation),
|
||||
|
||||
+156
-24
@@ -3,7 +3,6 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
io,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use bevy::{
|
||||
@@ -18,11 +17,12 @@ use bevy::{
|
||||
reflect::TypePath,
|
||||
world_serialization::{WorldInstanceReady, WorldInstanceSpawner},
|
||||
};
|
||||
use bevy_ggrs::prelude::GgrsSchedule;
|
||||
use bevy_ggrs::{GgrsFrameTiming, prelude::GgrsSchedule};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
fighter::{
|
||||
Fighter,
|
||||
animation::{AnimKind, FighterAnimationFrame},
|
||||
manifest::FighterManifest,
|
||||
visual::FighterAnimations,
|
||||
@@ -194,7 +194,10 @@ impl BakedFighterAnimations {
|
||||
pub fn sample_pose(&self, frame: &FighterAnimationFrame) -> Option<&[FixedMat4]> {
|
||||
let frames = &self.clip(frame.kind)?.frames;
|
||||
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 {
|
||||
frame.frame
|
||||
};
|
||||
@@ -224,6 +227,7 @@ pub struct FighterBoneMatrices {
|
||||
matrices: Vec<FixedMat4>,
|
||||
previous_frame_matrices: Vec<FixedMat4>,
|
||||
local_matrices: Vec<FixedMat4>,
|
||||
previous_local_matrices: Vec<FixedMat4>,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
@@ -263,7 +267,23 @@ impl FighterBoneMatrices {
|
||||
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!(
|
||||
bone_names.len(),
|
||||
local.len(),
|
||||
@@ -282,6 +302,7 @@ impl FighterBoneMatrices {
|
||||
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.local_matrices, &mut self.previous_local_matrices);
|
||||
self.local_matrices.clear();
|
||||
self.local_matrices.extend_from_slice(local);
|
||||
let mut model = vec![None; local.len()];
|
||||
@@ -289,9 +310,17 @@ impl FighterBoneMatrices {
|
||||
resolve_model_transform(index, parents, local, &mut model)
|
||||
.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
|
||||
.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();
|
||||
}
|
||||
}
|
||||
@@ -300,18 +329,32 @@ pub fn update_fighter_bone_matrices(
|
||||
mut fighters: Query<(
|
||||
&FighterAnimationFrame,
|
||||
&FighterAnimations,
|
||||
&Fighter,
|
||||
&mut FighterBoneMatrices,
|
||||
)>,
|
||||
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
|
||||
.get(&animations.baked)
|
||||
.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
|
||||
.sample_pose(frame)
|
||||
.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(
|
||||
mut nodes: Query<(&mut Transform, &BakedFighterBone)>,
|
||||
fighters: Query<&FighterBoneMatrices>,
|
||||
timing: Res<GgrsFrameTiming>,
|
||||
) {
|
||||
let overstep_fraction = timing.overstep_fraction();
|
||||
for (mut transform, bone) in &mut nodes {
|
||||
let Ok(matrices) = fighters.get(bone.fighter) else {
|
||||
continue;
|
||||
};
|
||||
let Some(local) = matrices.local(bone.index) else {
|
||||
let Some(local) = matrices.presentation_local(bone.index, overstep_fraction) else {
|
||||
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)
|
||||
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
|
||||
|
||||
// The processor's AssetServer reads processed assets. Reading this
|
||||
// source dependency through it would wait for a processed GLB while
|
||||
// this processor is still producing the first processed asset.
|
||||
let model_bytes = std::fs::read(Path::new("assets").join(&manifest.model_path))?;
|
||||
let model_bytes = load_context
|
||||
.read_asset_bytes(&manifest.model_path)
|
||||
.await
|
||||
.map_err(|error| io::Error::other(error.to_string()))?;
|
||||
Ok(FighterBakeSource {
|
||||
model_bytes,
|
||||
animations: manifest.animations,
|
||||
@@ -616,10 +674,10 @@ fn bake_gltf(
|
||||
.iter()
|
||||
.filter_map(Channel::duration)
|
||||
.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);
|
||||
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
|
||||
.iter()
|
||||
.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(
|
||||
index: usize,
|
||||
parents: &[Option<usize>],
|
||||
@@ -829,18 +898,19 @@ mod tests {
|
||||
let manifest: FighterManifest =
|
||||
ron::from_str(include_str!("../../assets/fighters/test/test.fighter.ron")).unwrap();
|
||||
let baked = bake_gltf(
|
||||
include_bytes!("../../assets/fighters/test/test-fighter-normal.glb"),
|
||||
include_bytes!("../../assets/fighters/test/test-fighter-2.glb"),
|
||||
&manifest.animations,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
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[&AnimKind::JumpSquat],
|
||||
baked.clip_indices[&AnimKind::Landing]
|
||||
);
|
||||
assert_eq!(
|
||||
baked.clip(AnimKind::Wait).unwrap().frames[0].len(),
|
||||
baked.bone_names.len()
|
||||
@@ -853,12 +923,14 @@ mod tests {
|
||||
let first = FixedMat4::IDENTITY;
|
||||
let mut second = FixedMat4::IDENTITY;
|
||||
second.translate(FGVec3::lit("2", "0", "0"));
|
||||
let mut terminal = FixedMat4::IDENTITY;
|
||||
terminal.translate(FGVec3::lit("4", "0", "0"));
|
||||
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]],
|
||||
frames: vec![vec![first], vec![second], vec![terminal]],
|
||||
}],
|
||||
clip_indices: HashMap::from([(AnimKind::Wait, 0)]),
|
||||
};
|
||||
@@ -870,12 +942,48 @@ mod tests {
|
||||
};
|
||||
let finished = FighterAnimationFrame {
|
||||
kind: AnimKind::Wait,
|
||||
frame: 3,
|
||||
frame: 4,
|
||||
repeat: false,
|
||||
};
|
||||
|
||||
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]
|
||||
@@ -888,6 +996,7 @@ mod tests {
|
||||
&["root".into(), "hand".into()],
|
||||
&[None, Some(0)],
|
||||
&[first, second],
|
||||
FGi32::ONE,
|
||||
);
|
||||
|
||||
assert_eq!(matrices.get_current("hand"), Some(second));
|
||||
@@ -909,6 +1018,7 @@ mod tests {
|
||||
&["root".into(), "hand".into()],
|
||||
&[None, Some(0)],
|
||||
&[root, hand],
|
||||
FGi32::ONE,
|
||||
);
|
||||
|
||||
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]
|
||||
fn fixed_matrix_transforms_points() {
|
||||
let transform = FixedMat4 {
|
||||
|
||||
@@ -341,14 +341,15 @@ pub fn attack_debug(
|
||||
for (hitboxes, matrices, translation, facing_direction, frame) in query {
|
||||
if let Some(script) = &hitboxes.attack_script {
|
||||
for solved_hitbox in hitboxes.active_hitboxes_solved.iter() {
|
||||
let start = solved_hitbox.start.to_vec3();
|
||||
let end = solved_hitbox.end.to_vec3();
|
||||
let center = (solved_hitbox.start + solved_hitbox.end) * FGi32::lit("0.5");
|
||||
let start = solved_hitbox.capsule.start.to_vec3();
|
||||
let end = solved_hitbox.capsule.end.to_vec3();
|
||||
let center =
|
||||
(solved_hitbox.capsule.start + solved_hitbox.capsule.end) * FGi32::lit("0.5");
|
||||
let length = (start - end).length();
|
||||
|
||||
let attack_capsule = Capsule3d {
|
||||
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);
|
||||
|
||||
+20
-5
@@ -26,13 +26,18 @@ pub mod visual;
|
||||
pub use attributes::FighterAttributes;
|
||||
pub use ecb::FighterECB;
|
||||
pub use manifest::FighterId;
|
||||
pub use motion::{FighterPreviousTranslation, FighterTranslation, FighterVelocity, Grounded};
|
||||
pub use motion::{
|
||||
FighterPreviousFacingDirection, FighterPreviousTranslation, FighterTranslation,
|
||||
FighterVelocity, Grounded,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
fighter::{
|
||||
attack::{FighterAttackPlugin, FighterAttackScriptAssets, FighterSolvedHurtboxes},
|
||||
attack::{
|
||||
FighterAttackPlugin, FighterAttackScriptAssets, FighterSolvedHitboxes,
|
||||
FighterSolvedHurtboxes,
|
||||
},
|
||||
baked_animation::FighterBoneMatrices,
|
||||
hurtbox::FixedCapsule,
|
||||
manifest::FighterManifest,
|
||||
state::{FighterState, fall::FallState},
|
||||
visual::FighterAnimations,
|
||||
@@ -113,7 +118,8 @@ impl Plugin for FighterPlugin {
|
||||
.add_systems(
|
||||
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),
|
||||
state::state_interrupt_system,
|
||||
)
|
||||
@@ -155,6 +161,8 @@ impl Plugin for FighterPlugin {
|
||||
.checksum_component_with_hash::<FighterTranslation>()
|
||||
.rollback_component_with_copy::<FighterPreviousTranslation>()
|
||||
.checksum_component_with_hash::<FighterPreviousTranslation>()
|
||||
.rollback_component_with_copy::<FighterPreviousFacingDirection>()
|
||||
.checksum_component_with_hash::<FighterPreviousFacingDirection>()
|
||||
.rollback_component_with_copy::<animation::FighterAnimationFrame>()
|
||||
.checksum_component_with_hash::<animation::FighterAnimationFrame>()
|
||||
.rollback_component_with_copy::<Grounded>()
|
||||
@@ -198,13 +206,15 @@ impl Plugin for FighterPlugin {
|
||||
pub struct FighterHitboxes {
|
||||
pub attack_script: Option<Handle<FighterAttackScript>>,
|
||||
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 {
|
||||
pub fn clear(&mut self) {
|
||||
self.attack_script = None;
|
||||
self.active_hitboxes.clear();
|
||||
self.hit_fighters.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,6 +223,7 @@ pub fn spawn_fighter(
|
||||
player_handle: usize,
|
||||
spawn_position: FGVec2,
|
||||
manifest: Handle<FighterManifest>,
|
||||
model_scale: FGi32,
|
||||
animations: FighterAnimations,
|
||||
attack_scripts: FighterAttackScriptAssets,
|
||||
visual_root: WorldAssetRoot,
|
||||
@@ -224,6 +235,8 @@ pub fn spawn_fighter(
|
||||
},
|
||||
Fighter { manifest: manifest },
|
||||
FighterFacingDirection::Right,
|
||||
FighterPreviousFacingDirection(FighterFacingDirection::Right),
|
||||
Transform::from_scale(Vec3::splat(model_scale.to_num())),
|
||||
(
|
||||
FighterECB {
|
||||
vertical_half: FGi32::lit("5.0"),
|
||||
@@ -233,6 +246,7 @@ pub fn spawn_fighter(
|
||||
attack_script: None,
|
||||
active_hitboxes: vec![],
|
||||
active_hitboxes_solved: vec![],
|
||||
hit_fighters: vec![],
|
||||
},
|
||||
),
|
||||
(
|
||||
@@ -248,6 +262,7 @@ pub fn spawn_fighter(
|
||||
(
|
||||
baked_animation::FighterBoneMatrices::default(),
|
||||
FighterSolvedHurtboxes::default(),
|
||||
FighterSolvedHitboxes::default(),
|
||||
FighterVisual,
|
||||
FighterInput::default(),
|
||||
crate::camera::FighterCameraExtents::default(),
|
||||
|
||||
@@ -10,6 +10,9 @@ use crate::{
|
||||
math::{int::FGi32, vec::FGVec2, vec3::FGVec3},
|
||||
};
|
||||
|
||||
#[derive(Component, Clone, Copy, Hash)]
|
||||
pub struct FighterPreviousFacingDirection(pub FighterFacingDirection);
|
||||
|
||||
#[derive(Component, Clone, Copy, Hash)]
|
||||
pub struct Grounded;
|
||||
|
||||
@@ -100,6 +103,20 @@ pub fn copy_fighter_transform_to_visuals(fighters: Query<(&FighterTranslation, &
|
||||
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -127,4 +144,34 @@ mod tests {
|
||||
|
||||
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
@@ -43,7 +43,10 @@ impl LocalInputAssignments {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,11 +87,12 @@ impl MoveCompiler {
|
||||
|
||||
fn into_attack_script(mut self) -> Result<FighterAttackScript, String> {
|
||||
self.remove_all_hitboxes().map_err(|err| err.to_string())?;
|
||||
let hitboxes = self
|
||||
let mut hitboxes: Vec<_> = self
|
||||
.hitboxes
|
||||
.into_iter()
|
||||
.map(|(_, hitbox)| hitbox)
|
||||
.collect();
|
||||
hitboxes.sort_unstable_by_key(|hitbox| hitbox.id);
|
||||
let iasa_frame = self.iasa_frame.unwrap_or(100000);
|
||||
let wont_autocancel_window = self.wont_autocancel_window.unwrap_or((0, 100000));
|
||||
Ok(FighterAttackScript {
|
||||
@@ -242,3 +243,41 @@ pub fn compile_script(text: &str) -> Result<FighterAttackScript, 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]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user