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
//! Functions and types relating to measuring and manipulating time.

use std::collections::VecDeque;

use std::time::Duration;

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. However, it can lead to some slight stutter if your
    /// rendering code does not account for the possibility for 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,
}

pub(crate) struct TimeContext {
    pub(crate) fps_tracker: VecDeque<f64>,
    pub(crate) ticks_per_second: Option<f64>,
    pub(crate) tick_rate: Option<Duration>,
    pub(crate) delta_time: Duration,
    pub(crate) accumulator: 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);

        let ticks_per_second = match timestep {
            Timestep::Fixed(tps) => Some(tps),
            Timestep::Variable => None,
        };

        let tick_rate = match timestep {
            Timestep::Fixed(tps) => Some(Duration::from_secs_f64(1.0 / tps)),
            Timestep::Variable => None,
        };

        TimeContext {
            fps_tracker,
            ticks_per_second,
            tick_rate,
            delta_time: Duration::from_secs(0),
            accumulator: Duration::from_secs(0),
        }
    }
}

pub(crate) fn reset(ctx: &mut Context) {
    ctx.time.delta_time = Duration::from_secs(0);
    ctx.time.accumulator = Duration::from_secs(0);
}

/// Returns the amount of time that has passed since the last update or draw.
///
/// This can be used 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 could do
/// `foo.y += 32.0 * time::get_delta_time(ctx).as_secs_f32()`.
///
/// When using a fixed time step, calling this function during an update will always
/// return the configured update rate. This is to prevent floating point error/non-determinism
/// from creeping into your game's calculations!
pub fn get_delta_time(ctx: &Context) -> Duration {
    ctx.time.delta_time
}

/// 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 {
    ctx.time.accumulator
}

/// 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.tick_rate {
        Some(tick_rate) => ctx.time.accumulator.as_secs_f32() / 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.tick_rate {
        Some(tick_rate) => ctx.time.accumulator.as_secs_f64() / 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.ticks_per_second {
        Some(tps) => Timestep::Fixed(tps),
        None => Timestep::Variable,
    }
}

/// Sets the timestep of the application.
pub fn set_timestep(ctx: &mut Context, timestep: Timestep) {
    ctx.time.ticks_per_second = match timestep {
        Timestep::Fixed(tps) => Some(tps),
        Timestep::Variable => None,
    };

    ctx.time.tick_rate = match timestep {
        Timestep::Fixed(tps) => Some(Duration::from_secs_f64(1.0 / tps)),
        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)
}