1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
//! Functions and types relating to measuring and manipulating time.

use std::collections::VecDeque;

use std::time::{Duration, Instant};

use crate::Context;

/// The different timestep modes that a game can have.
///
/// # Serde
///
/// Serialization and deserialization of this type (via [Serde](https://serde.rs/))
/// can be enabled via the `serde_support` feature.
#[derive(Debug, Copy, Clone)]
#[cfg_attr(
    feature = "serde_support",
    derive(serde::Serialize, serde::Deserialize)
)]
pub enum Timestep {
    /// In fixed timestep mode, updates will happen at a consistent rate (the `f64` value in the enum
    /// variant representing the number of times per second), while rendering will happen as fast as
    /// the hardware (and vsync settings) will allow.
    ///
    /// This has the advantage of making your game's updates deterministic, so they will act the same
    /// on hardware of different speeds. It also means that your update code does not need to use
    /// [`get_delta_time`] to integrate the amount of time passed into your calculations. However,
    /// it can lead to some slight stutter if your rendering code does not account for the possibility
    /// of updating and rendering to be out of sync with each other.
    ///
    /// To avoid stutter, you should interpolate your rendering using [`get_blend_factor`]. The
    /// [`interpolation`](https://github.com/17cupsofcoffee/tetra/blob/main/examples/interpolation.rs)
    /// example in the Tetra repository shows some different approaches to doing this.
    ///
    /// This mode is currently the default.
    Fixed(f64),

    /// In variable timestep mode, updates and rendering will happen in lockstep, one after the other,
    /// as fast as the hardware (and vsync settings) will allow.
    ///
    /// This has the advantage of being simple to reason about (updates can never happen multiple times
    /// or get skipped), but is not deterministic, so your updates may not act the same on every
    /// run of the game loop.
    ///
    /// To integrate the amount of time that has passed into your game's calculations, use
    /// [`get_delta_time`].
    Variable,
}

struct FixedTimeStepState {
    ticks_per_second: f64,
    tick_rate: Duration,
    accumulator: Duration,
}

pub(crate) struct TimeContext {
    timestep: Option<FixedTimeStepState>,
    fps_tracker: VecDeque<f64>,
    last_time: Instant,
    elapsed: Duration,
}

impl TimeContext {
    pub(crate) fn new(timestep: Timestep) -> TimeContext {
        // We fill the buffer with values so that the FPS counter doesn't jitter
        // at startup.
        let mut fps_tracker = VecDeque::with_capacity(200);
        fps_tracker.resize(200, 1.0 / 60.0);

        TimeContext {
            timestep: create_timestep_state(timestep),
            fps_tracker,
            last_time: Instant::now(),
            elapsed: Duration::from_secs(0),
        }
    }
}

pub(crate) fn reset(ctx: &mut Context) {
    ctx.time.last_time = Instant::now();

    if let Some(fixed) = &mut ctx.time.timestep {
        fixed.accumulator = Duration::from_secs(0);
    }
}

pub(crate) fn tick(ctx: &mut Context) {
    let current_time = Instant::now();
    ctx.time.elapsed = current_time - ctx.time.last_time;
    ctx.time.last_time = current_time;

    if let Some(fixed) = &mut ctx.time.timestep {
        fixed.accumulator += ctx.time.elapsed;
    }

    // Since we fill the buffer when we create the context, we can cycle it
    // here and it shouldn't reallocate.
    ctx.time.fps_tracker.pop_front();
    ctx.time
        .fps_tracker
        .push_back(ctx.time.elapsed.as_secs_f64());
}

pub(crate) fn is_fixed_update_ready(ctx: &mut Context) -> bool {
    match &mut ctx.time.timestep {
        Some(fixed) if fixed.accumulator >= fixed.tick_rate => {
            fixed.accumulator -= fixed.tick_rate;
            true
        }
        _ => false,
    }
}

/// Returns the amount of time that has passed since the last frame was rendered.
///
/// When using a variable time step, you should use this to integrate the amount of time that
/// has passed into your game's calculations. For example, if you wanted to move a
/// [`Vec2`](crate::math::Vec2) 32 units to the right per second, you would do
/// `foo.y += 32.0 * time::get_delta_time(ctx).as_secs_f32()`
///
/// When using a fixed time step, the above still applies, but only to rendering - you should
/// not integrate the delta time into your update calculations.
pub fn get_delta_time(ctx: &Context) -> Duration {
    ctx.time.elapsed
}

/// Returns the amount of time that has accumulated between updates.
///
/// When using a fixed time step, as time passes, this value will increase;
/// as updates occur, it will decrease.
///
/// When using a variable time step, this function always returns `Duration::from_secs(0)`.
pub fn get_accumulator(ctx: &Context) -> Duration {
    match &ctx.time.timestep {
        Some(fixed) => fixed.accumulator,
        None => Duration::from_secs(0),
    }
}

/// Returns a value between 0.0 and 1.0, representing how far between updates the game loop
/// currently is.
///
/// For example, if the value is 0.01, an update just happened; if the value is 0.99,
/// an update is about to happen.
///
/// This can be used to interpolate when rendering.
///
/// This function returns an [`f32`], which is usually what you want when blending - however,
/// if you need a more precise representation of the blend factor, you can call
/// [`get_blend_factor_precise`].
pub fn get_blend_factor(ctx: &Context) -> f32 {
    match &ctx.time.timestep {
        Some(fixed) => fixed.accumulator.as_secs_f32() / fixed.tick_rate.as_secs_f32(),
        None => 0.0,
    }
}

/// Returns a precise value between 0.0 and 1.0, representing how far between updates the game loop
/// currently is.
///
/// For example, if the value is 0.01, an update just happened; if the value is 0.99,
/// an update is about to happen.
///
/// This can be used to interpolate when rendering.
///
/// This function returns an [`f64`], which is a very precise representation of the blend factor,
/// but often difficult to use in game logic without casting. If you need an [`f32`], call
/// [`get_blend_factor`] instead.
pub fn get_blend_factor_precise(ctx: &Context) -> f64 {
    match &ctx.time.timestep {
        Some(fixed) => fixed.accumulator.as_secs_f64() / fixed.tick_rate.as_secs_f64(),
        None => 0.0,
    }
}

/// Gets the current timestep of the application.
pub fn get_timestep(ctx: &Context) -> Timestep {
    match &ctx.time.timestep {
        Some(fixed) => Timestep::Fixed(fixed.ticks_per_second),
        None => Timestep::Variable,
    }
}

/// Sets the timestep of the application.
pub fn set_timestep(ctx: &mut Context, timestep: Timestep) {
    ctx.time.timestep = create_timestep_state(timestep);
}

fn create_timestep_state(timestep: Timestep) -> Option<FixedTimeStepState> {
    match timestep {
        Timestep::Fixed(ticks_per_second) => Some(FixedTimeStepState {
            ticks_per_second,
            tick_rate: Duration::from_secs_f64(1.0 / ticks_per_second),
            accumulator: Duration::from_secs(0),
        }),
        Timestep::Variable => None,
    }
}

/// Returns the current frame rate, averaged out over the last 200 frames.
pub fn get_fps(ctx: &Context) -> f64 {
    1.0 / (ctx.time.fps_tracker.iter().sum::<f64>() / ctx.time.fps_tracker.len() as f64)
}