Move with time
This program moves a square across the window at 200 pixels each second. The speed stays the same on a slow computer and on a fast computer.
The full program
/* Move a square at a fixed speed, on any computer. */
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
#define SPEED 200.0f /* pixels per second */
#define MAX_DT 0.05f /* seconds */
static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static SDL_FRect square = { 0.0f, 216.0f, 48.0f, 48.0f };
static Uint64 last_ticks = 0;
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
if (!SDL_Init(SDL_INIT_VIDEO)) {
SDL_Log("SDL_Init failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
if (!SDL_CreateWindowAndRenderer("Move with time", WINDOW_WIDTH, WINDOW_HEIGHT, 0,
&window, &renderer)) {
SDL_Log("SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
SDL_SetRenderVSync(renderer, 1);
last_ticks = SDL_GetTicksNS();
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
if (event->type == SDL_EVENT_QUIT) {
return SDL_APP_SUCCESS;
}
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppIterate(void *appstate)
{
Uint64 now = SDL_GetTicksNS();
float dt = (float)(now - last_ticks) / (float)SDL_NS_PER_SECOND;
last_ticks = now;
if (dt > MAX_DT) {
dt = MAX_DT;
}
square.x += SPEED * dt;
if (square.x > WINDOW_WIDTH) {
square.x = -square.w;
}
SDL_SetRenderDrawColor(renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer, 255, 157, 92, SDL_ALPHA_OPAQUE);
SDL_RenderFillRect(renderer, &square);
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
}/* Move a square at a fixed speed, on any computer. */
#include <SDL.h>
#include <stdbool.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
#define SPEED 200.0f /* pixels per second */
#define MAX_DT 0.05f /* seconds */
static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static SDL_FRect square = { 0.0f, 216.0f, 48.0f, 48.0f };
static Uint64 last_ticks = 0;
static bool running = true;
static void frame(void)
{
SDL_Event event;
Uint64 now = SDL_GetPerformanceCounter();
float dt = (float)(now - last_ticks) / (float)SDL_GetPerformanceFrequency();
last_ticks = now;
if (dt > MAX_DT) {
dt = MAX_DT;
}
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
running = false;
}
}
square.x += SPEED * dt;
if (square.x > WINDOW_WIDTH) {
square.x = -square.w;
}
SDL_SetRenderDrawColor(renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer, 255, 157, 92, SDL_ALPHA_OPAQUE);
SDL_RenderFillRectF(renderer, &square);
SDL_RenderPresent(renderer);
}
int main(int argc, char *argv[])
{
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
SDL_Log("SDL_Init failed: %s", SDL_GetError());
return 1;
}
if (SDL_CreateWindowAndRenderer(WINDOW_WIDTH, WINDOW_HEIGHT, 0, &window, &renderer) != 0) {
SDL_Log("SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
SDL_Quit();
return 1;
}
SDL_SetWindowTitle(window, "Move with time");
SDL_RenderSetVSync(renderer, 1);
last_ticks = SDL_GetPerformanceCounter();
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop(frame, 0, 1);
#else
while (running) {
frame();
}
#endif
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}The problem
To move an object, you change its position in each frame. The first idea is to add a fixed number of pixels each time:
square.x += 3.0f;This code is wrong, because the number of frames each second is different on different computers. On a 60 Hz screen, the square moves 180 pixels each second. On a 144 Hz screen, the same code moves it 432 pixels each second. A game that uses this code runs too fast on a better screen.
The solution
Choose a speed in pixels per second. In each frame, multiply the speed by the time since the last frame. This time is the delta time, and most code calls it dt.
Uint64 now = SDL_GetTicksNS();
float dt = (float)(now - last_ticks) / (float)SDL_NS_PER_SECOND;
last_ticks = now;SDL_GetTicksNS gives the time in nanoseconds since SDL started. The program subtracts the time of the last frame, then divides to get seconds.
Uint64 now = SDL_GetPerformanceCounter();
float dt = (float)(now - last_ticks) / (float)SDL_GetPerformanceFrequency();
last_ticks = now;SDL_GetPerformanceCounter gives a high-resolution clock value. SDL_GetPerformanceFrequency gives the number of counts each second. The program subtracts the value of the last frame, then divides to get seconds.
The first frame needs a start value, so the program also sets last_ticks before the first frame. Without this start value, the first dt is the full time since SDL started.
The casts to float are necessary. Without them, the division uses integers, and a fraction of a second becomes 0. The compiler does not warn you about this, and the square does not move.
Use dt
square.x += SPEED * dt;
if (square.x > WINDOW_WIDTH) {
square.x = -square.w;
}A speed of 200 at 60 frames each second gives a step of about 3.3 pixels. The position must be a float, because an int drops the fraction in each frame.
When the square passes the right edge, the program moves it to just past the left edge. It then comes back into the window.
Limit dt
if (dt > MAX_DT) {
dt = MAX_DT;
}Sometimes a program stops for a long time. A browser stops a program in a hidden tab, for example. The next dt is then many seconds, and the object jumps a long way. The limit stops this jump. The program is slower for that one frame, and nobody sees it.
Try this
- Change
SPEEDto 400. The square crosses the window in half the time. - Add a second square that moves on a different row at a different speed.
- Make the square move from right to left. Use a negative speed, and change the test for the window edge.
What you learned
- The number of frames each second is different on different computers.
- Speed multiplied by
dtgives the distance for one frame. - A limit on
dtstops a jump after a long pause.