Basic Window

The foundation of any graphical program is a window containing everything the user can see. We'll set up that window using the following code.

#include <SDL.h>
#include <stdio.h>

int main(int argc, char* argv[]) {
	// Initialize SDL
	if (SDL_Init(SDL_INIT_VIDEO) != 0) {
		printf("SDL could not initialize: Error %s\n", SDL_GetError());
		return 1;
	}

	// SDL Window setup
	SDL_Window* window = SDL_CreateWindow("SDL2 Tutorial",
		SDL_WINDOWPOS_UNDEFINED,
		SDL_WINDOWPOS_UNDEFINED,
		640, 480, 0);

	if (window == nullptr) {
		printf("Unable to create window. Error %s\n", SDL_GetError());
		return 1;
	}

	// Wait for 2,000 miliseconds (2 seconds)
	SDL_Delay(2000);

	// Destroy the window and shutdown the program
	SDL_DestroyWindow(window);
	SDL_Quit();

	return 0;
}
!
Note - linux
If you're using linux, change the SDL include to #include <SDL2/SDL.h>

File Setup

#include <SDL.h>
#include <stdio.h>

There are only two libraries to #include. The first is <SDL.h>. This gives us access to the functions, data structures, enumerations, and hints provided by the SDL library. The second is <stdio.h> from the C standard library which will be used to output error messages and other troubleshooting messages.

!
Note - C functions
Despite saying at the start that we'd be using C++, most of the code in these tutorials will be standard C code. This is mostly due to personal preference. Instead of using #include <stdio.h> for error messages, you could instead #include <iostream>.
int main(int argc, char* argv[]) {
	// ...
}

Next is the main() function. Typically you don't have to include argc and argv when writing the main function since the compiler will include that on its own. But when working with SDL, they must be included in the main() functions arguments exactly as you see in the code above. SDL has its own main() function and redefines the function you write as a macro. It does this to ensure compatibility across multiple platforms.

Initializing SDL

// Initialize SDL
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
	printf("SDL could not initialize: Error %s\n", SDL_GetError());
	return 1;
}

Before constructing the window, we initialize SDL and the subsystems we want to use. This isn't strictly necessary, as SDL will go through an initialization process for the video subsystem when you build the window if it hasn’t been done already. But it's better to ensure everything is set up at the beginning to avoid unpredictable behavior when the program runs.

SDL_Init() accepts several flags that tell it what its suppose to initialize. Here we only need Video, so we pass in the flag SDL_INIT_VIDEO. We can pass in multiple values by OR'ing them together with the pipe character like this:

SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_EVENTS);

SDL_Init() will return a 0 when it completes successfully, and otherwise returns a negative number. When it does encounter an error, it will print out an error message using SDL_GetError(). Finally the main function exits with return 1, since there’s no point in continuing the program if SDL can’t be initialized.

!
Note - Error logging
For larger programs it would be better to send these error messages to a text file instead of printing them to the console. Especially considering that your final program likely won't have the console window open at all. But for now, we're doing things the quick and simple way. We can worry about properly logging errors later, as well as shutting down the program more gracefully.

Creating the Window

// SDL Window setup
SDL_Window* window = SDL_CreateWindow("SDL2 Tutorial",
	SDL_WINDOWPOS_UNDEFINED,
	SDL_WINDOWPOS_UNDEFINED,
	640, 480, 0);
  
if (window == nullptr) {
	printf("Unable to create window. Error %s\n", SDL_GetError());
	return 1;
}

The window is created with the function SDL_CreateWindow() and stored as an SDL_Window*. Creating the window is a complex process that will be different depending on what platform the program is running on. Fortunately, SDL takes care of the complexity and reduced it to six inputs.

// Window Title, X Position, Y Position, Width, Height, Flags
SDL_Window* SDL_CreateWindow(const char* title,
                             int x, int y,
                             int w, int h, Uint32 flags);

For now we won't use any flags to keep things simple. But later we'll explore using the flags to set it to fullscreen, borderless, and hiding/revealing the window.

We set the window's x/y positions to SDL_WINDOWPOS_UNDEFINED because it doesn't really matter. This will end up displaying the window in the center of the screen.

