Port to fixed point math

This commit is contained in:
2026-08-24 21:54:23 +02:00
parent 91c5c4e05f
commit 73855ea354
27 changed files with 703 additions and 229 deletions
Generated
+1
View File
@@ -3234,6 +3234,7 @@ dependencies = [
"az", "az",
"bytemuck", "bytemuck",
"half", "half",
"serde",
"typenum", "typenum",
] ]
+1 -1
View File
@@ -29,7 +29,7 @@ serde = "1.0.229"
bitfields = "3.0.0" bitfields = "3.0.0"
clap = { version = "4.6.6", features = ["derive"] } clap = { version = "4.6.6", features = ["derive"] }
bevy_wind_waker_shader = { git = "https://github.com/lavaeater/bevy_wind_waker_shader.git" } bevy_wind_waker_shader = { git = "https://github.com/lavaeater/bevy_wind_waker_shader.git" }
fixed = "1.31.0" fixed = { version = "1.31.0", features = ["serde"] }
# Enable a small amount of optimization in the dev profile. # Enable a small amount of optimization in the dev profile.
[profile.dev] [profile.dev]
+1 -1
View File
@@ -13,7 +13,7 @@ pub struct FighterAnimationPlayerLink(Entity);
pub fn apply_fighter_translation_to_visuals(query: Query<(&FighterTranslation, &FighterECB, &mut Transform)>) { pub fn apply_fighter_translation_to_visuals(query: Query<(&FighterTranslation, &FighterECB, &mut Transform)>) {
for (translation, ecb, mut transform) in query { for (translation, ecb, mut transform) in query {
let translation = translation.0 + ecb.get_bottom_point(); let translation = translation.0 + ecb.get_bottom_point();
transform.translation = Vec3::new(translation.x, translation.y, 0.0); transform.translation = Vec3::new(translation.x.to_num(), translation.y.to_num(), 0.0);
} }
} }
+15 -14
View File
@@ -1,31 +1,32 @@
use bevy::prelude::*; use bevy::prelude::*;
use crate::input::FighterInputFrame; use crate::{input::FighterInputFrame, math::int::FGi32};
/// Per-fighter tuning values. These are character data, not global rules — /// Per-fighter tuning values. These are character data, not global rules —
/// anything that applies to every fighter belongs in /// anything that applies to every fighter belongs in
/// [`crate::game_settings::FighterSettingsCommon`] instead. /// [`crate::game_settings::FighterSettingsCommon`] instead.
#[derive(Component, Clone, Copy)] #[derive(Component, Clone, Copy)]
pub struct FighterAttributes { pub struct FighterAttributes {
pub base_walk_accel: f32, pub base_walk_accel: FGi32,
pub stick_walk_accel: f32, pub stick_walk_accel: FGi32,
pub max_walk_vel: f32, pub max_walk_vel: FGi32,
pub dash_duration: u32, pub dash_duration: u32,
pub dash_initial_velocity: f32, pub dash_initial_velocity: FGi32,
pub base_dash_accel: f32, pub base_dash_accel: FGi32,
pub stick_dash_accel: f32, pub stick_dash_accel: FGi32,
pub max_dash_vel: f32, pub max_dash_vel: FGi32,
pub jump_vertical_velocity: f32, pub jumpsquat_duration: u32,
pub jump_vertical_velocity: FGi32,
pub ground_friction: f32, pub ground_friction: FGi32,
pub terminal_velocity: f32, pub terminal_velocity: FGi32,
pub gravity: f32, pub gravity: FGi32,
} }
impl FighterAttributes { impl FighterAttributes {
pub fn get_accel_and_target_dashrun(&self, input: &FighterInputFrame) -> (f32, f32) { pub fn get_accel_and_target_dashrun(&self, input: &FighterInputFrame) -> (FGi32, FGi32) {
let accel = input.movement.x * self.stick_dash_accel; let accel = input.movement.x * self.stick_dash_accel;
let accel = accel + input.movement.x.signum() * self.base_dash_accel; let accel = accel + input.movement.x.signum() * self.base_dash_accel;
let target_vel = input.movement.x * self.max_dash_vel; let target_vel = input.movement.x * self.max_dash_vel;
@@ -33,7 +34,7 @@ impl FighterAttributes {
(accel, target_vel) (accel, target_vel)
} }
pub fn get_accel_and_target_walk(&self, input: &FighterInputFrame) -> (f32, f32) { pub fn get_accel_and_target_walk(&self, input: &FighterInputFrame) -> (FGi32, FGi32) {
let accel = input.movement.x * self.stick_walk_accel; let accel = input.movement.x * self.stick_walk_accel;
let accel = accel + input.movement.x.signum() * self.base_walk_accel; let accel = accel + input.movement.x.signum() * self.base_walk_accel;
let target_vel = input.movement.x * self.max_walk_vel; let target_vel = input.movement.x * self.max_walk_vel;
+5 -3
View File
@@ -5,6 +5,8 @@ use bevy::{color::palettes::css::HOT_PINK, prelude::*};
use crate::fighter::ecb::FighterPreviousECB; use crate::fighter::ecb::FighterPreviousECB;
use crate::fighter::state::FighterStateContext; use crate::fighter::state::FighterStateContext;
use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity}; use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity};
use crate::math::segment::FGSegment2d;
use crate::math::vec::FGVec2;
use crate::stage::StagePoly; use crate::stage::StagePoly;
use crate::stage::line::{StageCollision, StageLineID}; use crate::stage::line::{StageCollision, StageLineID};
/* /*
@@ -36,14 +38,14 @@ pub fn collide_fighter_with_scene(
pub struct AirCollisionWithStageResult { pub struct AirCollisionWithStageResult {
pub line_id: StageLineID, pub line_id: StageLineID,
pub hit_position: Vec2, pub hit_position: FGVec2,
pub hit_normal: Vec2, pub hit_normal: FGVec2,
} }
pub fn air_collide_with_stage( pub fn air_collide_with_stage(
state_context: &FighterStateContext, state_context: &FighterStateContext,
) -> Option<AirCollisionWithStageResult> { ) -> Option<AirCollisionWithStageResult> {
let ray_segment = Segment2d::new( let ray_segment = FGSegment2d::new(
state_context.prev_translation.0 + state_context.prev_ecb.get_bottom_point(), state_context.prev_translation.0 + state_context.prev_ecb.get_bottom_point(),
state_context.translation.0 + state_context.ecb.get_bottom_point(), state_context.translation.0 + state_context.ecb.get_bottom_point(),
); );
+8 -4
View File
@@ -1,16 +1,20 @@
//! Fighter debug views: gizmos and the egui inspector window. //! Fighter debug views: gizmos and the egui inspector window.
use bevy::color::palettes::css::ORANGE;
use bevy::{color::palettes::css::YELLOW, prelude::*}; use bevy::{color::palettes::css::YELLOW, prelude::*};
use bevy_egui::{EguiContexts, egui}; use bevy_egui::{EguiContexts, egui};
use crate::fighter::ecb::FighterPreviousECB;
use crate::fighter::state::StateNameDebug; use crate::fighter::state::StateNameDebug;
use crate::fighter::{FighterECB, FighterTranslation, FighterVelocity}; use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity};
use crate::input::FighterInput; use crate::input::FighterInput;
pub fn debug_draw_ecb(ecbs: Query<(&FighterECB, &FighterTranslation)>, mut gizmos: Gizmos) { pub fn debug_draw_ecb(ecbs: Query<(&FighterECB, &FighterTranslation, &FighterPreviousECB, &FighterPreviousTranslation)>, mut gizmos: Gizmos) {
for (ecb, trf) in ecbs { for (ecb, trf, ecb_prev, trf_prev) in ecbs {
let transformed = ecb.to_3d_lineloop(Transform::from_xyz(trf.x, trf.y, 0.0)); let transformed = ecb.to_3d_lineloop(Transform::from_xyz(trf.x.to_num(), trf.y.to_num(), 0.0));
gizmos.lineloop(transformed, YELLOW); gizmos.lineloop(transformed, YELLOW);
let prev_transformed = ecb_prev.to_3d_lineloop(Transform::from_xyz(trf_prev.x.to_num(), trf_prev.y.to_num(), -0.1));
gizmos.lineloop(prev_transformed, ORANGE);
} }
} }
+53 -16
View File
@@ -1,4 +1,7 @@
use bevy::prelude::*; use bevy::prelude::*;
use fixed::traits::Fixed;
use crate::math::{int::FGi32, vec::FGVec2};
/// Environment collision box: the diamond used to resolve a fighter against /// Environment collision box: the diamond used to resolve a fighter against
/// the stage. Purely geometry — the collision response lives in /// the stage. Purely geometry — the collision response lives in
@@ -6,40 +9,74 @@ use bevy::prelude::*;
#[derive(Component, Debug, Clone, Copy)] #[derive(Component, Debug, Clone, Copy)]
#[require(FighterPreviousECB)] #[require(FighterPreviousECB)]
pub struct FighterECB { pub struct FighterECB {
pub vertical_half: f32, pub vertical_half: FGi32,
pub horizontal_half: f32, pub horizontal_half: FGi32,
} }
#[derive(Component, Debug, Clone, Copy, Default)] #[derive(Component, Debug, Clone, Copy, Default)]
pub struct FighterPreviousECB { pub struct FighterPreviousECB {
pub vertical_half: f32, pub vertical_half: FGi32,
pub horizontal_half: f32, pub horizontal_half: FGi32,
}
impl From<FighterECB> for FighterPreviousECB {
fn from(ecb: FighterECB) -> Self {
Self {
vertical_half: ecb.vertical_half,
horizontal_half: ecb.horizontal_half,
}
}
}
pub fn snapshot_fighter_ecb(
fighters: Query<(&FighterECB, &mut FighterPreviousECB)>,
) {
for (ecb, mut previous_ecb) in fighters {
*previous_ecb = (*ecb).into();
}
} }
impl FighterECB { impl FighterECB {
pub fn to_3d_lineloop(&self, trf: Transform) -> [Vec3; 4] { pub fn to_3d_lineloop(&self, trf: Transform) -> [Vec3; 4] {
let rel_up = trf.transform_point(Vec3::new(0.0, self.vertical_half, 0.0)); let rel_up = trf.transform_point(Vec3::new(0.0, self.vertical_half.to_num(), 0.0));
let rel_down = trf.transform_point(Vec3::new(0.0, -self.vertical_half, 0.0)); let rel_down = trf.transform_point(Vec3::new(0.0, (-self.vertical_half).to_num(), 0.0));
let rel_left = trf.transform_point(Vec3::new(-self.horizontal_half, 0.0, 0.0)); let rel_left = trf.transform_point(Vec3::new((-self.horizontal_half).to_num(), 0.0, 0.0));
let rel_right = trf.transform_point(Vec3::new(self.horizontal_half, 0.0, 0.0)); let rel_right = trf.transform_point(Vec3::new(self.horizontal_half.to_num(), 0.0, 0.0));
[rel_up, rel_right, rel_down, rel_left] [rel_up, rel_right, rel_down, rel_left]
} }
pub fn get_bottom_point(&self) -> Vec2 { pub fn get_bottom_point(&self) -> FGVec2 {
Vec2::new(0.0, -self.vertical_half) FGVec2::new(FGi32::ZERO, -self.vertical_half)
} }
} }
impl FighterPreviousECB { impl FighterPreviousECB {
pub fn to_3d_lineloop(&self, trf: Transform) -> [Vec3; 4] { pub fn to_3d_lineloop(&self, trf: Transform) -> [Vec3; 4] {
let rel_up = trf.transform_point(Vec3::new(0.0, self.vertical_half, 0.0)); let rel_up = trf.transform_point(Vec3::new(0.0, self.vertical_half.to_num(), 0.0));
let rel_down = trf.transform_point(Vec3::new(0.0, -self.vertical_half, 0.0)); let rel_down = trf.transform_point(Vec3::new(0.0, (-self.vertical_half).to_num(), 0.0));
let rel_left = trf.transform_point(Vec3::new(-self.horizontal_half, 0.0, 0.0)); let rel_left = trf.transform_point(Vec3::new((-self.horizontal_half).to_num(), 0.0, 0.0));
let rel_right = trf.transform_point(Vec3::new(self.horizontal_half, 0.0, 0.0)); let rel_right = trf.transform_point(Vec3::new(self.horizontal_half.to_num(), 0.0, 0.0));
[rel_up, rel_right, rel_down, rel_left] [rel_up, rel_right, rel_down, rel_left]
} }
pub fn get_bottom_point(&self) -> Vec2 { pub fn get_bottom_point(&self) -> FGVec2 {
Vec2::new(0.0, -self.vertical_half) FGVec2::new(FGi32::ZERO, -self.vertical_half)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn previous_ecb_copies_current_dimensions() {
let current = FighterECB {
vertical_half: FGi32::lit("0.5"),
horizontal_half: FGi32::lit("0.25"),
};
let previous = FighterPreviousECB::from(current);
assert_eq!(previous.vertical_half, current.vertical_half);
assert_eq!(previous.horizontal_half, current.horizontal_half);
} }
} }
+7 -1
View File
@@ -45,7 +45,12 @@ impl Plugin for FighterPlugin {
// Every state gets a chance to interrupt itself, in priority order. // Every state gets a chance to interrupt itself, in priority order.
.add_systems( .add_systems(
GgrsSchedule, GgrsSchedule,
(animation::setup_fighter_animation_player, state::state_interrupt_system,).in_set(GameplaySet::Interrupt), (
ecb::snapshot_fighter_ecb.before(state_interrupt_system),
animation::setup_fighter_animation_player,
state::state_interrupt_system,
)
.in_set(GameplaySet::Interrupt),
) )
.add_systems( .add_systems(
GgrsSchedule, GgrsSchedule,
@@ -74,6 +79,7 @@ impl Plugin for FighterPlugin {
// components; a component simulated here but missing from this list // components; a component simulated here but missing from this list
// is a desync waiting to happen. // is a desync waiting to happen.
.rollback_component_with_copy::<FighterECB>() .rollback_component_with_copy::<FighterECB>()
.rollback_component_with_copy::<ecb::FighterPreviousECB>()
.rollback_component_with_copy::<FighterVelocity>() .rollback_component_with_copy::<FighterVelocity>()
.rollback_component_with_copy::<FighterTranslation>() .rollback_component_with_copy::<FighterTranslation>()
.rollback_component_with_copy::<FighterPreviousTranslation>() .rollback_component_with_copy::<FighterPreviousTranslation>()
+5 -5
View File
@@ -3,20 +3,20 @@
use bevy::prelude::*; use bevy::prelude::*;
use crate::fighter::FighterAttributes; use crate::{fighter::FighterAttributes, math::vec::FGVec2};
#[derive(Component, Clone, Copy)] #[derive(Component, Clone, Copy)]
pub struct Grounded; pub struct Grounded;
#[derive(Component, Deref, DerefMut, Default, Debug, Clone, Copy)] #[derive(Component, Deref, DerefMut, Default, Debug, Clone, Copy)]
pub struct FighterVelocity(pub Vec2); pub struct FighterVelocity(pub FGVec2);
#[derive(Component, Deref, DerefMut, Default, Clone, Copy)] #[derive(Component, Deref, DerefMut, Default, Clone, Copy)]
pub struct FighterPreviousTranslation(pub Vec2); pub struct FighterPreviousTranslation(pub FGVec2);
#[derive(Component, Deref, DerefMut, Default, Clone, Copy)] #[derive(Component, Deref, DerefMut, Default, Clone, Copy)]
#[require(FighterPreviousTranslation)] #[require(FighterPreviousTranslation)]
pub struct FighterTranslation(pub Vec2); pub struct FighterTranslation(pub FGVec2);
pub fn apply_air_motion( pub fn apply_air_motion(
fighters: Query<( fighters: Query<(
@@ -37,7 +37,7 @@ pub fn copy_fighter_transform_to_visuals(
fighters: Query<(&FighterTranslation, &mut Transform)>, fighters: Query<(&FighterTranslation, &mut Transform)>,
) { ) {
for (translation, mut transform) in fighters { for (translation, mut transform) in fighters {
transform.translation = Vec3::new(translation.x, translation.y, 0.0); transform.translation = Vec3::new(translation.x.to_num(), translation.y.to_num(), 0.0);
} }
} }
+7 -6
View File
@@ -2,7 +2,7 @@ use std::hash::{Hash, Hasher};
use bevy::prelude::*; use bevy::prelude::*;
use super::{FighterStateImpl, FighterStateContext, FighterStateTransition, ground, run, wait, walk}; use super::{FighterStateImpl, FighterStateContext, ground, run, wait, walk};
use crate::fighter::state::ground::GroundedStateCommon; use crate::fighter::state::ground::GroundedStateCommon;
use crate::fighter::state::wait::WaitState; use crate::fighter::state::wait::WaitState;
use crate::fighter::state::{FighterState}; use crate::fighter::state::{FighterState};
@@ -12,11 +12,12 @@ use crate::input::{
FighterCommands::{SmashMoveLeft, SmashMoveRight}, FighterCommands::{SmashMoveLeft, SmashMoveRight},
FighterInput, FighterInput,
}; };
use crate::math::int::FGi32;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DashState { pub struct DashState {
frames_in_dash: u32, frames_in_dash: u32,
direction: f32, direction: FGi32,
ground_common: GroundedStateCommon ground_common: GroundedStateCommon
} }
@@ -28,7 +29,7 @@ impl std::hash::Hash for DashState {
} }
impl DashState { impl DashState {
pub fn new(direction: f32, ground_common: GroundedStateCommon) -> Self { pub fn new(direction: FGi32, ground_common: GroundedStateCommon) -> Self {
Self { Self {
frames_in_dash: 0, frames_in_dash: 0,
direction, direction,
@@ -98,11 +99,11 @@ impl FighterStateImpl for DashState {
} }
} }
fn check_smash_input_with_dir(state_context: &FighterStateContext) -> Option<f32> { fn check_smash_input_with_dir(state_context: &FighterStateContext) -> Option<FGi32> {
if state_context.input.has_command(SmashMoveLeft) { if state_context.input.has_command(SmashMoveLeft) {
return Some(-1.0); return Some(FGi32::NEG_ONE);
} else if state_context.input.has_command(SmashMoveRight) { } else if state_context.input.has_command(SmashMoveRight) {
return Some(1.0); return Some(FGi32::ONE);
} else { } else {
None None
} }
+1 -1
View File
@@ -1,6 +1,6 @@
use bevy::prelude::*; use bevy::prelude::*;
use super::{FighterStateImpl, FighterStateContext, FighterStateTransition, ground, walk}; use super::{FighterStateImpl, FighterStateContext, ground, walk};
use crate::fighter::state::{FighterState, air}; use crate::fighter::state::{FighterState, air};
use crate::fighter::state::ground::GroundedStateCommon; use crate::fighter::state::ground::GroundedStateCommon;
use crate::fighter::state::wait::WaitState; use crate::fighter::state::wait::WaitState;
+21 -23
View File
@@ -7,17 +7,20 @@ use crate::fighter::{
FighterAttributes, FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity, FighterAttributes, FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity,
}; };
use crate::game_settings::GameSettings; use crate::game_settings::GameSettings;
use crate::math::int::FGi32;
use crate::math::vec::FGVec2;
use crate::stage::StagePoly; use crate::stage::StagePoly;
use crate::stage::line::{StageCollision, StageLineID, StagePolyLineSegment, StagePolyLineSegmentType}; use crate::stage::line::{StageCollision, StageLineID, StagePolyLineSegment, StagePolyLineSegmentType};
use bevy::prelude::*; use bevy::prelude::*;
use fixed::traits::Fixed;
#[derive(Reflect, Hash, Clone, Debug)] #[derive(Reflect, Hash, Clone, Debug)]
pub struct GroundedStateCommon { pub struct GroundedStateCommon {
pub current_line_id: StageLineID, pub current_line_id: StageLineID,
} }
pub fn apply_grounded_friction(friction: f32, ground_vel: f32) -> f32 { pub fn apply_grounded_friction(friction: FGi32, ground_vel: FGi32) -> FGi32 {
if friction.abs() > ground_vel.abs() { if friction.abs() > ground_vel.abs() {
-ground_vel -ground_vel
} else { } else {
@@ -28,21 +31,21 @@ pub fn apply_grounded_friction(friction: f32, ground_vel: f32) -> f32 {
pub fn move_accelerate() {} pub fn move_accelerate() {}
pub fn compute_ground_accel( pub fn compute_ground_accel(
accel: f32, accel: FGi32,
target_vel: f32, target_vel: FGi32,
gr_vel: f32, gr_vel: FGi32,
attribs: &FighterAttributes, attribs: &FighterAttributes,
game_settings: &GameSettings, game_settings: &GameSettings,
) -> f32 { ) -> FGi32 {
if target_vel == 0.0 { if target_vel == 0.0 {
apply_grounded_friction(attribs.ground_friction, gr_vel) apply_grounded_friction(attribs.ground_friction, gr_vel)
} else { } else {
let mut accel = accel; let mut accel = accel;
let ground_max_horizontal_velocity = let ground_max_horizontal_velocity =
game_settings.figher_common.ground_max_horizontal_velocity; game_settings.figher_common.ground_max_horizontal_velocity;
if !(gr_vel * accel < 0.0) { if !(gr_vel * accel < FGi32::ZERO) {
// accelerating, not reversing // accelerating, not reversing
if accel > 0.0 { if accel > FGi32::ZERO {
if gr_vel + accel > target_vel { if gr_vel + accel > target_vel {
accel = -attribs.ground_friction; accel = -attribs.ground_friction;
if gr_vel + accel < target_vel { if gr_vel + accel < target_vel {
@@ -78,7 +81,7 @@ pub fn apply_grounded_motion(
) { ) {
// Vertical velocity should be 0 on the ground // Vertical velocity should be 0 on the ground
prev_translation.0 = translation.0; prev_translation.0 = translation.0;
translation.0 += Vec2::new(velocity.0.x ,0.0); translation.0 += FGVec2::new(velocity.0.x ,FGi32::ZERO);
} }
pub fn collide_with_stage_grounded( pub fn collide_with_stage_grounded(
@@ -96,34 +99,29 @@ pub fn collide_with_stage_grounded(
if let Some(stage_poly) = stage_poly { if let Some(stage_poly) = stage_poly {
let ecb_bottom_pos = translation.0 + ecb.get_bottom_point(); let ecb_bottom_pos = translation.0 + ecb.get_bottom_point();
let left_segment_idx = ground_common let current_segment_idx = ground_common.current_line_id.segment;
.current_line_id
.segment
.checked_sub(1)
.unwrap_or(0);
let right_segment_idx =
(ground_common.current_line_id.segment + 1) % stage_poly.segments.len();
// First, we check the current segment, if that fails, we check the surrounding ones // First, we check the current segment, if that fails, we check the surrounding ones
let grounded_ecb_bottom_pos = [ let grounded_ecb_bottom_pos = [
ground_common.current_line_id.segment.clone(), Some(current_segment_idx),
left_segment_idx, stage_poly.get_prev_segment_index(current_segment_idx),
right_segment_idx, stage_poly.get_next_segment_index(current_segment_idx),
] ]
.iter() .into_iter()
.flatten()
.find_map(|segment_idx| { .find_map(|segment_idx| {
let segment = stage_poly.get_segment(*segment_idx)?; let segment = stage_poly.get_segment(segment_idx)?;
if segment.segment_type != StagePolyLineSegmentType::Floor { if segment.segment_type != StagePolyLineSegmentType::Floor {
return None; return None;
} }
let line_dir = segment.segment.direction(); let line_dir = segment.segment.direction();
let projected_point = segment.segment.point1() + (ecb_bottom_pos - segment.segment.point1()).project_onto(line_dir.as_vec2()); let projected_point = segment.segment.point1() + (ecb_bottom_pos - segment.segment.point1()).project_onto(line_dir);
let closest_point = segment.segment.closest_point(ecb_bottom_pos); let closest_point = segment.segment.closest_point(ecb_bottom_pos);
let distance = closest_point.distance(projected_point); let distance = closest_point.distance(projected_point);
if distance < 0.01 { if distance < 0.01 {
return Some((projected_point, *segment_idx, segment.normal)); return Some((projected_point, segment_idx, segment.normal));
} }
None None
+85 -20
View File
@@ -1,41 +1,106 @@
use bevy::prelude::*; use bevy::prelude::*;
use crate::fighter::state::{FighterState, FighterStateImpl}; use crate::{
fighter::state::{
FighterState, FighterStateContext, FighterStateImpl,
fall::FallState,
ground::{self, GroundedMotionResult, GroundedStateCommon},
},
input::FighterCommands,
};
pub struct JumpState { pub enum JumpType {
pub counter: u32 ShortHop,
LongJump,
DoubleJump,
} }
#[derive(Debug, Clone, Hash)]
pub struct JumpState {}
#[derive(Debug, Clone, Hash)]
pub struct JumpSquatState { pub struct JumpSquatState {
pub counter: u32 pub grounded_common: GroundedStateCommon,
pub duration_counter: u32,
} }
impl JumpSquatState {
pub fn create(grounded_common: GroundedStateCommon) -> Self {
Self {
grounded_common,
duration_counter: 0,
}
}
}
impl FighterStateImpl for JumpSquatState { impl FighterStateImpl for JumpSquatState {
const NAME: &'static str = "JumpSquat"; const NAME: &'static str = "JumpSquat";
fn check_interrupt(
&self, fn check_interrupt(&self, state_context: &FighterStateContext) -> Option<FighterState> {
state_context: &super::FighterStateContext, if self.duration_counter >= state_context.fighter_attribs.jumpsquat_duration {
) -> Option<FighterState> { Some(FighterState::Jump(JumpState {}))
None } else {
None
}
} }
fn update(&mut self, _state_context: &mut super::FighterStateContext) { fn update(&mut self, state_context: &mut FighterStateContext) {
todo!() self.duration_counter += 1;
let friction =
if state_context.velocity.x.abs() > state_context.fighter_attribs.max_walk_vel {
state_context.fighter_attribs.ground_friction
* state_context
.game_settings
.figher_common
.ground_friction_over_walk_speed_multiplier
} else {
state_context.fighter_attribs.ground_friction
};
let ground_velocity = state_context.velocity.x;
state_context.velocity.x += ground::apply_grounded_friction(friction, ground_velocity);
ground::apply_grounded_motion(
state_context.velocity,
state_context.translation,
state_context.prev_translation,
);
}
fn check_collision_interrupt(
&mut self,
state_context: &mut FighterStateContext,
) -> Option<FighterState> {
if let GroundedMotionResult::InAir =
ground::collide_with_stage_grounded(state_context, &mut self.grounded_common, false)
{
Some(FighterState::Fall(FallState))
} else {
None
}
} }
} }
impl FighterStateImpl for JumpState { impl FighterStateImpl for JumpState {
const NAME: &'static str = "Jump"; const NAME: &'static str = "Jump";
fn check_interrupt( fn check_interrupt(&self, state_context: &FighterStateContext) -> Option<FighterState> {
&self,
state_context: &super::FighterStateContext,
) -> Option<FighterState> {
None None
} }
fn update(&mut self, _state_context: &mut super::FighterStateContext) { fn update(&mut self, _state_context: &mut FighterStateContext) {}
todo!() }
}
} pub fn check_input(
state_context: &FighterStateContext,
ground_common: &GroundedStateCommon,
) -> Option<FighterState> {
state_context
.input
.has_command(FighterCommands::Jump)
.then_some(FighterState::JumpSquat(JumpSquatState::create(
ground_common.clone(),
)));
None
}
+13 -13
View File
@@ -11,6 +11,7 @@ use crate::fighter::ecb::FighterPreviousECB;
use crate::fighter::{FighterAttributes, FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity}; use crate::fighter::{FighterAttributes, FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity};
use crate::game_settings::GameSettings; use crate::game_settings::GameSettings;
use crate::input::FighterInput; use crate::input::FighterInput;
use crate::math::int::FGi32;
use crate::schedule::GameplaySet; use crate::schedule::GameplaySet;
use crate::stage::StagePoly; use crate::stage::StagePoly;
use crate::stage::line::StageCollision; use crate::stage::line::StageCollision;
@@ -24,14 +25,6 @@ pub mod fall;
pub mod air; pub mod air;
pub mod jump; pub mod jump;
pub enum FighterStateTransition {
Wait,
Walk,
Dash(f32),
Run,
Fall,
}
macro_rules! fighter_states { macro_rules! fighter_states {
($($variant:ident => $ty:ty),* $(,)?) => { ($($variant:ident => $ty:ty),* $(,)?) => {
#[derive(Component, Clone, Debug, Hash)] #[derive(Component, Clone, Debug, Hash)]
@@ -80,7 +73,7 @@ pub trait FighterStateImpl: Sized {
} }
} }
pub struct FighterStateContext<'a> { pub struct FighterStateContext<'a, 'w, 's> {
pub translation: &'a mut FighterTranslation, pub translation: &'a mut FighterTranslation,
pub prev_translation: &'a mut FighterPreviousTranslation, pub prev_translation: &'a mut FighterPreviousTranslation,
pub input: &'a mut FighterInput, pub input: &'a mut FighterInput,
@@ -91,6 +84,7 @@ pub struct FighterStateContext<'a> {
pub game_settings: &'a GameSettings, pub game_settings: &'a GameSettings,
pub entity: Entity, pub entity: Entity,
pub stage_collision: &'a StageCollision, pub stage_collision: &'a StageCollision,
pub gizmos: Option<&'a mut Gizmos<'w, 's>>,
} }
fighter_states! { fighter_states! {
@@ -98,7 +92,9 @@ fighter_states! {
Walk => walk::WalkState, Walk => walk::WalkState,
Dash => dash::DashState, Dash => dash::DashState,
Run => run::RunState, Run => run::RunState,
Fall => fall::FallState Fall => fall::FallState,
JumpSquat => jump::JumpSquatState,
Jump => jump::JumpState
} }
#[derive(Component)] #[derive(Component)]
@@ -128,6 +124,7 @@ pub fn state_update_system(
game_settings: &game_settings, game_settings: &game_settings,
entity: ent, entity: ent,
stage_collision: &stage_collision, stage_collision: &stage_collision,
gizmos: Some(&mut gizmos),
}; };
state.update(&mut update_ctx); state.update(&mut update_ctx);
} }
@@ -150,7 +147,8 @@ pub fn state_interrupt_system (
fighter_attribs: &attribs, fighter_attribs: &attribs,
game_settings: &game_settings, game_settings: &game_settings,
entity: ent, entity: ent,
stage_collision: &stage_collision stage_collision: &stage_collision,
gizmos: None,
}; };
if let Some(new_state) = state.check_interrupt(&state_context) { if let Some(new_state) = state.check_interrupt(&state_context) {
let mut new_state = new_state; let mut new_state = new_state;
@@ -171,6 +169,7 @@ pub fn state_collision_interrupt_system(
stage_collision: Res<StageCollision>, stage_collision: Res<StageCollision>,
game_settings: Res<GameSettings>, game_settings: Res<GameSettings>,
mut commands: Commands, mut commands: Commands,
mut gizmos: Gizmos,
) { ) {
for (ent, mut state, mut translation, mut prev_translation, mut input, mut velocity, mut ecb, mut prev_ecb, attribs) in query { for (ent, mut state, mut translation, mut prev_translation, mut input, mut velocity, mut ecb, mut prev_ecb, attribs) in query {
let mut state_context = FighterStateContext { let mut state_context = FighterStateContext {
@@ -183,7 +182,8 @@ pub fn state_collision_interrupt_system(
fighter_attribs: &attribs, fighter_attribs: &attribs,
game_settings: &game_settings, game_settings: &game_settings,
entity: ent, entity: ent,
stage_collision: &stage_collision stage_collision: &stage_collision,
gizmos: Some(&mut gizmos),
}; };
if let Some(new_state) = state.check_collision_interrupt(&mut state_context) { if let Some(new_state) = state.check_collision_interrupt(&mut state_context) {
let mut new_state = new_state; let mut new_state = new_state;
@@ -197,4 +197,4 @@ pub fn state_collision_interrupt_system(
info!("State {} -> {}", old_state_name, new_state_name); info!("State {} -> {}", old_state_name, new_state_name);
} }
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
use bevy::prelude::*; use bevy::prelude::*;
use super::{FighterStateImpl, FighterStateContext, FighterStateTransition, ground, wait, walk}; use super::{FighterStateImpl, FighterStateContext, ground, wait, walk};
use crate::fighter::state::FighterState; use crate::fighter::state::FighterState;
use crate::fighter::state::fall::FallState; use crate::fighter::state::fall::FallState;
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon}; use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
+5 -2
View File
@@ -1,6 +1,6 @@
use bevy::prelude::*; use bevy::prelude::*;
use super::{FighterStateImpl, FighterStateContext, FighterStateTransition, ground, walk}; use super::{FighterStateImpl, FighterStateContext, ground, walk};
use crate::fighter::state::FighterState; use crate::fighter::state::FighterState;
use crate::fighter::state::fall::FallState; use crate::fighter::state::fall::FallState;
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon}; use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
@@ -33,7 +33,10 @@ impl FighterStateImpl for WaitState {
} else { } else {
state_context.fighter_attribs.ground_friction state_context.fighter_attribs.ground_friction
}; };
state_context.velocity.x += ground::apply_grounded_friction(friction, state_context.velocity.x);
let ground_velocity = state_context.velocity.x;
state_context.velocity.x += ground::apply_grounded_friction(friction, ground_velocity);
ground::apply_grounded_motion( ground::apply_grounded_motion(
state_context.velocity, state_context.velocity,
+1 -1
View File
@@ -1,6 +1,6 @@
use bevy::prelude::*; use bevy::prelude::*;
use super::{FighterStateImpl, FighterStateContext, FighterStateTransition, dash, ground, wait}; use super::{FighterStateImpl, FighterStateContext, dash, ground, wait};
use crate::fighter::state::fall::FallState; use crate::fighter::state::fall::FallState;
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon}; use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
use crate::fighter::state::{self, FighterState}; use crate::fighter::state::{self, FighterState};
+5 -3
View File
@@ -1,9 +1,11 @@
use bevy::prelude::*; use bevy::prelude::*;
use crate::math::int::FGi32;
pub struct FighterSettingsCommon { pub struct FighterSettingsCommon {
pub walk_speed_ease: f32, pub walk_speed_ease: FGi32,
pub ground_max_horizontal_velocity: f32, pub ground_max_horizontal_velocity: FGi32,
pub ground_friction_over_walk_speed_multiplier: f32 pub ground_friction_over_walk_speed_multiplier: FGi32
} }
pub struct InputSettingsCommon { pub struct InputSettingsCommon {
pub stick_deadzone: f32, pub stick_deadzone: f32,
+4
View File
@@ -17,6 +17,7 @@ struct FighterInputBufferedActions {}
pub enum FighterCommands { pub enum FighterCommands {
SmashMoveLeft, SmashMoveLeft,
SmashMoveRight, SmashMoveRight,
Jump,
} }
/// Frames of life remaining for each buffered command. Indexed by /// Frames of life remaining for each buffered command. Indexed by
@@ -25,6 +26,7 @@ pub enum FighterCommands {
pub struct FighterCommandLifetimes { pub struct FighterCommandLifetimes {
smash_move_left: u8, smash_move_left: u8,
smash_move_right: u8, smash_move_right: u8,
jump: u8,
} }
impl Index<FighterCommands> for FighterCommandLifetimes { impl Index<FighterCommands> for FighterCommandLifetimes {
@@ -34,6 +36,7 @@ impl Index<FighterCommands> for FighterCommandLifetimes {
match command { match command {
FighterCommands::SmashMoveLeft => &self.smash_move_left, FighterCommands::SmashMoveLeft => &self.smash_move_left,
FighterCommands::SmashMoveRight => &self.smash_move_right, FighterCommands::SmashMoveRight => &self.smash_move_right,
FighterCommands::Jump => &self.jump,
} }
} }
} }
@@ -43,6 +46,7 @@ impl IndexMut<FighterCommands> for FighterCommandLifetimes {
match command { match command {
FighterCommands::SmashMoveLeft => &mut self.smash_move_left, FighterCommands::SmashMoveLeft => &mut self.smash_move_left,
FighterCommands::SmashMoveRight => &mut self.smash_move_right, FighterCommands::SmashMoveRight => &mut self.smash_move_right,
FighterCommands::Jump => &mut self.jump,
} }
} }
} }
+4 -2
View File
@@ -1,10 +1,12 @@
use bevy::prelude::*; use bevy::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::math::vec::FGVec2;
/// One frame of a player's raw input. This is the type GGRS serializes and /// One frame of a player's raw input. This is the type GGRS serializes and
/// sends over the wire, so keep it small and keep it `PartialEq`. /// sends over the wire, so keep it small and keep it `PartialEq`.
#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
pub struct FighterInputFrame { pub struct FighterInputFrame {
pub movement: Vec2, pub movement: FGVec2,
pub directional_attack: Vec2, pub directional_attack: FGVec2,
} }
+5 -4
View File
@@ -6,6 +6,7 @@ use bevy::prelude::*;
use bevy_ggrs::{LocalInputs, LocalPlayers}; use bevy_ggrs::{LocalInputs, LocalPlayers};
use crate::input::{BaseInputMap, FighterInputFrame, InputActionState, InputMapAction}; use crate::input::{BaseInputMap, FighterInputFrame, InputActionState, InputMapAction};
use crate::math::int::FGi32;
use crate::netcode::GGRSCfg; use crate::netcode::GGRSCfg;
pub fn preprocess_keyboard_input( pub fn preprocess_keyboard_input(
@@ -32,16 +33,16 @@ pub fn preprocess_keyboard_input(
}; };
let action_strength = match action_state { let action_strength = match action_state {
InputActionState::JustPressed | InputActionState::Pressed => 1.0f32, InputActionState::JustPressed | InputActionState::Pressed => FGi32::ONE,
InputActionState::JustReleased | InputActionState::Released => 0.0f32, InputActionState::JustReleased | InputActionState::Released => FGi32::ZERO,
}; };
match keyboard_elment.action { match keyboard_elment.action {
InputMapAction::MovementXDir(sign) => { InputMapAction::MovementXDir(sign) => {
input_frame.movement.x += (sign as f32) * action_strength input_frame.movement.x += (sign) * action_strength
} }
InputMapAction::MovementYDir(sign) => { InputMapAction::MovementYDir(sign) => {
input_frame.movement.y += (sign as f32) * action_strength input_frame.movement.y += (sign) * action_strength
} }
InputMapAction::Jump => todo!(), InputMapAction::Jump => todo!(),
} }
+18 -18
View File
@@ -29,7 +29,7 @@ use jackdaw_runtime::prelude::*;
use crate::{ use crate::{
fighter::{ fighter::{
FighterAttributes, FighterECB, FighterTranslation, FighterVelocity, FighterVisual, animation::FighterAnimations, state::{FighterState, fall::FallState, wait::WaitState}, FighterAttributes, FighterECB, FighterTranslation, FighterVelocity, FighterVisual, animation::FighterAnimations, state::{FighterState, fall::FallState, wait::WaitState},
}, game_settings::{FighterSettingsCommon, GameSettings, InputSettingsCommon}, input::FighterInput, player::Player, schedule::GameplaySchedulePlugin, }, game_settings::{FighterSettingsCommon, GameSettings, InputSettingsCommon}, input::FighterInput, math::{int::FGi32, vec::FGVec2}, player::Player, schedule::GameplaySchedulePlugin,
}; };
mod args; mod args;
@@ -62,24 +62,24 @@ pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
Name("Fighter".into()), Name("Fighter".into()),
Player { handle: 0 }, Player { handle: 0 },
FighterECB { FighterECB {
vertical_half: 0.5, vertical_half: FGi32::lit("0.5"),
horizontal_half: 0.25, horizontal_half: FGi32::lit("0.25"),
}, },
FighterVelocity::default(), FighterVelocity::default(),
FighterTranslation(Vec2::new(0.5, 1.25)), FighterTranslation(FGVec2::lit("0.5", "1.25")),
FighterAttributes { FighterAttributes {
base_walk_accel: 0.01, base_walk_accel: FGi32::lit("0.01"),
stick_walk_accel: 0.02, stick_walk_accel: FGi32::lit("0.02"),
max_walk_vel: 0.16, max_walk_vel: FGi32::lit("0.16"),
ground_friction: 0.008, ground_friction: FGi32::lit("0.008"),
dash_duration: 15, dash_duration: 15,
base_dash_accel: 0.002, base_dash_accel: FGi32::lit("0.002"),
stick_dash_accel: 0.01, stick_dash_accel: FGi32::lit("0.01"),
max_dash_vel: 0.22, max_dash_vel: FGi32::lit("0.22"),
jump_vertical_velocity: 0.25, jump_vertical_velocity: FGi32::lit("0.25"),
dash_initial_velocity: 0.19, dash_initial_velocity: FGi32::lit("0.19"),
terminal_velocity: 0.28, terminal_velocity: FGi32::lit("0.28"),
gravity: 0.023, gravity: FGi32::lit("0.023"),
}, },
FighterState::Fall(FallState), FighterState::Fall(FallState),
FighterInput::default(), FighterInput::default(),
@@ -115,9 +115,9 @@ impl Plugin for GamePlugin {
.insert_resource(args::Args::parse()) .insert_resource(args::Args::parse())
.insert_resource(GameSettings { .insert_resource(GameSettings {
figher_common: FighterSettingsCommon { figher_common: FighterSettingsCommon {
walk_speed_ease: 0.5, walk_speed_ease: FGi32::lit("0.5"),
ground_max_horizontal_velocity: 0.3, ground_max_horizontal_velocity: FGi32::lit("0.3"),
ground_friction_over_walk_speed_multiplier: 2.0, ground_friction_over_walk_speed_multiplier: FGi32::lit("2.0"),
}, },
input_common: InputSettingsCommon::default(), input_common: InputSettingsCommon::default(),
}); });
+79 -34
View File
@@ -1,12 +1,13 @@
use bevy::math::{Vec2, primitives::Segment2d}; use bevy::math::{Vec2, primitives::Segment2d};
use bevy::prelude::*;
use fixed::prelude::*; use fixed::prelude::*;
use crate::math::vec::FGVec2; use crate::math::vec::FGVec2;
type FGi32 = fixed::FixedI32<fixed::types::extra::U16>; type FGi32 = fixed::FixedI32<fixed::types::extra::U16>;
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq, Reflect)]
pub struct FGSegment2d { pub struct FGSegment2d {
point1: FGVec2, point1: FGVec2,
point2: FGVec2 point2: FGVec2
@@ -29,14 +30,72 @@ impl FGSegment2d {
pub fn point2(&self) -> FGVec2 { pub fn point2(&self) -> FGVec2 {
self.point2 self.point2
} }
}
pub trait SegmentIntersection { /// Compute the normalized direction pointing from the first endpoint to the second endpoint.
fn segment_intersection(&self, rhs: &Segment2d) -> Option<FGVec2>; ///
} /// For the non-panicking version, see [`FGVec2::try_direction`].
///
/// # Panics
///
/// Panics if a valid direction could not be computed, for example when the endpoints are coincident, NaN, or infinite.
#[inline]
pub fn direction(&self) -> FGVec2 {
self.try_direction().expect("Failed to compute the direction of a line segment")
}
impl SegmentIntersection for Segment2d { /// Try to compute the normalized direction pointing from the first endpoint to the second endpoint.
fn segment_intersection(&self, rhs: &Segment2d) -> Option<Vec2> { ///
/// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if a valid direction could not be computed,
/// for example when the endpoints are coincident, NaN, or infinite.
#[inline]
pub fn try_direction(&self) -> Option<FGVec2> {
self.point1.direction_to(self.point2)
}
/// Returns the point on the [`Segment2d`] that is closest to the specified `point`.
#[inline]
pub fn closest_point(&self, point: FGVec2) -> FGVec2 {
// `point`
// x
// ^|
// / |
//`offset`/ |
// / | `segment_vector`
// x----.-------------->x
// 0 t 1
let segment_vector = self.point2 - self.point1;
let offset = point - self.point1;
// The signed projection of `offset` onto `segment_vector`, scaled by the length of the segment.
let projection_scaled = segment_vector.dot(offset);
// `point` is too far "left" in the picture
if projection_scaled <= 0.0 {
return self.point1;
}
let length_squared = segment_vector.length_squared();
// `point` is too far "right" in the picture
if projection_scaled >= length_squared {
return self.point2;
}
// Point lies somewhere in the middle, we compute the closest point by finding the parameter along the line.
let t = projection_scaled / length_squared;
self.point1 + segment_vector * t
}
/// Compute the normalized counterclockwise normal on the left-hand side of the line segment.
///
/// For the non-panicking version, see [`Segment2d::try_left_normal`].
///
/// # Panics
///
/// Panics if a valid normal could not be computed, for example when the endpoints are coincident
pub fn left_normal(&self) -> FGVec2 {
self.point1.direction_to(self.point2).map(|o| { FGVec2::new(-o.y, o.x)}).expect("a valid normal could not be computed")
}
pub fn segment_intersection(&self, rhs: &FGSegment2d) -> Option<FGVec2> {
let p0 = self.point1(); let p0 = self.point1();
let p1 = self.point2(); let p1 = self.point2();
let p2 = rhs.point1(); let p2 = rhs.point1();
@@ -48,45 +107,31 @@ impl SegmentIntersection for Segment2d {
let s2_y = p3.y - p2.y; let s2_y = p3.y - p2.y;
let denom = -s2_x * s1_y + s1_x * s2_y; let denom = -s2_x * s1_y + s1_x * s2_y;
if denom.abs() < f32::EPSILON { if denom == FGi32::ZERO {
return None; // Parallel or collinear return None; // Parallel or collinear
} }
let s = (-s1_y * (p0.x - p2.x) + s1_x * (p0.y - p2.y)) / denom; let s = (-s1_y * (p0.x - p2.x) + s1_x * (p0.y - p2.y)) / denom;
let t = ( s2_x * (p0.y - p2.y) - s2_y * (p0.x - p2.x)) / denom; let t = ( s2_x * (p0.y - p2.y) - s2_y * (p0.x - p2.x)) / denom;
if s >= 0.0 && s <= 1.0 && t >= 0.0 && t <= 1.0 { if s >= FGi32::ZERO && s <= FGi32::ONE && t >= FGi32::ZERO && t <= FGi32::ONE {
Some(Vec2::new(p0.x + (t * s1_x), p0.y + (t * s1_y))) Some(FGVec2::new(p0.x + (t * s1_x), p0.y + (t * s1_y)))
} else { } else {
None None
} }
} }
} }
impl SegmentIntersection for Segment2d { #[cfg(test)]
fn segment_intersection(&self, rhs: &Segment2d) -> Option<Vec2> { mod tests {
let p0 = self.point1(); use super::*;
let p1 = self.point2();
let p2 = rhs.point1();
let p3 = rhs.point2();
let s1_x = p1.x - p0.x;
let s1_y = p1.y - p0.y;
let s2_x = p3.x - p2.x;
let s2_y = p3.y - p2.y;
let denom = -s2_x * s1_y + s1_x * s2_y; #[test]
if denom.abs() < f32::EPSILON { fn closest_point_projects_and_clamps_to_segment() {
return None; // Parallel or collinear let segment = FGSegment2d::new(FGVec2::lit("-4", "0"), FGVec2::lit("4", "0"));
}
let s = (-s1_y * (p0.x - p2.x) + s1_x * (p0.y - p2.y)) / denom; assert_eq!(segment.closest_point(FGVec2::lit("2", "1")), FGVec2::lit("2", "0"));
let t = ( s2_x * (p0.y - p2.y) - s2_y * (p0.x - p2.x)) / denom; assert_eq!(segment.closest_point(FGVec2::lit("-6", "1")), segment.point1());
assert_eq!(segment.closest_point(FGVec2::lit("6", "1")), segment.point2());
if s >= 0.0 && s <= 1.0 && t >= 0.0 && t <= 1.0 {
Some(Vec2::new(p0.x + (t * s1_x), p0.y + (t * s1_y)))
} else {
None
}
} }
} }
+206 -8
View File
@@ -1,14 +1,24 @@
use bevy::math::Vec2; use bevy::math::Vec2;
use std::ops::{Add, Div, Mul, Sub}; use bevy::prelude::*;
use fixed::types::I32F32;
use serde::{Deserialize, Serialize};
use std::ops::{AddAssign, Add, Div, Mul, Sub};
use super::int::FGi32; use super::int::FGi32;
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq, Default, Debug, Reflect, Serialize, Deserialize)]
#[reflect(opaque)]
pub struct FGVec2 { pub struct FGVec2 {
pub x: FGi32, pub x: FGi32,
pub y: FGi32, pub y: FGi32,
} }
impl std::fmt::Display for FGVec2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}, {}]", self.x, self.y)
}
}
impl FGVec2 { impl FGVec2 {
pub const ZERO: Self = Self::new(FGi32::ZERO, FGi32::ZERO); pub const ZERO: Self = Self::new(FGi32::ZERO, FGi32::ZERO);
@@ -17,6 +27,11 @@ impl FGVec2 {
Self { x, y } Self { x, y }
} }
#[inline(always)]
pub const fn lit(x: &str, y: &str) -> Self {
Self { x: FGi32::lit(x), y: FGi32::lit(y) }
}
#[inline(always)] #[inline(always)]
pub fn from_i32(x: i32, y: i32) -> Self { pub fn from_i32(x: i32, y: i32) -> Self {
Self { Self {
@@ -31,17 +46,78 @@ impl FGVec2 {
} }
#[inline] #[inline]
pub fn length(&self) -> FGi32 { pub fn length(self) -> FGi32 {
(self.x * self.x + self.y * self.y).sqrt() (self.x * self.x + self.y * self.y).sqrt()
} }
/// Returns the vector projection of `self` onto `rhs`.
///
/// `rhs` must be normalized.
///
/// # Panics
///
/// Will panic if `rhs` is not normalized when `glam_assert` is enabled.
#[inline] #[inline]
pub fn normalized_or_zero(&self) -> Self { #[must_use]
let len = self.length(); pub fn project_onto_normalized(self, rhs: Self) -> Self {
if len.is_zero() { assert!(rhs.is_normalized());
Self::ZERO
rhs * self.dot(rhs)
}
const EPS: FGi32 = FGi32::from_bits(6);
#[inline]
#[must_use]
pub fn is_normalized(self) -> bool {
let diff = self.length() - FGi32::ONE;
diff.abs() <= Self::EPS
}
#[inline]
#[must_use]
pub fn dot(self, rhs: Self) -> FGi32 {
(self.x * rhs.x) + (self.y * rhs.y)
}
#[inline]
#[must_use]
pub fn project_onto(self, rhs: Self) -> Self {
let other_len_sq = rhs.dot(rhs);
assert!(other_len_sq != FGi32::ZERO);
rhs * (self.dot(rhs) / other_len_sq)
}
#[inline]
pub fn normalize_or_zero(&self) -> Self {
self.normalize().unwrap_or(FGVec2::ZERO)
}
#[inline]
pub fn direction_to(self, to: Self) -> Option<Self> {
(to - self).normalize()
}
#[inline]
#[must_use]
pub fn distance(self, to: Self) -> FGi32 {
(to - self).length()
}
#[inline]
pub fn normalize(&self) -> Option<Self> {
let x = I32F32::from_num(self.x);
let y = I32F32::from_num(self.y);
let length_squared = x * x + y * y;
if length_squared.is_zero() {
None
} else { } else {
self / len let length = length_squared.sqrt();
Some(Self::new(
FGi32::from_num(x / length),
FGi32::from_num(y / length),
))
} }
} }
@@ -78,6 +154,14 @@ impl Sub for FGVec2 {
} }
} }
impl Sub<&FGVec2> for &FGVec2 {
type Output = FGVec2;
#[inline]
fn sub(self, rhs: &FGVec2) -> FGVec2 {
(*self).sub(*rhs)
}
}
impl Div for FGVec2 { impl Div for FGVec2 {
type Output = Self; type Output = Self;
@@ -113,3 +197,117 @@ impl Div<FGi32> for &FGVec2 {
(*self).div(rhs) (*self).div(rhs)
} }
} }
impl AddAssign for FGVec2 {
fn add_assign(&mut self, rhs: Self) {
*self = Self {
x: self.x + rhs.x,
y: self.y + rhs.y
}
}
}
#[cfg(test)]
mod is_normalized_tests {
use super::*;
use fixed::types::I16F16 as FGi32;
// Normalization is computed in Q32.32, so the Q16.16 error comes from the
// two final component conversions and length_squared()'s multiplications.
const EPS_FIXED_BITS: i32 = 4;
#[test]
fn normalizes_small_axis_vector_without_losing_squared_precision() {
let normalized = FGVec2::lit("0.01", "0").normalize().unwrap();
assert_eq!(normalized, FGVec2::lit("1", "0"));
assert!(normalized.is_normalized());
}
#[test]
fn measure_normalize_error_bound() {
let mut max_abs_diff_bits: i32 = 0;
let mut worst_case: Option<(f64, f64, i32)> = None;
let angle_steps = 3600; // 0.1 degree resolution
let magnitudes: &[f64] = &[
0.001, 0.01, 0.1, 0.5, 1.0, 2.0, 10.0, 100.0, 1000.0, 30000.0,
];
for &mag in magnitudes {
for i in 0..angle_steps {
let theta = (i as f64) * std::f64::consts::TAU / (angle_steps as f64);
let x = mag * theta.cos();
let y = mag * theta.sin();
let Some(fx) = FGi32::checked_from_num(x) else { continue };
let Some(fy) = FGi32::checked_from_num(y) else { continue };
let v = FGVec2 { x: fx, y: fy };
if let Some(normalized) = v.normalize() {
let ls = normalized.length_squared();
let diff_bits = ls.to_bits() - FGi32::ONE.to_bits();
let abs_diff_bits = diff_bits.abs();
if abs_diff_bits > max_abs_diff_bits {
max_abs_diff_bits = abs_diff_bits;
worst_case = Some((x, y, abs_diff_bits));
}
}
}
}
let ulp_value = FGi32::DELTA.to_num::<f64>();
println!(
"Max |length_squared - 1.0| = {} raw bits ({:e} real), worst case: {:?}",
max_abs_diff_bits,
max_abs_diff_bits as f64 * ulp_value,
worst_case
);
assert!(
max_abs_diff_bits <= EPS_FIXED_BITS,
"measured error ({} bits) exceeds analytical bound ({} bits) — \
revisit the epsilon derivation",
max_abs_diff_bits,
EPS_FIXED_BITS
);
println!(
"Bound: {} bits, measured: {} bits (headroom: {} bits)",
EPS_FIXED_BITS,
max_abs_diff_bits,
EPS_FIXED_BITS - max_abs_diff_bits
);
}
#[test]
fn is_normalized_behaves_correctly() {
let angle_steps = 360;
for i in 0..angle_steps {
let theta = (i as f64) * std::f64::consts::TAU / (angle_steps as f64);
for &mag in &[0.01, 1.0, 100.0, 10000.0] {
let x = mag * theta.cos();
let y = mag * theta.sin();
let Some(fx) = FGi32::checked_from_num(x) else { continue };
let Some(fy) = FGi32::checked_from_num(y) else { continue };
let v = FGVec2 { x: fx, y: fy };
if let Some(normalized) = v.normalize() {
assert!(
normalized.is_normalized(),
"normalized({}, {}) failed is_normalized(): length_squared = {:?}",
x, y, normalized.length_squared()
);
}
}
}
let non_unit = FGVec2 { x: FGi32::from_num(2.0), y: FGi32::from_num(0.0) };
assert!(!non_unit.is_normalized());
let non_unit_small = FGVec2 { x: FGi32::from_num(0.5), y: FGi32::from_num(0.0) };
assert!(!non_unit_small.is_normalized());
}
}
+3 -3
View File
@@ -5,11 +5,11 @@ use crate::stage::{StagePoly, line::StageCollision};
pub fn debug_draw_scene(mut gizmos: Gizmos, stage_collision: Res<StageCollision>) { pub fn debug_draw_scene(mut gizmos: Gizmos, stage_collision: Res<StageCollision>) {
for poly in stage_collision.stage_polys.iter() { for poly in stage_collision.stage_polys.iter() {
for segment in poly.segments.iter() { for segment in poly.segments.iter() {
let from = segment.segment.point1(); let from = segment.segment.point1().to_vec2();
let to = segment.segment.point2(); let to = segment.segment.point2().to_vec2();
let from_3d = Vec3::new(from.x, from.y, 0.0); let from_3d = Vec3::new(from.x, from.y, 0.0);
let end_3d = Vec3::new(to.x, to.y, 0.0); let end_3d = Vec3::new(to.x, to.y, 0.0);
let normal_3d = Vec3::new(segment.normal.x, segment.normal.y, 0.0); let normal_3d = Vec3::new(segment.normal.x.to_num(), segment.normal.y.to_num(), 0.0);
gizmos.line(from_3d, end_3d, RED); gizmos.line(from_3d, end_3d, RED);
let midpoint = (from_3d + end_3d) * 0.5; let midpoint = (from_3d + end_3d) * 0.5;
gizmos.arrow(midpoint, midpoint + normal_3d, RED); gizmos.arrow(midpoint, midpoint + normal_3d, RED);
+141 -37
View File
@@ -4,7 +4,7 @@ use std::default;
use bevy::{math::InvalidDirectionError, prelude::*}; use bevy::{math::InvalidDirectionError, prelude::*};
use crate::math::segment::SegmentIntersection; use crate::math::{segment::{FGSegment2d}, vec::FGVec2};
#[derive(Reflect, Clone, Copy, PartialEq)] #[derive(Reflect, Clone, Copy, PartialEq)]
pub enum StagePolyLineSegmentType { pub enum StagePolyLineSegmentType {
@@ -14,8 +14,8 @@ pub enum StagePolyLineSegmentType {
#[derive(Reflect, Clone)] #[derive(Reflect, Clone)]
pub struct StagePolyLineSegment { pub struct StagePolyLineSegment {
pub segment: Segment2d, pub segment: FGSegment2d,
pub normal: Vec2, pub normal: FGVec2,
pub segment_type: StagePolyLineSegmentType pub segment_type: StagePolyLineSegmentType
} }
@@ -40,9 +40,9 @@ pub struct StagePoly {
} }
pub struct StagePlaneIntersectResult { pub struct StagePlaneIntersectResult {
pub position: Vec2, pub position: FGVec2,
pub segment_idx: usize, pub segment_idx: usize,
pub normal: Vec2 pub normal: FGVec2
} }
#[derive(Resource, Reflect, Clone, Default)] #[derive(Resource, Reflect, Clone, Default)]
@@ -52,18 +52,30 @@ pub struct StageCollision {
} }
impl StagePoly { impl StagePoly {
pub fn intersect_ray(&self, ray_segment: Segment2d) -> Option<StagePlaneIntersectResult> { pub fn intersect_ray(&self, ray_segment: FGSegment2d) -> Option<StagePlaneIntersectResult> {
for (i, segment) in self.segments.iter().enumerate() { let ray_start = ray_segment.point1();
if let Some(intersection) = segment.segment.segment_intersection(&ray_segment) { let (segment_idx, segment, position) = self
println!("DIST3 {} {}", intersection, segment.segment.closest_point(intersection)); .segments
return Some(StagePlaneIntersectResult { .iter()
position: intersection, .enumerate()
segment_idx: i, .filter_map(|(segment_idx, segment)| {
normal: segment.normal segment
}); .segment
} .segment_intersection(&ray_segment)
} .map(|position| (segment_idx, segment, position))
None })
.min_by_key(|(_, _, position)| {
let offset = *position - ray_start;
let x = i128::from(offset.x.to_bits());
let y = i128::from(offset.y.to_bits());
x * x + y * y
})?;
Some(StagePlaneIntersectResult {
position,
segment_idx,
normal: segment.normal,
})
} }
pub fn get_segment(&self, segment_idx: usize) -> Option<&StagePolyLineSegment> { pub fn get_segment(&self, segment_idx: usize) -> Option<&StagePolyLineSegment> {
@@ -71,24 +83,65 @@ impl StagePoly {
} }
pub fn get_next_segment(&self, segment_idx: usize) -> Option<&StagePolyLineSegment> { pub fn get_next_segment(&self, segment_idx: usize) -> Option<&StagePolyLineSegment> {
self.segments.get(segment_idx % segment_idx + 1) self.get_next_segment_index(segment_idx)
.and_then(|index| self.segments.get(index))
} }
pub fn get_prev_segment(&self, segment_idx: usize) -> Option<&StagePolyLineSegment> { pub fn get_prev_segment(&self, segment_idx: usize) -> Option<&StagePolyLineSegment> {
self.segments.get(segment_idx % segment_idx + 1) self.get_prev_segment_index(segment_idx)
.and_then(|index| self.segments.get(index))
} }
pub fn build(poly_type: StagePolyType, points: &[(Vec2, StagePolyLineSegmentType)]) -> Self { pub fn get_next_segment_index(&self, segment_idx: usize) -> Option<usize> {
let windows = points.iter().zip(points.iter().cycle().skip(1)); if segment_idx >= self.segments.len() {
return None;
}
let next = segment_idx + 1;
if next < self.segments.len() {
Some(next)
} else if matches!(&self.poly_type, StagePolyType::Closed) {
Some(0)
} else {
None
}
}
pub fn get_prev_segment_index(&self, segment_idx: usize) -> Option<usize> {
if segment_idx >= self.segments.len() {
return None;
}
if segment_idx > 0 {
Some(segment_idx - 1)
} else if matches!(&self.poly_type, StagePolyType::Closed) {
self.segments.len().checked_sub(1)
} else {
None
}
}
pub fn build(poly_type: StagePolyType, points: &[(FGVec2, StagePolyLineSegmentType)]) -> Self {
let segment_count = if points.len() < 2 {
0
} else if matches!(&poly_type, StagePolyType::Closed) {
points.len()
} else {
points.len() - 1
};
let windows = points
.iter()
.zip(points.iter().cycle().skip(1))
.take(segment_count);
let segments = windows.map(|segment_window| { let segments = windows.map(|segment_window| {
let (first_point, segment_type) = segment_window.0; let (first_point, segment_type) = segment_window.0;
let (second_point, _) = segment_window.1; let (second_point, _) = segment_window.1;
let segment = Segment2d::new(*first_point, *second_point); let segment = FGSegment2d::new(*first_point, *second_point);
StagePolyLineSegment { StagePolyLineSegment {
segment_type: *segment_type, segment_type: *segment_type,
segment, segment,
normal: segment.left_normal().as_vec2() normal: segment.left_normal()
} }
}).collect(); }).collect();
@@ -100,46 +153,97 @@ impl StagePoly {
} }
impl StagePolyLineSegment { impl StagePolyLineSegment {
pub fn project(&self, vec: Vec2) -> Vec2 { pub fn project(&self, vec: FGVec2) -> FGVec2 {
vec - vec.project_onto_normalized(self.normal) vec - vec.project_onto_normalized(self.normal)
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::math::int::FGi32;
use super::*; use super::*;
fn stage_segment(point1: FGVec2, point2: FGVec2) -> StagePolyLineSegment {
let segment = FGSegment2d::new(point1, point2);
StagePolyLineSegment {
segment,
normal: segment.left_normal(),
segment_type: StagePolyLineSegmentType::Floor,
}
}
#[test]
fn platform_is_open_and_closed_poly_wraps() {
let points = [
(FGVec2::lit("0", "0"), StagePolyLineSegmentType::Floor),
(FGVec2::lit("1", "0"), StagePolyLineSegmentType::Floor),
(FGVec2::lit("2", "0"), StagePolyLineSegmentType::Floor),
];
let platform = StagePoly::build(StagePolyType::Platform, &points);
let closed = StagePoly::build(StagePolyType::Closed, &points);
assert_eq!(platform.segments.len(), 2);
assert_eq!(platform.get_next_segment_index(0), Some(1));
assert_eq!(platform.get_next_segment_index(1), None);
assert_eq!(platform.get_prev_segment_index(0), None);
assert_eq!(platform.get_prev_segment_index(1), Some(0));
assert_eq!(closed.segments.len(), 3);
assert_eq!(closed.get_next_segment_index(2), Some(0));
assert_eq!(closed.get_prev_segment_index(0), Some(2));
}
#[test]
fn ray_intersection_returns_nearest_hit() {
let stage_poly = StagePoly {
poly_type: StagePolyType::Platform,
segments: vec![
stage_segment(FGVec2::lit("-1", "0"), FGVec2::lit("1", "0")),
stage_segment(FGVec2::lit("-1", "1"), FGVec2::lit("1", "1")),
],
};
let hit = stage_poly
.intersect_ray(FGSegment2d::new(FGVec2::lit("0", "2"), FGVec2::lit("0", "-1")))
.unwrap();
assert_eq!(hit.segment_idx, 1);
assert_eq!(hit.position, FGVec2::lit("0", "1"));
}
#[test] #[test]
fn ray_intersection_test() { fn ray_intersection_test() {
let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[ let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[
(Vec2::new(-4.5, 0.0), StagePolyLineSegmentType::Floor), (FGVec2::lit("-4.5", "0.0"), StagePolyLineSegmentType::Floor),
(Vec2::new(4.5, 0.0), StagePolyLineSegmentType::Floor), (FGVec2::lit("4.5", "0.0"), StagePolyLineSegmentType::Floor),
]); ]);
let res = stage_poly.intersect_ray(Segment2d::new(Vec2::new(0.0, 1.0), Vec2::new(0.0, -1.0))); let res = stage_poly.intersect_ray(FGSegment2d::new(FGVec2::lit("0.0", "1.0"), FGVec2::lit("0.0", "-1.0")));
assert!(res.is_some()); assert!(res.is_some());
assert_eq!(res.unwrap().position, Vec2::ZERO); assert_eq!(res.unwrap().position, FGVec2::ZERO);
} }
#[test] #[test]
fn ray_intersection_test_side() { fn ray_intersection_test_side() {
let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[ let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[
(Vec2::new(-4.5, 0.0), StagePolyLineSegmentType::Floor), (FGVec2::lit("-4.5", "0.0"), StagePolyLineSegmentType::Floor),
(Vec2::new(4.5, 0.0), StagePolyLineSegmentType::Floor), (FGVec2::lit("4.5", "0.0"), StagePolyLineSegmentType::Floor),
]); ]);
let res = stage_poly.intersect_ray(Segment2d::new(Vec2::new(2.0, 1.0), Vec2::new(2.0, -1.0))); let res = stage_poly.intersect_ray(FGSegment2d::new(FGVec2::lit("2.0", "1.0"), FGVec2::lit("2.0", "-1.0")));
assert!(res.is_some()); assert!(res.is_some());
assert_eq!(res.unwrap().position, Vec2::new(2.0, 0.0)); assert!(res.unwrap().position.distance(FGVec2::lit("2.0", "0.0")) < FGi32::from_bits(4));
} }
#[test] #[test]
fn ray_intersection_test_diagonal() { fn ray_intersection_test_diagonal() {
let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[ let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[
(Vec2::new(0.0, 0.0), StagePolyLineSegmentType::Floor), (FGVec2::lit("0.0", "0.0"), StagePolyLineSegmentType::Floor),
(Vec2::new(2.0, 2.0), StagePolyLineSegmentType::Floor), (FGVec2::lit("2.0", "2.0"), StagePolyLineSegmentType::Floor),
]); ]);
let res = stage_poly.intersect_ray(Segment2d::new(Vec2::new(1.0, 2.0), Vec2::new(1.0, -2.0))); let res = stage_poly.intersect_ray(FGSegment2d::new(FGVec2::lit("1.0", "2.0"), FGVec2::lit("1.0", "-2.0")));
assert!(res.is_some()); assert!(res.is_some());
assert_eq!(res.unwrap().position, Vec2::new(1.0, 1.0)); assert_eq!(res.unwrap().position, FGVec2::lit("1.0", "1.0"));
} }
} }
+8 -8
View File
@@ -4,19 +4,19 @@ use std::f32::consts::PI;
use bevy::{camera_controller::free_camera::FreeCamera, prelude::*}; use bevy::{camera_controller::free_camera::FreeCamera, prelude::*};
use crate::stage::{StagePoly, line::StagePolyLineSegmentType, line::StageCollision}; use crate::{math::vec::FGVec2, stage::{StagePoly, line::{StageCollision, StagePolyLineSegmentType}}};
/// set up a simple 3D scene /// set up a simple 3D scene
pub fn test_scene() -> impl SceneList { pub fn test_scene() -> impl SceneList {
let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Closed, &[ let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Closed, &[
(Vec2::new(-5.6, -0.35), StagePolyLineSegmentType::Floor), (FGVec2::lit("-5.6", "-0.35"), StagePolyLineSegmentType::Floor),
(Vec2::new(-3.92, 0.0), StagePolyLineSegmentType::Floor), (FGVec2::lit("-3.92", "0.0"), StagePolyLineSegmentType::Floor),
(Vec2::new(0.0, 0.0), StagePolyLineSegmentType::Floor), (FGVec2::lit("0.0", "0.0"), StagePolyLineSegmentType::Floor),
(Vec2::new(3.92, 0.0), StagePolyLineSegmentType::Floor), (FGVec2::lit("3.92", "0.0"), StagePolyLineSegmentType::Floor),
(Vec2::new(5.6, -0.35), StagePolyLineSegmentType::Wall), (FGVec2::lit("5.6", "-0.35"), StagePolyLineSegmentType::Wall),
(Vec2::new(5.6, -20.0), StagePolyLineSegmentType::Ceiling), (FGVec2::lit("5.6", "-20.0"), StagePolyLineSegmentType::Ceiling),
(Vec2::new(-5.6, -20.0), StagePolyLineSegmentType::Wall) (FGVec2::lit("-5.6", "-20.0"), StagePolyLineSegmentType::Wall)
]); ]);
bsn_list! [ bsn_list! [