Improve ECB behaviour, add ECB interpolation, wall/ceiling support etc (#94)
CI / build-and-test (push) Successful in 1m43s
CI / production-build (push) Successful in 11m52s

Reviewed-on: #94
Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
This commit was merged in pull request #94.
This commit is contained in:
2026-10-13 19:20:29 +00:00
committed by EIREXE
parent f1da5c1172
commit 472a0b6a19
7 changed files with 874 additions and 163 deletions
+4 -6
View File
@@ -134,12 +134,10 @@ pub fn collision_interrupt(
}
let res = ctx.air_collide_with_stage(can_ignore_soft_platforms)?;
let diff = res.hit_position - ctx.translation.0;
ctx.ecb.bottom -= diff;
ctx.ecb.top -= diff;
ctx.ecb.left -= diff;
ctx.ecb.right -= diff;
ctx.translation.0 = res.hit_position;
ctx.translation.0 = res.resolved_translation;
let bottom_offset = ctx.ecb.rebase_onto_bottom();
ctx.translation.0 += bottom_offset;
*ctx.desired_ecb = crate::fighter::ecb::FighterDesiredECB::from(*ctx.ecb);
ctx.presentation_snap.0 = true;
ctx.surface.land(res.line_id, ctx.ledge_info);
Some(FighterState::Land(LandingState::create(landing_type)))
+75 -54
View File
@@ -4,16 +4,18 @@
use crate::fighter::state::air_dodge::AirDodgeState;
use crate::fighter::state::{
CollisionContext, FighterState, InterruptContext, air, crouch, dash, fall, ground_attack, turn, wait, walk,
};
use crate::fighter::{
FighterAttributes, FighterTranslation, FighterVelocity,
CollisionContext, FighterState, InterruptContext, air, crouch, dash, fall, ground_attack, turn,
wait, walk,
};
use crate::fighter::{collision, ecb};
use crate::fighter::{FighterAttributes, FighterTranslation, FighterVelocity};
use crate::game_settings::GameSettings;
use crate::input::FighterInput;
use crate::math::int::{FGWide, FGi32};
use crate::math::vec::FGVec2;
use crate::stage::line::{StageLineID, StagePoly, StagePolyLineSegment, StagePolyLineSegmentType};
use crate::stage::line::{StagePoly, StagePolyLineSegment, StagePolyLineSegmentType};
#[cfg(test)]
use crate::stage::line::StageLineID;
pub fn apply_grounded_friction(friction: FGi32, ground_vel: FGi32) -> FGi32 {
if friction.abs() > ground_vel.abs() {
@@ -148,10 +150,7 @@ fn resolve_grounded_position(
GroundedPositionResult::InAir
}
pub fn apply_grounded_motion(
velocity: &FighterVelocity,
translation: &mut FighterTranslation,
) {
pub fn apply_grounded_motion(velocity: &FighterVelocity, translation: &mut FighterTranslation) {
// Vertical velocity should be 0 on the ground
translation.0 += FGVec2::new(velocity.0.x, FGi32::ZERO);
}
@@ -176,61 +175,80 @@ pub fn collide_with_stage_grounded(
state_context: &mut CollisionContext,
can_walk_off: bool,
) -> GroundedMotionResult {
let translation = &mut state_context.translation;
let ecb = &state_context.ecb;
let stage_collision = state_context.stage_collision;
let Some(current_line_id) = state_context.surface.line_id() else {
let Some(mut line_id) = state_context.surface.line_id() else {
state_context.apply_desired_ecb();
return GroundedMotionResult::InAir;
};
let requested_translation = state_context.translation.0;
let movement = requested_translation - state_context.prev_translation.0;
let largest_change = movement
.x
.abs()
.max(movement.y.abs())
.max(ecb::ecb_max_delta(state_context.ecb, state_context.desired_ecb));
let steps = collision::collision_step_count(largest_change);
let movement_per_step = movement / FGi32::from_num(steps);
let mut current_position = state_context.prev_translation.0;
let mut current_ecb = *state_context.ecb;
let stage_poly = stage_collision.stage_polys.get(current_line_id.polygon);
for step in 0..steps {
ecb::interpolate_ecb(
&mut current_ecb,
*state_context.desired_ecb,
FGi32::ONE / FGi32::from_num(steps - step),
);
current_position = if step + 1 == steps {
requested_translation
} else {
state_context.prev_translation.0
+ movement_per_step * FGi32::from_num(step + 1)
};
if let Some(stage_poly) = stage_poly {
let ecb_bottom_pos = translation.0 + ecb.get_bottom_point();
let Some(stage_poly) = state_context.stage_collision.stage_polys.get(line_id.polygon) else {
*state_context.ecb = current_ecb;
state_context.translation.0 = current_position;
state_context.surface.apply_airborne_policy(
crate::fighter::surface::EcbLockPolicy::StartOnDetach,
state_context.leave_ground_ecb_lock_duration,
);
state_context.ecb_advanced = true;
return GroundedMotionResult::InAir;
};
match resolve_grounded_position(
stage_poly,
current_line_id.segment,
ecb_bottom_pos,
line_id.segment,
current_position + current_ecb.bottom,
can_walk_off,
) {
GroundedPositionResult::InGround {
point,
segment_idx,
blocked,
} => {
let current_segment = stage_poly.get_segment(segment_idx).unwrap();
translation.0 = point - ecb.get_bottom_point();
GroundedPositionResult::InGround { point, segment_idx, blocked } => {
line_id.segment = segment_idx;
current_position = point - current_ecb.bottom;
let segment = stage_poly.get_segment(segment_idx).unwrap();
state_context.velocity.0 = if blocked {
FGVec2::ZERO
} else {
state_context.velocity.0
- state_context
.velocity
.0
.project_onto_normalized(current_segment.normal)
- state_context.velocity.0.project_onto_normalized(segment.normal)
};
state_context.surface.land(StageLineID {
polygon: current_line_id.polygon,
segment: segment_idx,
}, state_context.ledge_info);
GroundedMotionResult::InGround
}
GroundedPositionResult::InAir => {
*state_context.ecb = current_ecb;
state_context.translation.0 = current_position;
state_context.surface.apply_airborne_policy(
crate::fighter::surface::EcbLockPolicy::StartOnDetach,
state_context.leave_ground_ecb_lock_duration,
);
GroundedMotionResult::InAir
state_context.ecb_advanced = true;
return GroundedMotionResult::InAir;
}
}
} else {
state_context.surface.apply_airborne_policy(
crate::fighter::surface::EcbLockPolicy::StartOnDetach,
state_context.leave_ground_ecb_lock_duration,
);
GroundedMotionResult::InAir
}
*state_context.ecb = current_ecb;
state_context.translation.0 = current_position;
state_context.surface.land(line_id, state_context.ledge_info);
state_context.ecb_advanced = true;
GroundedMotionResult::InGround
}
pub fn collision_interrupt(ctx: &mut CollisionContext, can_walk_off: bool) -> Option<FighterState> {
@@ -245,11 +263,13 @@ pub fn collision_interrupt(ctx: &mut CollisionContext, can_walk_off: bool) -> Op
#[cfg(test)]
mod tests {
use super::*;
use crate::fighter::ecb::FighterPreviousECB;
use crate::fighter::{FighterECB, FighterLedgeInformation, FighterPreviousTranslation, FighterSurfaceState};
use crate::math::rect::FGRect;
use crate::stage::line::StagePolyType;
use crate::fighter::ecb::{FighterDesiredECB, FighterPreviousECB};
use crate::fighter::{
FighterECB, FighterLedgeInformation, FighterPreviousTranslation, FighterSurfaceState,
};
use crate::input::FighterInput;
use crate::math::rect::FGRect;
use crate::stage::line::StagePolyType;
fn two_segment_floor() -> StagePoly {
StagePoly::build(
@@ -379,6 +399,7 @@ use crate::stage::line::StagePolyType;
right: FGVec2::lit("0.5", "0.0"),
};
let previous_ecb = FighterPreviousECB::from(ecb);
let mut desired_ecb = FighterDesiredECB::from(ecb);
let mut presentation_snap = crate::fighter::FighterPresentationSnap::default();
let mut context = CollisionContext {
translation: &mut translation,
@@ -387,6 +408,7 @@ use crate::stage::line::StagePolyType;
velocity: &mut velocity,
surface: &mut surface,
ecb: &mut ecb,
desired_ecb: &mut desired_ecb,
ledge_info: &mut ledge_info,
prev_ecb: &previous_ecb,
stage_collision: &stage_collision,
@@ -397,6 +419,7 @@ use crate::stage::line::StagePolyType;
origin: FGVec2::ZERO,
end: FGVec2::ZERO,
},
ecb_advanced: false,
};
assert!(matches!(
@@ -440,6 +463,7 @@ use crate::stage::line::StagePolyType;
right: FGVec2::lit("0.5", "0.0"),
};
let previous_ecb = FighterPreviousECB::from(ecb);
let mut desired_ecb = FighterDesiredECB::from(ecb);
let mut presentation_snap = crate::fighter::FighterPresentationSnap::default();
let mut ledge_info = FighterLedgeInformation::default();
let mut context = CollisionContext {
@@ -449,6 +473,7 @@ use crate::stage::line::StagePolyType;
velocity: &mut velocity,
surface: &mut surface,
ecb: &mut ecb,
desired_ecb: &mut desired_ecb,
ledge_info: &mut ledge_info,
prev_ecb: &previous_ecb,
stage_collision: &stage_collision,
@@ -459,6 +484,7 @@ use crate::stage::line::StagePolyType;
origin: FGVec2::ZERO,
end: FGVec2::ZERO,
},
ecb_advanced: false,
};
assert!(matches!(
@@ -498,15 +524,10 @@ pub fn grounded_movement_standstill_common_interrupts(
pub fn perform_wavedash(input: &FighterInput, game_settings: &GameSettings) -> FighterState {
// If aiming sideways and in the deadzone, do a perfect wavedash
// TODO: Improve this so we don't have to go through the airborne state first
let mut wavedash_dir =
input
.get_last_frame()
.movement
.normalize_or_zero();
let mut wavedash_dir = input.get_last_frame().movement.normalize_or_zero();
let movement = input.get_last_frame().movement;
if movement.y.is_zero() && !movement.x.is_zero() {
wavedash_dir.y =
-game_settings.input_common.stick_deadzone - FGi32::lit("-0.01");
wavedash_dir.y = -game_settings.input_common.stick_deadzone - FGi32::lit("-0.01");
wavedash_dir.x = movement.x.signum();
}
@@ -517,4 +538,4 @@ pub fn perform_wavedash(input: &FighterInput, game_settings: &GameSettings) -> F
FighterState::AirDodge(AirDodgeState {
direction: wavedash_dir.normalize_or_zero(),
})
}
}
+49 -19
View File
@@ -12,7 +12,7 @@ use crate::fighter::attack::{
};
use crate::fighter::attributes::FighterAttributes;
use crate::fighter::baked_animation::{BakedFighterAnimations, FighterBoneMatrices};
use crate::fighter::ecb::FighterPreviousECB;
use crate::fighter::ecb::{self, FighterDesiredECB, FighterPreviousECB};
use crate::fighter::manifest::FighterManifest;
use crate::fighter::state::hitstun::HitstunState;
use crate::fighter::surface::EcbLockPolicy;
@@ -49,14 +49,14 @@ pub mod ground_attack;
pub mod hitstun;
pub mod jump;
pub mod land;
pub mod ledge_action;
pub mod ledge_grab;
pub mod ledge_wait;
pub mod platform_pass;
pub mod run;
pub mod turn;
pub mod wait;
pub mod walk;
pub mod ledge_wait;
pub mod ledge_action;
pub mod ledge_grab;
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum SurfacePolicy {
@@ -192,10 +192,7 @@ impl EnterContext<'_> {
let position = self
.baked_animations
.initial_root_position(self.animation_frame.kind, self.model_scale)?;
Some(
self.translation.0
+ animation_position_to_gameplay(position, self.facing_direction),
)
Some(self.translation.0 + animation_position_to_gameplay(position, self.facing_direction))
}
pub fn get_animation_duration(&self, kind: AnimKind) -> Option<usize> {
@@ -255,6 +252,7 @@ pub struct CollisionContext<'a> {
pub velocity: &'a mut FighterVelocity,
pub surface: &'a mut FighterSurfaceState,
pub ecb: &'a mut FighterECB,
pub desired_ecb: &'a mut FighterDesiredECB,
pub prev_ecb: &'a FighterPreviousECB,
pub stage_collision: &'a StageCollision,
pub leave_ground_ecb_lock_duration: u32,
@@ -262,25 +260,42 @@ pub struct CollisionContext<'a> {
pub ledge_info: &'a mut FighterLedgeInformation,
pub fighter_ledge_collision_box: FGRect,
pub downward_stick_threshold: FGi32,
pub(crate) ecb_advanced: bool,
}
impl CollisionContext<'_> {
/// Commit the target pose before resolving this frame. The collision
/// substep resolver can later replace this with fractional steps.
pub fn apply_desired_ecb(&mut self) {
ecb::interpolate_ecb(self.ecb, *self.desired_ecb, FGi32::ONE);
self.ecb_advanced = true;
}
pub fn air_collide_with_stage(
&self,
&mut self,
can_ignore_soft_platforms: bool,
) -> Option<crate::fighter::collision::AirCollisionWithStageResult> {
let ignored_polys = self.surface.ignored_polygon_ids();
let ignore_soft_platforms = can_ignore_soft_platforms
&& self.input.get_last_frame().movement.y <= -self.downward_stick_threshold;
crate::fighter::collision::air_collide_with_stage(
let result = crate::fighter::collision::air_collide_with_stage_substepped(
self.prev_translation,
self.translation,
self.prev_ecb,
self.velocity,
self.ecb,
self.desired_ecb,
self.stage_collision,
&ignored_polys,
ignore_soft_platforms,
)
);
self.ecb_advanced = true;
result
}
fn finish_ecb_frame(&mut self) {
if !self.ecb_advanced {
self.apply_desired_ecb();
}
}
pub fn air_collide_with_ledges(
&self,
@@ -330,6 +345,7 @@ pub struct FighterFrameQuery {
pub knockback_velocity: &'static mut FighterKnockbackVelocity,
pub stale_move_queue: &'static mut FighterStaleMoveQueue,
pub ecb: &'static mut FighterECB,
pub desired_ecb: &'static mut FighterDesiredECB,
pub prev_ecb: &'static FighterPreviousECB,
pub facing_direction: &'static mut FighterFacingDirection,
pub animation_frame: &'static mut FighterAnimationFrame,
@@ -477,6 +493,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
velocity: &mut self.velocity,
surface: &mut self.surface,
ecb: &mut self.ecb,
desired_ecb: &mut self.desired_ecb,
ledge_info: &mut self.ledge_information,
prev_ecb: &self.prev_ecb,
stage_collision,
@@ -485,6 +502,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
.leave_ground_ecb_frame_lock_duration,
downward_stick_threshold: game_settings.input_common.run_stick_threshold,
fighter_ledge_collision_box,
ecb_advanced: false,
}
}
}
@@ -500,7 +518,6 @@ pub struct StatefulFighterQuery {
pub player: &'static Player,
pub pending_hit_reaction: &'static mut FighterPendingHitReaction,
pub hitstop: &'static mut FighterHitstop,
pub ledge_info: &'static mut FighterLedgeInformation,
pub hitstop_shake: &'static mut FighterHitstopShake,
pub pending_respawn: &'static mut FighterPendingRespawn,
pub damage: &'static mut FighterDamage,
@@ -710,7 +727,6 @@ pub fn state_collision_interrupt_system(
mut pending_respawn,
player,
mut damage,
mut ledge_info,
..
} = fighter;
@@ -761,7 +777,7 @@ pub fn state_collision_interrupt_system(
*damage = FighterDamage::default();
// reset ledge
*ledge_info = FighterLedgeInformation::default();
*frame.ledge_information = FighterLedgeInformation::default();
transition_state(
&mut state,
@@ -781,9 +797,13 @@ pub fn state_collision_interrupt_system(
continue;
}
let new_state = state.check_collision_interrupt(
&mut frame.collision_context(&stage_collision, manifest, &game_settings),
);
let new_state = {
let mut collision_context =
frame.collision_context(&stage_collision, manifest, &game_settings);
let new_state = state.check_collision_interrupt(&mut collision_context);
collision_context.finish_ecb_frame();
new_state
};
if let Some(new_state) = new_state {
transition_state(
&mut state,
@@ -840,19 +860,26 @@ mod tests {
let mut velocity = FighterVelocity::default();
let mut surface = FighterSurfaceState::default();
let mut ecb = FighterECB::default();
let mut desired_ecb = FighterDesiredECB {
top: FGVec2::lit("0", "2"),
bottom: FGVec2::ZERO,
left: FGVec2::lit("-1", "1"),
right: FGVec2::lit("1", "1"),
};
let mut ledge_info = FighterLedgeInformation {
regrab_lockout_remaining_frames: 5,
..default()
};
let previous_ecb = FighterPreviousECB::from(ecb);
let mut presentation_snap = FighterPresentationSnap::default();
let context = CollisionContext {
let mut context = CollisionContext {
translation: &mut translation,
prev_translation: &previous_translation,
input: &input,
velocity: &mut velocity,
surface: &mut surface,
ecb: &mut ecb,
desired_ecb: &mut desired_ecb,
ledge_info: &mut ledge_info,
prev_ecb: &previous_ecb,
stage_collision: &stage_collision,
@@ -863,6 +890,7 @@ mod tests {
end: FGVec2::lit("1", "-1"),
},
downward_stick_threshold: FGi32::ONE,
ecb_advanced: false,
};
assert!(context.air_collide_with_ledges().is_none());
@@ -871,5 +899,7 @@ mod tests {
context.air_collide_with_ledges().map(|result| result.ledge),
Some(ledge_id)
);
context.finish_ecb_frame();
assert_eq!(context.ecb.top, FGVec2::lit("0", "2"));
}
}