!
Note - Nullptr vs null
nullptr was introduced in C++ 11, and it's one of the reasons we're programming in C++ and not regular C. While NULL can be used to represent a pointer to nothing, nullptr is explicitly a pointer value. NULL is actually an integer that gets converted to a void* when applied to a pointer. Always prefer nullptr to NULL for code readability and error checking.

Keep the Window Open

// Wait for 2,000 miliseconds (2 seconds)
SDL_Delay(2000);
  

All operating systems provide various functions that can be called by programs to achieve things like pausing a process. But the method of doing this is different between the operating systems. Both Windows and Linux provide a function called sleep(), but to access it on Windows you #include <Windows.h> while on Linux its #include <unistd.h>.

In order to simplify this, SDL provides us the function SDL_Delay() which accepts a 32 bit integer which represents milliseconds.

This is not something you should use often. There's rarely any reason to call this function other than in testing. In this case, without this pause the program would immediately shutdown. We want the window open for at least enough time to verify that it's working.

Close the Program

	// Destroy the window and shutdown the program
	SDL_DestroyWindow(window);
	SDL_Quit();

Here we come to the end of the program. Since our SDL_Window is a pointer, we need to free up it's memory now that it's no longer being used. To do this, we call SDL_DestroyWindow() and pass in our window pointer.

!
Note - SDL pointers
If you create a pointer using an SDL function (such as the window pointer from SDL_CreateWindow()), there is likely an SDL function to destroy that object, usually starting with SDL_Destroy or SDL_Free.

Finally we use SDL_Quit() to shutdown any subsystems we initialized throughout the program. Always remember to destroy and shut down things in a "last in, first out" pattern. In this case, the last thing we made was the window, so its the first to be destroyed.

When a program shuts down, the operating system will completely free up the memory used so other programs can use the resources. So it isn't absolutely required that we destroy the window and free up the memory. However it's better to strictly manage your memory when programming in C/C++. It's a habit that will save you a lot of trouble later on.

When a program shuts down, the operating system will completely free up the memory used so other programs can use the resources. So it isn't absolutely required that we destroy the window and free up the memory. However it's better to strictly manage your memory when programming in C/C++. It's a habit that will save you a lot of trouble later on.

Basic Input

In this next step the code gains the capacity to handle basic input and keep the program running until instructed to close. Additionally the code is refactored to keep things clean and organized.

#include <SDL.h>
#include <stdio.h>

SDL_Window* window = nullptr;

bool startup() {
	// Initialize SDL
	if (SDL_Init(SDL_INIT_VIDEO) != 0) {
		printf("SDL could not initialize: Error %s\n", SDL_GetError());
		return false;
	}

	// Window setup
	window = SDL_CreateWindow("SDL2 Tutorial",
		SDL_WINDOWPOS_UNDEFINED,
		SDL_WINDOWPOS_UNDEFINED,
		640, 480, 0);

	if (window == nullptr) {
		printf("Unable to create window. Error %s\n", SDL_GetError());
		return false;
	}

	return true;
}

void shutdown() {
	SDL_DestroyWindow(window);
	SDL_Quit();
}

int main(int argc, char* argv[]) {
	bool running = startup();
	SDL_Event event;

	// Game Loop
	while (running) {

		// User Input
		while (SDL_PollEvent(&event) != 0) {
			if (event.type == SDL_QUIT) {
				running = false;
			}
		}
	}

	shutdown();
	return 0;
}

Code Refactoring

#include <SDL.h>
#include <stdio.h>

SDL_Window* window = nullptr;

bool startup() {
	// ...
}

void shutdown() {
	// ...
}

int main(int argc, char* argv[]) {
	bool running = startup();
	SDL_Event event;

  	while (running) {
		// Game Loop ...
	}

	shutdown();
	return 0;
}

In order to keep the code clean while working on it, it's necessary to refactor occasionally. In the original main() function we have two sections that can be placed into separate functions. The first involves the initializing SDL and creating the window, and the second involves shutting down SDL and destroying the window.

