Tick Rate and Time Deltas

Learn how to create smooth, time-aware game loops that behave consistently across different hardware configurations
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Posted

In the previous lesson, we introduced the concept of ticking and tick functions, which allow our game objects to update on every iteration of our application loop.

However, we should note that when we’re implementing logic in a Tick() function, we don’t know how quickly our objects will tick. That is, we do not know how much time has passed since the previous invocation of that object’s Tick() function.

// Goblin.h
#pragma once
#include "GameObject.h"

class Goblin : public GameObject {
 public:
  int xPosition;

  void Tick() override {
    // This object moves by one pixel per
    // invocation of Tick(), but how frequently
    // is Tick() invoked?
    xPosition += 1;
  }
};

As we add more complexity to our tick functions or more ticking objects, our game will need to perform more work on each iteration of our application loop. As such, it will iterate less frequently, meaning Tick() is invoked less frequently, meaning our object will move slower.

On the other hand, if we perform optimizations or our user has a more powerful computer, our loop can iterate faster, causing our objects to move faster.

To address this inconsistency, we typically want to define properties like movement speed in terms of real-world units of time, such as seconds or milliseconds. Let’s learn how to do this.

SDL_GetTicks64()

The SDL_GetTicks64() function returns a 64-bit unsigned integer, representing the number of milliseconds that have passed since SDL was initialized. Let’s call it on every iteration of our application loop:

// main.cpp
#include <SDL.h>
#include <iostream>

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  
  SDL_Event Event;
  bool shouldContinue{true};
  while (shouldContinue) {
while (SDL_PollEvent(&Event)) {/*...*/} std::cout << "\nMilliseconds Passed: " << SDL_GetTicks64(); } SDL_Quit(); return 0; }

The value returned from this function will increase over time, although subsequent calls may return the same value if invoked in quick succession:

Milliseconds Passed: 0
Milliseconds Passed: 1
Milliseconds Passed: 1
Milliseconds Passed: 2
...

SDL_GetTicks()

SDL also includes an SDL_GetTicks() function, which returns the same value in the form of a 32-bit unsigned integer. This function is considered deprecated now due to the limited storage capacity of 32 bits.

32 bits can store a maximum value of approximately 4.3 billion, which requires workarounds if our program can run for longer than 4.3 billion milliseconds (~49 days)

Time Deltas

To understand how many milliseconds have passed between each invocation of our application loop, we need to compare the value returned by SDL_GetTicks64() on this iteration to the value it returned in the previous iteration:

// main.cpp
#include <SDL.h>
#include <iostream>

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);

  SDL_Event Event;
  bool shouldContinue{true};
  Uint64 PreviousFrame{0};
  while (shouldContinue) {
while (SDL_PollEvent(&Event)) {/*...*/} Uint64 ThisFrame{SDL_GetTicks64()}; std::cout << "\nTime Delta: " << ThisFrame - PreviousFrame; PreviousFrame = ThisFrame; } SDL_Quit(); return 0; }

This value is often called a time delta. The greek letter delta - δ\delta or Δ\Delta - is often used in maths and physics to represent change. As such, a time delta is how much time has passed between two points - two calls to SDL_GetTicks64(), in this case.

Given this program is very simple, our application loop can complete many iterations per millisecond. So, even though some time has passed, the time delta when expressed in milliseconds will often be 0:

Time Delta: 0
Time Delta: 1
Time Delta: 0
Time Delta: 0

We cover high-resolution clocks with sub-millisecond accuracy later in this section. Real-world interactive applications typically aim to have their loops iterate around 20-100 times per second, meaning time deltas will be in the region of 10-50 milliseconds.

Using Time Deltas in Tick Functions

Now that we have calculated the time delta in our application loop, we now need to give our objects access to that value so they can use it in their Tick() logic.

The easiest and most common approach is to simply provide it as a parameter. Let’s update our GameObject base class:

// GameObject.h
// ...

class GameObject {
 public:
  virtual void Tick(Uint64 TimeDelta) {
    // ...
  }
};

We’ll also need to update the class that manages all of our GameObject instances:

// World.h
// ...

class World {
 public:
  void Tick(Uint64 TimeDelta) {
    for (auto& Object : Objects) {
      Object->Tick(TimeDelta);
    }
  }
  
  // ...
  
 private:
  std::vector<std::unique_ptr<GameObject>> Objects;
};

Finally, let’s pass it through as an argument from our application loop:

// main.cpp
#include <SDL.h>
#include "World.h"

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  World GameWorld;

  SDL_Event Event;
  bool shouldContinue{true};
  Uint64 PreviousFrame{0};

  while (shouldContinue) {
while (SDL_PollEvent(&Event)) {/*...*/} Uint64 ThisFrame{SDL_GetTicks64()}; Uint64 TimeDelta{ThisFrame - PreviousFrame}; PreviousFrame = ThisFrame; GameWorld.Tick(TimeDelta); } SDL_Quit(); return 0; }

Remember, any logic we build based on the time delta should consider the possibility that the value will be 0. For example, if we try to divide something by the time delta, and it has a value of 0, we will run into problems.

Converting Time Deltas to Seconds

For most objects, receiving the time delta in terms of seconds will be more intuitive than working with milliseconds. We can do this by updating our parameter type to be a floating point number, and dividing the millisecond value by 1,000.0 before passing it as an argument:

