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)
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.
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.
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
...
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;
}
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:
This lesson covered essential time management techniques for game development with SDL and C++. Key points included:
SDL_GetTicks64()
to measure elapsed timeBy applying these concepts, you can create games that run smoothly on various hardware configurations.
Learn how to create smooth, time-aware game loops that behave consistently across different hardware configurations
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games