dimforge / bevy_rapier

Official Rapier plugin for the Bevy game engine.
https://rapier.rs
Apache License 2.0
1.27k stars 259 forks source link

Tracking simulation steps with interpolation for determinism #465

Open phisn opened 10 months ago

phisn commented 10 months ago

I am currently building an application where I would like to use interpolation as well as keeping track of inputs to achieve determinism in replays.

In the following code snippet there is no sophisticated way to track the amount of iterations the loop has taken. I looked into the rapier physics pipeline and it does not have a fitting feature either. https://github.com/dimforge/bevy_rapier/blob/c6bcce4695d596a7a9c8e91748d4dbb3d31f6d13/src/plugin/context.rs#L242-L281

This problem does neither occur in fixed time steps since there the simulation is always only stepped once nor is it a problem in variable time steps since determinism is not a thing there.

A solution would be to either have a counter that is exposed and keeps track of the number of iterations in each schedule cycle or some sort of event / callback that is called whenever a step is taken. Since I don't really see any need for individual callbacks and there also is no additional meaningful information to be delivered, the counter solution does seem more reasonable. The counter could then also be implemented for the other time steps as a constant one for consistency.

A possible implementation could be:

@@ -24,6 +24,8 @@ use rapier::control::CharacterAutostep;
 #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
 #[derive(Resource)]
 pub struct RapierContext {
+    /// Keeps track of physics steps taken in the previous simulation step
+    pub step_counter: i32,
     /// The island manager, which detects what object is sleeping
     /// (not moving much) to reduce computations.
     pub islands: IslandManager,
@@ -235,11 +237,14 @@ impl RapierContext {
                 time_scale,
                 substeps,
             } => {
+                self.step_counter = 0;
                 self.integration_parameters.dt = dt;

                 sim_to_render_time.diff += time.delta_seconds();

                 while sim_to_render_time.diff > 0.0 {
+                    self.step_counter += 1;
+                    
                     // NOTE: in this comparison we do the same computations we
                     // will do for the next `while` iteration test, to make sure we
                     // don't get bit by potential float inaccuracy.
@@ -285,6 +290,7 @@ impl RapierContext {
                 time_scale,
                 substeps,
             } => {
+                self.step_counter = 1;
                 self.integration_parameters.dt = (time.delta_seconds() * time_scale).min(max_dt);

                 let mut substep_integration_parameters = self.integration_parameters;
@@ -309,6 +315,7 @@ impl RapierContext {
                 }
             }
             TimestepMode::Fixed { dt, substeps } => {
+                self.step_counter = 1;
                 self.integration_parameters.dt = dt;

                 let mut substep_integration_parameters = self.integration_parameters;
phisn commented 10 months ago

I continued thinking about this problem and I think the whole interpolation timestep design is flawed if your goal is to achieve determinism. This is because the system sets before and after the simulation steps are not called with each physics step call as well as my own physics.

For now I would result into trying to ignore the existing interpolation implementation, use fixed time steps and implement interpolation myself.