Working with SDL2 and C

This post will show snippets of code that can be used to make an SDL2 application. This post serves mostly as a personal reminder of how the basics of SDL2 work. Much more advanced things can be done using SDL2, especially if it’s combined with a language like C++. C++ has classes, which make it much easier to create more advanced programs (like a basic game).

What is SDL2?

The libSDL website describes the software as follows:

“Simple DirectMedia Layer is a cross-platform development library designed to provide low level access to
audio, keyboard, mouse, joystick, and graphics hardware via OpenGL and Direct3D.”

So, SDL2 is not like visual studio where you can drag and drop buttons into place. It is a lower level library which can draw 2D lines, squares, images and text. That’s it. If you want buttons, you create them yourself. If you want some sort of clickable menu, you have to create it. SDL2 does help a lot by making difficult parts easier, for example the ability to play audio or display an image on the screen.

Creating a window that opens and closes

Before any drawing can begin, a window and renderer have to be created. After the program window opens and closes without errors, other elements can be created.

Code:

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <math.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <SDL2/SDL_ttf.h>
#include <SDL2/SDL_mixer.h>

#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480

void sdl2_create_window(SDL_Window** window, SDL_Renderer** renderer, const char* window_title) {
    unsigned int window_flags = SDL_WINDOW_RESIZABLE;
    unsigned int renderer_flags = SDL_RENDERER_PRESENTVSYNC | SDL_RENDERER_ACCELERATED; // Use hardware rendering and vsync

    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        printf("Couldn't initialize SDL: %s\n", SDL_GetError());
        exit(1);
    }

    *window = SDL_CreateWindow(window_title, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_WIDTH, WINDOW_HEIGHT, window_flags);
    if (window == NULL) {
        printf("Failed to create window: %s\n", SDL_GetError());
        exit(1);
    }

    *renderer = SDL_CreateRenderer(*window, -1, renderer_flags);
    if (renderer == NULL) {
        printf("Failed to create renderer: %s\n", SDL_GetError());
        exit(1);
    }
}

int main() {
    SDL_Window* window = NULL;
    SDL_Renderer* renderer = NULL;
    SDL_Event event;
    bool running = true;

    // Create a new window to run the in
    sdl2_create_window(&window, &renderer, "SDL2 example");

	// Main loop
    while(running) {
		SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
        SDL_RenderClear(renderer);

        // Do rendering here

		// Check if the close button is pressed
		// and break out of the loop if it is.
		if(SDL_PollEvent(&event)) {
			if(event.type == SDL_QUIT) {
				// The program needs to stop running!
				running = false;
			}
		}

        SDL_RenderPresent(renderer);
    }

    // Stop everything
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();

    return 0;
}

Makefile to create the binary:

CC = gcc
CFLAGS = -std=c99 -Wall -Wextra -Wconversion -Ofast -Wpedantic -Werror \
         `sdl2-config --cflags` -lSDL2 -lSDL2_ttf -lSDL2_image -lSDL2_mixer -lm

all:
	@echo Building...
	$(CC) src/main.c -o main $(CFLAGS)

When the resulting binary is executed, the follwing empty window should appear:

SDL2 empty window

Small code examples

Drawing a line

To draw a line over the whole window, the following code can be used:

// Get begin and end values for the line
int x_start = 0;
int y_start = 0;
int x_end, y_end;
// Make sure the line ends at the bottom right of the screen
SDL_GetWindowSize(window, &x_end, &y_end);

// Set the line color
SDL_SetRenderDrawColor(renderer, 255, 255, 255, SDL_ALPHA_OPAQUE);
// Draw the line
SDL_RenderDrawLine(renderer, x_start, y_start, x_end, y_end);

If the application is now compiled and run again, a line should appear on the screen, going from the top left (0,0) to the bottom right (<window width>,<window height>). The line also changes shape when the window gets resized:

SDL2 high line

SDL2 wide line

Drawing a rectangle

Drawing a rectangle is something SDL2 can do by itself. It can either draw only the outline or fill it in with a color.

// Set draw color
SDL_SetRenderDrawColor(renderer, 255, 255, 255, SDL_ALPHA_OPAQUE);

// Create a rectangle
SDL_Rect example_rectangle;
example_rectangle.x = 50;	// Set horizontal top left coordinate
example_rectangle.y = 50;	// Set vertical top left coordinate
example_rectangle.w = 50;	// Set width
example_rectangle.h = 50;	// Set height

// Draw a colored rectangle
SDL_RenderDrawRect(renderer, &example_rectangle);
// Or, to fill in the rectangle as well
//SDL_RenderFillRect(renderer, &example_rectangle);

SDL2 rectangle

Drawing a circle

Drawing a circle is not something SDL2 can do by itself, but it can be done using a function and a custom datatype:

// Create a datatype called "SDL_Circle"
typedef struct {
	int x, y, r;	// point x, point y, radius
} SDL_Circle;

SDL_Circle circle;
circle.x = 240;
circle.y = 240;
circle.r = 240;

