SDL_AddTimer()
to provide functions that are executed on time-based intervalsIn this lesson, we’ll introduce SDL’s timer mechanisms, which interact with function pointers to implement commonly required functionality. The capabilities this unlocks primarily fall into two categories:
To use SDL timers, we should pass the SDL_INIT_TIMER
flag to SDL_Init()
:
SDL_Init(SDL_INIT_TIMER);
We can combine multiple initialization flags using the bitwise |
operator:
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER);
For this lesson, let’s imagine we have some EnemySpawner
object, responsible for adding enemy monsters into the world.
As a starting point, we’ll create a basic application loop that forwards keydown events to a Spawner
object:
// main.cpp
#include <SDL.h>
#include <iostream>
#include "Window.h"
#include "Spawner.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER);
Window GameWindow;
Spawner EnemySpawner;
SDL_Event Event;
bool shouldContinue{true};
while (shouldContinue) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
shouldContinue = false;
} else if (Event.type == SDL_KEYDOWN) {
EnemySpawner.HandleKeyDownEvent(Event.key);
}
}
GameWindow.Update();
}
SDL_Quit();
return 0;
}
Our Spawner
class currently looks like this, which we’ll expand with timers throughout this lesson.
// Spawner.h
#pragma once
#include <SDL.h>
#include <iostream>
class Spawner {
public:
void HandleKeyDownEvent(SDL_KeyboardEvent& E) {
std::cout << "Button Pressed\n";
}
};
After running our program and pressing some keyboard buttons, we should see our EnemySpawner
responding to events:
Button Pressed
Button Pressed
SDL_AddTimer()
SDL_AddTimer()
is the main way we create timers. It requires three arguments - an unsigned integer representing a time delay in milliseconds, a function pointer we want to call after that delay, and a void pointer (void*
).
To create a timer that invokes SpawnGoblin()
after 1,000 milliseconds (one second), our call would look like the following. We’ll discuss what the void pointer is for later - for now, let’s just pass a nullptr
there:
SDL_AddTimer(1000, SpawnGoblin, nullptr);
Our callback should return a Uint32
and accept Uint32
and void*
arguments. We’ll discuss what these values represent later - for now, let’s just ignore the parameters and return 0
:
Uint32 SpawnGoblin(Uint32, void*) {
std::cout << "Spawning a Goblin\n";
return 0;
}
Let’s make our Spawner
objects invoke SpawnGoblin()
one second (1,000 milliseconds) after the space bar is pressed:
// Spawner.h
#pragma once
#include <SDL.h>
#include <iostream>
Uint32 SpawnGoblin(Uint32, void*) {/*...*}
class Spawner {
public:
void HandleKeyDownEvent(
SDL_KeyboardEvent& E
) {
if (E.keysym.sym == SDLK_SPACE) {
StartTimer();
}
}
void StartTimer() {
SDL_AddTimer(1000, SpawnGoblin, nullptr);
}
};
Running our program and pressing the spacebar should generate the expected output after a second:
Spawning a Goblin
Our previous example invoked the callback a single time, but SDL_AddTimer()
allows us to call a function repeatedly based on some time interval.
The value returned by our SpawnGoblin()
callback lets us specify the delay until the function is called again, or we can return 0
if we don’t want it to be called again.
Let’s update our program to call SpawnGoblin()
every second, by having the function return 1000
:
// Spawner.h
#pragma once
#include <SDL.h>
#include <iostream>
Uint32 SpawnGoblin(Uint32, void*) {
std::cout << "Spawning a Goblin\n";
return 1000;
}
class Spawner {/*...*/};
Now, every time we press the spacebar, we kick off an endless sequence that spawns a goblin every second:
Spawning a Goblin
Spawning a Goblin
Spawning a Goblin
Repeated presses of the spacebar will cause multiple timers to run concurrently. We can prevent that if needed:
// Spawner.h
#pragma once
#include <SDL.h>
#include <iostream>
Uint32 SpawnGoblin(Uint32, void*) {/*...*}
class Spawner {
public:
void HandleKeyDownEvent(SDL_KeyboardEvent&)
void StartTimer() {
if (isSpawning) {
std::cout << "Already spawning"
" - ignoring event\n";
} else {
std::cout << "Starting Spawner\n";
SDL_AddTimer(1000, SpawnGoblin, nullptr);
isSpawning = true;
}
}
private:
bool isSpawning{false};
};
Starting Spawner
Already spawnining - ignoring event
Spawning a Goblin
Spawning a Goblin
The Uint32
argument passed to our callback represents the current value of the interval. As such, we can improve our previous implementation by returning this value, rather than fixing it to 1000
:
Uint32 SpawnGoblin(Uint32 Interval, void*) {
std::cout << "Spawning a Goblin\n";
return Interval;
}
Of course, we can use this parameter in any way we want - we don’t just have to return it. In the following example, we halve the interval on every invocation, causing our Goblins to spawn faster over time, capping at every 200 milliseconds:
Uint32 SpawnGoblin(Uint32 Interval, void*) {
std::cout << "Spawning a Goblin - Interval: "
<< Interval << '\n';
Uint32 NewInterval{Interval * 0.5};
return std::max(NewInterval, Uint32(200));
}
Starting Spawner
Spawning a Goblin - NewInterval: 1000
Spawning a Goblin - NewInterval: 500
Spawning a Goblin - NewInterval: 250
Spawning a Goblin - NewInterval: 200
Spawning a Goblin - NewInterval: 200
The third argument to SDL_AddTimer()
allows us to pass any arbitrary data to the callback function. The callback receives this as its second argument:
// Spawner.h
#pragma once
#include <SDL.h>
#include <iostream>
Uint32 SpawnGoblin(Uint32 Interval, void* Ptr) {
std::cout << "Receiving Address " << Ptr << '\n';
return 0;
}
class Spawner {
public:
void HandleKeyDownEvent(SDL_KeyboardEvent&)
void StartTimer() {
std::cout << "Passing Address "
<< &SomeValue << '\n';
SDL_AddTimer(1000, SpawnGoblin, &SomeValue);
}
private:
int SomeValue;
};
Passing Address 000000505BEFFCC0
Receiving Address 000000505BEFFCC0
As usual, we need to static_cast
the void pointer to the correct type before we can use it. One of our previous examples invoked our callback on a decreasing interval, down to a minimum value of 200 milliseconds.
Let’s update this to allow the caller to specify this minimum value through the void pointer:
// Spawner.h
#pragma once
#include <SDL.h>
#include <iostream>
Uint32 SpawnGoblin(
Uint32 CurrentDelay,
void* MinimumIntervalPtr
) {
std::cout << "Spawning a Goblin - Delay: "
<< CurrentDelay << '\n';
Uint32 NextDelay{CurrentDelay * 0.5};
Uint32* MinimumInterval{
static_cast<Uint32*>(MinimumIntervalPtr)};
return std::max(NextDelay, *MinimumInterval);
}
class Spawner {
public:
void HandleKeyDownEvent(SDL_KeyboardEvent&)
void StartTimer() {
if (isSpawning) {
std::cout << "Already spawning"
" - ignoring event\n";
} else {
std::cout << "Starting Spawner\n";
SDL_AddTimer(1000, SpawnGoblin, &MinInterval);
isSpawning = true;
}
}
private:
bool isSpawning{false};
Uint32 MinInterval{300};
};
Starting Spawner
Spawning a Goblin - NewInterval: 1000
Spawning a Goblin - NewInterval: 500
Spawning a Goblin - NewInterval: 300
Spawning a Goblin - NewInterval: 300
Given the argument is a void pointer, it can point to any type of data we need, including custom data. We just need to update our static_cast
call to specify the correct type.
Let's define a custom struct that provides both the interval multiplier and minimum value:
// Spawner.h
#pragma once
#include <SDL.h>
#include <iostream>
struct SpawnerConfig {
float IntervalMultiplier;
Uint32 MinInterval;
};
Uint32 SpawnGoblin(
Uint32 CurrentDelay,
void* ConfigPtr
) {
std::cout << "Spawning a Goblin - Delay: "
<< CurrentDelay << '\n';
SpawnerConfig* Config{
static_cast<SpawnerConfig*>(ConfigPtr)};
Uint32 NextDelay{static_cast<Uint32>(
CurrentDelay * Config->IntervalMultiplier)};
return std::max(NextDelay, Config->MinInterval);
}
class Spawner {
public:
void HandleKeyDownEvent(SDL_KeyboardEvent&)
void StartTimer() {
if (isSpawning) {
std::cout << "Already spawning"
" - ignoring event\n";
} else {
std::cout << "Starting Spawner\n";
SDL_AddTimer(1000, SpawnGoblin, &Config);
isSpawning = true;
}
}
private:
bool isSpawning{false};
SpawnerConfig Config{0.8f, 600};
};
Starting Spawner
Spawning a Goblin - Delay: 1000
Spawning a Goblin - Delay: 800
Spawning a Goblin - Delay: 640
Spawning a Goblin - Delay: 600
Spawning a Goblin - Delay: 600
As usual, we should be careful with memory management here. Specifically, we should ensure the memory location being pointing at remains alive as long as the callback still needs it.
SDL_TimerID
The SDL_AddTimer()
function returns a timer identifier, which is a simple integer aliased to SDL_TimerID
. This value has two uses.
Firstly, we can store it and use it later to cancel the timer. We cover this in the next section
Secondly, we can examine its value to check if the timer was created successfully. If the value is 0
, the creation of the timer failed, and we can call SDL_GetError()
to find out why:
SDL_TimerID Timer{SDL_AddTimer(
1000, SpawnGoblin, nullptr)};
if (!Timer) {
std::cout << "Error creating timer: "
<< SDL_GetError();
}
Let’s update our Spawner
objects to keep track of the SDL_TimerID
they’re managing:
// Spawner.h
#pragma once
#include <SDL.h>
#include <iostream>
Uint32 SpawnGoblin(Uint32, void*) {/*...*}
class Spawner {
public:
void HandleKeyDownEvent(SDL_KeyboardEvent&)
void StartTimer() {
if (Timer) {
std::cout << "Already spawning"
" - ignoring event\n";
} else {
std::cout << "Starting Spawner\n";
Timer = SDL_AddTimer(
1000, SpawnGoblin, nullptr);
if (!Timer) {
std::cout << "Error creating timer: "
<< SDL_GetError();
}
}
}
SDL_TimerID Timer{0};
};
SDL_RemoveTimer()
If we need to cancel a timer, we call SDL_RemoveTimer()
, and pass the SDL_TimerID
that was returned by the SDL_AddTimer()
call that we want to cancel.
Let’s update our program to allow a timer to be canceled using the escape key:
// Spawner.h
#pragma once
#include <SDL.h>
#include <iostream>
Uint32 SpawnGoblin(Uint32, void*) {/*...*}
class Spawner {
public:
void HandleKeyDownEvent(
SDL_KeyboardEvent& E
) {
if (E.keysym.sym == SDLK_SPACE) {
StartTimer();
} else if (E.keysym.sym == SDLK_ESCAPE) {
RemoveTimer();
}
}
void StartTimer() {/*...*/}
void RemoveTimer() {
std::cout << "Removing Timer\n";
SDL_RemoveTimer(Timer);
Timer = 0;
}
SDL_TimerID Timer{0};
};
Starting Spawner
Spawning a Goblin
Spawning a Goblin
Spawning a Goblin
Removing Timer
It is safe to pass 0
, or any other integer to this function. It will return true
if it was able to find and remove the timer, or false
if no timer was found with the provided ID:
// Spawner.h
#pragma once
#include <SDL.h>
#include <iostream>
Uint32 SpawnGoblin(Uint32, void*) {/*...*}
class Spawner {
public:
void HandleKeyDownEvent(SDL_KeyboardEvent&)
void StartTimer() {/*...*/}
void RemoveTimer() {
std::cout << "Removing Timer: ";
if(SDL_RemoveTimer(Timer)) {
std::cout << "Timer with ID " << Timer
<< " Removed Successfully\n";
Timer = 0;
} else {
std::cout << "Timer with ID " << Timer
<< " was not found\n";
}
}
SDL_TimerID Timer{0};
};
Removing Timer: Timer with ID 0 was not found
Starting Spawner
Spawning a Goblin
Spawning a Goblin
Removing Timer: Timer with ID 1 Removed Successfully
If an object is managing a timer and it is no longer relevant after the object is destroyed, we should ensure we remove that timer in the destructor:
class Spawner {
public:
// ...
~Spawner() {
if (Timer) {
SDL_RemoveTimer(Timer);
}
}
private:
// ...
SDL_TimerID Timer{0};
}
In many scenarios where we’re using a timer, the expiration of that timer represents some event that may be meaningful to other components within our game.
As such, objects that create timers will often also broadcast that expiration to our event system. The following example creates and pushes an ENEMY_SPAWN_EVENT
to SDL’s event queue every time our timer expires:
// Spawner.h
#pragma once
#include <SDL.h>
#include <iostream>
// Custom event type for our timer
const Uint32 ENEMY_SPAWN_EVENT = SDL_RegisterEvents(1);
Uint32 QueueSpawnEvent(Uint32 Interval, void*) {
SDL_Event SpawnEvent;
SpawnEvent.type = ENEMY_SPAWN_EVENT;
SDL_PushEvent(&SpawnEvent);
return Interval; // Continue spawning at same interval
}
class Spawner {
public:
void HandleEvent(SDL_Event& E) {
if (E.type == ENEMY_SPAWN_EVENT) {
SpawnEnemy();
} else if (E.type == SDL_KEYDOWN) {
HandleKeyDownEvent(E.key);
}
}
void HandleKeyDownEvent(SDL_KeyboardEvent& E) {
if (E.keysym.sym == SDLK_SPACE) {
StartTimer();
} else if (E.keysym.sym == SDLK_ESCAPE) {
RemoveTimer();
}
}
void StartTimer() {
if (Timer) {
std::cout << "Already spawning - ignoring event\n";
} else {
std::cout << "Starting Spawner\n";
Timer = SDL_AddTimer(1000, QueueSpawnEvent, nullptr);
if (!Timer) {
std::cout << "Error creating timer: " << SDL_GetError();
}
}
}
void SpawnEnemy() {
std::cout << "Enemy spawned!\n";
}
void RemoveTimer() {
if (SDL_RemoveTimer(Timer)) {
std::cout << "Timer removed successfully\n";
Timer = 0;
}
}
private:
SDL_TimerID Timer{0};
};
Our previous examples spawned enemies at some regular interval, meaning we have an endless stream of monsters being created. That may be what we want, a common alternative requirement is to respawn enemies some time after they have been defeated.
There are a few ways to set this up, but they commonly require the defeated object to notify the relevant spawner that it has been defeated.
This can be done through the event queue, or simply by having the enemy remember what spawner created it, and then invoking some function on that spawner at the appropriate time:
// Enemy.h
#pragma once
#include "Spawner.h"
class Enemy {
public:
Enemy(Spawner* Owner) : MySpawner(Owner) {}
void TakeDamage(int Amount) {
Health -= Amount;
if (Health <= 0) {
Die();
}
}
private:
void Die() {
std::cout << "Enemy defeated!\n";
MySpawner->ScheduleRespawn();
}
Spawner* MySpawner;
int Health{100};
};
Our Spawner
holds a std::unique_ptr
to the Enemy
it is managing. When that enemy calls its spawner’s ScheduleRespawn()
method, the spawner will delete the enemy, and create a new one 10 seconds later:
// Spawner.h
#pragma once
#include <SDL.h>
#include <iostream>
#include <memory>
#include "Enemy.h"
class Spawner {
public:
void ScheduleRespawn() {
// Delete the current enemy
CurrentEnemy.reset();
std::cout << "Scheduling respawn in 10s\n";
Timer = SDL_AddTimer(
10000, SpawnCallback, this);
}
static Uint32 SpawnCallback(
Uint32, void* SpawnerPtr) {
auto* Spawner{
static_cast<class Spawner*>(SpawnerPtr)};
Spawner->SpawnEnemy();
return 0; // Don't repeat
}
void SpawnEnemy() {
CurrentEnemy = std::make_unique<Enemy>(this);
std::cout << "Enemy Spawned!\n";
Timer = 0;
}
private:
SDL_TimerID Timer{0};
std::unique_ptr<Enemy> CurrentEnemy;
};
Enemy Spawned!
Enemy Defeated!
Scheduling respawn in 10s
Enemy Spawned!
We start the Spawner
by calling ScheduleRespawn()
:
Spawner MySpawner;
MySpawner.ScheduleRespawn();
Scheduling respawn in 10s
Enemy Spawned!
When the Enemy
it is managing is defeated, it will automatically respawn it:
Enemy Defeated!
Scheduling respawn in 10s
Enemy Spawned!
Finally, let’s see a more advanced example. Below, our Spawner
class is managing two timers:
SpawnTimer
that creates enemies on a short intervalDifficultyTimer
, which changes the behavior of the spawner on a longer interval. Every 30 seconds, the spawner upgrades to spawn more powerful enemies at a shorter interval.// Spawner.h
#pragma once
#include <SDL.h>
#include <iostream>
#include <vector>
struct DifficultyLevel {
Uint32 SpawnInterval;
int EnemyHealth;
float EnemySpeed;
};
class Spawner {
public:
Spawner() {
// Configure difficulty progression
Difficulty = {
{2000, 100, 1.0f},
{1500, 120, 1.2f},
{1000, 150, 1.5f},
{800, 200, 1.8f},
{500, 250, 2.0f}
};
}
static Uint32 DifficultyCallback(
Uint32 Interval, void* SpawnerPtr) {
auto* Spawner = static_cast<
class Spawner*>(SpawnerPtr);
Spawner->IncreaseDifficulty();
return Interval;
}
void Start() {
// Start spawning enemies
UpdateSpawnTimer();
// Start difficulty scaling timer
DifficultyTimer = SDL_AddTimer(30000,
DifficultyCallback, this);
}
void IncreaseDifficulty() {
if (CurrentLevel < Difficulty.size() - 1) {
CurrentLevel++;
std::cout << "Difficulty increased to "
"level " << CurrentLevel + 1 << "!\n";
UpdateSpawnTimer();
}
}
private:
void UpdateSpawnTimer() {
if (SpawnTimer) {
SDL_RemoveTimer(SpawnTimer);
}
// Start new timer with current difficulty
auto& Level = Difficulty[CurrentLevel];
SpawnTimer = SDL_AddTimer(
Level.SpawnInterval, SpawnEnemy, this);
}
static Uint32 SpawnEnemy(
Uint32 Interval, void* SpawnerPtr) {
auto* Spawner{
static_cast<class Spawner*>(SpawnerPtr)};
auto& Level{Spawner->DifficultyLevels[
Spawner->CurrentLevel]};
std::cout << "Spawning enemy with:\n"
<< " Health: " << Level.EnemyHealth << "\n"
<< " Speed: " << Level.EnemySpeed << "\n";
return Interval;
}
std::vector<DifficultyLevel> Difficulty;
size_t CurrentLevel{0};
SDL_TimerID SpawnTimer{0};
SDL_TimerID DifficultyTimer{0};
};
To start spawning enemies, we call the Start()
method:
Spawner MySpawner;
MySpawner.Start();
Spawning enemy with:
Health: 100
Speed: 1
Spawning enemy with:
Health: 100
Speed: 1
...
Difficulty increased to level 2!
Spawning enemy with:
Health: 120
Speed: 1.2
Spawning enemy with:
Health: 120
Speed: 1.2
In this lesson, we've covered SDL's timer functionality and how it integrates with games. Let's recap what we've covered:
SDL_AddTimer()
SDL_RemoveTimer()
Learn how to use callbacks with SDL_AddTimer()
to provide functions that are executed on time-based intervals
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games