Home

Fix your time step in rust and ggez

Fix Your Timestep! | Gaffer On Games is the go to article for how to handle game loops and the ggez::timer documentation points to it on how to handle frame timings. This article gives rust and ggez versions of the different game loops and is aimed at someone making 2D games, but I recommend you also read "Fix Your Timestep" if you haven't done so already!

Fixed time steps using Vsync

Using Vsync is recommended by the ggez documentation as the "generally the best way to cap your displayed frame rate". Using Vsync is a safe way to limit the CPU usage of your game loop and a good place to start from if we're in the midst of a game jam!

use std::{env, path};

use ggez::event::{self, EventHandler};
use ggez::nalgebra::Point2;
use ggez::{filesystem, graphics, timer};
use ggez::{Context, GameResult};

impl EventHandler for VSync {
    fn update(&mut self, ctx: &mut Context) -> GameResult<()> {
        println!(
            "[update] ticks: {}\tfps: {}\tdelta: {:?}",
            timer::ticks(ctx),
            timer::fps(ctx),
            timer::delta(ctx)
        );
        Ok(())
    }

    fn draw(&mut self, ctx: &mut Context) -> GameResult<()> {
        let fps = timer::fps(ctx);
        let fps_display = graphics::Text::new(format!("FPS: {}", fps));
        println!(
            "[draw] ticks: {}\tfps: {}\tdelta: {:?}",
            timer::ticks(ctx),
            fps,
            timer::delta(ctx)
        );
        graphics::clear(ctx, graphics::WHITE);
        graphics::draw(ctx, &fps_display, (Point2::new(0.0, 0.0), graphics::BLACK))?;
        graphics::present(ctx)
    }
}

struct VSync {}

impl VSync {
    pub fn new(_ctx: &mut Context) -> VSync {
        VSync {}
    }
}

fn main() -> GameResult {
    let mut cb = ggez::ContextBuilder::new("name", "author");
    if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
        let path = path::PathBuf::from(manifest_dir).join("resources");
        cb = cb.add_resource_path(path);
    }
    let (ctx, event_loop) = &mut cb.build()?;
    println!("{:#?}", filesystem::read_config(ctx));
    let mut vsync_demo = VSync::new(ctx);
    event::run(ctx, event_loop, &mut vsync_demo)
}

Testing with Vsync on and off

The default setting is vsync = true in your conf.toml. In this example, we've setup the conf.toml for easy development. Depending on you system, you might need to take some additional steps to test without Vsync. In Windows 10 you'll need to run in fullscreen mode and for linux it might depend on what drivers you're using. Here I'm using mesa and you can set Vsync using an environment variable vblank_mode=1 cargo run --bin vsync. The output will be a white game screen with a FPS counter and the output on the command line will be something like:

$ vblank_mode=1 cargo run --bin vsync
    Finished dev [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/vsync`
ATTENTION: default value of option vblank_mode overridden by environment.
...
[update] ticks: 450	fps: 59.93609493685643	delta: 16.671497ms
[draw] ticks: 450	fps: 59.93609493685643	delta: 16.671497ms
[update] ticks: 451	fps: 59.932165179519906	delta: 15.95258ms
[draw] ticks: 451	fps: 59.932165179519906	delta: 15.95258ms

The result is a game loop running at sixty frames per second, with a game tick per frame.

Variable delta time

We're taking the ggez example code moving a simple shape across the screen to demonstrate the effect of each loop. The drawing and simulation code is less important than the code in the EventHandler. It demonstrates we're passing the amount of time that has passed each frame, timer::delta(), to our simulation step in update and using self.pos_x as the position to render the circle in our draw call.

use std::time::Duration;
use std::{env, path};

use ggez::event::{self, EventHandler};
use ggez::nalgebra::Point2;
use ggez::{filesystem, graphics, timer};
use ggez::{Context, GameResult};

impl EventHandler for VariableDeltaTime {
    fn update(&mut self, ctx: &mut Context) -> GameResult<()> {
        self.simulate(timer::delta(ctx));
        Ok(())
    }

    fn draw(&mut self, ctx: &mut Context) -> GameResult<()> {
        graphics::clear(ctx, graphics::WHITE);
        self.draw_fps_counter(ctx)?;
        self.draw_circle(ctx, self.pos_x)?;
        graphics::present(ctx)?;
        // timer::sleep(Duration::from_secs(2));
        Ok(())
    }
}

struct VariableDeltaTime {
    pos_x: f32,
    velocity_x: f32,
}

impl VariableDeltaTime {
    pub fn new(_ctx: &mut Context) -> VariableDeltaTime {
        VariableDeltaTime {
            pos_x: 0.0,
            velocity_x: 60.0,
        }
    }

    pub fn draw_fps_counter(&self, ctx: &mut Context) -> GameResult<()> {
        let fps = timer::fps(ctx);
        let delta = timer::delta(ctx);
        let stats_display = graphics::Text::new(format!("FPS: {}, delta: {:?}", fps, delta));
        println!(
            "[draw] ticks: {}\tfps: {}\tdelta: {:?}",
            timer::ticks(ctx),
            fps,
            delta,
        );
        graphics::draw(
            ctx,
            &stats_display,
            (Point2::new(0.0, 0.0), graphics::BLACK),
        )
    }

    pub fn draw_circle(&self, ctx: &mut Context, x: f32) -> GameResult<()> {
        let circle = graphics::Mesh::new_circle(
            ctx,
            graphics::DrawMode::fill(),
            Point2::new(0.0, 0.0),
            100.0,
            2.0,
            graphics::BLACK,
        )?;
        graphics::draw(ctx, &circle, (Point2::new(x, 380.0),))
    }

    pub fn simulate(&mut self, time: Duration) {
        let distance = self.velocity_x * time.as_secs_f32();
        println!(
            "[update] distance {}\ttime: {}",
            distance,
            time.as_secs_f64()
        );
        self.pos_x = self.pos_x % 800.0 + distance;
    }
}

fn main() -> GameResult {
    let mut cb = ggez::ContextBuilder::new("name", "author");
    if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
        let path = path::PathBuf::from(manifest_dir).join("resources");
        cb = cb.add_resource_path(path);
    }
    let (ctx, event_loop) = &mut cb.build()?;
    println!("{:#?}", filesystem::read_config(ctx));
    let mut vsync_demo = VariableDeltaTime::new(ctx);
    event::run(ctx, event_loop, &mut vsync_demo)
}

The downside of this loop is we can pass any possible size delta time to our simulation step, this is demonstrated if we uncomment the timer::sleep() line to simulate some heavy physics calculations per step.

Variable delta time
slow physics steps means collisions might be skipped

Instead of smoothly moving, from both the rendering AND simulation's perspective, the circle is "jumping" across the screen, if we added collision detection and another object in the path of the circle, there's a chance the circle would not collide with the object as our simulation is taking large steps running at ~2 second intervals.

Semi-fixed time step

To fix the "jumping" issue, we consume the delta time in dt size slices. So even if the frame stutters, then we'll gracefully simulate the remaining steps even if you don't see it on screen, helping stabilize our simulation. Here we've set dt to 60 fps.

use std::cmp::Ordering;
use std::time::Duration;
use std::{env, path};

use ggez::event::{self, EventHandler};
use ggez::nalgebra::Point2;
use ggez::{filesystem, graphics, timer};
use ggez::{Context, GameResult};

impl EventHandler for SemiFixedTimeStep {
    fn update(&mut self, ctx: &mut Context) -> GameResult<()> {
        let mut frame_time = timer::delta(ctx).as_secs_f64();

        while frame_time > 0.0 {
            let cmp = frame_time.partial_cmp(&self.dt).expect("float NaN error");
            let delta_time: f64 = if let Ordering::Less = cmp {
                frame_time
            } else {
                self.dt
            };
            self.simulate(delta_time);
            frame_time -= delta_time;
        }

        Ok(())
    }

    fn draw(&mut self, ctx: &mut Context) -> GameResult<()> {
        graphics::clear(ctx, graphics::WHITE);
        self.draw_fps_counter(ctx)?;
        self.draw_circle(ctx, self.pos_x)?;
        graphics::present(ctx)?;
        // timer::sleep(Duration::from_secs(2));
        Ok(())
    }
}

struct SemiFixedTimeStep {
    pos_x: f32,
    velocity_x: f32,
    dt: f64, // upper bound for our time step;
}

impl SemiFixedTimeStep {
    pub fn new(_ctx: &mut Context) -> SemiFixedTimeStep {
        SemiFixedTimeStep {
            pos_x: 0.0,
            velocity_x: 60.0,
            dt: 1.0f64 / 60.0f64,
        }
    }

    pub fn draw_fps_counter(&self, ctx: &mut Context) -> GameResult<()> {
        let fps = timer::fps(ctx);
        let delta = timer::delta(ctx);
        let stats_display = graphics::Text::new(format!("FPS: {}, delta: {:?}", fps, delta));
        println!(
            "[draw] ticks: {}\tfps: {}\tdelta: {:?}",
            timer::ticks(ctx),
            fps,
            delta,
        );
        graphics::draw(
            ctx,
            &stats_display,
            (Point2::new(0.0, 0.0), graphics::BLACK),
        )
    }

    pub fn draw_circle(&self, ctx: &mut Context, x: f32) -> GameResult<()> {
        let circle = graphics::Mesh::new_circle(
            ctx,
            graphics::DrawMode::fill(),
            Point2::new(0.0, 0.0),
            100.0,
            2.0,
            graphics::BLACK,
        )?;
        graphics::draw(ctx, &circle, (Point2::new(x, 380.0),))
    }

    pub fn simulate(&mut self, time: f64) {
        let distance = self.velocity_x as f64 * time;
        self.pos_x = self.pos_x % 800.0 + distance as f32;
        println!("[update] distance {}", distance);
    }
}

fn main() -> GameResult {
    let mut cb = ggez::ContextBuilder::new("name", "author");
    if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
        let path = path::PathBuf::from(manifest_dir).join("resources");
        cb = cb.add_resource_path(path);
    }
    let (ctx, event_loop) = &mut cb.build()?;
    println!("{:#?}", filesystem::read_config(ctx));
    let mut vsync_demo = SemiFixedTimeStep::new(ctx);
    event::run(ctx, event_loop, &mut vsync_demo)
}

If you run this you'll see the update and draw calls, and the distance the circle moves per update call is at most 1. Any remaining delta time is handled in the next iteration of the while loop.

[update] distance 1
[update] distance 0.002592200000000114
[draw] ticks: 11	fps: 48.79560730232119	delta: 16.70987ms
[update] distance 0.9988522200000001
[draw] ticks: 12	fps: 49.51035741824082	delta: 16.647537ms
[update] distance 1
[update] distance 0.0030228800000001166
[draw] ticks: 13	fps: 50.12740129676579	delta: 16.717048ms

Free the physics

We "free the physics" by leaving any of those small left over time slices for the next game loop. Luckily for us, ggez is built to handle this sort of game loop easily and our example code here matches the documentation in ggez::timer::check_update_time

use std::{env, path};

use ggez::event::{self, EventHandler};
use ggez::nalgebra::Point2;
use ggez::{filesystem, graphics, timer};
use ggez::{Context, GameResult};

const PHYSICS_SIMULATION_FPS: u32 = 50;

impl EventHandler for FreeThePhysics {
    fn update(&mut self, ctx: &mut Context) -> GameResult<()> {
        while timer::check_update_time(ctx, PHYSICS_SIMULATION_FPS) {
            let physics_delta_time = 1.0 / f64::from(PHYSICS_SIMULATION_FPS);
            self.simulate(physics_delta_time);
        }
        Ok(())
    }

    fn draw(&mut self, ctx: &mut Context) -> GameResult<()> {
        graphics::clear(ctx, graphics::WHITE);
        self.draw_fps_counter(ctx)?;
        self.draw_circle(ctx, self.pos_x)?;
        graphics::present(ctx)?;
        Ok(())
    }
}

struct FreeThePhysics {
    pos_x: f32,
    velocity_x: f32,
}

impl FreeThePhysics {
    pub fn new(_ctx: &mut Context) -> FreeThePhysics {
        FreeThePhysics {
            pos_x: 0.0,
            velocity_x: 60.0,
        }
    }

    pub fn draw_fps_counter(&self, ctx: &mut Context) -> GameResult<()> {
        let fps = timer::fps(ctx);
        let delta = timer::delta(ctx);
        let stats_display = graphics::Text::new(format!("FPS: {}, delta: {:?}", fps, delta));
        println!(
            "[draw] ticks: {}\tfps: {}\tdelta: {:?}",
            timer::ticks(ctx),
            fps,
            delta,
        );
        graphics::draw(
            ctx,
            &stats_display,
            (Point2::new(0.0, 0.0), graphics::BLACK),
        )
    }

    pub fn draw_circle(&self, ctx: &mut Context, x: f32) -> GameResult<()> {
        let circle = graphics::Mesh::new_circle(
            ctx,
            graphics::DrawMode::fill(),
            Point2::new(0.0, 0.0),
            100.0,
            2.0,
            graphics::BLACK,
        )?;
        graphics::draw(ctx, &circle, (Point2::new(x, 380.0),))
    }

    pub fn simulate(&mut self, time: f64) {
        let distance = self.velocity_x as f64 * time;
        println!("[update] distance {}", distance);
        self.pos_x = self.pos_x % 800.0 + distance as f32;
    }
}

fn main() -> GameResult {
    let mut cb = ggez::ContextBuilder::new("name", "author");
    if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
        let path = path::PathBuf::from(manifest_dir).join("resources");
        cb = cb.add_resource_path(path);
    }
    let (ctx, event_loop) = &mut cb.build()?;
    println!("{:#?}", filesystem::read_config(ctx));
    let mut vsync_demo = FreeThePhysics::new(ctx);
    event::run(ctx, event_loop, &mut vsync_demo)
}

The command line output from this will be similar to.

[update] distance 1.2
[draw] ticks: 253	fps: 59.93176289340483	delta: 16.820925ms
[update] distance 1.2
[draw] ticks: 254	fps: 59.92244717042309	delta: 17.190653ms
[draw] ticks: 255	fps: 59.90459594050515	delta: 18.768633ms
[update] distance 1.2
[draw] ticks: 256	fps: 59.93270396402694	delta: 14.042466ms

No matter how many draw calls occur, our simulation step always moves our circle by 1.2 and the left over delta time is thrown over to the next frame.

The final touch!

Up until this point, our draw function has remained mostly unchanged in all these game loops. This is addressed in "the final touch", since our game loop is running at a different fps to our render, the final image may visually stutter, as the circle is always being rendered to the location where our last simulated step occured.

We can address this by storing the previous physics step and blending it with the current simulated physics step.

use std::{env, path};

use ggez::event::{self, EventHandler};
use ggez::nalgebra::Point2;
use ggez::{filesystem, graphics, timer};
use ggez::{Context, GameResult};

const PHYSICS_SIMULATION_FPS: u32 = 100;
const PHYSICS_DELTA_TIME: f64 = 1.0 / PHYSICS_SIMULATION_FPS as f64;

impl EventHandler for TheFinalTouch {
    fn update(&mut self, ctx: &mut Context) -> GameResult<()> {
        while timer::check_update_time(ctx, PHYSICS_SIMULATION_FPS) {
            let distance = self.velocity_x as f64 * PHYSICS_DELTA_TIME;
            println!("[update] distance {}", distance);
            self.previous_x = self.pos_x;
            self.pos_x = self.pos_x % 800.0 + distance as f32;
        }
        Ok(())
    }

    fn draw(&mut self, ctx: &mut Context) -> GameResult<()> {
        graphics::clear(ctx, graphics::WHITE);
        self.draw_fps_counter(ctx)?;

        let blended_x = self.interpolate(ctx);
        self.draw_circles(ctx, blended_x)?;
        graphics::present(ctx)
    }
}

struct TheFinalTouch {
    previous_x: f32,
    pos_x: f32,
    velocity_x: f32,
}

impl TheFinalTouch {
    pub fn interpolate(&self, ctx: &mut Context) -> f32 {
        let remainder = timer::remaining_update_time(ctx).as_secs_f64();
        let alpha = remainder / PHYSICS_DELTA_TIME;
        let previous_x = if self.pos_x >= self.previous_x {
            self.previous_x
        } else {
            // if we're wrapping round, interpolating in between the two would mean
            // circle would zip from right to left instead of going off screen
            800.0 - self.previous_x
        };
        let blended_x = (self.pos_x * alpha as f32) + (previous_x * (1.0 - alpha as f32));
        blended_x
    }

    pub fn draw_circles(&self, ctx: &mut Context, blended_x: f32) -> GameResult<()> {
        let circle = graphics::Mesh::new_circle(
            ctx,
            graphics::DrawMode::fill(),
            Point2::new(0.0, 0.0),
            100.0,
            2.0,
            graphics::BLACK,
        )?;
        println!("{:?} {:?}", self.pos_x, blended_x);
        graphics::draw(ctx, &circle, (Point2::new(self.pos_x, 150.0),))?;
        graphics::draw(ctx, &circle, (Point2::new(blended_x, 380.0),))
    }

    pub fn new(_ctx: &mut Context) -> TheFinalTouch {
        TheFinalTouch {
            previous_x: 0.0,
            pos_x: 0.0,
            velocity_x: 150.0,
        }
    }

    pub fn draw_fps_counter(&self, ctx: &mut Context) -> GameResult<()> {
        let fps = timer::fps(ctx);
        let delta = timer::delta(ctx);
        let stats_display = graphics::Text::new(format!("FPS: {}, delta: {:?}", fps, delta));
        println!(
            "[draw] ticks: {}\tfps: {}\tdelta: {:?}",
            timer::ticks(ctx),
            fps,
            delta,
        );
        graphics::draw(
            ctx,
            &stats_display,
            (Point2::new(0.0, 0.0), graphics::BLACK),
        )
    }
}

fn main() -> GameResult {
    let mut cb = ggez::ContextBuilder::new("name", "author");
    if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
        let path = path::PathBuf::from(manifest_dir).join("resources");
        cb = cb.add_resource_path(path);
    }
    let (ctx, event_loop) = &mut cb.build()?;
    println!("{:#?}", filesystem::read_config(ctx));
    let mut vsync_demo = TheFinalTouch::new(ctx);
    event::run(ctx, event_loop, &mut vsync_demo)
}

We've set our PHYSICS_SIMULATION_FPS to run at 5 frames per second, to demonstrate that now matter how slow our simulation runs, it appears smooth during the rendering step thanks to the interpolation in the draw function. I've added the drawing of the "free the physics!" step for a comparison.

interpolated
interpolating physics states to smooth animation

But the final touch is lagging behind!

Yes! It's accentuated as we've set the physics step to be so low in comparison to the render loop. I recommend looking at Fixed-Time-Step Implementation | L. Spiro Engine if you're not convinced, but the short version is that many of your favourite games, even twitch based shooters will likely be using a loop like this. This is our same example with PHYSICS_SIMULATION_FPS set at 100fps and Vsync turned off, the gif has been limited to 60fps, so the best way would be to run this for yourself.

final touch no vsync

The living end...?

Do I really need to do all this interpolation stuff?

Depending on your game, maybe not! Whilst this might be suitable for something like asteroids, if you're just making a tetris clone, then you will probably want the tetrominoes to fall in fixed increments instead of smoothly falling.

If you're making a game with pixel art, you probably don't need to use this technique as you probably don't need this unless you have a clever way of interpolating pixel art. In these cases are probably safe using the "Free the physics" style loop where the fixed time step is the same as your animations' frames per second.

Do I want to skip frames with pixel art?

The while loop in "Free the physics" also means that if the game slows down or stutters during the game loop, we might simulate multiple steps, but only have one draw call. If you're making a 2D fighting game, you could argue that people would get annoyed if frames were skipped and their game sprite was suddenly hit in the face. In this case, players might prefer slow down over skipping frames.

On the other hand If you're deciding to make something like an x-com clone or any other turn based game where visual artifacts aren't quite so critical, keeping your game running smooth is probably a higher priority.

This is usually implemented as a user configurable "frame skip", letting the user choose whether they want the simulation to continue instead of slowing down the entire game

let FRAME_SKIP = 1;

fn update(&mut self, ctx: &mut Context) -> GameResult<()> {
    let mut frames_run = 0;
    while timer::check_update_time(ctx, PHYSICS_SIMULATION_FPS) {
        let physics_delta_time = 1.0 / f64::from(PHYSICS_SIMULATION_FPS);
        if frames_run < FRAME_SKIP {
            self.simulate(physics_delta_time);
            frames_run += 1;
        }
    }
    Ok(())
}

You've used timer::sleep() in these examples

timer::sleep() has been used to simulate really long physics steps or the game loop taking a long time to execute. Since sleep is generally inaccurate you probably want to avoid using it in your game loop and stick to Vsync to limit the cpu usage, see Fixed-Time-Step Implementation | L. Spiro Engine and c++ - Update and render in separate threads - Game Development Stack Exchange.

Code for this article is available at GitHub - joetsoi/ggez-tutorials.

Meta

Code tested on