Tick Rate and Time Deltas
Learn how to create smooth, time-aware game loops that behave consistently across different hardware configurations
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 - or - 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.
High-Resolution Timers
Learn to measure time intervals with high accuracy in your games