initial work on hitbox and hurtbox collisions

This commit is contained in:
2026-09-08 11:49:58 +02:00
parent bbd85a12c5
commit 71820a25dd
7 changed files with 271 additions and 116 deletions
+2
View File
@@ -83,4 +83,6 @@ pub enum AnimKind {
// Attacks
AttackJab1,
AttackUpTilt,
AttackForwardTilt,
AttackDownTilt,
}
+116 -13
View File
@@ -6,18 +6,11 @@ use serde::{Deserialize, Serialize};
use crate::{
fighter::{
Fighter, FighterAttributes, FighterFacingDirection, FighterTranslation,
animation::AnimKind,
baked_animation::{FighterBoneMatrices, FixedMat4},
hurtbox::FixedAffineCapsule,
manifest::FighterManifest,
},
math::{
Fighter, FighterAttributes, FighterFacingDirection, FighterHitboxes, FighterTranslation, animation::{AnimKind, FighterAnimationFrame}, baked_animation::{FighterBoneMatrices, FixedMat4}, hurtbox::{FixedAffineCapsule, FixedCapsule}, manifest::FighterManifest,
}, math::{
int::{FGi32, FGi32Ext},
vec3::FGVec3,
},
schedule::GameplaySet,
scripting::FighterAttackScript,
}, schedule::GameplaySet, scripting::FighterAttackScript,
};
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct FighterDamage(FGi32);
@@ -77,17 +70,35 @@ impl AttackAngle {
#[derive(Serialize, Deserialize, Reflect, PartialEq, Eq, Hash, Clone, Debug)]
pub enum AttackKind {
NAir,
NeutralAir,
ForwardAir,
BackAir,
DownAir,
UpAir,
Jab,
UpTilt,
DownTilt,
ForwardTilt,
UpSmash,
DownSmash,
ForwardSmash,
}
impl AttackKind {
pub fn get_animation(&self) -> AnimKind {
match self {
AttackKind::NAir => AnimKind::AttackJab1,
AttackKind::NeutralAir => AnimKind::AttackJab1,
AttackKind::Jab => AnimKind::AttackJab1,
AttackKind::UpTilt => AnimKind::AttackUpTilt,
AttackKind::ForwardAir => AnimKind::AttackUpTilt,
AttackKind::BackAir => AnimKind::AttackUpTilt,
AttackKind::DownAir => AnimKind::AttackUpTilt,
AttackKind::UpAir => AnimKind::AttackUpTilt,
AttackKind::DownTilt => AnimKind::AttackDownTilt,
AttackKind::ForwardTilt => AnimKind::AttackForwardTilt,
AttackKind::UpSmash => AnimKind::AttackUpTilt,
AttackKind::DownSmash => AnimKind::AttackUpTilt,
AttackKind::ForwardSmash => AnimKind::AttackUpTilt,
}
}
}
@@ -181,6 +192,69 @@ pub struct FighterSolvedHurtbox {
pub capsule: FixedAffineCapsule,
}
#[derive(Clone, Debug, PartialEq, Hash)]
pub struct FighterSolvedHitbox {
pub hitbox_idx: usize,
pub capsule: FixedCapsule,
}
#[derive(Component, Clone, Debug, Default, Hash)]
pub struct FighterSolvedHitboxes(pub Vec<FighterSolvedHitbox>);
pub fn solve_hitboxes(
mut query: Query<(
&FighterBoneMatrices,
&FighterTranslation,
&FighterFacingDirection,
&mut FighterHitboxes,
&FighterAnimationFrame
)>,
attack_scripts: Res<Assets<FighterAttackScript>>,
) {
for (matrices, translation, 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 active_hitboxes = hitboxes.active_hitboxes.clone();
for hitbox_idx in active_hitboxes {
let hitbox = attack_script.hitboxes.get(hitbox_idx).expect("Hitbox IDX should be valid");
let matrices = matrices.get(&hitbox.bone);
if let None = matrices {
warn!("Bone {} not found!", hitbox.bone);
continue;
}
let (prev_bone_matrix, curr_bone_matrix) = matrices.unwrap();
let curr_hbox_matrix = fighter_trf.mul(curr_bone_matrix);
let curr_hbox_pos = curr_hbox_matrix.transform_point(hitbox.offset);
if hitbox.start_frame == animation_frame.frame {
// Sphere only
hitboxes.active_hitboxes_solved.push(
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_pos = prev_hbox_matrix.transform_point(hitbox.offset);
hitboxes.active_hitboxes_solved.push(
FixedCapsule {
start: prev_hbox_pos,
end: curr_hbox_pos,
radius: hitbox.radius
}
);
}
}
}
}
}
#[derive(Component)]
pub struct FighterAttackScriptAssets {
pub scripts: HashMap<AttackKind, Handle<FighterAttackScript>>,
@@ -229,13 +303,42 @@ pub fn solve_hurtboxes(
}
}
pub fn intersect_attacks_with_hurtboxes(query: Query<(Entity, &FighterSolvedHurtboxes, &FighterHitboxes)>, attack_scripts: Res<Assets<FighterAttackScript>>) {
/*for (entity_hurting, _, hitboxes) in query {
if hitboxes.attack_script.is_none() {
continue;
}
if let Some(ref attack_script) = hitboxes.attack_script {
let attack_script = attack_scripts.get(attack_script);
if attack_script.is_none() {
continue;
}
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 {
continue;
}
}
}
}*/
}
pub struct FighterAttackPlugin;
impl Plugin for FighterAttackPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
GgrsSchedule,
solve_hurtboxes
(solve_hurtboxes, solve_hitboxes).chain()
.after(crate::fighter::baked_animation::update_fighter_bone_matrices)
.in_set(GameplaySet::Animation),
);
+22 -77
View File
@@ -19,6 +19,7 @@ use crate::fighter::{
FighterTranslation, FighterVelocity,
};
use crate::input::FighterInput;
use crate::math::int::FGi32;
use crate::math::vec3::FGVec3;
use crate::player::Player;
use crate::scripting::FighterAttackScript;
@@ -250,8 +251,7 @@ fn capsule_wireframe() -> &'static CapsuleWireframe {
let mut lower_previous = lower_ring[index];
let mut upper_previous = upper_ring[index];
for step in 1..HEMISPHERE_STEPS {
let latitude = step as f32 * std::f32::consts::FRAC_PI_2
/ HEMISPHERE_STEPS as f32;
let latitude = step as f32 * std::f32::consts::FRAC_PI_2 / HEMISPHERE_STEPS as f32;
let (sin_latitude, cos_latitude) = latitude.sin_cos();
let lower = vertices.len();
@@ -324,86 +324,31 @@ pub fn attack_debug(
) {
for (hitboxes, matrices, translation, facing_direction, frame) in query {
if let Some(script) = &hitboxes.attack_script {
let script = attack_scripts
.get(script)
.expect("Script ref should be valid");
let player_trf = translation.get_3d_transform(facing_direction);
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 length = (start - end).length();
for i in hitboxes.active_hitboxes.iter() {
let offset = script.hitboxes[*i].offset;
let radius = script.hitboxes[*i].radius;
let bone = &script.hitboxes[*i].bone;
let attack_capsule = Capsule3d {
half_length: length * 0.5,
radius: solved_hitbox.radius.to_num(),
};
let trfs = matrices.get(bone);
let aim_dir = (end - start).normalize_or(Vec3::Y);
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 mut capsule_draw_trf = Transform::IDENTITY;
capsule_draw_trf.translation = center.to_vec3();
capsule_draw_trf.align(Dir3::Y, aim_dir, Dir3::X, aim_dir.any_orthonormal_vector());
let curr_pos_draw = Vec3::new(
curr_pos.x.to_num(),
curr_pos.y.to_num(),
curr_pos.z.to_num(),
);
gizmos.primitive_3d(
&attack_capsule,
capsule_draw_trf.to_isometry(),
bevy::color::palettes::css::RED,
);
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(),
};
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.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.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);
}
gizmos.cross(start, 1.0, bevy::color::palettes::css::GREEN);
gizmos.cross(end, 1.0, bevy::color::palettes::css::GREEN);
}
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ pub struct FixedAffineCapsule {
}
/// An ordinary world-space capsule, used for swept attack volumes.
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, Debug, PartialEq, Hash)]
pub struct FixedCapsule {
pub start: FGVec3,
pub end: FGVec3,
+5 -11
View File
@@ -30,17 +30,8 @@ pub use motion::{FighterPreviousTranslation, FighterTranslation, FighterVelocity
use crate::{
fighter::{
attack::{FighterAttackPlugin, FighterAttackScriptAssets, FighterSolvedHurtboxes},
manifest::FighterManifest,
state::{FighterState, fall::FallState},
visual::FighterAnimations,
},
input::FighterInput,
math::{int::FGi32, vec::FGVec2},
player::Player,
schedule::GameplaySet,
scripting::FighterAttackScript,
stage::line::StageCollision,
attack::{FighterAttackPlugin, FighterAttackScriptAssets, FighterSolvedHurtboxes}, baked_animation::FighterBoneMatrices, hurtbox::FixedCapsule, manifest::FighterManifest, state::{FighterState, fall::FallState}, visual::FighterAnimations,
}, input::FighterInput, math::{int::FGi32, vec::FGVec2}, player::Player, schedule::GameplaySet, scripting::FighterAttackScript, stage::line::StageCollision,
};
use state::state_interrupt_system;
@@ -182,6 +173,7 @@ impl Plugin for FighterPlugin {
)
.rollback_component_with_clone::<FighterState>()
.rollback_component_with_clone::<FighterHitboxes>()
.rollback_component_with_clone::<FighterBoneMatrices>()
.checksum_component_with_hash::<FighterHitboxes>()
.rollback_component_with_clone::<FighterSolvedHurtboxes>()
.checksum_component_with_hash::<FighterSolvedHurtboxes>()
@@ -195,6 +187,7 @@ impl Plugin for FighterPlugin {
pub struct FighterHitboxes {
pub attack_script: Option<Handle<FighterAttackScript>>,
pub active_hitboxes: Vec<usize>,
pub active_hitboxes_solved: Vec<FixedCapsule>,
}
impl FighterHitboxes {
@@ -228,6 +221,7 @@ pub fn spawn_fighter(
FighterHitboxes {
attack_script: None,
active_hitboxes: vec![],
active_hitboxes_solved: vec![]
},
),
(
+1 -6
View File
@@ -71,12 +71,7 @@ pub fn check_interrupt(
.input
.has_command(crate::input::FighterCommands::Attack)
{
let attack_kind = if state_context.input.get_last_frame().movement.y > FGi32::ZERO {
AttackKind::UpTilt
} else {
AttackKind::Jab
};
let attack_kind = state_context.input.get_attack_command_type().to_attack_kind_grounded();
Some(FighterState::GroundAttack(GroundAttackState {
attack_kind,
grounded_common: grounded_common.clone(),