Compare commits

..

6 Commits

Author SHA1 Message Date
b33329a26a physics v0.0.2-surf2 with surf bodge 2026-03-31 12:53:33 -07:00
f3d705dc2b bodge surf 2026-03-31 12:52:57 -07:00
a482c9859a physics: stateful strafe ticks
strafe ticks need to be stateful so they can participate in zero-time events.
2026-03-31 12:51:49 -07:00
e6206c0e80 session: fix time travel bug 2026-03-31 12:51:03 -07:00
77a3dc4aad it: fail test, needs stateful strafe ticks to pass 2026-03-31 11:41:18 -07:00
fc9a720bc1 it: press_w unit test 2026-03-31 11:02:56 -07:00
7 changed files with 89 additions and 96 deletions

2
Cargo.lock generated
View File

@@ -4045,7 +4045,7 @@ dependencies = [
[[package]]
name = "strafesnet_physics"
version = "0.0.2"
version = "0.0.2-surf2"
dependencies = [
"arrayvec",
"glam",

View File

@@ -1,6 +1,6 @@
[package]
name = "strafesnet_physics"
version = "0.0.2"
version = "0.0.2-surf2"
edition = "2024"
[dependencies]

View File

@@ -174,8 +174,7 @@ fn ground_things(walk_settings:&gameplay_style::WalkSettings,contact:&ContactCol
let gravity=touching.base_acceleration(models,style,camera,input_state);
let control_dir=style.get_y_control_dir(camera,input_state.controls);
let target_velocity=walk_settings.get_walk_target_velocity(control_dir,normal);
let contacts=touching.contacts(models,hitbox_mesh);
let target_velocity_clipped=touching.constrain_velocity(&contacts,target_velocity).velocity;
let target_velocity_clipped=touching.constrain_velocity(models,hitbox_mesh,target_velocity);
(gravity,target_velocity_clipped)
}
fn ladder_things(ladder_settings:&gameplay_style::LadderSettings,contact:&ContactCollision,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,style:&StyleModifiers,camera:&PhysicsCamera,input_state:&InputState)->(Planar64Vec3,Planar64Vec3){
@@ -183,8 +182,7 @@ fn ladder_things(ladder_settings:&gameplay_style::LadderSettings,contact:&Contac
let gravity=touching.base_acceleration(models,style,camera,input_state);
let control_dir=style.get_y_control_dir(camera,input_state.controls);
let target_velocity=ladder_settings.get_ladder_target_velocity(control_dir,normal);
let contacts=touching.contacts(models,hitbox_mesh);
let target_velocity_clipped=touching.constrain_velocity(&contacts,target_velocity).velocity;
let target_velocity_clipped=touching.constrain_velocity(models,hitbox_mesh,target_velocity);
(gravity,target_velocity_clipped)
}
@@ -503,6 +501,27 @@ impl StyleHelper for StyleModifiers{
}
}
#[derive(Clone,Debug)]
struct StrafeTickState{
tick_number:u64,
}
impl StrafeTickState{
fn new(time:Time,settings:&gameplay_style::StrafeSettings)->Self{
// let time=n*seconds/ticks;
let time=time.nanos() as i128;
let ticks=settings.tick_rate.num() as i128;
let seconds=settings.tick_rate.den() as i128;
let tick_number=(time*ticks/seconds) as u64;
StrafeTickState{tick_number}
}
fn next_tick(&self,settings:&gameplay_style::StrafeSettings)->Time{
let n=self.tick_number as i128;
let ticks=settings.tick_rate.num() as i128;
let seconds=settings.tick_rate.den() as i128;
let time=n*seconds/ticks;
Time::from_nanos(time as i64)
}
}
#[derive(Clone,Debug)]
enum MoveState{
Air,
Walk(ContactMoveState),
@@ -522,8 +541,7 @@ impl MoveState{
// calculate base acceleration
let base_acceleration=touching.base_acceleration(models,style,camera,input_state);
// constrain_acceleration clips according to contacts
let contacts=touching.contacts(models,hitbox_mesh);
touching.constrain_acceleration(&contacts,base_acceleration).acceleration
touching.constrain_acceleration(models,hitbox_mesh,base_acceleration)
},
MoveState::Walk(walk_state)
|MoveState::Ladder(walk_state)
@@ -581,7 +599,7 @@ impl MoveState{
=>None,
}
}
fn next_move_instruction(&self,strafe:&Option<gameplay_style::StrafeSettings>,time:Time)->Option<TimedInstruction<InternalInstruction,Time>>{
fn next_move_instruction(&self)->Option<TimedInstruction<InternalInstruction,Time>>{
//check if you have a valid walk state and create an instruction
match self{
MoveState::Walk(walk_state)|MoveState::Ladder(walk_state)=>match &walk_state.target{
@@ -593,13 +611,7 @@ impl MoveState{
|TransientAcceleration::Reached
=>None,
}
MoveState::Air=>strafe.as_ref().map(|strafe|{
TimedInstruction{
time:strafe.next_tick(time),
//only poll the physics if there is a before and after mouse event
instruction:InternalInstruction::StrafeTick
}
}),
MoveState::Air=>None,
MoveState::Water=>None,//TODO
MoveState::Fly=>None,
}
@@ -756,18 +768,6 @@ impl Collision{
}
}
}
struct Contacts<'a>{
contacts:Vec<crate::push_solve::Contact>,
lifetime:core::marker::PhantomData<&'a ()>,
}
struct ConstrainedVelocity<'a>{
velocity:Planar64Vec3,
constraints:crate::push_solve::Conts<'a>,
}
struct ConstrainedAcceleration<'a>{
acceleration:Planar64Vec3,
constraints:crate::push_solve::Conts<'a>,
}
#[derive(Clone,Debug,Default)]
struct TouchingState{
// This is kind of jank, it's a ContactCollision
@@ -823,8 +823,8 @@ impl TouchingState{
//TODO: add water
a
}
fn contacts<'a>(&'a self,models:&PhysicsModels,hitbox_mesh:&HitboxMesh)->Contacts<'a>{
let contacts=self.contacts.iter().map(|(convex_mesh_id,face_id)|{
fn constrain_velocity(&self,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,velocity:Planar64Vec3)->Planar64Vec3{
let contacts:Vec<_>=self.contacts.iter().map(|(convex_mesh_id,face_id)|{
let n=contact_normal(models,hitbox_mesh,convex_mesh_id,*face_id);
crate::push_solve::Contact{
position:vec3::zero(),
@@ -832,24 +832,18 @@ impl TouchingState{
normal:n,
}
}).collect();
Contacts{
contacts,
lifetime:core::marker::PhantomData,
}
crate::push_solve::push_solve(&contacts,velocity).0
}
fn constrain_velocity<'a>(&self,contacts:&'a Contacts<'_>,velocity:Planar64Vec3)->ConstrainedVelocity<'a>{
let (velocity,constraints)=crate::push_solve::push_solve(&contacts.contacts,velocity);
ConstrainedVelocity{
velocity,
constraints
}
}
fn constrain_acceleration<'a>(&self,contacts:&'a Contacts<'_>,acceleration:Planar64Vec3)->ConstrainedAcceleration<'a>{
let (acceleration,constraints)=crate::push_solve::push_solve(&contacts.contacts,acceleration);
ConstrainedAcceleration{
acceleration,
constraints
}
fn constrain_acceleration(&self,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,acceleration:Planar64Vec3)->Planar64Vec3{
let contacts:Vec<_>=self.contacts.iter().map(|(convex_mesh_id,face_id)|{
let n=contact_normal(models,hitbox_mesh,convex_mesh_id,*face_id);
crate::push_solve::Contact{
position:vec3::zero(),
velocity:n,
normal:n,
}
}).collect();
crate::push_solve::push_solve(&contacts,acceleration).0
}
fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector<InternalInstruction,Time>,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,trajectory:&Trajectory,start_time:Time){
// let relative_body=body.relative_to(&Body::ZERO);
@@ -899,6 +893,7 @@ pub struct PhysicsState{
//gameplay_state
mode_state:ModeState,
move_state:MoveState,
strafe_tick_state:StrafeTickState,
//run is non optional: when you spawn in a run is created
//the run cannot be finished unless you start it by visiting
//a start zone. If you change mode, a new run is created.
@@ -917,6 +912,7 @@ impl Default for PhysicsState{
input_state:InputState::default(),
_world:WorldState{},
mode_state:ModeState::default(),
strafe_tick_state:StrafeTickState::new(Time::ZERO,&StyleModifiers::default().strafe.unwrap()),
run:run::Run::new(),
}
}
@@ -963,7 +959,7 @@ impl PhysicsState{
*self=Self::default();
}
fn next_move_instruction(&self)->Option<TimedInstruction<InternalInstruction,Time>>{
self.move_state.next_move_instruction(&self.style.strafe,self.time)
self.move_state.next_move_instruction()
}
fn set_move_state(&mut self,data:&PhysicsData,move_state:MoveState){
self.move_state.set_move_state(move_state,&mut self.body,&self.touching,&data.models,&data.hitbox_mesh,&self.style,&self.camera,&self.input_state);
@@ -1221,8 +1217,18 @@ fn next_instruction_internal(state:&PhysicsState,data:&PhysicsData,time_limit:Ti
//JUST POLLING!!! NO MUTATION
let mut collector=instruction::InstructionCollector::new(time_limit);
// walking
collector.collect(state.next_move_instruction());
// strafe tick
collector.collect(state.style.strafe.as_ref().map(|strafe|{
TimedInstruction{
time:state.strafe_tick_state.next_tick(strafe),
//only poll the physics if there is a before and after mouse event
instruction:InternalInstruction::StrafeTick
}
}));
let trajectory=state.body.with_acceleration(state.acceleration(data));
//check for collision ends
state.touching.predict_collision_end(&mut collector,&data.models,&data.hitbox_mesh,&trajectory,state.time);
@@ -1348,50 +1354,22 @@ fn set_position(
recalculate_touching(move_state,body,touching,run,mode_state,mode,models,hitbox_mesh,bvh,style,camera,input_state,time);
point
}
/// Returns true when a contact was removed
fn set_velocity_cull(body:&mut Body,touching:&mut TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,v:Planar64Vec3)->bool{
let contacts=touching.contacts(models,hitbox_mesh);
let ConstrainedVelocity{velocity,mut constraints}=touching.constrain_velocity(&contacts,v);
// prep list for drain
constraints.sort_by_key(|&cont|{
let cont_ptr:*const crate::push_solve::Contact=cont;
contacts.contacts.len()-(cont_ptr as usize-contacts.contacts.as_ptr() as usize)
});
// create a list of indices to retain
let mut indices:arrayvec::ArrayVec<_,4>=constraints.iter().map(|&cont|{
let cont_ptr:*const crate::push_solve::Contact=cont;
cont_ptr as usize-contacts.contacts.as_ptr() as usize
}).collect();
drop(constraints);
let mut is_contact_removed=false;
// Delete contacts which do not constrain the velocity
let mut i=0;
touching.contacts.retain(|_,_|{
if let Some(&next_i)=indices.last(){
let is_active=i==next_i;
if is_active{
indices.pop();
}else{
is_contact_removed=true;
}
i+=1;
return is_active
//This is not correct but is better than what I have
let mut culled=false;
touching.contacts.retain(|convex_mesh_id,face_id|{
let n=contact_normal(models,hitbox_mesh,convex_mesh_id,*face_id);
let r=(n.dot(v)>>52).is_positive();
if r{
culled=true;
}
is_contact_removed=true;
false
!r
});
body.velocity=velocity;
is_contact_removed
set_velocity(body,touching,models,hitbox_mesh,v);
culled
}
fn set_velocity(body:&mut Body,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,v:Planar64Vec3){
let contacts=touching.contacts(models,hitbox_mesh);
body.velocity=touching.constrain_velocity(&contacts,v).velocity;
body.velocity=touching.constrain_velocity(models,hitbox_mesh,v);
}
fn teleport(
@@ -1831,6 +1809,7 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
),
},
InternalInstruction::StrafeTick=>{
state.strafe_tick_state.tick_number+=1;
//TODO make this less huge
if let Some(strafe_settings)=&state.style.strafe{
let controls=state.input_state.controls;

View File

@@ -10,7 +10,7 @@ use strafesnet_common::ray::Ray;
// A stack-allocated variable-size list that holds up to 4 elements
// Direct references are used instead of indices i0, i1, i2, i3
pub type Conts<'a>=arrayvec::ArrayVec<&'a Contact,4>;
type Conts<'a>=arrayvec::ArrayVec<&'a Contact,4>;
// hack to allow comparing ratios to zero
const RATIO_ZERO:Ratio<F64_32,F64_32>=Ratio::new(Fixed::ZERO,Fixed::EPSILON);

View File

@@ -244,18 +244,18 @@ impl InstructionConsumer<Instruction<'_>> for Session{
self.clear_recording();
let mode_id=self.simulation.physics.mode();
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Reset));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(MiscInstruction::SetSensitivity(self.user_settings().calculate_sensitivity())));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Restart(mode_id)));
// TODO: think about this harder. This works around a bug where you fall infinitely when you reset.
self.simulation.timer.set_time(ins.time,PhysicsTime::ZERO);
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(MiscInstruction::SetSensitivity(self.user_settings().calculate_sensitivity())));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Restart(mode_id)));
},
Instruction::Input(SessionInputInstruction::Mode(ImplicitModeInstruction::ResetAndSpawn(mode_id,spawn_id)))=>{
self.clear_recording();
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Reset));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(MiscInstruction::SetSensitivity(self.user_settings().calculate_sensitivity())));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Spawn(mode_id,spawn_id)));
// TODO: think about this harder. This works around a bug where you fall infinitely when you reset.
self.simulation.timer.set_time(ins.time,PhysicsTime::ZERO);
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(MiscInstruction::SetSensitivity(self.user_settings().calculate_sensitivity())));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Spawn(mode_id,spawn_id)));
},
Instruction::Input(SessionInputInstruction::Misc(misc_instruction))=>{
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(misc_instruction));