!
Note - Refactoring
In programming, refactoring is the practice of restructuring code to improve efficiency and maintainability. Here's we're mostly focused on the latter.

I've also placed the SDL_Window* in the global scope so that both functions can easily access it. Alternatively, the window pointer could be in main() and then be passed into startup() and shutdown() as an argument. Another way would be to create a Window class with the startup() and shutdown() functions being refactored as the constructor and destructor.

Since I don't yet know how I want to structure this code, I'm choosing to go with the simplest option (window as a global) while still grouping the startup and shutdown code into their unique functions.

Originally, during the initialization, if something went wrong we used return 1 to exit the main() function and indicate failure. Now that the startup code has its own function, we can't do that. Instead, we use the functions return value to see if startup was successful. The value that startup() returns will be stored in running. We'll use this variable to determine if the program should still be running.

The Game Loop

while (running) {

	// User Input
	while (SDL_PollEvent(&event) != 0) {
		if (event.type == SDL_QUIT) {
			running = false;
		}
	}
}

First we tried SDL_Delay() to keep the window open, but that wasn't very useful. The program needs to stay open until it receives an instruction from the user to shutdown. To accomplish this, we create a while loop, and use the running variable to exit the loop. This loop is a special kind of loop called the game loop.

For a game, we need to be able to process input from the user (mouse, keyboard, gamepad), update the programs internal state (level up, take damage, cast magic), and show, or render, what the player can see on the screen. This idea of input-update-render is part nearly every game in existence, which is why it's called the game loop.

Right now we need some way to close the program when we choose. For this we use SDL_PollEvent().

int SDL_PollEvent(SDL_Event* event);

An event is something that happens during the program execution that the program wants to know about. Things like input from a mouse or keyboard. These things usually don't happen at predictable intervals. You might, for instance, click the mouse while the render function is happening. Instead of ignoring it, the computer puts the event into an event queue to be processed by any programs that might want it. SDL_PollEvent() accesses that queue and stores the most recent event in the SDL_Event* that we've named event.

SDL_PollEvent() returns 0 if there are no events, and a 1 if there are any events to process. Some inputs might require multiple key presses, like W and D to go diagonally right, so we want to handle all events each frame, which is why we have this in a while loop.

Within the loop is an if statement that checks for a special kind of event. SDL_QUIT is an event type that happens when the user tries to X out of the program. Without this check, the X won't do anything. In fact, you can have it do whatever you want. On clicking it, it could move the window, minimize it, spawn enemies in game, or anything else. No matter what, the program will only stop once it reaches the end of the main function.

There might be other things to add to this later, such as saving or prompting the user if they want to save. But for now, all we have to do is change running to false. Once the loop starts over, the condition will be false, and the program will carry on to the shutdown() function, and then main() will return 0.

Basic Graphics

The next step is to draw something to the screen. In a full game you'll have dozens to hundreds of things that need to be drawn. Every tile, sprite, 3D model, and HUD or UI element will need to be drawn to the screen. In the Graphics section are more tutorials for things like animations, drawing multiple objects, setting the size of objects, and so on.

For this first introduction, we're just going to fill the screen with a single picture. It doesn't matter what the picture is for this, but if you want to ensure the whole image is displayed, it will need to be 640 x 480 pixels in size, otherwise it will show only that much of the image, starting from the top left corner. Or, if the image is smaller, it will show all of it and the rest of the screen will be black.

#include <SDL.h>
#include <stdio.h>

SDL_Window* window = nullptr;
SDL_Surface* image = nullptr;

void loadImage() {
	image = SDL_LoadBMP("image.bmp");

	if (image == nullptr) {
		printf("Image failed to load. Error %s\n", SDL_GetError());
	}
}

bool startup() {
	// . . .
}

void shutdown() {
	SDL_FreeSurface(image);
	SDL_DestroyWindow(window);
	SDL_Quit();
}

int main(int argc, char* argv[]) {
	bool running = startup();
	SDL_Event event;

	// Create and Show Image
	loadImage();
	SDL_BlitSurface(image, nullptr, SDL_GetWindowSurface(window), nullptr);
	SDL_UpdateWindowSurface(window);

	// Game Loop
	while (running) {
		// . . .
	}

	shutdown();
	return 0;
}

