/ let-engine / src / tick_system.rs
tick_system.rs
  1  use std::{
  2      sync::Arc,
  3      time::{Duration, Instant},
  4  };
  5  
  6  use crossbeam::atomic::AtomicCell;
  7  use derive_builder::Builder;
  8  use let_engine_core::{CustomError, backend::Backends};
  9  use parking_lot::Mutex;
 10  
 11  use crate::{Game, GameWrapper};
 12  
 13  pub(crate) struct TickSystem {
 14      settings: Mutex<TickSettings>,
 15      report: AtomicCell<Tick>,
 16      tick_pause_lock: (parking_lot::Mutex<bool>, parking_lot::Condvar),
 17  }
 18  
 19  impl TickSystem {
 20      pub(crate) fn new(settings: TickSettings) -> Self {
 21          Self {
 22              settings: Mutex::new(settings),
 23              report: AtomicCell::new(Tick::default()),
 24              tick_pause_lock: (parking_lot::Mutex::new(false), parking_lot::Condvar::new()),
 25          }
 26      }
 27  }
 28  
 29  /// Runs the games `tick` function after every iteration.
 30  pub(super) fn run<G: Game<B, E>, E: CustomError, B: Backends>(game: Arc<GameWrapper<G, E, B>>) {
 31      let mut index: usize = 0;
 32  
 33      loop {
 34          #[allow(unused_mut)]
 35          let mut settings: TickSettings = {
 36              let interface = &game.backends.tick_system;
 37  
 38              // wait if paused
 39              interface
 40                  .tick_pause_lock
 41                  .1
 42                  .wait_while(&mut interface.tick_pause_lock.0.lock(), |x| *x);
 43              interface.settings.lock().clone()
 44          };
 45  
 46          // capture tick start time.
 47          let start_time = Instant::now();
 48  
 49          // Run the logic
 50          game.tick();
 51  
 52          // update the physics in case they are active in the tick settings.
 53          #[cfg(feature = "physics")]
 54          if settings.update_physics {
 55              game.scene
 56                  .lock()
 57                  .physics_iteration()
 58                  .expect("Expected physics iteration to succeed") // TODO: Handle problems
 59          }
 60  
 61          // record the elapsed time.
 62          let elapsed_time = start_time.elapsed();
 63  
 64          // Lock the thread in case the time scale is 0.
 65          if game.time.scale() == 0.0 {
 66              let mut guard = game.time.zero_cvar.0.lock();
 67              game.time.zero_cvar.1.wait(&mut guard);
 68          }
 69  
 70          let tick_wait = if settings.time_scale_influence {
 71              // Multiply the waiting duration with the inverse time scale.
 72              settings.tick_wait.mul_f64(1.0 / game.time.scale())
 73          } else {
 74              settings.tick_wait
 75          };
 76  
 77          // calculate waiting time
 78          // ((1.0 / time_scale) * tick_wait) - elapsed_time
 79          let waiting_time = if let TimeStep::Variable = settings.timestep_mode {
 80              // Subtract the tick logic execution time from the waiting time to make the waiting time between ticks more consistent.
 81              tick_wait.saturating_sub(elapsed_time)
 82          } else {
 83              tick_wait
 84          };
 85  
 86          // Spin sleep so windows users with their lower quality sleep functions get the same sleep duration
 87          spin_sleep::sleep(waiting_time);
 88  
 89          // report tick in case a reporter is active
 90          game.backends.tick_system.report.store(Tick {
 91              duration: elapsed_time,
 92              waiting_time,
 93              index,
 94          });
 95  
 96          index += 1;
 97  
 98          if game.exit.get().is_some() {
 99              break;
100          }
101      }
102  }
103  
104  /// The settings for the tick system of the game engine.
105  #[derive(Clone, Debug, Builder)]
106  pub struct TickSettings {
107      /// The target duration to wait after every tick.
108      ///
109      /// ## Default
110      ///
111      /// - 1 / 62 seconds
112      ///
113      /// 62 ticks per second.
114      #[builder(setter(into), default = "Duration::from_secs_f64(1.0/62.0)")]
115      pub tick_wait: Duration,
116      /// The waiting behaviour of this tick system.
117      ///
118      /// ## Default
119      ///
120      /// `TimeStep::Variable`
121      ///
122      /// Prevents the game from slowing down in case ticks become more time expensive.
123      #[builder(default)]
124      pub timestep_mode: TimeStep,
125      /// If true this tick system will also iterate all the physics systems in the scene and update them.
126      ///
127      /// ## Default
128      ///
129      /// `true`
130      #[builder(default = "true")]
131      #[cfg(feature = "physics")]
132      pub update_physics: bool,
133      /// If this is true the tick system will be paused.
134      ///
135      /// ## Default
136      ///
137      /// `false`
138      #[builder(default)]
139      pub paused: bool,
140      /// If this is true the tick systems tick rate will be influenced by the time scale.
141      ///
142      /// ## Default
143      ///
144      /// `true`
145      #[builder(default = "true")]
146      pub time_scale_influence: bool,
147  }
148  
149  impl Default for TickSettings {
150      fn default() -> Self {
151          Self {
152              tick_wait: Duration::from_secs_f64(1.0 / 62.0),
153              #[cfg(feature = "physics")]
154              update_physics: true,
155              timestep_mode: TimeStep::default(),
156              paused: false,
157              time_scale_influence: true,
158          }
159      }
160  }
161  impl TickSettings {
162      pub fn into_builder(self) -> TickSettingsBuilder {
163          self.into()
164      }
165  }
166  impl From<TickSettings> for TickSettingsBuilder {
167      fn from(value: TickSettings) -> Self {
168          Self {
169              tick_wait: Some(value.tick_wait),
170              timestep_mode: Some(value.timestep_mode),
171              #[cfg(feature = "physics")]
172              update_physics: Some(value.update_physics),
173              paused: Some(value.paused),
174              time_scale_influence: Some(value.time_scale_influence),
175          }
176      }
177  }
178  
179  /// A tick report.
180  #[derive(Clone, Default, Copy, PartialEq, Eq, PartialOrd, Ord)]
181  pub struct Tick {
182      /// Time it took to execute this tick.
183      pub duration: Duration,
184      /// Time the tick waited before running the next one.
185      pub waiting_time: Duration,
186      /// The index of this tick.
187      pub index: usize,
188  }
189  
190  impl Tick {
191      pub fn duration(&self) -> &Duration {
192          &self.duration
193      }
194      pub fn waiting_time(&self) -> &Duration {
195          &self.waiting_time
196      }
197      pub fn index(&self) -> usize {
198          self.index
199      }
200      /// Returns true if the tick execution time takes longer than the expected waiting time.
201      ///
202      /// Because if the tick execution takes longer than the target waiting time the rate decreases making the logic behind the tick system slower.
203      pub fn has_slowdown(&self) -> bool {
204          self.waiting_time.is_zero()
205      }
206  }
207  
208  impl std::fmt::Debug for Tick {
209      fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210          f.debug_struct("Tick")
211              .field("duration", &self.duration)
212              .field("waiting time", &self.waiting_time)
213              .field("index", &self.index)
214              .field("has slowdown", &self.has_slowdown())
215              .finish()
216      }
217  }
218  
219  /// The waiting behaviour of the tick system.
220  ///
221  /// Set to variable by default.
222  #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
223  pub enum TimeStep {
224      /// Wait a fixed time after every tick, not caring about the duration the tick actually lasted for.
225      Fixed,
226  
227      /// Wait a variable time using the tick_wait field as a target duration.
228      ///
229      /// That means the tick system waits less the longer the tick took to execute.
230      /// This for example prevents the physics system from slowing down in case the iterations get more expensive.
231      #[default]
232      Variable,
233  }