diff --git a/crates/bevy_time/src/fixed_timestep.rs b/crates/bevy_time/src/fixed_timestep.rs index 66be2f41ef2ff..4da69dc438d2b 100644 --- a/crates/bevy_time/src/fixed_timestep.rs +++ b/crates/bevy_time/src/fixed_timestep.rs @@ -24,7 +24,7 @@ use crate::Time; use bevy_app::FixedUpdate; use bevy_ecs::{system::Resource, world::World}; -use bevy_utils::Duration; +use bevy_utils::{tracing::warn, Duration}; use thiserror::Error; /// The amount of time that must pass before the fixed timestep schedule is run again. @@ -34,14 +34,25 @@ pub struct FixedTime { /// Defaults to 1/60th of a second. /// To configure this value, simply mutate or overwrite this resource. pub period: Duration, + /// The maximum number of times that the [`FixedUpdate`] schedule will be allowed to run per main schedule pass. + /// Defaults to 20: this may be too high for your game and will result in very low (but stable) FPS with a long fixed update period. + /// + /// If this value is set to `None`, the schedule will run as many times as possible. + /// Be careful when setting this value to `None`, as it can cause the app to stop rendering + /// as the fixed update schedule will run an increasing number of times per frame as it falls behind. + pub max_ticks_per_frame: Option, } impl FixedTime { + /// Sets the default maximum number of times that the [`FixedUpdate`] schedule will be allowed to run per main schedule pass. + const DEFAULT_MAX_TICKS_PER_FRAME: usize = 20; + /// Creates a new [`FixedTime`] struct pub fn new(period: Duration) -> Self { FixedTime { accumulated: Duration::ZERO, period, + max_ticks_per_frame: Some(Self::DEFAULT_MAX_TICKS_PER_FRAME), } } @@ -50,6 +61,7 @@ impl FixedTime { FixedTime { accumulated: Duration::ZERO, period: Duration::from_secs_f32(period), + max_ticks_per_frame: Some(Self::DEFAULT_MAX_TICKS_PER_FRAME), } } @@ -82,10 +94,7 @@ impl FixedTime { impl Default for FixedTime { fn default() -> Self { - FixedTime { - accumulated: Duration::ZERO, - period: Duration::from_secs_f32(1. / 60.), - } + FixedTime::new_from_secs(1. / 60.) } } @@ -106,9 +115,25 @@ pub fn run_fixed_update_schedule(world: &mut World) { let mut fixed_time = world.resource_mut::(); fixed_time.tick(delta_time); + let mut ticks_this_frame = 0; + + // Copy this out to appease the borrow checker + let maybe_max_ticks = fixed_time.max_ticks_per_frame; + // Run the schedule until we run out of accumulated time let _ = world.try_schedule_scope(FixedUpdate, |world, schedule| { while world.resource_mut::().expend().is_ok() { + ticks_this_frame += 1; + + // This is required to avoid entering a death spiral where the fixed update schedule falls behind + // and needs to run more and more times per frame to catch up. + if let Some(max_ticks) = maybe_max_ticks { + if ticks_this_frame > max_ticks { + warn!("The fixed update schedule is falling behind. Consider increasing the period or decreasing the amount of work done in the fixed update schedule."); + break; + } + } + schedule.run(world); } });