Add affine transformations for capsules.
This commit is contained in:
+76
-3
@@ -1,11 +1,22 @@
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
use bevy::{asset::Handle, ecs::component::Component, reflect::Reflect};
|
||||
use bevy::prelude::*;
|
||||
use bevy_ggrs::prelude::GgrsSchedule;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
fighter::{FighterAttributes, animation::AnimKind},
|
||||
math::{int::FGi32, vec3::FGVec3},
|
||||
fighter::{
|
||||
Fighter, FighterAttributes, FighterFacingDirection, FighterTranslation,
|
||||
animation::AnimKind,
|
||||
baked_animation::{FighterBoneMatrices, FixedMat4},
|
||||
hurtbox::FixedAffineCapsule,
|
||||
manifest::FighterManifest,
|
||||
},
|
||||
math::{
|
||||
int::{FGi32, FGi32Ext},
|
||||
vec3::FGVec3,
|
||||
},
|
||||
schedule::GameplaySet,
|
||||
scripting::FighterAttackScript,
|
||||
};
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
@@ -164,7 +175,69 @@ pub fn update_active_hitbox_list(script: &FighterAttackScript, list: &mut Vec<us
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct FighterSolvedHurtbox {
|
||||
pub hurtbox_idx: usize,
|
||||
pub capsule: FixedAffineCapsule,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct FighterAttackScriptAssets {
|
||||
pub scripts: HashMap<AttackKind, Handle<FighterAttackScript>>,
|
||||
}
|
||||
#[derive(Component, Clone, Debug, Default)]
|
||||
pub struct FighterSolvedHurtboxes(pub Vec<FighterSolvedHurtbox>);
|
||||
|
||||
pub fn solve_hurtboxes(
|
||||
mut query: Query<(
|
||||
&FighterBoneMatrices,
|
||||
&FighterTranslation,
|
||||
&FighterFacingDirection,
|
||||
&mut FighterSolvedHurtboxes,
|
||||
&Fighter,
|
||||
)>,
|
||||
manifests: Res<Assets<FighterManifest>>,
|
||||
) {
|
||||
for (matrices, translation, facing_direction, mut solved_hurtboxes, fighter) in &mut query {
|
||||
let manifest = manifests
|
||||
.get(&fighter.manifest)
|
||||
.expect("Manifest should be valid");
|
||||
let fighter_trf = translation.get_3d_transform(facing_direction);
|
||||
solved_hurtboxes.0.clear();
|
||||
for (hurtbox_idx, hurtbox) in manifest.hurtboxes.iter().enumerate() {
|
||||
let bone_matrix = matrices.get_current(&hurtbox.bone);
|
||||
|
||||
if let Some(bone_matrix) = bone_matrix {
|
||||
let to_global = fighter_trf.mul(bone_matrix);
|
||||
let hurtbox_trf = FixedMat4::IDENTITY
|
||||
.with_rotation(FGVec3::new(
|
||||
hurtbox.rotation.x.to_radians(),
|
||||
hurtbox.rotation.y.to_radians(),
|
||||
hurtbox.rotation.z.to_radians(),
|
||||
))
|
||||
.with_translation(hurtbox.offset);
|
||||
solved_hurtboxes.0.push(FighterSolvedHurtbox {
|
||||
hurtbox_idx,
|
||||
capsule: FixedAffineCapsule::new(
|
||||
to_global.mul(hurtbox_trf),
|
||||
hurtbox.half_length,
|
||||
hurtbox.radius,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FighterAttackPlugin;
|
||||
|
||||
impl Plugin for FighterAttackPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(
|
||||
GgrsSchedule,
|
||||
solve_hurtboxes
|
||||
.after(crate::fighter::baked_animation::update_fighter_bone_matrices)
|
||||
.in_set(GameplaySet::Animation),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ use crate::{
|
||||
visual::FighterAnimations,
|
||||
},
|
||||
math::{
|
||||
int::{FGWide, FGi32},
|
||||
int::{FGWide, FGi32, FGi32Ext},
|
||||
vec3::FGVec3,
|
||||
},
|
||||
schedule::GameplaySet,
|
||||
@@ -108,6 +108,28 @@ impl FixedMat4 {
|
||||
self.cols[3][2] = translation.z;
|
||||
}
|
||||
|
||||
pub fn with_translation(mut self, translation: FGVec3) -> Self {
|
||||
self.translate(translation);
|
||||
self
|
||||
}
|
||||
|
||||
/// Replaces the rotation with XYZ Euler angles expressed in radians.
|
||||
pub fn with_rotation(mut self, rotation: FGVec3) -> Self {
|
||||
let (sin_x, cos_x) = rotation.x.sin_cos();
|
||||
let (sin_y, cos_y) = rotation.y.sin_cos();
|
||||
let (sin_z, cos_z) = rotation.z.sin_cos();
|
||||
self.cols[0][0] = cos_y * cos_z;
|
||||
self.cols[0][1] = cos_x * sin_z + sin_x * sin_y * cos_z;
|
||||
self.cols[0][2] = sin_x * sin_z - cos_x * sin_y * cos_z;
|
||||
self.cols[1][0] = -cos_y * sin_z;
|
||||
self.cols[1][1] = cos_x * cos_z - sin_x * sin_y * sin_z;
|
||||
self.cols[1][2] = sin_x * cos_z + cos_x * sin_y * sin_z;
|
||||
self.cols[2][0] = sin_y;
|
||||
self.cols[2][1] = -sin_x * cos_y;
|
||||
self.cols[2][2] = cos_x * cos_y;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_translation(&self) -> FGVec3 {
|
||||
FGVec3::new(self.cols[3][0], self.cols[3][1], self.cols[3][2])
|
||||
}
|
||||
@@ -949,4 +971,26 @@ mod tests {
|
||||
FGVec3::lit("10", "2", "-4")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_matrix_builders_set_euler_rotation_and_translation() {
|
||||
let transform = FixedMat4::IDENTITY
|
||||
.with_rotation(FGVec3::lit("0", "0", "1.5707963267948966"))
|
||||
.with_translation(FGVec3::lit("10", "2", "-3"));
|
||||
|
||||
assert_eq!(
|
||||
transform.transform_point(FGVec3::lit("1", "0", "0")),
|
||||
FGVec3::lit("10", "3", "-3")
|
||||
);
|
||||
assert_eq!(transform.get_translation(), FGVec3::lit("10", "2", "-3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_matrix_rotation_builder_preserves_translation() {
|
||||
let transform = FixedMat4::IDENTITY
|
||||
.with_translation(FGVec3::lit("4", "5", "6"))
|
||||
.with_rotation(FGVec3::lit("0", "0", "1.5707963267948966"));
|
||||
|
||||
assert_eq!(transform.get_translation(), FGVec3::lit("4", "5", "6"));
|
||||
}
|
||||
}
|
||||
|
||||
+151
-46
@@ -1,21 +1,21 @@
|
||||
//! Fighter debug views: gizmos and the egui inspector window.
|
||||
|
||||
use std::ops::Mul;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use bevy::color::palettes::css::ORANGE;
|
||||
use bevy::gizmos::primitives::dim3::Capsule3dBuilder;
|
||||
use bevy::{color::palettes::css::YELLOW, prelude::*};
|
||||
use bevy_egui::{EguiContexts, egui};
|
||||
|
||||
use crate::debug_tools::{DebugSettings, FighterDebugRowKind};
|
||||
use crate::fighter::animation::FighterAnimationFrame;
|
||||
use crate::fighter::attack::FighterSolvedHurtboxes;
|
||||
use crate::fighter::baked_animation::{BakedFighterAnimations, FighterBoneMatrices};
|
||||
use crate::fighter::ecb::FighterPreviousECB;
|
||||
use crate::fighter::manifest::FighterManifest;
|
||||
use crate::fighter::hurtbox::FixedAffineCapsule;
|
||||
use crate::fighter::state::StateNameDebug;
|
||||
use crate::fighter::visual::FighterAnimations;
|
||||
use crate::fighter::{
|
||||
Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterPreviousTranslation,
|
||||
FighterECB, FighterFacingDirection, FighterHitboxes, FighterPreviousTranslation,
|
||||
FighterTranslation, FighterVelocity,
|
||||
};
|
||||
use crate::input::FighterInput;
|
||||
@@ -160,52 +160,157 @@ pub fn animation_debug(query: Query<(&GlobalTransform, &FighterBoneMatrices)>, m
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hurtbox_debug(
|
||||
query: Query<(
|
||||
&FighterBoneMatrices,
|
||||
&FighterTranslation,
|
||||
&FighterFacingDirection,
|
||||
&Fighter,
|
||||
)>,
|
||||
manifests: Res<Assets<FighterManifest>>,
|
||||
mut gizmos: Gizmos,
|
||||
) {
|
||||
for (matrices, translation, facing_direction, fighter) in query {
|
||||
let manifest = manifests
|
||||
.get(&fighter.manifest)
|
||||
.expect("Manifest should be valid");
|
||||
let fighter_trf = translation.get_3d_transform(facing_direction);
|
||||
for hurtbox in manifest.hurtboxes.iter() {
|
||||
let bone_matrix = matrices.get_current(&hurtbox.bone);
|
||||
|
||||
if let Some(bone_matrix) = bone_matrix {
|
||||
let to_global = fighter_trf.mul(bone_matrix);
|
||||
let capsule = Capsule3d {
|
||||
half_length: hurtbox.half_length.to_num(),
|
||||
radius: hurtbox.radius.to_num(),
|
||||
};
|
||||
|
||||
let hurtbox_trf = Transform::IDENTITY
|
||||
.with_rotation(Quat::from_euler(
|
||||
EulerRot::XYZ,
|
||||
f32::to_radians(hurtbox.rotation.x.to_num()),
|
||||
f32::to_radians(hurtbox.rotation.y.to_num()),
|
||||
f32::to_radians(hurtbox.rotation.z.to_num()),
|
||||
))
|
||||
.with_translation(hurtbox.offset.to_vec3());
|
||||
|
||||
let hurtbox_trf = Transform::from_matrix(to_global.to_mat4()).mul(hurtbox_trf);
|
||||
|
||||
gizmos.primitive_3d(
|
||||
&capsule,
|
||||
hurtbox_trf.to_isometry(),
|
||||
bevy::color::palettes::css::YELLOW,
|
||||
);
|
||||
}
|
||||
pub fn hurtbox_debug(query: Query<&FighterSolvedHurtboxes>, mut gizmos: Gizmos) {
|
||||
for hurtboxes in &query {
|
||||
for hurtbox in &hurtboxes.0 {
|
||||
draw_affine_capsule(&mut gizmos, hurtbox.capsule, YELLOW);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_affine_capsule(
|
||||
gizmos: &mut Gizmos,
|
||||
capsule: FixedAffineCapsule,
|
||||
color: impl Into<Color> + Copy,
|
||||
) {
|
||||
let color = color.into();
|
||||
let wireframe = capsule_wireframe();
|
||||
let transform = capsule.transform.to_mat4();
|
||||
let radius = capsule.radius.to_num::<f32>();
|
||||
let half_length = capsule.half_length.to_num::<f32>();
|
||||
let vertices: Vec<_> = wireframe
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|vertex| {
|
||||
transform.transform_point3(
|
||||
vertex.direction * radius + Vec3::Y * vertex.center_sign * half_length,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
for &(start, end) in &wireframe.lines {
|
||||
gizmos.line(vertices[start], vertices[end], color);
|
||||
}
|
||||
}
|
||||
|
||||
struct CapsuleWireVertex {
|
||||
direction: Vec3,
|
||||
center_sign: f32,
|
||||
}
|
||||
|
||||
struct CapsuleWireframe {
|
||||
vertices: Vec<CapsuleWireVertex>,
|
||||
lines: Vec<(usize, usize)>,
|
||||
}
|
||||
|
||||
fn capsule_wireframe() -> &'static CapsuleWireframe {
|
||||
const RESOLUTION: usize = 16;
|
||||
const HEMISPHERE_STEPS: usize = 4;
|
||||
static WIREFRAME: OnceLock<CapsuleWireframe> = OnceLock::new();
|
||||
|
||||
WIREFRAME.get_or_init(|| {
|
||||
let mut vertices = Vec::new();
|
||||
let mut lines = Vec::new();
|
||||
let mut lower_ring = Vec::with_capacity(RESOLUTION);
|
||||
let mut upper_ring = Vec::with_capacity(RESOLUTION);
|
||||
|
||||
for index in 0..RESOLUTION {
|
||||
let angle = index as f32 * std::f32::consts::TAU / RESOLUTION as f32;
|
||||
let (sin, cos) = angle.sin_cos();
|
||||
lower_ring.push(vertices.len());
|
||||
vertices.push(CapsuleWireVertex {
|
||||
direction: Vec3::new(cos, 0.0, sin),
|
||||
center_sign: -1.0,
|
||||
});
|
||||
upper_ring.push(vertices.len());
|
||||
vertices.push(CapsuleWireVertex {
|
||||
direction: Vec3::new(cos, 0.0, sin),
|
||||
center_sign: 1.0,
|
||||
});
|
||||
}
|
||||
|
||||
let lower_pole = vertices.len();
|
||||
vertices.push(CapsuleWireVertex {
|
||||
direction: Vec3::NEG_Y,
|
||||
center_sign: -1.0,
|
||||
});
|
||||
let upper_pole = vertices.len();
|
||||
vertices.push(CapsuleWireVertex {
|
||||
direction: Vec3::Y,
|
||||
center_sign: 1.0,
|
||||
});
|
||||
|
||||
for index in 0..RESOLUTION {
|
||||
let next = (index + 1) % RESOLUTION;
|
||||
lines.push((lower_ring[index], lower_ring[next]));
|
||||
lines.push((upper_ring[index], upper_ring[next]));
|
||||
lines.push((lower_ring[index], upper_ring[index]));
|
||||
|
||||
let angle = index as f32 * std::f32::consts::TAU / RESOLUTION as f32;
|
||||
let (sin_azimuth, cos_azimuth) = angle.sin_cos();
|
||||
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 (sin_latitude, cos_latitude) = latitude.sin_cos();
|
||||
|
||||
let lower = vertices.len();
|
||||
vertices.push(CapsuleWireVertex {
|
||||
direction: Vec3::new(
|
||||
cos_azimuth * cos_latitude,
|
||||
-sin_latitude,
|
||||
sin_azimuth * cos_latitude,
|
||||
),
|
||||
center_sign: -1.0,
|
||||
});
|
||||
lines.push((lower_previous, lower));
|
||||
lower_previous = lower;
|
||||
|
||||
let upper = vertices.len();
|
||||
vertices.push(CapsuleWireVertex {
|
||||
direction: Vec3::new(
|
||||
cos_azimuth * cos_latitude,
|
||||
sin_latitude,
|
||||
sin_azimuth * cos_latitude,
|
||||
),
|
||||
center_sign: 1.0,
|
||||
});
|
||||
lines.push((upper_previous, upper));
|
||||
upper_previous = upper;
|
||||
}
|
||||
lines.push((lower_previous, lower_pole));
|
||||
lines.push((upper_previous, upper_pole));
|
||||
}
|
||||
|
||||
CapsuleWireframe { vertices, lines }
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod capsule_debug_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn capsule_wireframe_caps_extend_in_opposite_directions() {
|
||||
let wireframe = capsule_wireframe();
|
||||
let lower: Vec<_> = wireframe
|
||||
.vertices
|
||||
.iter()
|
||||
.filter(|vertex| vertex.center_sign < 0.0)
|
||||
.collect();
|
||||
let upper: Vec<_> = wireframe
|
||||
.vertices
|
||||
.iter()
|
||||
.filter(|vertex| vertex.center_sign > 0.0)
|
||||
.collect();
|
||||
|
||||
assert!(lower.iter().all(|vertex| vertex.direction.y <= 0.0));
|
||||
assert!(upper.iter().all(|vertex| vertex.direction.y >= 0.0));
|
||||
assert!(lower.iter().any(|vertex| vertex.direction == Vec3::NEG_Y));
|
||||
assert!(upper.iter().any(|vertex| vertex.direction == Vec3::Y));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attack_debug(
|
||||
query: Query<(
|
||||
&FighterHitboxes,
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
//! Fixed-point convex geometry used by fighter hurtboxes.
|
||||
|
||||
use crate::{
|
||||
fighter::baked_animation::FixedMat4,
|
||||
math::{
|
||||
int::{FGWide, FGi32},
|
||||
vec3::FGVec3,
|
||||
},
|
||||
};
|
||||
|
||||
/// A capsule after an arbitrary affine transform. Non-uniform scale turns the
|
||||
/// spherical caps and circular cylinder into ellipsoidal geometry.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct FixedAffineCapsule {
|
||||
pub transform: FixedMat4,
|
||||
pub half_length: FGi32,
|
||||
pub radius: FGi32,
|
||||
}
|
||||
|
||||
/// An ordinary world-space capsule, used for swept attack volumes.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct FixedCapsule {
|
||||
pub start: FGVec3,
|
||||
pub end: FGVec3,
|
||||
pub radius: FGi32,
|
||||
}
|
||||
|
||||
impl FixedCapsule {
|
||||
pub fn new(start: FGVec3, end: FGVec3, radius: FGi32) -> Self {
|
||||
Self { start, end, radius }
|
||||
}
|
||||
}
|
||||
|
||||
impl FixedAffineCapsule {
|
||||
pub fn new(transform: FixedMat4, half_length: FGi32, radius: FGi32) -> Self {
|
||||
Self {
|
||||
transform,
|
||||
half_length,
|
||||
radius,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transform_point(self, point: FGVec3) -> FGVec3 {
|
||||
self.transform.transform_point(point)
|
||||
}
|
||||
|
||||
/// Tests the exact affine hurtbox against an ordinary world-space capsule.
|
||||
pub fn intersects(self, rhs: FixedCapsule) -> bool {
|
||||
let mut direction = rhs.start - self.transform.get_translation();
|
||||
if direction == FGVec3::ZERO {
|
||||
direction = FGVec3::new(FGi32::ONE, FGi32::ZERO, FGi32::ZERO);
|
||||
}
|
||||
let mut simplex = [FGVec3::ZERO; 4];
|
||||
let mut simplex_len = 0;
|
||||
for _ in 0..20 {
|
||||
let point = self.support(direction) - rhs.support(-direction);
|
||||
if point.dot(direction) < FGWide::ZERO {
|
||||
return false;
|
||||
}
|
||||
for index in (1..=simplex_len.min(3)).rev() {
|
||||
simplex[index] = simplex[index - 1];
|
||||
}
|
||||
simplex[0] = point;
|
||||
simplex_len += 1;
|
||||
if update_simplex(&mut simplex, &mut simplex_len, &mut direction) {
|
||||
return true;
|
||||
}
|
||||
if direction == FGVec3::ZERO {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// A capped GJK iteration means the origin remains enclosed by the
|
||||
// current simplex to fixed-point precision, so conservatively collide.
|
||||
true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
trait SupportMap {
|
||||
fn support(self, direction: FGVec3) -> FGVec3;
|
||||
}
|
||||
|
||||
impl SupportMap for FixedAffineCapsule {
|
||||
fn support(self, direction: FGVec3) -> FGVec3 {
|
||||
let local_direction = FGVec3::new(
|
||||
dot_column(self.transform, 0, direction),
|
||||
dot_column(self.transform, 1, direction),
|
||||
dot_column(self.transform, 2, direction),
|
||||
);
|
||||
let round = local_direction.normalized().unwrap_or(FGVec3::new(
|
||||
FGi32::ONE,
|
||||
FGi32::ZERO,
|
||||
FGi32::ZERO,
|
||||
)) * self.radius;
|
||||
let endpoint = if local_direction.y >= FGi32::ZERO {
|
||||
self.half_length
|
||||
} else {
|
||||
-self.half_length
|
||||
};
|
||||
self.transform_point(round + FGVec3::new(FGi32::ZERO, endpoint, FGi32::ZERO))
|
||||
}
|
||||
}
|
||||
|
||||
impl SupportMap for FixedCapsule {
|
||||
fn support(self, direction: FGVec3) -> FGVec3 {
|
||||
let endpoint = if self.start.dot(direction) >= self.end.dot(direction) {
|
||||
self.start
|
||||
} else {
|
||||
self.end
|
||||
};
|
||||
endpoint
|
||||
+ direction
|
||||
.normalized()
|
||||
.unwrap_or(FGVec3::new(FGi32::ONE, FGi32::ZERO, FGi32::ZERO))
|
||||
* self.radius
|
||||
}
|
||||
}
|
||||
|
||||
fn dot_column(matrix: FixedMat4, column: usize, vector: FGVec3) -> FGi32 {
|
||||
FGi32::from_num(
|
||||
FGWide::from_num(matrix.cols[column][0]) * FGWide::from_num(vector.x)
|
||||
+ FGWide::from_num(matrix.cols[column][1]) * FGWide::from_num(vector.y)
|
||||
+ FGWide::from_num(matrix.cols[column][2]) * FGWide::from_num(vector.z),
|
||||
)
|
||||
}
|
||||
|
||||
fn cross(lhs: FGVec3, rhs: FGVec3) -> FGVec3 {
|
||||
FGVec3::new(
|
||||
FGi32::from_num(
|
||||
FGWide::from_num(lhs.y) * FGWide::from_num(rhs.z)
|
||||
- FGWide::from_num(lhs.z) * FGWide::from_num(rhs.y),
|
||||
),
|
||||
FGi32::from_num(
|
||||
FGWide::from_num(lhs.z) * FGWide::from_num(rhs.x)
|
||||
- FGWide::from_num(lhs.x) * FGWide::from_num(rhs.z),
|
||||
),
|
||||
FGi32::from_num(
|
||||
FGWide::from_num(lhs.x) * FGWide::from_num(rhs.y)
|
||||
- FGWide::from_num(lhs.y) * FGWide::from_num(rhs.x),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn triple_cross(lhs: FGVec3, middle: FGVec3, rhs: FGVec3) -> FGVec3 {
|
||||
cross(cross(lhs, middle), rhs)
|
||||
}
|
||||
|
||||
fn update_simplex(
|
||||
simplex: &mut [FGVec3; 4],
|
||||
simplex_len: &mut usize,
|
||||
direction: &mut FGVec3,
|
||||
) -> bool {
|
||||
let a = simplex[0];
|
||||
let ao = -a;
|
||||
match *simplex_len {
|
||||
2 => {
|
||||
let ab = simplex[1] - a;
|
||||
if ab.dot(ao) > FGWide::ZERO {
|
||||
*direction = triple_cross(ab, ao, ab);
|
||||
} else {
|
||||
*simplex_len = 1;
|
||||
*direction = ao;
|
||||
}
|
||||
false
|
||||
}
|
||||
3 => {
|
||||
let b = simplex[1];
|
||||
let c = simplex[2];
|
||||
let ab = b - a;
|
||||
let ac = c - a;
|
||||
let abc = cross(ab, ac);
|
||||
if cross(abc, ac).dot(ao) > FGWide::ZERO {
|
||||
if ac.dot(ao) > FGWide::ZERO {
|
||||
simplex[1] = c;
|
||||
*simplex_len = 2;
|
||||
*direction = triple_cross(ac, ao, ac);
|
||||
} else {
|
||||
*simplex_len = 2;
|
||||
*direction = triple_cross(ab, ao, ab);
|
||||
}
|
||||
} else if cross(ab, abc).dot(ao) > FGWide::ZERO {
|
||||
*simplex_len = 2;
|
||||
*direction = triple_cross(ab, ao, ab);
|
||||
} else if abc.dot(ao) > FGWide::ZERO {
|
||||
*direction = abc;
|
||||
} else {
|
||||
simplex.swap(1, 2);
|
||||
*direction = -abc;
|
||||
}
|
||||
false
|
||||
}
|
||||
4 => {
|
||||
let b = simplex[1];
|
||||
let c = simplex[2];
|
||||
let d = simplex[3];
|
||||
for (first, second, opposite) in [(b, c, d), (c, d, b), (d, b, c)] {
|
||||
let mut normal = cross(first - a, second - a);
|
||||
if normal.dot(opposite - a) > FGWide::ZERO {
|
||||
normal = -normal;
|
||||
}
|
||||
if normal.dot(ao) > FGWide::ZERO {
|
||||
simplex[0] = a;
|
||||
simplex[1] = first;
|
||||
simplex[2] = second;
|
||||
*simplex_len = 3;
|
||||
*direction = normal;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
_ => {
|
||||
*direction = ao;
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn affine_capsule(x: &str, scale_y: &str) -> FixedAffineCapsule {
|
||||
let mut transform = FixedMat4::IDENTITY;
|
||||
transform.cols[1][1] = FGi32::lit(scale_y);
|
||||
transform.translate(FGVec3::lit(x, "0", "0"));
|
||||
FixedAffineCapsule::new(transform, FGi32::lit("1"), FGi32::lit("1"))
|
||||
}
|
||||
|
||||
fn capsule(x: &str) -> FixedCapsule {
|
||||
FixedCapsule::new(
|
||||
FGVec3::lit(x, "-1", "0"),
|
||||
FGVec3::lit(x, "1", "0"),
|
||||
FGi32::lit("1"),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_touching_and_separated_capsules() {
|
||||
assert!(affine_capsule("0", "1").intersects(capsule("2")));
|
||||
assert!(!affine_capsule("0", "1").intersects(capsule("2.01")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn affine_scale_changes_capsule_intersection() {
|
||||
let sphere = FixedAffineCapsule::new(
|
||||
FixedMat4::IDENTITY,
|
||||
FGi32::ZERO,
|
||||
FGi32::lit("1"),
|
||||
);
|
||||
let mut squashed_transform = FixedMat4::IDENTITY;
|
||||
squashed_transform.cols[1][1] = FGi32::lit("0.5");
|
||||
let squashed = FixedAffineCapsule::new(
|
||||
squashed_transform,
|
||||
FGi32::ZERO,
|
||||
FGi32::lit("1"),
|
||||
);
|
||||
let target = FixedCapsule::new(
|
||||
FGVec3::lit("0", "1.4", "0"),
|
||||
FGVec3::lit("0", "1.4", "0"),
|
||||
FGi32::lit("0.5"),
|
||||
);
|
||||
|
||||
assert!(sphere.intersects(target));
|
||||
assert!(!squashed.intersects(target));
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,10 @@ use strum::IntoEnumIterator;
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
use crate::{
|
||||
AppState, fighter::{FighterAttributes, FighterCameraProfile, animation::AnimKind, attack::AttackKind}, game_settings::GameSettings, math::{int::FGi32, vec3::FGVec3},
|
||||
AppState,
|
||||
fighter::{FighterAttributes, FighterCameraProfile, animation::AnimKind, attack::AttackKind},
|
||||
game_settings::GameSettings,
|
||||
math::{int::FGi32, vec3::FGVec3},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Reflect, EnumIter)]
|
||||
@@ -23,7 +26,7 @@ pub struct FighterHurtbox {
|
||||
#[reflect(ignore)]
|
||||
pub half_length: FGi32,
|
||||
#[reflect(ignore)]
|
||||
pub radius: FGi32
|
||||
pub radius: FGi32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Asset, Reflect)]
|
||||
|
||||
+12
-7
@@ -13,6 +13,7 @@ pub mod baked_animation;
|
||||
pub mod collision;
|
||||
pub mod debug;
|
||||
pub mod ecb;
|
||||
pub mod hurtbox;
|
||||
pub mod manifest;
|
||||
pub mod motion;
|
||||
pub mod state;
|
||||
@@ -29,7 +30,7 @@ pub use motion::{FighterPreviousTranslation, FighterTranslation, FighterVelocity
|
||||
|
||||
use crate::{
|
||||
fighter::{
|
||||
attack::FighterAttackScriptAssets,
|
||||
attack::{FighterAttackPlugin, FighterAttackScriptAssets, FighterSolvedHurtboxes},
|
||||
manifest::FighterManifest,
|
||||
state::{FighterState, fall::FallState},
|
||||
visual::FighterAnimations,
|
||||
@@ -105,7 +106,7 @@ pub struct Fighter {
|
||||
|
||||
impl Plugin for FighterPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app
|
||||
app.add_plugins(FighterAttackPlugin)
|
||||
// Every state gets a chance to interrupt itself, in priority order.
|
||||
.add_systems(
|
||||
GgrsSchedule,
|
||||
@@ -171,10 +172,11 @@ impl Plugin for FighterPlugin {
|
||||
)
|
||||
.add_systems(
|
||||
FixedPostUpdate,
|
||||
debug::hurtbox_debug
|
||||
debug::hurtbox_debug.run_if(crate::debug_tools::hurtboxes_enabled),
|
||||
)
|
||||
.rollback_component_with_clone::<FighterState>()
|
||||
.rollback_component_with_clone::<FighterHitboxes>()
|
||||
.rollback_component_with_clone::<FighterSolvedHurtboxes>()
|
||||
.checksum_component_with_hash::<FighterState>()
|
||||
.checksum_component_with_hash::<FighterFacingDirection>()
|
||||
.register_type::<FighterFacingDirection>();
|
||||
@@ -230,9 +232,12 @@ pub fn spawn_fighter(
|
||||
attack_scripts,
|
||||
FighterState::Fall(FallState),
|
||||
animation::FighterAnimationFrame::new(animation::AnimKind::Wait, true),
|
||||
baked_animation::FighterBoneMatrices::default(),
|
||||
FighterVisual,
|
||||
FighterInput::default(),
|
||||
crate::camera::FighterCameraExtents::default(),
|
||||
(
|
||||
baked_animation::FighterBoneMatrices::default(),
|
||||
FighterSolvedHurtboxes::default(),
|
||||
FighterVisual,
|
||||
FighterInput::default(),
|
||||
crate::camera::FighterCameraExtents::default(),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
|
||||
|
||||
use crate::{
|
||||
fighter::{
|
||||
animation::AnimKind,
|
||||
collision,
|
||||
state::{
|
||||
FighterState, FighterStateContext, FighterStateImpl, air,
|
||||
fall::FallState,
|
||||
ground::GroundedStateCommon,
|
||||
land::LandingState,
|
||||
FighterState, FighterStateContext, FighterStateImpl, air, fall::FallState,
|
||||
ground::GroundedStateCommon, land::LandingState,
|
||||
},
|
||||
},
|
||||
input,
|
||||
@@ -51,9 +47,7 @@ impl FighterStateImpl for AirDodgeState {
|
||||
let grounded_common = GroundedStateCommon {
|
||||
current_line_id: res.line_id,
|
||||
};
|
||||
Some(FighterState::Land(LandingState {
|
||||
grounded_common
|
||||
}))
|
||||
Some(FighterState::Land(LandingState { grounded_common }))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::hash::{Hasher};
|
||||
use std::hash::Hasher;
|
||||
|
||||
use super::{FighterStateContext, FighterStateImpl, ground, run, wait, walk};
|
||||
use crate::fighter::FighterFacingDirection;
|
||||
|
||||
@@ -45,9 +45,7 @@ impl FighterStateImpl for FallState {
|
||||
let grounded_common = GroundedStateCommon {
|
||||
current_line_id: res.line_id,
|
||||
};
|
||||
Some(FighterState::Land(LandingState {
|
||||
grounded_common
|
||||
}))
|
||||
Some(FighterState::Land(LandingState { grounded_common }))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
//! ECS types so it stays cheap to reason about and to unit-test — it has to be
|
||||
//! bit-for-bit deterministic for rollback.
|
||||
|
||||
use crate::fighter::state::{FighterState, FighterStateContext, dash, ground_attack, turn, wait, walk};
|
||||
use crate::fighter::state::{
|
||||
FighterState, FighterStateContext, dash, ground_attack, turn, wait, walk,
|
||||
};
|
||||
use crate::fighter::{
|
||||
FighterAttributes, FighterPreviousTranslation, FighterTranslation, FighterVelocity,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
use fixed::types::I16F16;
|
||||
|
||||
use crate::{fighter::{attack::{AttackKind, update_active_hitbox_list}, state::{FighterState, FighterStateImpl, ground::GroundedStateCommon, wait::WaitState}}, math::int::FGi32};
|
||||
use crate::{
|
||||
fighter::{
|
||||
attack::{AttackKind, update_active_hitbox_list},
|
||||
state::{FighterState, FighterStateImpl, ground::GroundedStateCommon, wait::WaitState},
|
||||
},
|
||||
math::int::FGi32,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Hash)]
|
||||
pub struct GroundAttackState {
|
||||
@@ -11,10 +17,13 @@ pub struct GroundAttackState {
|
||||
impl FighterStateImpl for GroundAttackState {
|
||||
const NAME: &'static str = "GroundAttack";
|
||||
|
||||
fn check_interrupt(&self, state_context: &mut super::FighterStateContext) -> Option<FighterState> {
|
||||
fn check_interrupt(
|
||||
&self,
|
||||
state_context: &mut super::FighterStateContext,
|
||||
) -> Option<FighterState> {
|
||||
if state_context.is_current_animation_finished() {
|
||||
Some(FighterState::Wait(WaitState {
|
||||
grounded_common: self.grounded_common.clone()
|
||||
grounded_common: self.grounded_common.clone(),
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
@@ -22,23 +31,30 @@ impl FighterStateImpl for GroundAttackState {
|
||||
}
|
||||
|
||||
fn update(&mut self, state_context: &mut super::FighterStateContext) {
|
||||
let script = state_context.attack_scripts.get(&state_context.attack_script_assets.scripts[&self.attack_kind]);
|
||||
if let Some(script) = script {
|
||||
let script = state_context
|
||||
.attack_scripts
|
||||
.get(&state_context.attack_script_assets.scripts[&self.attack_kind]);
|
||||
if let Some(script) = script {
|
||||
let animation_frame = state_context.get_current_animation_frame();
|
||||
update_active_hitbox_list(script, &mut state_context.hitboxes.active_hitboxes, animation_frame);
|
||||
update_active_hitbox_list(
|
||||
script,
|
||||
&mut state_context.hitboxes.active_hitboxes,
|
||||
animation_frame,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn on_enter(&mut self, state_context: &mut super::FighterStateContext) {
|
||||
state_context.play_animation(self.attack_kind.get_animation(), false);
|
||||
state_context.hitboxes.attack_script = Some(state_context.attack_script_assets.scripts[&self.attack_kind].clone());
|
||||
state_context.hitboxes.attack_script =
|
||||
Some(state_context.attack_script_assets.scripts[&self.attack_kind].clone());
|
||||
state_context.hitboxes.active_hitboxes.clear();
|
||||
}
|
||||
|
||||
|
||||
fn on_exit(&mut self, state_context: &mut super::FighterStateContext) {
|
||||
state_context.hitboxes.clear();
|
||||
}
|
||||
|
||||
|
||||
fn check_collision_interrupt(
|
||||
&mut self,
|
||||
_state_context: &mut super::FighterStateContext,
|
||||
@@ -47,9 +63,14 @@ impl FighterStateImpl for GroundAttackState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_interrupt(state_context: &super::FighterStateContext, grounded_common: &GroundedStateCommon) -> Option<FighterState> {
|
||||
if state_context.input.has_command(crate::input::FighterCommands::Attack) {
|
||||
|
||||
pub fn check_interrupt(
|
||||
state_context: &super::FighterStateContext,
|
||||
grounded_common: &GroundedStateCommon,
|
||||
) -> Option<FighterState> {
|
||||
if state_context
|
||||
.input
|
||||
.has_command(crate::input::FighterCommands::Attack)
|
||||
{
|
||||
let attack_kind = if state_context.input.get_last_frame().movement.y > FGi32::ZERO {
|
||||
AttackKind::UpTilt
|
||||
} else {
|
||||
@@ -58,9 +79,9 @@ pub fn check_interrupt(state_context: &super::FighterStateContext, grounded_comm
|
||||
|
||||
Some(FighterState::GroundAttack(GroundAttackState {
|
||||
attack_kind,
|
||||
grounded_common: grounded_common.clone()
|
||||
grounded_common: grounded_common.clone(),
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::{
|
||||
@@ -239,9 +238,7 @@ impl FighterStateImpl for JumpState {
|
||||
let grounded_common = GroundedStateCommon {
|
||||
current_line_id: res.line_id,
|
||||
};
|
||||
Some(FighterState::Land(LandingState {
|
||||
grounded_common
|
||||
}))
|
||||
Some(FighterState::Land(LandingState { grounded_common }))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -13,14 +13,12 @@ use crate::fighter::{
|
||||
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub struct LandingState {
|
||||
pub grounded_common: GroundedStateCommon
|
||||
pub grounded_common: GroundedStateCommon,
|
||||
}
|
||||
|
||||
impl LandingState {
|
||||
pub fn create(grounded_common: GroundedStateCommon) -> Self {
|
||||
Self {
|
||||
grounded_common
|
||||
}
|
||||
Self { grounded_common }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +28,9 @@ impl FighterStateImpl for LandingState {
|
||||
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> {
|
||||
if state_context.is_current_animation_finished() {
|
||||
grounded_movement_standstill_common_interrupts(state_context, &self.grounded_common)
|
||||
} else if state_context.get_current_animation_frame() >= state_context.fighter_manifest.attributes.landing_iasa {
|
||||
} else if state_context.get_current_animation_frame()
|
||||
>= state_context.fighter_manifest.attributes.landing_iasa
|
||||
{
|
||||
dash::check_input(state_context, &self.grounded_common).or_else(|| {
|
||||
ground::grounded_movement_common_interrupts(state_context, &self.grounded_common)
|
||||
.or_else(|| walk::check_input(state_context, &self.grounded_common))
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use super::{FighterStateContext, FighterStateImpl, ground, wait, walk};
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::{FighterStateContext, FighterStateImpl};
|
||||
use crate::fighter::animation::AnimKind;
|
||||
use crate::fighter::state::fall::FallState;
|
||||
use crate::fighter::state::ground::{self, GroundedMotionResult, GroundedStateCommon};
|
||||
use crate::fighter::state::{dash, jump, wait, walk, FighterState};
|
||||
use crate::fighter::state::{FighterState, dash, jump, wait, walk};
|
||||
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub struct TurnState {
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
|
||||
|
||||
use super::{FighterStateContext, FighterStateImpl, ground};
|
||||
use crate::fighter::animation::AnimKind;
|
||||
use crate::fighter::state::fall::FallState;
|
||||
use crate::fighter::state::ground::{
|
||||
GroundedMotionResult, GroundedStateCommon,
|
||||
};
|
||||
use crate::fighter::state::FighterState;
|
||||
use crate::fighter::state::fall::FallState;
|
||||
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
|
||||
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub struct WaitState {
|
||||
@@ -16,12 +12,14 @@ pub struct WaitState {
|
||||
impl FighterStateImpl for WaitState {
|
||||
const NAME: &'static str = "Wait";
|
||||
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> {
|
||||
if let Some(state) = ground::grounded_movement_common_interrupts(state_context, &self.grounded_common) {
|
||||
if let Some(state) =
|
||||
ground::grounded_movement_common_interrupts(state_context, &self.grounded_common)
|
||||
{
|
||||
Some(state)
|
||||
} else if let Some(state) = ground::grounded_movement_standstill_common_interrupts(
|
||||
state_context,
|
||||
&self.grounded_common)
|
||||
{
|
||||
&self.grounded_common,
|
||||
) {
|
||||
// Don't transition into yourself
|
||||
if let FighterState::Wait(_) = state {
|
||||
None
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
|
||||
use super::{FighterStateContext, FighterStateImpl, dash, ground, wait};
|
||||
use crate::fighter::animation::AnimKind;
|
||||
use crate::fighter::state::FighterState;
|
||||
use crate::fighter::state::fall::FallState;
|
||||
use crate::fighter::state::turn;
|
||||
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
|
||||
use crate::fighter::state::turn;
|
||||
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub struct WalkState {
|
||||
@@ -15,9 +14,8 @@ impl FighterStateImpl for WalkState {
|
||||
const NAME: &'static str = "Walk";
|
||||
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> {
|
||||
if let Some(state) = turn::check_input(state_context, &self.grounded_common) {
|
||||
return Some(state)
|
||||
}
|
||||
else if let Some(state) =
|
||||
return Some(state);
|
||||
} else if let Some(state) =
|
||||
ground::grounded_movement_common_interrupts(state_context, &self.grounded_common)
|
||||
{
|
||||
return Some(state);
|
||||
|
||||
Reference in New Issue
Block a user