use std::cell::Cell;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::time::Instant;
use log::debug;
pub struct GameLoop {
start_time: Instant,
skip_ticks: usize,
max_frameskip: usize,
next_game_tick: Cell<usize>,
}
#[derive(Debug)]
pub enum GameLoopError {
BadTps,
BadFrameSkip,
}
impl GameLoop {
pub fn new(tps: usize, max_frameskip: usize) -> Result<Self, GameLoopError> {
if tps < 1 {
return Err(GameLoopError::BadTps);
}
if max_frameskip < 1 {
return Err(GameLoopError::BadFrameSkip);
}
let start_time = Instant::now();
let skip_ticks = 1000 / tps;
debug!(
"initialized with {} ticks/second ({}ms/tick), with a max frame skip of {}",
tps, skip_ticks, max_frameskip
);
Ok(Self {
start_time,
max_frameskip,
skip_ticks,
next_game_tick: Cell::new(0),
})
}
pub fn actions(&self) -> impl Iterator<Item = FrameAction> + '_ {
FrameActions {
game_loop: self,
loops: 0,
rendered: false,
}
}
fn tick_count(&self) -> usize {
self.start_time.elapsed().as_millis() as usize
}
fn increment_next_game_tick(&self) {
let current = self.next_game_tick.get();
self.next_game_tick.set(current + self.skip_ticks);
}
}
pub struct FrameActions<'a> {
game_loop: &'a GameLoop,
loops: usize,
rendered: bool,
}
#[derive(Debug)]
pub enum FrameAction {
Tick,
Render { interpolation: f64 },
}
impl<'a> Iterator for FrameActions<'a> {
type Item = FrameAction;
fn next(&mut self) -> Option<Self::Item> {
let next_tick = self.game_loop.next_game_tick.get();
if self.game_loop.tick_count() > next_tick && self.loops < self.game_loop.max_frameskip {
self.game_loop.increment_next_game_tick();
self.loops += 1;
return Some(FrameAction::Tick);
}
if !self.rendered {
self.rendered = true;
let render_time = self.game_loop.tick_count();
let skip_ticks = self.game_loop.skip_ticks;
let interpolation: f64 =
((render_time + skip_ticks - next_tick) as f64) / (skip_ticks as f64);
return Some(FrameAction::Render { interpolation });
}
None
}
}
impl Display for GameLoopError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
GameLoopError::BadTps => write!(f, "Ticks per second must be >= 1"),
GameLoopError::BadFrameSkip => write!(f, "Max frame skip must be >= 1"),
}
}
}
impl Error for GameLoopError {}