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",
"bytemuck",
"half",
"serde",
"typenum",
]
+1 -1
View File
@@ -29,7 +29,7 @@ serde = "1.0.229"
bitfields = "3.0.0"
clap = { version = "4.6.6", features = ["derive"] }
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.
[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)>) {
for (translation, ecb, mut transform) in query {
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 crate::input::FighterInputFrame;
use crate::{input::FighterInputFrame, math::int::FGi32};
/// Per-fighter tuning values. These are character data, not global rules —
/// anything that applies to every fighter belongs in
/// [`crate::game_settings::FighterSettingsCommon`] instead.
#[derive(Component, Clone, Copy)]
pub struct FighterAttributes {
pub base_walk_accel: f32,
pub stick_walk_accel: f32,
pub max_walk_vel: f32,
pub base_walk_accel: FGi32,
pub stick_walk_accel: FGi32,
pub max_walk_vel: FGi32,
pub dash_duration: u32,
pub dash_initial_velocity: f32,
pub base_dash_accel: f32,
pub stick_dash_accel: f32,
pub max_dash_vel: f32,
pub dash_initial_velocity: FGi32,
pub base_dash_accel: FGi32,
pub stick_dash_accel: FGi32,
pub max_dash_vel: FGi32,
pub jump_vertical_velocity: f32,
pub jumpsquat_duration: u32,
pub jump_vertical_velocity: FGi32,
pub ground_friction: f32,
pub terminal_velocity: f32,
pub gravity: f32,
pub ground_friction: FGi32,
pub terminal_velocity: FGi32,
pub gravity: FGi32,
}
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 = accel + input.movement.x.signum() * self.base_dash_accel;
let target_vel = input.movement.x * self.max_dash_vel;
@@ -33,7 +34,7 @@ impl FighterAttributes {
(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 = accel + input.movement.x.signum() * self.base_walk_accel;
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::state::FighterStateContext;
use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity};
use crate::math::segment::FGSegment2d;
use crate::math::vec::FGVec2;
use crate::stage::StagePoly;
use crate::stage::line::{StageCollision, StageLineID};
/*
@@ -36,14 +38,14 @@ pub fn collide_fighter_with_scene(
pub struct AirCollisionWithStageResult {
pub line_id: StageLineID,
pub hit_position: Vec2,
pub hit_normal: Vec2,
pub hit_position: FGVec2,
pub hit_normal: FGVec2,
}
pub fn air_collide_with_stage(
state_context: &FighterStateContext,
) -> 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.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.
use bevy::color::palettes::css::ORANGE;
use bevy::{color::palettes::css::YELLOW, prelude::*};
use bevy_egui::{EguiContexts, egui};
use crate::fighter::ecb::FighterPreviousECB;
use crate::fighter::state::StateNameDebug;
use crate::fighter::{FighterECB, FighterTranslation, FighterVelocity};
use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity};
use crate::input::FighterInput;
pub fn debug_draw_ecb(ecbs: Query<(&FighterECB, &FighterTranslation)>, mut gizmos: Gizmos) {
for (ecb, trf) in ecbs {
let transformed = ecb.to_3d_lineloop(Transform::from_xyz(trf.x, trf.y, 0.0));
pub fn debug_draw_ecb(ecbs: Query<(&FighterECB, &FighterTranslation, &FighterPreviousECB, &FighterPreviousTranslation)>, mut gizmos: Gizmos) {
for (ecb, trf, ecb_prev, trf_prev) in ecbs {
let transformed = ecb.to_3d_lineloop(Transform::from_xyz(trf.x.to_num(), trf.y.to_num(), 0.0));
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 fixed::traits::Fixed;
use crate::math::{int::FGi32, vec::FGVec2};
/// Environment collision box: the diamond used to resolve a fighter against
/// the stage. Purely geometry — the collision response lives in
@@ -6,40 +9,74 @@ use bevy::prelude::*;
#[derive(Component, Debug, Clone, Copy)]
#[require(FighterPreviousECB)]
pub struct FighterECB {
pub vertical_half: f32,
pub horizontal_half: f32,
pub vertical_half: FGi32,
pub horizontal_half: FGi32,
}
#[derive(Component, Debug, Clone, Copy, Default)]
pub struct FighterPreviousECB {
pub vertical_half: f32,
pub horizontal_half: f32,
pub vertical_half: FGi32,
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 {
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_down = trf.transform_point(Vec3::new(0.0, -self.vertical_half, 0.0));
let rel_left = trf.transform_point(Vec3::new(-self.horizontal_half, 0.0, 0.0));
let rel_right = trf.transform_point(Vec3::new(self.horizontal_half, 0.0, 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).to_num(), 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.to_num(), 0.0, 0.0));
[rel_up, rel_right, rel_down, rel_left]
}
pub fn get_bottom_point(&self) -> Vec2 {
Vec2::new(0.0, -self.vertical_half)
pub fn get_bottom_point(&self) -> FGVec2 {
FGVec2::new(FGi32::ZERO, -self.vertical_half)
}
}
impl FighterPreviousECB {
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_down = trf.transform_point(Vec3::new(0.0, -self.vertical_half, 0.0));
let rel_left = trf.transform_point(Vec3::new(-self.horizontal_half, 0.0, 0.0));
let rel_right = trf.transform_point(Vec3::new(self.horizontal_half, 0.0, 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).to_num(), 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.to_num(), 0.0, 0.0));
[rel_up, rel_right, rel_down, rel_left]
}
pub fn get_bottom_point(&self) -> Vec2 {
Vec2::new(0.0, -self.vertical_half)
pub fn get_bottom_point(&self) -> FGVec2 {
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.
.add_systems(
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(
GgrsSchedule,
@@ -74,6 +79,7 @@ impl Plugin for FighterPlugin {
// components; a component simulated here but missing from this list
// is a desync waiting to happen.
.rollback_component_with_copy::<FighterECB>()
.rollback_component_with_copy::<ecb::FighterPreviousECB>()
.rollback_component_with_copy::<FighterVelocity>()
.rollback_component_with_copy::<FighterTranslation>()
.rollback_component_with_copy::<FighterPreviousTranslation>()
+5 -5
View File
@@ -3,20 +3,20 @@
use bevy::prelude::*;
use crate::fighter::FighterAttributes;
use crate::{fighter::FighterAttributes, math::vec::FGVec2};
#[derive(Component, Clone, Copy)]
pub struct Grounded;
#[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)]
pub struct FighterPreviousTranslation(pub Vec2);
pub struct FighterPreviousTranslation(pub FGVec2);
#[derive(Component, Deref, DerefMut, Default, Clone, Copy)]
#[require(FighterPreviousTranslation)]
pub struct FighterTranslation(pub Vec2);
pub struct FighterTranslation(pub FGVec2);
pub fn apply_air_motion(
fighters: Query<(
@@ -37,7 +37,7 @@ pub fn copy_fighter_transform_to_visuals(
fighters: Query<(&FighterTranslation, &mut Transform)>,
) {
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 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::wait::WaitState;
use crate::fighter::state::{FighterState};
@@ -12,11 +12,12 @@ use crate::input::{
FighterCommands::{SmashMoveLeft, SmashMoveRight},
FighterInput,
};
use crate::math::int::FGi32;
#[derive(Debug, Clone)]
pub struct DashState {
frames_in_dash: u32,
direction: f32,
direction: FGi32,
ground_common: GroundedStateCommon
}
@@ -28,7 +29,7 @@ impl std::hash::Hash for DashState {
}
impl DashState {
pub fn new(direction: f32, ground_common: GroundedStateCommon) -> Self {
pub fn new(direction: FGi32, ground_common: GroundedStateCommon) -> Self {
Self {
frames_in_dash: 0,
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) {
return Some(-1.0);
return Some(FGi32::NEG_ONE);
} else if state_context.input.has_command(SmashMoveRight) {
return Some(1.0);
return Some(FGi32::ONE);
} else {
None
}
+1 -1
View File
@@ -1,6 +1,6 @@
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::ground::GroundedStateCommon;
use crate::fighter::state::wait::WaitState;
+20 -22
View File
@@ -7,17 +7,20 @@ use crate::fighter::{
FighterAttributes, FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity,
};
use crate::game_settings::GameSettings;
use crate::math::int::FGi32;
use crate::math::vec::FGVec2;
use crate::stage::StagePoly;
use crate::stage::line::{StageCollision, StageLineID, StagePolyLineSegment, StagePolyLineSegmentType};
use bevy::prelude::*;
use fixed::traits::Fixed;
#[derive(Reflect, Hash, Clone, Debug)]
pub struct GroundedStateCommon {
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() {
-ground_vel
} else {
@@ -28,21 +31,21 @@ pub fn apply_grounded_friction(friction: f32, ground_vel: f32) -> f32 {
pub fn move_accelerate() {}
pub fn compute_ground_accel(
accel: f32,
target_vel: f32,
gr_vel: f32,
accel: FGi32,
target_vel: FGi32,
gr_vel: FGi32,
attribs: &FighterAttributes,
game_settings: &GameSettings,
) -> f32 {
) -> FGi32 {
if target_vel == 0.0 {
apply_grounded_friction(attribs.ground_friction, gr_vel)
} else {
let mut accel = accel;
let 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
if accel > 0.0 {
if accel > FGi32::ZERO {
if gr_vel + accel > target_vel {
accel = -attribs.ground_friction;
if gr_vel + accel < target_vel {
@@ -78,7 +81,7 @@ pub fn apply_grounded_motion(
) {
// Vertical velocity should be 0 on the ground
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(
@@ -96,34 +99,29 @@ pub fn collide_with_stage_grounded(
if let Some(stage_poly) = stage_poly {
let ecb_bottom_pos = translation.0 + ecb.get_bottom_point();
let left_segment_idx = ground_common
.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();
let current_segment_idx = ground_common.current_line_id.segment;
// First, we check the current segment, if that fails, we check the surrounding ones
let grounded_ecb_bottom_pos = [
ground_common.current_line_id.segment.clone(),
left_segment_idx,
right_segment_idx,
Some(current_segment_idx),
stage_poly.get_prev_segment_index(current_segment_idx),
stage_poly.get_next_segment_index(current_segment_idx),
]
.iter()
.into_iter()
.flatten()
.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 {
return None;
}
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 distance = closest_point.distance(projected_point);
if distance < 0.01 {
return Some((projected_point, *segment_idx, segment.normal));
return Some((projected_point, segment_idx, segment.normal));
}
None
+83 -18
View File
@@ -1,41 +1,106 @@
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 counter: u32
pub enum JumpType {
ShortHop,
LongJump,
DoubleJump,
}
#[derive(Debug, Clone, Hash)]
pub struct JumpState {}
#[derive(Debug, Clone, Hash)]
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 {
const NAME: &'static str = "JumpSquat";
fn check_interrupt(
&self,
state_context: &super::FighterStateContext,
) -> Option<FighterState> {
fn check_interrupt(&self, state_context: &FighterStateContext) -> Option<FighterState> {
if self.duration_counter >= state_context.fighter_attribs.jumpsquat_duration {
Some(FighterState::Jump(JumpState {}))
} else {
None
}
fn update(&mut self, _state_context: &mut super::FighterStateContext) {
todo!()
}
fn update(&mut self, state_context: &mut FighterStateContext) {
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 {
const NAME: &'static str = "Jump";
fn check_interrupt(
&self,
state_context: &super::FighterStateContext,
) -> Option<FighterState> {
fn check_interrupt(&self, state_context: &FighterStateContext) -> Option<FighterState> {
None
}
fn update(&mut self, _state_context: &mut super::FighterStateContext) {
todo!()
}
fn update(&mut self, _state_context: &mut FighterStateContext) {}
}
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
}
+12 -12
View File
@@ -11,6 +11,7 @@ use crate::fighter::ecb::FighterPreviousECB;
use crate::fighter::{FighterAttributes, FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity};
use crate::game_settings::GameSettings;
use crate::input::FighterInput;
use crate::math::int::FGi32;
use crate::schedule::GameplaySet;
use crate::stage::StagePoly;
use crate::stage::line::StageCollision;
@@ -24,14 +25,6 @@ pub mod fall;
pub mod air;
pub mod jump;
pub enum FighterStateTransition {
Wait,
Walk,
Dash(f32),
Run,
Fall,
}
macro_rules! fighter_states {
($($variant:ident => $ty:ty),* $(,)?) => {
#[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 prev_translation: &'a mut FighterPreviousTranslation,
pub input: &'a mut FighterInput,
@@ -91,6 +84,7 @@ pub struct FighterStateContext<'a> {
pub game_settings: &'a GameSettings,
pub entity: Entity,
pub stage_collision: &'a StageCollision,
pub gizmos: Option<&'a mut Gizmos<'w, 's>>,
}
fighter_states! {
@@ -98,7 +92,9 @@ fighter_states! {
Walk => walk::WalkState,
Dash => dash::DashState,
Run => run::RunState,
Fall => fall::FallState
Fall => fall::FallState,
JumpSquat => jump::JumpSquatState,
Jump => jump::JumpState
}
#[derive(Component)]
@@ -128,6 +124,7 @@ pub fn state_update_system(
game_settings: &game_settings,
entity: ent,
stage_collision: &stage_collision,
gizmos: Some(&mut gizmos),
};
state.update(&mut update_ctx);
}
@@ -150,7 +147,8 @@ pub fn state_interrupt_system (
fighter_attribs: &attribs,
game_settings: &game_settings,
entity: ent,
stage_collision: &stage_collision
stage_collision: &stage_collision,
gizmos: None,
};
if let Some(new_state) = state.check_interrupt(&state_context) {
let mut new_state = new_state;
@@ -171,6 +169,7 @@ pub fn state_collision_interrupt_system(
stage_collision: Res<StageCollision>,
game_settings: Res<GameSettings>,
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 {
let mut state_context = FighterStateContext {
@@ -183,7 +182,8 @@ pub fn state_collision_interrupt_system(
fighter_attribs: &attribs,
game_settings: &game_settings,
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) {
let mut new_state = new_state;
+1 -1
View File
@@ -1,6 +1,6 @@
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::fall::FallState;
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
+5 -2
View File
@@ -1,6 +1,6 @@
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::fall::FallState;
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
@@ -33,7 +33,10 @@ impl FighterStateImpl for WaitState {
} else {
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(
state_context.velocity,
+1 -1
View File
@@ -1,6 +1,6 @@
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::ground::{GroundedMotionResult, GroundedStateCommon};
use crate::fighter::state::{self, FighterState};
+5 -3
View File
@@ -1,9 +1,11 @@
use bevy::prelude::*;
use crate::math::int::FGi32;
pub struct FighterSettingsCommon {
pub walk_speed_ease: f32,
pub ground_max_horizontal_velocity: f32,
pub ground_friction_over_walk_speed_multiplier: f32
pub walk_speed_ease: FGi32,
pub ground_max_horizontal_velocity: FGi32,
pub ground_friction_over_walk_speed_multiplier: FGi32
}
pub struct InputSettingsCommon {
pub stick_deadzone: f32,
+4
View File
@@ -17,6 +17,7 @@ struct FighterInputBufferedActions {}
pub enum FighterCommands {
SmashMoveLeft,
SmashMoveRight,
Jump,
}
/// Frames of life remaining for each buffered command. Indexed by
@@ -25,6 +26,7 @@ pub enum FighterCommands {
pub struct FighterCommandLifetimes {
smash_move_left: u8,
smash_move_right: u8,
jump: u8,
}
impl Index<FighterCommands> for FighterCommandLifetimes {
@@ -34,6 +36,7 @@ impl Index<FighterCommands> for FighterCommandLifetimes {
match command {
FighterCommands::SmashMoveLeft => &self.smash_move_left,
FighterCommands::SmashMoveRight => &self.smash_move_right,
FighterCommands::Jump => &self.jump,
}
}
}
@@ -43,6 +46,7 @@ impl IndexMut<FighterCommands> for FighterCommandLifetimes {
match command {
FighterCommands::SmashMoveLeft => &mut self.smash_move_left,
FighterCommands::SmashMoveRight => &mut self.smash_move_right,
FighterCommands::Jump => &mut self.jump,
}
}
}
+4 -2
View File
@@ -1,10 +1,12 @@
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use crate::math::vec::FGVec2;
/// 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`.
#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
pub struct FighterInputFrame {
pub movement: Vec2,
pub directional_attack: Vec2,
pub movement: FGVec2,
pub directional_attack: FGVec2,
}
+5 -4
View File
@@ -6,6 +6,7 @@ use bevy::prelude::*;
use bevy_ggrs::{LocalInputs, LocalPlayers};
use crate::input::{BaseInputMap, FighterInputFrame, InputActionState, InputMapAction};
use crate::math::int::FGi32;
use crate::netcode::GGRSCfg;
pub fn preprocess_keyboard_input(
@@ -32,16 +33,16 @@ pub fn preprocess_keyboard_input(
};
let action_strength = match action_state {
InputActionState::JustPressed | InputActionState::Pressed => 1.0f32,
InputActionState::JustReleased | InputActionState::Released => 0.0f32,
InputActionState::JustPressed | InputActionState::Pressed => FGi32::ONE,
InputActionState::JustReleased | InputActionState::Released => FGi32::ZERO,
};
match keyboard_elment.action {
InputMapAction::MovementXDir(sign) => {
input_frame.movement.x += (sign as f32) * action_strength
input_frame.movement.x += (sign) * action_strength
}
InputMapAction::MovementYDir(sign) => {
input_frame.movement.y += (sign as f32) * action_strength
input_frame.movement.y += (sign) * action_strength
}
InputMapAction::Jump => todo!(),
}
+18 -18
View File
@@ -29,7 +29,7 @@ use jackdaw_runtime::prelude::*;
use crate::{
fighter::{
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;
@@ -62,24 +62,24 @@ pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
Name("Fighter".into()),
Player { handle: 0 },
FighterECB {
vertical_half: 0.5,
horizontal_half: 0.25,
vertical_half: FGi32::lit("0.5"),
horizontal_half: FGi32::lit("0.25"),
},
FighterVelocity::default(),
FighterTranslation(Vec2::new(0.5, 1.25)),
FighterTranslation(FGVec2::lit("0.5", "1.25")),
FighterAttributes {
base_walk_accel: 0.01,
stick_walk_accel: 0.02,
max_walk_vel: 0.16,
ground_friction: 0.008,
base_walk_accel: FGi32::lit("0.01"),
stick_walk_accel: FGi32::lit("0.02"),
max_walk_vel: FGi32::lit("0.16"),
ground_friction: FGi32::lit("0.008"),
dash_duration: 15,
base_dash_accel: 0.002,
stick_dash_accel: 0.01,
max_dash_vel: 0.22,
jump_vertical_velocity: 0.25,
dash_initial_velocity: 0.19,
terminal_velocity: 0.28,
gravity: 0.023,
base_dash_accel: FGi32::lit("0.002"),
stick_dash_accel: FGi32::lit("0.01"),
max_dash_vel: FGi32::lit("0.22"),
jump_vertical_velocity: FGi32::lit("0.25"),
dash_initial_velocity: FGi32::lit("0.19"),
terminal_velocity: FGi32::lit("0.28"),
gravity: FGi32::lit("0.023"),
},
FighterState::Fall(FallState),
FighterInput::default(),
@@ -115,9 +115,9 @@ impl Plugin for GamePlugin {
.insert_resource(args::Args::parse())
.insert_resource(GameSettings {
figher_common: FighterSettingsCommon {
walk_speed_ease: 0.5,
ground_max_horizontal_velocity: 0.3,
ground_friction_over_walk_speed_multiplier: 2.0,
walk_speed_ease: FGi32::lit("0.5"),
ground_max_horizontal_velocity: FGi32::lit("0.3"),
ground_friction_over_walk_speed_multiplier: FGi32::lit("2.0"),
},
input_common: InputSettingsCommon::default(),
});
+78 -33
View File
@@ -1,12 +1,13 @@
use bevy::math::{Vec2, primitives::Segment2d};
use bevy::prelude::*;
use fixed::prelude::*;
use crate::math::vec::FGVec2;
type FGi32 = fixed::FixedI32<fixed::types::extra::U16>;
#[derive(Clone, Copy, PartialEq)]
#[derive(Clone, Copy, PartialEq, Reflect)]
pub struct FGSegment2d {
point1: FGVec2,
point2: FGVec2
@@ -29,14 +30,72 @@ impl FGSegment2d {
pub fn point2(&self) -> FGVec2 {
self.point2
}
}
pub trait SegmentIntersection {
fn segment_intersection(&self, rhs: &Segment2d) -> Option<FGVec2>;
}
/// Compute the normalized direction pointing from the first endpoint to the second endpoint.
///
/// 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 {
fn segment_intersection(&self, rhs: &Segment2d) -> Option<Vec2> {
/// Try to compute the normalized direction pointing from the first endpoint to the second endpoint.
///
/// 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 p1 = self.point2();
let p2 = rhs.point1();
@@ -48,45 +107,31 @@ impl SegmentIntersection for Segment2d {
let s2_y = p3.y - p2.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
}
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;
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)))
if s >= FGi32::ZERO && s <= FGi32::ONE && t >= FGi32::ZERO && t <= FGi32::ONE {
Some(FGVec2::new(p0.x + (t * s1_x), p0.y + (t * s1_y)))
} else {
None
}
}
}
impl SegmentIntersection for Segment2d {
fn segment_intersection(&self, rhs: &Segment2d) -> Option<Vec2> {
let p0 = self.point1();
let p1 = self.point2();
let p2 = rhs.point1();
let p3 = rhs.point2();
#[cfg(test)]
mod tests {
use super::*;
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;
#[test]
fn closest_point_projects_and_clamps_to_segment() {
let segment = FGSegment2d::new(FGVec2::lit("-4", "0"), FGVec2::lit("4", "0"));
let denom = -s2_x * s1_y + s1_x * s2_y;
if denom.abs() < f32::EPSILON {
return None; // Parallel or collinear
}
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;
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
}
assert_eq!(segment.closest_point(FGVec2::lit("2", "1")), FGVec2::lit("2", "0"));
assert_eq!(segment.closest_point(FGVec2::lit("-6", "1")), segment.point1());
assert_eq!(segment.closest_point(FGVec2::lit("6", "1")), segment.point2());
}
}
+206 -8
View File
@@ -1,14 +1,24 @@
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;
#[derive(Clone, Copy, PartialEq)]
#[derive(Clone, Copy, PartialEq, Default, Debug, Reflect, Serialize, Deserialize)]
#[reflect(opaque)]
pub struct FGVec2 {
pub x: 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 {
pub const ZERO: Self = Self::new(FGi32::ZERO, FGi32::ZERO);
@@ -17,6 +27,11 @@ impl FGVec2 {
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)]
pub fn from_i32(x: i32, y: i32) -> Self {
Self {
@@ -31,17 +46,78 @@ impl FGVec2 {
}
#[inline]
pub fn length(&self) -> FGi32 {
pub fn length(self) -> FGi32 {
(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]
pub fn normalized_or_zero(&self) -> Self {
let len = self.length();
if len.is_zero() {
Self::ZERO
#[must_use]
pub fn project_onto_normalized(self, rhs: Self) -> Self {
assert!(rhs.is_normalized());
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 {
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 {
type Output = Self;
@@ -113,3 +197,117 @@ impl Div<FGi32> for &FGVec2 {
(*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>) {
for poly in stage_collision.stage_polys.iter() {
for segment in poly.segments.iter() {
let from = segment.segment.point1();
let to = segment.segment.point2();
let from = segment.segment.point1().to_vec2();
let to = segment.segment.point2().to_vec2();
let from_3d = Vec3::new(from.x, from.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);
let midpoint = (from_3d + end_3d) * 0.5;
gizmos.arrow(midpoint, midpoint + normal_3d, RED);
+140 -36
View File
@@ -4,7 +4,7 @@ use std::default;
use bevy::{math::InvalidDirectionError, prelude::*};
use crate::math::segment::SegmentIntersection;
use crate::math::{segment::{FGSegment2d}, vec::FGVec2};
#[derive(Reflect, Clone, Copy, PartialEq)]
pub enum StagePolyLineSegmentType {
@@ -14,8 +14,8 @@ pub enum StagePolyLineSegmentType {
#[derive(Reflect, Clone)]
pub struct StagePolyLineSegment {
pub segment: Segment2d,
pub normal: Vec2,
pub segment: FGSegment2d,
pub normal: FGVec2,
pub segment_type: StagePolyLineSegmentType
}
@@ -40,9 +40,9 @@ pub struct StagePoly {
}
pub struct StagePlaneIntersectResult {
pub position: Vec2,
pub position: FGVec2,
pub segment_idx: usize,
pub normal: Vec2
pub normal: FGVec2
}
#[derive(Resource, Reflect, Clone, Default)]
@@ -52,18 +52,30 @@ pub struct StageCollision {
}
impl StagePoly {
pub fn intersect_ray(&self, ray_segment: Segment2d) -> Option<StagePlaneIntersectResult> {
for (i, segment) in self.segments.iter().enumerate() {
if let Some(intersection) = segment.segment.segment_intersection(&ray_segment) {
println!("DIST3 {} {}", intersection, segment.segment.closest_point(intersection));
return Some(StagePlaneIntersectResult {
position: intersection,
segment_idx: i,
normal: segment.normal
});
}
}
None
pub fn intersect_ray(&self, ray_segment: FGSegment2d) -> Option<StagePlaneIntersectResult> {
let ray_start = ray_segment.point1();
let (segment_idx, segment, position) = self
.segments
.iter()
.enumerate()
.filter_map(|(segment_idx, segment)| {
segment
.segment
.segment_intersection(&ray_segment)
.map(|position| (segment_idx, segment, position))
})
.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> {
@@ -71,24 +83,65 @@ impl StagePoly {
}
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> {
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 {
let windows = points.iter().zip(points.iter().cycle().skip(1));
pub fn get_next_segment_index(&self, segment_idx: usize) -> Option<usize> {
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 (first_point, segment_type) = segment_window.0;
let (second_point, _) = segment_window.1;
let segment = Segment2d::new(*first_point, *second_point);
let segment = FGSegment2d::new(*first_point, *second_point);
StagePolyLineSegment {
segment_type: *segment_type,
segment,
normal: segment.left_normal().as_vec2()
normal: segment.left_normal()
}
}).collect();
@@ -100,46 +153,97 @@ impl StagePoly {
}
impl StagePolyLineSegment {
pub fn project(&self, vec: Vec2) -> Vec2 {
pub fn project(&self, vec: FGVec2) -> FGVec2 {
vec - vec.project_onto_normalized(self.normal)
}
}
#[cfg(test)]
mod tests {
use crate::math::int::FGi32;
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]
fn ray_intersection_test() {
let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[
(Vec2::new(-4.5, 0.0), StagePolyLineSegmentType::Floor),
(Vec2::new(4.5, 0.0), StagePolyLineSegmentType::Floor),
(FGVec2::lit("-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_eq!(res.unwrap().position, Vec2::ZERO);
assert_eq!(res.unwrap().position, FGVec2::ZERO);
}
#[test]
fn ray_intersection_test_side() {
let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[
(Vec2::new(-4.5, 0.0), StagePolyLineSegmentType::Floor),
(Vec2::new(4.5, 0.0), StagePolyLineSegmentType::Floor),
(FGVec2::lit("-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_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]
fn ray_intersection_test_diagonal() {
let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[
(Vec2::new(0.0, 0.0), StagePolyLineSegmentType::Floor),
(Vec2::new(2.0, 2.0), StagePolyLineSegmentType::Floor),
(FGVec2::lit("0.0", "0.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_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 crate::stage::{StagePoly, line::StagePolyLineSegmentType, line::StageCollision};
use crate::{math::vec::FGVec2, stage::{StagePoly, line::{StageCollision, StagePolyLineSegmentType}}};
/// set up a simple 3D scene
pub fn test_scene() -> impl SceneList {
let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Closed, &[
(Vec2::new(-5.6, -0.35), StagePolyLineSegmentType::Floor),
(Vec2::new(-3.92, 0.0), StagePolyLineSegmentType::Floor),
(Vec2::new(0.0, 0.0), StagePolyLineSegmentType::Floor),
(Vec2::new(3.92, 0.0), StagePolyLineSegmentType::Floor),
(Vec2::new(5.6, -0.35), StagePolyLineSegmentType::Wall),
(Vec2::new(5.6, -20.0), StagePolyLineSegmentType::Ceiling),
(Vec2::new(-5.6, -20.0), StagePolyLineSegmentType::Wall)
(FGVec2::lit("-5.6", "-0.35"), StagePolyLineSegmentType::Floor),
(FGVec2::lit("-3.92", "0.0"), StagePolyLineSegmentType::Floor),
(FGVec2::lit("0.0", "0.0"), StagePolyLineSegmentType::Floor),
(FGVec2::lit("3.92", "0.0"), StagePolyLineSegmentType::Floor),
(FGVec2::lit("5.6", "-0.35"), StagePolyLineSegmentType::Wall),
(FGVec2::lit("5.6", "-20.0"), StagePolyLineSegmentType::Ceiling),
(FGVec2::lit("-5.6", "-20.0"), StagePolyLineSegmentType::Wall)
]);
bsn_list! [