Load Image

SDL_Surface* image = nullptr;

void loadImage() {
	image = SDL_LoadBMP("image.bmp");

	if (image == nullptr) {
		printf("Image failed to load. Error %s\n", SDL_GetError());
	}
}
!
Note - Best Practice
If you're new to programming, ignore "best practices." Just write code. Write lots of code. Write absolute garbage code. The more code you write, the better you'll get. You can always rewrite bad code. You can't rewrite code that doesn't exist. This code isn't "good code" but it works for our purposes. Better to write something bad that works than to never write anything because you can't decide the best way to do it.

The load function follows the same format used for creating the window. First is an SDL function that returns an SDL_Surface* and then we check to make sure the pointer isn't a nullptr. An SDL_Surface is a data struct containing image data and the data necessary to render that image to the screen.

To load the image, we pass in a string to SDL_LoadBMP() containing the file path and name. For this, I have a file called image.bmp that lives in the same folder as the source code. If you wanted to keep your assets organized, you could make a folder to store things in. In that case, you'd pass in a string like assets/images/image.bmp.

Destroy Image

void shutdown() {
	SDL_FreeSurface(image);
	SDL_DestroyWindow(window);
	SDL_Quit();
}

The image, being a pointer, needs to be memory managed just like the window. SDL has a function for this: SDL_FreeSurface().

Display Image

// Create and Show Image
loadImage();
SDL_BlitSurface(image, nullptr, SDL_GetWindowSurface(window), nullptr);
SDL_UpdateWindowSurface(window);
!
Note - Window Surface
Here you can see that the window has a surface pointer of its own. Just like our image, that surface also needs to be destroyed. This is why we use SDL_DestroyWindow() rather than just deleting the window, or letting it go out of scope to clean it up. If SDL was written in C++ it would have been created as a class and this functionality would have been inside the destructor. Since it's written in C, the SDL_DestroyWindow() function serves as its destructor instead. Same for SDL_FreeSurface().

Before we enter the game loop we load the image and blit it to the window. To"blit" something is to copy information from one section of memory to another. In this case, we're copying our image surface to the window's surface.

int SDL_BlitSurface(SDL_Surface*    source,
                    const SDL_Rect* sourceRect,
                    SDL_Surface*    destination,
                    SDL_Rect*       destinationRect)

SDL_BlitSurface() takes four arguments. The first is the source. This is the surface we want to copy. The third argument is the destination, or where we want to copy the source to.

The second and fourth arguments aren't necessary for this example, so they can be set to nullptr. What they do is allow you to cut out a section of the source image and paste it into a specific location in the destination. This is covered in detail in Graphics.

In order to get the window's surface, we have to use SDL_GetWindowSurface() which returns a surface pointer. We do this because we can't directly access any data inside the window structure. If you examine the SDL source code, all you can see in the header file (SDL_video.h) is this:

typedef struct SDL_Window SDL_Window;

So just like classes in C++, all of the data members are essentially private and everything is accessed through functions like SDL_GetWindowSurface().

Both of these functions will return a 0 if they executed successfully, or a negative number on failure. However, I'm not going to error check these as it will be obvious that something failed if nothing appears on screen.

What's next?

Understanding how to display graphics and get user input are the most important things for any software with a graphical interface. These two things are closely linked, but input almost entirely depends on graphics, while graphics can be displayed on their own. You can create a button that can't be clicked, but it's much more difficult to click a button you can't see. The next step then is to learn more about displaying and manipulating graphics. After that continue on with learning more about input.

I recommend spending a lot of time on these. Use them, experiment with them. You can go far in designing a game without ever touching any of the other topics in these tutorials. It's also important to use what you learn before moving on to new material. You could read through everything on here, but you'll retain only a small fraction of what you could have. The time spent not learning is more valuable than the time spent learning.

To learn to play the piano, it's better to practice for ten minutes, three times a day, than it is to spend an hour in one sitting. The same holds true for this. Read for a while, practice the code, then write your own code, or go do something else. The mind thrives on variety.