From 755adeaefd92d219cf769e8b1e8ce9d71b145ae4 Mon Sep 17 00:00:00 2001 From: Quaternions Date: Thu, 1 Aug 2024 11:47:20 -0700 Subject: [PATCH] refactor physics instruction processing This is an important engine upgrade: idle events do not donate their timestamp to engine objects and pollute the timeline with unnecessary game ticks that can be represented as analytic continuations of previous game ticks. This means that all "render" tick updates can be dropped from bot timelines. In other words, progressing the physics simulation is invariant to differing subdivisions of an overall time advancement with no external input. --- src/physics.rs | 172 ++++++++++++++++++++++++++++++++----------------- src/worker.rs | 4 +- 2 files changed, 116 insertions(+), 60 deletions(-) diff --git a/src/physics.rs b/src/physics.rs index e0f28aeb..872efae4 100644 --- a/src/physics.rs +++ b/src/physics.rs @@ -13,8 +13,10 @@ use strafesnet_common::instruction::{self,InstructionEmitter,InstructionConsumer use strafesnet_common::integer::{self,Time,Planar64,Planar64Vec3,Planar64Mat3,Angle32,Ratio64Vec2}; use gameplay::ModeState; +//internal influence +//when the physics asks itself what happens next, this is how it's represented #[derive(Debug)] -pub enum PhysicsInstruction { +enum PhysicsInternalInstruction{ CollisionStart(Collision), CollisionEnd(Collision), StrafeTick, @@ -25,12 +27,11 @@ pub enum PhysicsInstruction { // bool,//true = Trigger; false = teleport // bool,//true = Force // ) - //InputInstructions conditionally activate RefreshWalkTarget (by doing what SetWalkTargetVelocity used to do and then flagging it) - Input(PhysicsInputInstruction), - SetSensitivity(Ratio64Vec2), } +//external influence +//this is how you influence the physics from outside #[derive(Debug)] -pub enum PhysicsInputInstruction { +pub enum PhysicsInputInstruction{ ReplaceMouse(MouseState,MouseState), SetNextMouse(MouseState), SetMoveRight(bool), @@ -47,6 +48,14 @@ pub enum PhysicsInputInstruction { //for interpolation / networking / playback reasons, most playback heads will always want //to be 1 instruction ahead to generate the next state for interpolation. PracticeFly, + SetSensitivity(Ratio64Vec2), +} +#[derive(Debug)] +enum PhysicsInstruction{ + Internal(PhysicsInternalInstruction), + //InputInstructions conditionally activate RefreshWalkTarget + //(by doing what SetWalkTargetVelocity used to do and then flagging it) + Input(PhysicsInputInstruction), } #[derive(Clone,Copy,Debug,Default,Hash)] @@ -559,13 +568,13 @@ impl MoveState{ =>None, } } - fn next_move_instruction(&self,strafe:&Option,time:Time)->Option>{ + fn next_move_instruction(&self,strafe:&Option,time:Time)->Option>{ //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{ &TransientAcceleration::Reachable{acceleration:_,time}=>Some(TimedInstruction{ time, - instruction:PhysicsInstruction::ReachWalkTargetVelocity + instruction:PhysicsInternalInstruction::ReachWalkTargetVelocity }), TransientAcceleration::Unreachable{acceleration:_} |TransientAcceleration::Reached @@ -575,7 +584,7 @@ impl MoveState{ TimedInstruction{ time:strafe.next_tick(time), //only poll the physics if there is a before and after mouse event - instruction:PhysicsInstruction::StrafeTick + instruction:PhysicsInternalInstruction::StrafeTick } }), MoveState::Water=>None,//TODO @@ -769,7 +778,7 @@ impl TouchingState{ } } } - fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,body:&Body,time:Time){ + fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,body:&Body,time:Time){ let relative_body=VirtualBody::relative(&Body::default(),body).body(time); for contact in &self.contacts{ //detect face slide off @@ -778,7 +787,7 @@ impl TouchingState{ collector.collect(minkowski.predict_collision_face_out(&relative_body,collector.time(),contact.face_id).map(|(_face,time)|{ TimedInstruction{ time, - instruction:PhysicsInstruction::CollisionEnd( + instruction:PhysicsInternalInstruction::CollisionEnd( Collision::Contact(ContactCollision{convex_mesh_id:contact.convex_mesh_id,face_id:contact.face_id}) ), } @@ -791,7 +800,7 @@ impl TouchingState{ collector.collect(minkowski.predict_collision_out(&relative_body,collector.time()).map(|(_face,time)|{ TimedInstruction{ time, - instruction:PhysicsInstruction::CollisionEnd( + instruction:PhysicsInternalInstruction::CollisionEnd( Collision::Intersect(IntersectCollision{convex_mesh_id:intersect.convex_mesh_id}) ), } @@ -950,15 +959,11 @@ impl Default for PhysicsData{ } } -impl PhysicsState { +impl PhysicsState{ fn clear(&mut self){ self.touching.clear(); } - fn advance_time(&mut self, time: Time){ - self.body.advance_time(time); - self.time=time; - } - fn next_move_instruction(&self)->Option>{ + fn next_move_instruction(&self)->Option>{ self.move_state.next_move_instruction(&self.style.strafe,self.time) } //lmao idk this is convenient @@ -1031,31 +1036,33 @@ pub struct PhysicsContext{ state:PhysicsState,//this captures the entire state of the physics. data:PhysicsData,//data currently loaded into memory which is needded for physics to run, but is not part of the state. } +//the physics consumes the generic PhysicsInstruction, but can only emit the more narrow PhysicsInternalInstruction impl instruction::InstructionConsumer for PhysicsContext{ fn process_instruction(&mut self,ins:TimedInstruction){ atomic_state_update(&mut self.state,&self.data,ins) } } -impl instruction::InstructionEmitter for PhysicsContext{ +impl instruction::InstructionEmitter for PhysicsContext{ //this little next instruction function can cache its return value and invalidate the cached value by watching the State. - fn next_instruction(&self,time_limit:Time)->Option>{ - literally_next_instruction_but_with_context(&self.state,&self.data,time_limit) + fn next_instruction(&self,time_limit:Time)->Option>{ + next_instruction_internal(&self.state,&self.data,time_limit) } } impl PhysicsContext{ pub fn clear(&mut self){ self.state.clear(); } + //TODO: remove non-standard interfaces to process_instruction pub fn load_user_settings(&mut self,user_settings:&crate::settings::UserSettings){ - self.process_instruction(TimedInstruction{ + self.run_input_instruction(TimedInstruction{ time:self.state.time, - instruction:PhysicsInstruction::SetSensitivity(user_settings.calculate_sensitivity()), + instruction:PhysicsInputInstruction::SetSensitivity(user_settings.calculate_sensitivity()), }); } pub fn spawn(&mut self){ - self.process_instruction(TimedInstruction{ + self.run_input_instruction(TimedInstruction{ time:self.state.time, - instruction:PhysicsInstruction::Input(PhysicsInputInstruction::Reset), + instruction:PhysicsInputInstruction::Reset, }); } pub const fn output(&self)->PhysicsOutputState{ @@ -1141,16 +1148,19 @@ impl PhysicsContext{ } //tickless gaming - fn run(&mut self,time_limit:Time){ + fn run_internal_exhaustive(&mut self,time_limit:Time){ //prepare is ommitted - everything is done via instructions. while let Some(instruction)=self.next_instruction(time_limit){//collect //process - self.process_instruction(instruction); + self.process_instruction(TimedInstruction{ + time:instruction.time, + instruction:PhysicsInstruction::Internal(instruction.instruction), + }); //write hash lol } } pub fn run_input_instruction(&mut self,instruction:TimedInstruction){ - self.run(instruction.time); + self.run_internal_exhaustive(instruction.time); self.process_instruction(TimedInstruction{ time:instruction.time, instruction:PhysicsInstruction::Input(instruction.instruction), @@ -1158,7 +1168,8 @@ impl PhysicsContext{ } } - fn literally_next_instruction_but_with_context(state:&PhysicsState,data:&PhysicsData,time_limit:Time)->Option>{ + //this is the one who asks + fn next_instruction_internal(state:&PhysicsState,data:&PhysicsData,time_limit:Time)->Option>{ //JUST POLLING!!! NO MUTATION let mut collector = instruction::InstructionCollector::new(time_limit); @@ -1180,7 +1191,7 @@ impl PhysicsContext{ //temp (?) code to avoid collision loops .map_or(None,|(face,time)|if time==state.time{None}else{Some((face,time))}) .map(|(face,time)|{ - TimedInstruction{time,instruction:PhysicsInstruction::CollisionStart(match data.models.attr(convex_mesh_id.model_id){ + TimedInstruction{time,instruction:PhysicsInternalInstruction::CollisionStart(match data.models.attr(convex_mesh_id.model_id){ PhysicsCollisionAttributes::Contact{contacting:_,general:_}=>Collision::Contact(ContactCollision{convex_mesh_id,face_id:face}), PhysicsCollisionAttributes::Intersect{intersecting:_,general:_}=>Collision::Intersect(IntersectCollision{convex_mesh_id}), })} @@ -1316,27 +1327,18 @@ fn run_teleport_behaviour(wormhole:&Option,models } } - fn atomic_state_update(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction){ - match &ins.instruction{ - PhysicsInstruction::Input(PhysicsInputInstruction::Idle) - |PhysicsInstruction::Input(PhysicsInputInstruction::SetNextMouse(_)) - |PhysicsInstruction::Input(PhysicsInputInstruction::ReplaceMouse(_,_)) - |PhysicsInstruction::StrafeTick=>(), - _=>println!("{}|{:?}",ins.time,ins.instruction), - } - //selectively update body - match &ins.instruction{ - PhysicsInstruction::Input(PhysicsInputInstruction::Idle)=>state.time=ins.time,//idle simply updates time - PhysicsInstruction::Input(_) - |PhysicsInstruction::ReachWalkTargetVelocity - |PhysicsInstruction::CollisionStart(_) - |PhysicsInstruction::CollisionEnd(_) - |PhysicsInstruction::StrafeTick - |PhysicsInstruction::SetSensitivity(_) - =>state.advance_time(ins.time), - } +fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction){ + let should_advance_body=match ins.instruction{ + PhysicsInternalInstruction::CollisionStart(_) + |PhysicsInternalInstruction::CollisionEnd(_) + |PhysicsInternalInstruction::StrafeTick + |PhysicsInternalInstruction::ReachWalkTargetVelocity=>true, + }; + if should_advance_body{ + state.body.advance_time(state.time); + } match ins.instruction{ - PhysicsInstruction::CollisionStart(collision)=>{ + PhysicsInternalInstruction::CollisionStart(collision)=>{ let convex_mesh_id=collision.convex_mesh_id(); match (data.models.attr(convex_mesh_id.model_id),&collision){ (PhysicsCollisionAttributes::Contact{contacting,general},&Collision::Contact(contact))=>{ @@ -1445,7 +1447,7 @@ fn run_teleport_behaviour(wormhole:&Option,models _=>panic!("invalid pair"), } }, - PhysicsInstruction::CollisionEnd(collision)=>{ + PhysicsInternalInstruction::CollisionEnd(collision)=>{ match (data.models.attr(collision.convex_mesh_id().model_id),&collision){ (PhysicsCollisionAttributes::Contact{contacting:_,general:_},&Collision::Contact(contact))=>{ state.touching.remove(&collision);//remove contact before calling contact_constrain_acceleration @@ -1478,7 +1480,7 @@ fn run_teleport_behaviour(wormhole:&Option,models _=>panic!("invalid pair"), } }, - PhysicsInstruction::StrafeTick=>{ + PhysicsInternalInstruction::StrafeTick=>{ //TODO make this less huge if let Some(strafe_settings)=&state.style.strafe{ let controls=state.input_state.controls; @@ -1496,7 +1498,7 @@ fn run_teleport_behaviour(wormhole:&Option,models } } } - PhysicsInstruction::ReachWalkTargetVelocity=>{ + PhysicsInternalInstruction::ReachWalkTargetVelocity=>{ match &mut state.move_state{ MoveState::Air |MoveState::Water @@ -1520,10 +1522,48 @@ fn run_teleport_behaviour(wormhole:&Option,models } } }, - PhysicsInstruction::SetSensitivity(sensitivity)=>state.camera.sensitivity=sensitivity, - PhysicsInstruction::Input(input_instruction)=>{ + } + } + +fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction){ + let should_advance_body=match ins.instruction{ + //the body may as well be a quantum wave function + //as far as these instruction are concerned (they don't care where it is) + PhysicsInputInstruction::SetSensitivity(..) + |PhysicsInputInstruction::Reset + |PhysicsInputInstruction::SetZoom(..) + |PhysicsInputInstruction::Idle=>false, + //these controls only update the body if you are on the ground + PhysicsInputInstruction::SetNextMouse(..) + |PhysicsInputInstruction::ReplaceMouse(..) + |PhysicsInputInstruction::SetMoveForward(..) + |PhysicsInputInstruction::SetMoveLeft(..) + |PhysicsInputInstruction::SetMoveBack(..) + |PhysicsInputInstruction::SetMoveRight(..) + |PhysicsInputInstruction::SetMoveUp(..) + |PhysicsInputInstruction::SetMoveDown(..) + |PhysicsInputInstruction::SetJump(..)=>{ + //technically this could be refined further + //and only advance if you are moving relative to the contact + //but this is good enough for now + match &state.move_state{ + MoveState::Fly + |MoveState::Water + |MoveState::Walk(_) + |MoveState::Ladder(_)=>true, + MoveState::Air=>false, + } + }, + //the body must be updated unconditionally + PhysicsInputInstruction::PracticeFly=>true, + }; + if should_advance_body{ + state.body.advance_time(state.time); + } + //TODO: UNTAB let mut b_refresh_walk_target=true; - match input_instruction{ + match ins.instruction{ + PhysicsInputInstruction::SetSensitivity(sensitivity)=>state.camera.sensitivity=sensitivity, PhysicsInputInstruction::SetNextMouse(m)=>{ state.camera.move_mouse(state.input_state.mouse_delta()); state.input_state.set_next_mouse(m); @@ -1539,7 +1579,6 @@ fn run_teleport_behaviour(wormhole:&Option,models PhysicsInputInstruction::SetMoveUp(s)=>state.input_state.set_control(Controls::MoveUp,s), PhysicsInputInstruction::SetMoveDown(s)=>state.input_state.set_control(Controls::MoveDown,s), PhysicsInputInstruction::SetJump(s)=>{ - b_refresh_walk_target=false; state.input_state.set_control(Controls::Jump,s); if let Some(walk_state)=state.move_state.get_walk_state(){ if let Some(jump_settings)=&state.style.jump{ @@ -1548,6 +1587,7 @@ fn run_teleport_behaviour(wormhole:&Option,models state.cull_velocity(&data,jumped_velocity); } } + b_refresh_walk_target=false; }, PhysicsInputInstruction::SetZoom(s)=>{ state.input_state.set_control(Controls::Zoom,s); @@ -1577,13 +1617,29 @@ fn run_teleport_behaviour(wormhole:&Option,models } b_refresh_walk_target=false; }, - PhysicsInputInstruction::Idle=>{b_refresh_walk_target=false;},//literally idle! + PhysicsInputInstruction::Idle=>{ + //literally idle! + b_refresh_walk_target=false; + }, } if b_refresh_walk_target{ state.apply_input_and_body(data); state.cull_velocity(data,state.body.velocity); } - }, +} + + fn atomic_state_update(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction){ + match &ins.instruction{ + PhysicsInstruction::Input(PhysicsInputInstruction::Idle) + |PhysicsInstruction::Input(PhysicsInputInstruction::SetNextMouse(_)) + |PhysicsInstruction::Input(PhysicsInputInstruction::ReplaceMouse(_,_)) + |PhysicsInstruction::Internal(PhysicsInternalInstruction::StrafeTick)=>(), + _=>println!("{}|{:?}",ins.time,ins.instruction), + } + state.time=ins.time; + match ins.instruction{ + PhysicsInstruction::Internal(instruction)=>atomic_internal_instruction(state,data,TimedInstruction{time:ins.time,instruction}), + PhysicsInstruction::Input(instruction)=>atomic_input_instruction(state,data,TimedInstruction{time:ins.time,instruction}), } } diff --git a/src/worker.rs b/src/worker.rs index b0a3b432..325a3b95 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -190,7 +190,7 @@ mod test{ for _ in 0..5 { let task = instruction::TimedInstruction{ time:integer::Time::ZERO, - instruction:physics::PhysicsInstruction::StrafeTick, + instruction:physics::PhysicsInputInstruction::Idle, }; worker.send(task).unwrap(); } @@ -204,7 +204,7 @@ mod test{ // Send a new task let task = instruction::TimedInstruction{ time:integer::Time::ZERO, - instruction:physics::PhysicsInstruction::StrafeTick, + instruction:physics::PhysicsInputInstruction::Idle, }; worker.send(task).unwrap();