// Render a circle by drawing 360 lines
void SDL_RenderDrawCircle(SDL_Renderer* renderer, const SDL_Circle* circle) {
	const double PI = acos(-1);
	double angle = 360;
	do {
		// Calculate a point
		double rotation = angle * 2*PI / 360;
		double point_x_start = circle->x + cos(rotation) * circle->r;
		double point_y_start = circle->y + sin(rotation) * circle->r;

		// Calculate the next point
		rotation = (angle-1) * 2*PI / 360;
		double point_x_end = circle->x + cos(rotation) * circle->r;
		double point_y_end = circle->y + sin(rotation) * circle->r;

		// Draw a line between the two points
		SDL_RenderDrawLine(renderer, (int)point_x_start, (int)point_y_start, (int)point_x_end, (int)point_y_end);
	} while(angle-->1);
}

// in the main loop, call the function as follows after setting the right color:
SDL_RenderDrawCircle(renderer, &circle);

This results in the following drawing being made:

SDL2 circle

Drawing an image

SDL2 can draw images as sprites:

// Enable support for JPG, PNG and/or TIF before creating the window
IMG_Init(IMG_INIT_JPG|IMG_INIT_PNG|IMG_INIT_TIF);

// Import image files before running the main loop
SDL_Texture* example_texture = IMG_LoadTexture(renderer, "image.jpg");

// Render the texture inside the main loop
SDL_Rect dstrect;
int rotation = 0;
dstrect.x = 0;
dstrect.y = 0;
SDL_GetWindowSize(window, &dstrect.w, &dstrect.h);
SDL_RenderCopyEx(renderer, example_texture, NULL, &dstrect, rotation, NULL, SDL_FLIP_NONE);

// After the main window, before the window closes, run IMG_Quit();
IMG_Quit();

This results in the image being drawn on the screen:

SDL2 image rendering

Playing audio

To play audio, the following code can be used:

// Initialize audio before the main loop
int number_of_channels = 50;    // SDL2 will play max 50 audio clips at the same time

if(Mix_OpenAudio(44100, AUDIO_S16SYS, 2, 512) < 0) {
    fprintf(stderr, "Could not open audio: %s\n", SDL_GetError());
    exit(1);
}
if(Mix_AllocateChannels(number_of_channels) < 0) {
    fprintf(stderr, "Could not allocate mixing channels: %s\n", SDL_GetError());
    exit(1);
}

// Before the main loop, import all audio files as mix chunks
Mix_Chunk* example_audio = Mix_LoadWAV("audio.wav");

// During the main loop, when audio needs to play, it can be done using the following code
Mix_PlayChannel(-1, example_audio, 0);  // -1 automatically selects the first available audio channel

// After the main loop, before the program shuts down, make sure to close the audio mixer
Mix_CloseAudio();

Rendering text

To render text in SDL2, a font file with the extension .ttf is needed. It is to be placed in the same directory as the binary, or in a subdirectory within the project.

First, enable font support and load a font by running the code below:

TTF_Init();
int font_size = 16;
TTF_Font* font = TTF_OpenFont("path/to/font-file.ttf", font_size);

Then, to render text on the screen using this font, the following function can be used:

void text_render(SDL_Renderer* renderer, TTF_Font* font, char* message, int position_x, int position_y, int rotation_angle, bool center, SDL_Color* text_color) {
    // Declare variables that will contain text size values generated by TTF_SizeText()
    int text_width;
    int text_height;

    // Tell SDL2 to convert the imported ttf file and message into a texture
    SDL_Surface* message_surface = TTF_RenderText_Solid(font, message, *text_color);
    SDL_Texture* message_texture = SDL_CreateTextureFromSurface(renderer, message_surface);

    // Define dimensions of the text
    TTF_SizeText(font, message, &text_width, &text_height);
    SDL_Rect message_rect;
    if(center) {
        message_rect.x = position_x - (text_width / 2);
        message_rect.y = position_y - (text_height / 2);
    } else {
        message_rect.x = position_x;
        message_rect.y = position_y;
    }
    message_rect.w = text_width;
    message_rect.h = text_height;

    // Add the text to the rendering queue.
    // SDL_RenderCopy(renderer, message_texture, NULL, &message_rect);
    SDL_RenderCopyEx(renderer, message_texture, NULL, &message_rect, rotation_angle, NULL, SDL_FLIP_NONE);

    // Delete the texture, since it's written to the screen already
    SDL_FreeSurface(message_surface);
    SDL_DestroyTexture(message_texture);
}

The function can now be called somewhere in main():

// Define a text color
SDL_Color color;
color.r = 255;               // red
color.g = 100;               // green
color.b = 100;               // blue
color.a = SDL_ALPHA_OPAQUE;  // alpha

// Define the message
char* message = "Example text";

// Render it all in the main loop
int x_end, y_end;
SDL_GetWindowSize(window, &x_end, &y_end);
text_render(renderer, font, message, x_end / 2, y_end / 2, -10, true, &color);

Just before the window is closed, TTF_Quit() needs to be called to free some memory.

This will result in the following window being shown:

SDL2 text rendering

Keyboard input

Game-like keyboard input

todo

Program-like keyboard input

todo

Mouse input

todo

Controller input

todo