refactor physics instruction processing

This is an important engine upgrade: idle events do not donate their timestamp to engine state 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.
This commit is contained in:
Quaternions 2024-08-01 11:47:20 -07:00
parent e04e754abb
commit 8bf70e28cd
2 changed files with 116 additions and 60 deletions

View File

@ -13,8 +13,10 @@ use strafesnet_common::instruction::{self,InstructionEmitter,InstructionConsumer
use strafesnet_common::integer::{self,Time,Planar64,Planar64Vec3,Planar64Mat3,Angle32,Ratio64Vec2}; use strafesnet_common::integer::{self,Time,Planar64,Planar64Vec3,Planar64Mat3,Angle32,Ratio64Vec2};
use gameplay::ModeState; use gameplay::ModeState;
//internal influence
//when the physics asks itself what happens next, this is how it's represented
#[derive(Debug)] #[derive(Debug)]
pub enum PhysicsInstruction { enum PhysicsInternalInstruction{
CollisionStart(Collision), CollisionStart(Collision),
CollisionEnd(Collision), CollisionEnd(Collision),
StrafeTick, StrafeTick,
@ -25,10 +27,9 @@ pub enum PhysicsInstruction {
// bool,//true = Trigger; false = teleport // bool,//true = Trigger; false = teleport
// bool,//true = Force // 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)] #[derive(Debug)]
pub enum PhysicsInputInstruction{ pub enum PhysicsInputInstruction{
ReplaceMouse(MouseState,MouseState), ReplaceMouse(MouseState,MouseState),
@ -47,6 +48,14 @@ pub enum PhysicsInputInstruction {
//for interpolation / networking / playback reasons, most playback heads will always want //for interpolation / networking / playback reasons, most playback heads will always want
//to be 1 instruction ahead to generate the next state for interpolation. //to be 1 instruction ahead to generate the next state for interpolation.
PracticeFly, 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)] #[derive(Clone,Copy,Debug,Default,Hash)]
@ -559,13 +568,13 @@ impl MoveState{
=>None, =>None,
} }
} }
fn next_move_instruction(&self,strafe:&Option<gameplay_style::StrafeSettings>,time:Time)->Option<TimedInstruction<PhysicsInstruction>>{ fn next_move_instruction(&self,strafe:&Option<gameplay_style::StrafeSettings>,time:Time)->Option<TimedInstruction<PhysicsInternalInstruction>>{
//check if you have a valid walk state and create an instruction //check if you have a valid walk state and create an instruction
match self{ match self{
MoveState::Walk(walk_state)|MoveState::Ladder(walk_state)=>match &walk_state.target{ MoveState::Walk(walk_state)|MoveState::Ladder(walk_state)=>match &walk_state.target{
&TransientAcceleration::Reachable{acceleration:_,time}=>Some(TimedInstruction{ &TransientAcceleration::Reachable{acceleration:_,time}=>Some(TimedInstruction{
time, time,
instruction:PhysicsInstruction::ReachWalkTargetVelocity instruction:PhysicsInternalInstruction::ReachWalkTargetVelocity
}), }),
TransientAcceleration::Unreachable{acceleration:_} TransientAcceleration::Unreachable{acceleration:_}
|TransientAcceleration::Reached |TransientAcceleration::Reached
@ -575,7 +584,7 @@ impl MoveState{
TimedInstruction{ TimedInstruction{
time:strafe.next_tick(time), time:strafe.next_tick(time),
//only poll the physics if there is a before and after mouse event //only poll the physics if there is a before and after mouse event
instruction:PhysicsInstruction::StrafeTick instruction:PhysicsInternalInstruction::StrafeTick
} }
}), }),
MoveState::Water=>None,//TODO MoveState::Water=>None,//TODO
@ -769,7 +778,7 @@ impl TouchingState{
} }
} }
} }
fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector<PhysicsInstruction>,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,body:&Body,time:Time){ fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector<PhysicsInternalInstruction>,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,body:&Body,time:Time){
let relative_body=VirtualBody::relative(&Body::default(),body).body(time); let relative_body=VirtualBody::relative(&Body::default(),body).body(time);
for contact in &self.contacts{ for contact in &self.contacts{
//detect face slide off //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)|{ collector.collect(minkowski.predict_collision_face_out(&relative_body,collector.time(),contact.face_id).map(|(_face,time)|{
TimedInstruction{ TimedInstruction{
time, time,
instruction:PhysicsInstruction::CollisionEnd( instruction:PhysicsInternalInstruction::CollisionEnd(
Collision::Contact(ContactCollision{convex_mesh_id:contact.convex_mesh_id,face_id:contact.face_id}) 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)|{ collector.collect(minkowski.predict_collision_out(&relative_body,collector.time()).map(|(_face,time)|{
TimedInstruction{ TimedInstruction{
time, time,
instruction:PhysicsInstruction::CollisionEnd( instruction:PhysicsInternalInstruction::CollisionEnd(
Collision::Intersect(IntersectCollision{convex_mesh_id:intersect.convex_mesh_id}) Collision::Intersect(IntersectCollision{convex_mesh_id:intersect.convex_mesh_id})
), ),
} }
@ -954,11 +963,7 @@ impl PhysicsState {
fn clear(&mut self){ fn clear(&mut self){
self.touching.clear(); self.touching.clear();
} }
fn advance_time(&mut self, time: Time){ fn next_move_instruction(&self)->Option<TimedInstruction<PhysicsInternalInstruction>>{
self.body.advance_time(time);
self.time=time;
}
fn next_move_instruction(&self)->Option<TimedInstruction<PhysicsInstruction>>{
self.move_state.next_move_instruction(&self.style.strafe,self.time) self.move_state.next_move_instruction(&self.style.strafe,self.time)
} }
//lmao idk this is convenient //lmao idk this is convenient
@ -1031,31 +1036,33 @@ pub struct PhysicsContext{
state:PhysicsState,//this captures the entire state of the physics. 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. 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<PhysicsInstruction> for PhysicsContext{ impl instruction::InstructionConsumer<PhysicsInstruction> for PhysicsContext{
fn process_instruction(&mut self,ins:TimedInstruction<PhysicsInstruction>){ fn process_instruction(&mut self,ins:TimedInstruction<PhysicsInstruction>){
atomic_state_update(&mut self.state,&self.data,ins) atomic_state_update(&mut self.state,&self.data,ins)
} }
} }
impl instruction::InstructionEmitter<PhysicsInstruction> for PhysicsContext{ impl instruction::InstructionEmitter<PhysicsInternalInstruction> for PhysicsContext{
//this little next instruction function can cache its return value and invalidate the cached value by watching the State. //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<TimedInstruction<PhysicsInstruction>>{ fn next_instruction(&self,time_limit:Time)->Option<TimedInstruction<PhysicsInternalInstruction>>{
literally_next_instruction_but_with_context(&self.state,&self.data,time_limit) next_instruction_internal(&self.state,&self.data,time_limit)
} }
} }
impl PhysicsContext{ impl PhysicsContext{
pub fn clear(&mut self){ pub fn clear(&mut self){
self.state.clear(); self.state.clear();
} }
//TODO: remove non-standard interfaces to process_instruction
pub fn load_user_settings(&mut self,user_settings:&crate::settings::UserSettings){ 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, time:self.state.time,
instruction:PhysicsInstruction::SetSensitivity(user_settings.calculate_sensitivity()), instruction:PhysicsInputInstruction::SetSensitivity(user_settings.calculate_sensitivity()),
}); });
} }
pub fn spawn(&mut self){ pub fn spawn(&mut self){
self.process_instruction(TimedInstruction{ self.run_input_instruction(TimedInstruction{
time:self.state.time, time:self.state.time,
instruction:PhysicsInstruction::Input(PhysicsInputInstruction::Reset), instruction:PhysicsInputInstruction::Reset,
}); });
} }
pub const fn output(&self)->PhysicsOutputState{ pub const fn output(&self)->PhysicsOutputState{
@ -1141,16 +1148,19 @@ impl PhysicsContext{
} }
//tickless gaming //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. //prepare is ommitted - everything is done via instructions.
while let Some(instruction)=self.next_instruction(time_limit){//collect while let Some(instruction)=self.next_instruction(time_limit){//collect
//process //process
self.process_instruction(instruction); self.process_instruction(TimedInstruction{
time:instruction.time,
instruction:PhysicsInstruction::Internal(instruction.instruction),
});
//write hash lol //write hash lol
} }
} }
pub fn run_input_instruction(&mut self,instruction:TimedInstruction<PhysicsInputInstruction>){ pub fn run_input_instruction(&mut self,instruction:TimedInstruction<PhysicsInputInstruction>){
self.run(instruction.time); self.run_internal_exhaustive(instruction.time);
self.process_instruction(TimedInstruction{ self.process_instruction(TimedInstruction{
time:instruction.time, time:instruction.time,
instruction:PhysicsInstruction::Input(instruction.instruction), 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<TimedInstruction<PhysicsInstruction>>{ //this is the one who asks
fn next_instruction_internal(state:&PhysicsState,data:&PhysicsData,time_limit:Time)->Option<TimedInstruction<PhysicsInternalInstruction>>{
//JUST POLLING!!! NO MUTATION //JUST POLLING!!! NO MUTATION
let mut collector = instruction::InstructionCollector::new(time_limit); let mut collector = instruction::InstructionCollector::new(time_limit);
@ -1180,7 +1191,7 @@ impl PhysicsContext{
//temp (?) code to avoid collision loops //temp (?) code to avoid collision loops
.map_or(None,|(face,time)|if time==state.time{None}else{Some((face,time))}) .map_or(None,|(face,time)|if time==state.time{None}else{Some((face,time))})
.map(|(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::Contact{contacting:_,general:_}=>Collision::Contact(ContactCollision{convex_mesh_id,face_id:face}),
PhysicsCollisionAttributes::Intersect{intersecting:_,general:_}=>Collision::Intersect(IntersectCollision{convex_mesh_id}), PhysicsCollisionAttributes::Intersect{intersecting:_,general:_}=>Collision::Intersect(IntersectCollision{convex_mesh_id}),
})} })}
@ -1316,27 +1327,18 @@ fn run_teleport_behaviour(wormhole:&Option<gameplay_attributes::Wormhole>,models
} }
} }
fn atomic_state_update(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<PhysicsInstruction>){ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<PhysicsInternalInstruction>){
match &ins.instruction{ let should_advance_body=match ins.instruction{
PhysicsInstruction::Input(PhysicsInputInstruction::Idle) PhysicsInternalInstruction::CollisionStart(_)
|PhysicsInstruction::Input(PhysicsInputInstruction::SetNextMouse(_)) |PhysicsInternalInstruction::CollisionEnd(_)
|PhysicsInstruction::Input(PhysicsInputInstruction::ReplaceMouse(_,_)) |PhysicsInternalInstruction::StrafeTick
|PhysicsInstruction::StrafeTick=>(), |PhysicsInternalInstruction::ReachWalkTargetVelocity=>true,
_=>println!("{}|{:?}",ins.time,ins.instruction), };
} if should_advance_body{
//selectively update body state.body.advance_time(state.time);
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),
} }
match ins.instruction{ match ins.instruction{
PhysicsInstruction::CollisionStart(collision)=>{ PhysicsInternalInstruction::CollisionStart(collision)=>{
let convex_mesh_id=collision.convex_mesh_id(); let convex_mesh_id=collision.convex_mesh_id();
match (data.models.attr(convex_mesh_id.model_id),&collision){ match (data.models.attr(convex_mesh_id.model_id),&collision){
(PhysicsCollisionAttributes::Contact{contacting,general},&Collision::Contact(contact))=>{ (PhysicsCollisionAttributes::Contact{contacting,general},&Collision::Contact(contact))=>{
@ -1445,7 +1447,7 @@ fn run_teleport_behaviour(wormhole:&Option<gameplay_attributes::Wormhole>,models
_=>panic!("invalid pair"), _=>panic!("invalid pair"),
} }
}, },
PhysicsInstruction::CollisionEnd(collision)=>{ PhysicsInternalInstruction::CollisionEnd(collision)=>{
match (data.models.attr(collision.convex_mesh_id().model_id),&collision){ match (data.models.attr(collision.convex_mesh_id().model_id),&collision){
(PhysicsCollisionAttributes::Contact{contacting:_,general:_},&Collision::Contact(contact))=>{ (PhysicsCollisionAttributes::Contact{contacting:_,general:_},&Collision::Contact(contact))=>{
state.touching.remove(&collision);//remove contact before calling contact_constrain_acceleration state.touching.remove(&collision);//remove contact before calling contact_constrain_acceleration
@ -1478,7 +1480,7 @@ fn run_teleport_behaviour(wormhole:&Option<gameplay_attributes::Wormhole>,models
_=>panic!("invalid pair"), _=>panic!("invalid pair"),
} }
}, },
PhysicsInstruction::StrafeTick=>{ PhysicsInternalInstruction::StrafeTick=>{
//TODO make this less huge //TODO make this less huge
if let Some(strafe_settings)=&state.style.strafe{ if let Some(strafe_settings)=&state.style.strafe{
let controls=state.input_state.controls; let controls=state.input_state.controls;
@ -1496,7 +1498,7 @@ fn run_teleport_behaviour(wormhole:&Option<gameplay_attributes::Wormhole>,models
} }
} }
} }
PhysicsInstruction::ReachWalkTargetVelocity=>{ PhysicsInternalInstruction::ReachWalkTargetVelocity=>{
match &mut state.move_state{ match &mut state.move_state{
MoveState::Air MoveState::Air
|MoveState::Water |MoveState::Water
@ -1520,10 +1522,48 @@ fn run_teleport_behaviour(wormhole:&Option<gameplay_attributes::Wormhole>,models
} }
} }
}, },
PhysicsInstruction::SetSensitivity(sensitivity)=>state.camera.sensitivity=sensitivity, }
PhysicsInstruction::Input(input_instruction)=>{ }
fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<PhysicsInputInstruction>){
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; let mut b_refresh_walk_target=true;
match input_instruction{ match ins.instruction{
PhysicsInputInstruction::SetSensitivity(sensitivity)=>state.camera.sensitivity=sensitivity,
PhysicsInputInstruction::SetNextMouse(m)=>{ PhysicsInputInstruction::SetNextMouse(m)=>{
state.camera.move_mouse(state.input_state.mouse_delta()); state.camera.move_mouse(state.input_state.mouse_delta());
state.input_state.set_next_mouse(m); state.input_state.set_next_mouse(m);
@ -1539,7 +1579,6 @@ fn run_teleport_behaviour(wormhole:&Option<gameplay_attributes::Wormhole>,models
PhysicsInputInstruction::SetMoveUp(s)=>state.input_state.set_control(Controls::MoveUp,s), PhysicsInputInstruction::SetMoveUp(s)=>state.input_state.set_control(Controls::MoveUp,s),
PhysicsInputInstruction::SetMoveDown(s)=>state.input_state.set_control(Controls::MoveDown,s), PhysicsInputInstruction::SetMoveDown(s)=>state.input_state.set_control(Controls::MoveDown,s),
PhysicsInputInstruction::SetJump(s)=>{ PhysicsInputInstruction::SetJump(s)=>{
b_refresh_walk_target=false;
state.input_state.set_control(Controls::Jump,s); state.input_state.set_control(Controls::Jump,s);
if let Some(walk_state)=state.move_state.get_walk_state(){ if let Some(walk_state)=state.move_state.get_walk_state(){
if let Some(jump_settings)=&state.style.jump{ if let Some(jump_settings)=&state.style.jump{
@ -1548,6 +1587,7 @@ fn run_teleport_behaviour(wormhole:&Option<gameplay_attributes::Wormhole>,models
state.cull_velocity(&data,jumped_velocity); state.cull_velocity(&data,jumped_velocity);
} }
} }
b_refresh_walk_target=false;
}, },
PhysicsInputInstruction::SetZoom(s)=>{ PhysicsInputInstruction::SetZoom(s)=>{
state.input_state.set_control(Controls::Zoom,s); state.input_state.set_control(Controls::Zoom,s);
@ -1577,13 +1617,29 @@ fn run_teleport_behaviour(wormhole:&Option<gameplay_attributes::Wormhole>,models
} }
b_refresh_walk_target=false; 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{ if b_refresh_walk_target{
state.apply_input_and_body(data); state.apply_input_and_body(data);
state.cull_velocity(data,state.body.velocity); state.cull_velocity(data,state.body.velocity);
} }
}, }
fn atomic_state_update(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<PhysicsInstruction>){
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}),
} }
} }

View File

@ -190,7 +190,7 @@ mod test{
for _ in 0..5 { for _ in 0..5 {
let task = instruction::TimedInstruction{ let task = instruction::TimedInstruction{
time:integer::Time::ZERO, time:integer::Time::ZERO,
instruction:physics::PhysicsInstruction::StrafeTick, instruction:physics::PhysicsInputInstruction::Idle,
}; };
worker.send(task).unwrap(); worker.send(task).unwrap();
} }
@ -204,7 +204,7 @@ mod test{
// Send a new task // Send a new task
let task = instruction::TimedInstruction{ let task = instruction::TimedInstruction{
time:integer::Time::ZERO, time:integer::Time::ZERO,
instruction:physics::PhysicsInstruction::StrafeTick, instruction:physics::PhysicsInputInstruction::Idle,
}; };
worker.send(task).unwrap(); worker.send(task).unwrap();