10 releases

0.3.1 Nov 28, 2023
0.3.0 Nov 28, 2023
0.2.0 Feb 15, 2023
0.1.7 Feb 12, 2023
0.1.4 Jan 25, 2023

#167 in Game dev

Used in 2 crates

MIT license

362 lines


A library that aids in the creation of tick based game engines. A tick based game engine is a game engine where all game logic is run at a set interval. This has a few advantages over more traditional game engines. For example, it is very easy to split game logic and rendering into separate threads. Because the interval of your game logic, or its TPS, will most likely be different from the FPS you are rendering at, interpolation can be used to keep everything smooth.

Table of contents

Components of Saunter

Saunter has a few main components:

  • TickLoop
  • Snapshot(s)
  • Interpolation


The tick loop is the heart of Saunter; It runs all of your code at a set tick rate (TPS). If your code takes longer than the tick interval to run, the tick loop will run as fast as possible until it catches back up.


Every time your listener runs, it will generate a snapshot. A snapshot is a representation of the state of your game at a given tick. The snapshot is then put into a Snapshots, which is used to interpolate between snapshots outside of the tick loop. In general, it is advised to put as little data as possible into your snapshot, as it is moved around in memory quite a bit.


Saunter provides utilities for interpolating data. Mainly, it provides an Interpolate trait and many common interpolators. The Interpolate trait is already implemented for many types in the standard library, including all of the number primitives and vectors that hold them. An Interpolate derive proc macro is also provided for ease of use, when using the derive feature. Interpolation is very neccessary to make games in your engine look smooth. Without it, your game will look very choppy, especially at low TPS.


The first step of using Saunter is to create a TickLoop. The easiest way to do this is to call TickLoop::init which does some setup for you.

let (tick_loop, event_sender, ctrl, snapshots) = TickLoop::init(
    listener: move |dt, events, ctrl, time| {
        // Your engine logic goes here
        // Note that this won't work because we aren't returning a snapshot yet.
    tps: 60.0, // This can be any positive float.

This function takes a lot of input and returns a lot of output. Let's go over each of them.


  • listener: This is a FnMut closure that will be called every tick and returns your snapshot type. It takes 4 arguments:
    • dt: The time since the last tick in seconds.
    • events: A vector of events that have been sent to the loop since the last tick.
    • ctrl: A TickLoopControl that can be used to control the state of the tick loop.
    • time: The current time, used for creating snapshots (they need to store the time of creation).
  • tps: The TPS of the loop.


  • tick_loop: The tick loop itself.
  • event_sender: A Sender that can be used to send events to the tick loop.
  • ctrl: A TickLoopControl that can be used to control the state of the tick loop from outside of the loop.
  • snapshots: A Snapshots that holds all of the snapshots generated by the tick loop.

With that out of the way, let's make our snapshot type. This is a very simple example, but you can put as much data as you need into your snapshot.

#[derive(Debug, Interpolate)]
struct ExampleSnapshot {
    time: Instant,
    value: f64,
impl Snapshot for ExampleSnapshot {
    fn get_time(&self) -> &Instant {

Now we can put it to use!

let mut value = 0.0;

let _ = TickLoop::init(
    move |dt, events, ctrl, time| {
        value = 1.0 - value;
        ExampleSnapshot {
    ExampleSnapshot {
            time: Instant::now(),
            value: 0.0,

Finally, we can start our tick loop!


Starting a tick loop blocks the thread until it is stopped. For this reason you probably want to send the tick loop to a seperate thread before running it.

Now you have a working tick loop! You can send events to it using the event_sender and control it using ctrl.


~43K SLoC