Hard time making my main loop

Hi there,

I’ve read several articles about the possible implementations for a game loop including the popular fix your timestep one. Since I’m making a simple 2D game not requiring lots of graphics, I decided to go for the constant frame rate implementation which has the benefits of being simple and battery friendly. Also, my last game loop measurements in my game showed that I don’t spend more than 1ms per frame which gives me enough room for a 60 FPS game (which allows around 16ms per frame). Finally, I didn’t want to go through the complicated implementation that require drawing interpolation (my game has already too much code where drawing function do not take an interpolation number).

Anyway, in my game I have some choppy animations at some point and my game player movement isn’t smooth. So I’ve tried to reproduce the issue with a minimalist ping-pong example. This code shows non-smooth movement of the red square when it moves from the left-right <-> right-left of the screen edges.

#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>

#include <SDL.h>
#include <SDL_ttf.h>

#define WIDTH   640
#define HEIGHT  480

/* Ball speed: px/sec */
#define SPEED 100

static SDL_Window *win;
static SDL_Renderer *rdr;

static TTF_Font *font;

static struct {
	int dx;
	int x;
} ball = {
	.dx = 1
};

static void
die(const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	vfprintf(stderr, fmt, ap);
	fputc('\n', stderr);
	va_end(ap);

	exit(1);
}

static void
print(const char *fmt, ...)
{
	SDL_Texture *tex;
	SDL_Surface *surface;
	char buf[1024];
	va_list ap;

	va_start(ap, fmt);
	vsnprintf(buf, sizeof (buf), fmt, ap);
	va_end(ap);

	surface = TTF_RenderUTF8_Blended(font, buf,
	    (const SDL_Color){0, 0, 0, 255});

	if (!surface)
		die("%s", SDL_GetError());
	if (!(tex = SDL_CreateTextureFromSurface(rdr, surface)))
		die("%s", SDL_GetError());

	SDL_RenderCopy(rdr, tex, NULL, &(const SDL_Rect){
		.x = 10,
		.y = 10,
		.w = surface->w,
		.h = surface->h
	});
	SDL_FreeSurface(surface);
	SDL_DestroyTexture(tex);
}

static void
init(void)
{
	if (SDL_Init(SDL_INIT_VIDEO) < 0 || TTF_Init() < 0)
		die("%s", SDL_GetError());

	win = SDL_CreateWindow("Test", SDL_WINDOWPOS_UNDEFINED,
	    SDL_WINDOWPOS_UNDEFINED, WIDTH, HEIGHT, 0);

	if (!win)
		die("%s", SDL_GetError());

	rdr = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED);

	if (!rdr)
		die("%s", SDL_GetError());
	if (!(font = TTF_OpenFont("DejaVuSansMono.ttf", 10)))
		die("%s", SDL_GetError());
}

static void
loop(void)
{
	bool jesus_returns = false;
	float framerate = 60.0;
	float fps = 1000.0 / framerate;

	Uint32 last = 0;

	while (!jesus_returns) {
		Uint32 start = SDL_GetTicks();

		for (SDL_Event ev; SDL_PollEvent(&ev); ) {
			switch (ev.type) {
			case SDL_QUIT:
				jesus_returns = true;
				break;
			case SDL_KEYDOWN:
				switch (ev.key.keysym.sym) {
				case SDLK_LEFT:
					if (framerate > 10)
						framerate -= 5;

					fps = 1000.0 / framerate;
					break;
				case SDLK_RIGHT:
					if (framerate < 110)
						framerate += 5;

					fps = 1000.0 / framerate;
					break;
				default:
					break;
				}
			default:
				break;
			}
		}

		ball.x += ball.dx * (SPEED * fps / 1000.0);

		if (ball.x < 0) {
			ball.x = 0;
			ball.dx = 1;
		} else if (ball.x > WIDTH) {
			ball.x = WIDTH;
			ball.dx = -1;
		}

		SDL_SetRenderDrawColor(rdr, 255, 255, 255, 255);
		SDL_RenderClear(rdr);
		SDL_SetRenderDrawColor(rdr, 255, 0, 0, 255);
		SDL_RenderFillRect(rdr, &(const SDL_Rect){
			.x = ball.x,
			.y = HEIGHT / 2,
			.w = 10,
			.h = 10
		});

		print("frame time: %u. framerate=%f/sec",
		    (unsigned int)last, framerate);
		SDL_RenderPresent(rdr);

		last = SDL_GetTicks() - start;

		if (last < fps)
			SDL_Delay(fps - last);
	}
}

