Skip to content

Commit

Permalink
Fix fixed timestep using max_delta_overstep (Jondolf#225)
Browse files Browse the repository at this point in the history
# Objective

Fixes Jondolf#223.

In the migration to Bevy 0.12, the time resources were changed to use Bevy's unified `Time` APIs. However, the fixed timestep broke, as it now runs once every frame with the same timestep, which causes frame rate dependent behavior where bodies seem to move faster at higher frame rates.

## Solution

Add a `max_delta_overstep` property to `TimestepMode::Fixed` that limits how much time can be added to the accumulated `overstep` during a single frame. This is mostly equivalent to getting the elapsed time using `Time<Virtual>` with some `max_delta`, but for now, it's nice to keep the `Time<Physics>` behavior separate from the virtual clock.
  • Loading branch information
Jondolf authored Nov 5, 2023
1 parent bca0f3d commit d45e1ab
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 16 deletions.
33 changes: 18 additions & 15 deletions src/plugins/setup/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,22 +239,25 @@ fn run_physics_schedule(world: &mut World, mut is_first_run: Local<IsFirstRun>)
// For `TimestepMode::Fixed`, this is computed using the accumulated overstep.
let mut queued_steps = 1;

if let TimestepMode::Fixed { delta, overstep } =
world.resource_mut::<Time<Physics>>().timestep_mode_mut()
{
// If paused, add the `Physics` delta time, otherwise add real time.
if is_paused {
*overstep += old_delta;
} else {
// TODO: This should probably use real time and not the fixed timestep,
// but it seems to quickly lag and fall into the "spiral of death".
*overstep += timestep;
if !is_first_run.0 {
if let TimestepMode::Fixed {
delta,
overstep,
max_delta_overstep,
} = world.resource_mut::<Time<Physics>>().timestep_mode_mut()
{
// If paused, add the `Physics` delta time, otherwise add real time.
if is_paused {
*overstep += old_delta;
} else {
*overstep += real_delta.min(*max_delta_overstep);
}

// Consume as many steps as possible with the fixed `delta`.
queued_steps = (overstep.as_secs_f64() / delta.as_secs_f64()) as usize;
*overstep -= delta.mul_f64(queued_steps as f64);
}

// Consume as many steps as possible with the fixed `delta`.
queued_steps = (overstep.as_secs_f64() / delta.as_secs_f64()) as usize;
*overstep -= delta.mul_f64(queued_steps as f64);
};
}

// Advance physics clock by timestep if not paused.
if !is_paused {
Expand Down
23 changes: 22 additions & 1 deletion src/plugins/setup/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,23 @@ pub enum TimestepMode {
/// This means that physics can run 0, 1, 2 or more times per frame based on how long the
/// previous frames took.
///
/// To avoid "death spirals" where each frame takes longer and longer
/// to simulate, `overstep` can only be advanced by `max_delta_overstep`
/// during a single frame.
///
/// A fixed timestep allows consistent behavior across different machines and frame rates.
Fixed {
/// The amount of time that the simulation should be advanced by during a step.
delta: Duration,
/// The amount of accumulated time. The simulation will consume it in steps of `delta`
/// to try to catch up to real time.
overstep: Duration,
/// The maximum amount of time that can be added to [`overstep`] during a single frame.
/// Lower values help prevent "death spirals" where each frame takes longer and longer
/// to simulate.
///
/// Defaults to `1.0 / 60.0` seconds (60 Hz).
max_delta_overstep: Duration,
},
/// **Fixed delta, once per frame**: The physics simulation will be advanced by
/// a fixed `delta` amount of time once per frame. This should only be used
Expand All @@ -37,11 +47,21 @@ pub enum TimestepMode {
/// The maximum amount of time the physics simulation can be advanced at once.
/// This makes sure that the simulation doesn't break when the delta time is large.
///
/// A good default is `1.0 / 60.0` (60 Hz)
/// A good default is `1.0 / 60.0` seconds (60 Hz).
max_delta: Duration,
},
}

impl Default for TimestepMode {
fn default() -> Self {
Self::Fixed {
delta: Duration::default(),
overstep: Duration::default(),
max_delta_overstep: Duration::from_secs_f64(1.0 / 60.0),
}
}
}

/// The clock representing physics time, following `Time<Real>`.
/// Can be configured to use a fixed or variable timestep.
///
Expand Down Expand Up @@ -232,6 +252,7 @@ impl Physics {
Self::from_timestep(TimestepMode::Fixed {
delta: Duration::from_secs_f64(hz),
overstep: Duration::ZERO,
max_delta_overstep: Duration::from_secs_f64(1.0 / 60.0),
})
}

Expand Down

0 comments on commit d45e1ab

Please sign in to comment.