// main.cpp
#include <SDL.h>
#include "Engine/Window.h"
#include "World.h"
#include "Goblin.h"

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  World GameWorld;
  GameWorld.SpawnGoblin();

  SDL_Event Event;
  bool shouldContinue{true};
  Uint64 PreviousFrame{0};

  while (shouldContinue) {
while (SDL_PollEvent(&Event)) {/*...*/} Uint64 ThisFrame{SDL_GetTicks64()}; Uint64 TimeDelta{ThisFrame - PreviousFrame}; PreviousFrame = ThisFrame; GameWorld.Tick(TimeDelta / 1000.0); } SDL_Quit(); return 0; }

We’d also update our Tick() functions to accept this parameter type, and update the logic to reflect that the time delta is now specified in seconds.

Let’s update our Goblin to move with a Velocity of 1 unit per second:

// Goblin.h
// ...

class Goblin : public GameObject {
public:
  float xPosition;
  float Velocity{1};

  void Tick(float TimeDelta) override {
    xPosition += Velocity * TimeDelta;

    std::cout
      << "\nx = " << xPosition;
  }
};
x = 0.097
x = 0.1
x = 0.1
x = 0.1
x = 0.101
...

Capping Tick Rate

In some scenarios, we may find it valuable to limit how fast our application loop (or any other loop) can iterate.

The SDL_Delay() function accepts an integer argument and will wait for at least that many milliseconds before returning:

// main.cpp
#include <SDL.h>
#include <iostream>

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);

  while (true) {
   std::cout << "Ticking ("
      << SDL_GetTicks64() << ")\n";
      
    SDL_Delay(1000);
  }

  SDL_Quit();
  return 0;
}
Ticking (6)
Ticking (1007)
Ticking (2008)
Ticking (3008)

We can use this to limit the run rate of a loop using the following recipe. If the DoWork() part of our loop body completes in under 1,000ms, we use SDL_Delay() to extend the duration of our iteration:

// main.cpp
#include <SDL.h>
#include <iostream>

void DoWork() {
  // ...
}

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);

  while (true) {
    Uint64 StartTimer{SDL_GetTicks64()};
    std::cout << "Ticking ("
      << SDL_GetTicks64() << ")\n";

    DoWork();

    Uint64 EndTimer{SDL_GetTicks64()};
    Uint64 TimeElapsed{EndTimer - StartTimer};
    if (TimeElapsed <= 1000) {
      SDL_Delay(1000 - TimeElapsed);
    }
  }

  SDL_Quit();
  return 0;
}
Ticking (9)
Ticking (1010)
Ticking (2010)
Ticking (3011)

Let’s use it to ensure each iteration of our application loop takes at least 10 milliseconds to complete, thereby ensuring our tick rate doesn’t exceed 100 ticks per second:

// main.cpp
#include <SDL.h>

#include "Window.h"
#include "World.h"

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  Window GameWindow;
  World GameWorld;

  SDL_Event Event;
  bool shouldContinue{true};
  Uint64 PreviousFrame{0};

  while (shouldContinue) {
    Uint64 StartTimer{SDL_GetTicks64()};
while (SDL_PollEvent(&Event)) {/*...*/} // Update Objects Uint64 ThisFrame{SDL_GetTicks64()}; Uint64 TimeDelta{ThisFrame - PreviousFrame}; PreviousFrame = ThisFrame; GameWorld.Tick(TimeDelta / 1000.0); // Render GameWorld.Render(GameWindow.GetSurface()); // Capping Frame Rate Uint64 EndTimer{SDL_GetTicks64()}; Uint64 TimeElapsed{EndTimer - StartTimer}; if (TimeElapsed <= 10) { SDL_Delay(10 - TimeElapsed); } } SDL_Quit(); return 0; }

Time Drift

SDL_Delay() causes our application to pause for at least the number of milliseconds we pass as an argument. Because of nuances in how work is scheduled, the delay will always be slightly longer.

We can see this in previous examples, where a SDL_Delay(1000) invocation often causes a delay of 1,001 milliseconds:

Ticking (6)
Ticking (1007)
Ticking (2008)
Ticking (3008)

These inaccuracies are not a problem for most use cases, but it does mean we should avoid using SDL_Delay() or similar techniques to keep track of time.

A millisecond delay happening every second accumulates to significant inaccuracies over time, sometimes referred to as time drift or clock drift.

Our lesson on std::chrono provides more accurate techniques for working with dates and times:

Summary

This lesson covered essential time management techniques for game development with SDL and C++. Key points included:

  • Using SDL_GetTicks64() to measure elapsed time
  • Calculating time deltas between frames
  • Implementing time-based movement for game objects
  • Capping tick rates for consistent performance

By applying these concepts, you can create games that run smoothly on various hardware configurations.

Was this lesson useful?

Next Lesson

High-Resolution Timers

Learn to measure time intervals with high accuracy in your games
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Posted
Lesson Contents

Tick Rate and Time Deltas

Learn how to create smooth, time-aware game loops that behave consistently across different hardware configurations

sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Ticks, Timers and Callbacks
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, unlimited access

This course includes:

  • 67 Lessons
  • 100+ Code Samples
  • 91% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

High-Resolution Timers

Learn to measure time intervals with high accuracy in your games
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved