Tick Rate and Time Deltas

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

Ryan McCombe
Updated

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.

Using 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
...

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; }

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.

Next Lesson
Lesson 40 of 128

High-Resolution Timers

Learn to measure time intervals with high accuracy in your games

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

Handling Large Time Deltas
How can we handle very large time deltas that might occur if the game is paused or minimized?
Fixed vs Variable Time Step
What are the pros and cons of using a fixed time step versus a variable time step?
Different Update Rates for Game Objects
Is it possible to have different objects in the game world update at different rates?
Smooth Acceleration and Deceleration
How can we create smooth acceleration and deceleration effects using time deltas?
Alternatives to SDL_Delay()
What are some alternatives to SDL_Delay() for more precise timing control?
Or Ask your Own Question
Get an immediate answer to your specific question using our AI assistant