static void
quit(void)
{
	SDL_DestroyRenderer(rdr);
	SDL_DestroyWindow(win);
}

int
main(int argc, char **argv)
{
	(void)argc;
	(void)argv;

	init();
	loop();
	quit();

	return 0;
}

What’s even worse, is that the square goes faster when it traverse the screen from the right edge to the left! I can’t understand why.

So after reading other articles, some people told me that I can use vsync to get proper smooth animations. I’ve changed the code to remove the custom sleep and retrieve the screen refreshrate to sync with:

#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>

#include <SDL.h>
#include <SDL_ttf.h>

#define WIDTH   640
#define HEIGHT  480

/* Ball speed: px/sec */
#define SPEED 100

static SDL_Window *win;
static SDL_Renderer *rdr;
static SDL_DisplayMode mode;

static TTF_Font *font;

static struct {
	int dx;
	int x;
} ball = {
	.dx = 1
};

static void
die(const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	vfprintf(stderr, fmt, ap);
	fputc('\n', stderr);
	va_end(ap);

	exit(1);
}

static void
print(const char *fmt, ...)
{
	SDL_Texture *tex;
	SDL_Surface *surface;
	char buf[1024];
	va_list ap;

	va_start(ap, fmt);
	vsnprintf(buf, sizeof (buf), fmt, ap);
	va_end(ap);

	surface = TTF_RenderUTF8_Blended(font, buf,
	    (const SDL_Color){0, 0, 0, 255});

	if (!surface)
		die("%s", SDL_GetError());
	if (!(tex = SDL_CreateTextureFromSurface(rdr, surface)))
		die("%s", SDL_GetError());

	SDL_RenderCopy(rdr, tex, NULL, &(const SDL_Rect){
		.x = 10,
		.y = 10,
		.w = surface->w,
		.h = surface->h
	});
	SDL_FreeSurface(surface);
	SDL_DestroyTexture(tex);
}

static void
init(void)
{
	if (SDL_Init(SDL_INIT_VIDEO) < 0 || TTF_Init() < 0)
		die("%s", SDL_GetError());

	win = SDL_CreateWindow("Test", SDL_WINDOWPOS_UNDEFINED,
	    SDL_WINDOWPOS_UNDEFINED, WIDTH, HEIGHT, 0);

	if (!win)
		die("%s", SDL_GetError());

	/* Get frame rate. */
	if (SDL_GetWindowDisplayMode(win, &mode) < 0)
		die("%s", SDL_GetError());

	printf("Screen has %d framerate\n", mode.refresh_rate);

	rdr = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);

	if (!rdr)
		die("%s", SDL_GetError());
	if (!(font = TTF_OpenFont("DejaVuSansMono.ttf", 10)))
		die("%s", SDL_GetError());
}

static void
loop(void)
{
	bool jesus_returns = false;
	float fps = 1000.0 / mode.refresh_rate;

	Uint32 last = 0;

	while (!jesus_returns) {
		Uint32 start = SDL_GetTicks();

		for (SDL_Event ev; SDL_PollEvent(&ev); ) {
			switch (ev.type) {
			case SDL_QUIT:
				jesus_returns = true;
				break;
			default:
				break;
			}
		}

		ball.x += ball.dx * (SPEED * fps / 1000.0);

		if (ball.x < 0) {
			ball.x = 0;
			ball.dx = 1;
		} else if (ball.x > WIDTH) {
			ball.x = WIDTH;
			ball.dx = -1;
		}

		SDL_SetRenderDrawColor(rdr, 255, 255, 255, 255);
		SDL_RenderClear(rdr);
		SDL_SetRenderDrawColor(rdr, 255, 0, 0, 255);
		SDL_RenderFillRect(rdr, &(const SDL_Rect){
			.x = ball.x,
			.y = HEIGHT / 2,
			.w = 10,
			.h = 10
		});

		print("frame time: %u. framerate=%d/sec",
		    (unsigned int)last, mode.refresh_rate);
		SDL_RenderPresent(rdr);

		last = SDL_GetTicks() - start;
	}
}

static void
quit(void)
{
	SDL_DestroyRenderer(rdr);
	SDL_DestroyWindow(win);
}

int
main(int argc, char **argv)
{
	(void)argc;
	(void)argv;

	init();
	loop();
	quit();

	return 0;
}

With that code, it’s smooth and there are no choppy movements however:

  1. The square goes still faster from right to left.
  2. I’ve been told that vsync isn’t supported everywhere and can even be disabled by the user. How should we handle this case?

Any help is appreciated.