View File

@@ -111,3 +111,21 @@ fn bug_3(){
assert_eq!(body.velocity,vec3::int(0,0,0));
assert_eq!(body.time,Time::from_secs(2));
}
#[test]
fn press_w(){
let physics_data=test_scene();
let mut physics=PhysicsState::default();
// press W
PhysicsContext::run_input_instruction(&mut physics,&physics_data,strafesnet_common::instruction::TimedInstruction{
time:Time::ZERO,
instruction:strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveForward(true)),
});
// wait 10 ms
PhysicsContext::run_input_instruction(&mut physics,&physics_data,strafesnet_common::instruction::TimedInstruction{
time:Time::from_millis(10),
instruction:strafesnet_common::physics::Instruction::Idle,
});
// observe current velocity
assert_eq!(physics.body().velocity,vec3::raw_xyz(0,-1<<32,-0x2b3333333));
}

View File

@@ -2,7 +2,6 @@ const VALVE_SCALE:Planar64=Planar64::raw(1<<28);// 1/16
use crate::integer::{int,vec3::int as int3,AbsoluteTime,Ratio64,Planar64,Planar64Vec3};
use crate::controls_bitflag::Controls;
use crate::physics::Time as PhysicsTime;
#[derive(Clone,Debug)]
pub struct StyleModifiers{
@@ -273,9 +272,6 @@ impl StrafeSettings{
false=>None,
}
}
pub fn next_tick(&self,time:PhysicsTime)->PhysicsTime{
PhysicsTime::from_nanos(self.tick_rate.rhs_div_int(self.tick_rate.mul_int(time.nanos())+1))
}
pub const fn activates(&self,controls:Controls)->bool{
self.enable.activates(controls)
}