Hello SDL3
Last Updated: Oct 19th, 2024
This tutorial covers the first major stepping stone: getting a window to pop up.Now that you have SDL set up, it's time to make a bare bones SDL graphics application that renders an image on the screen.
/* Headers */ //Using SDL and STL string #include <SDL3/SDL.h> #include <SDL3/SDL_main.h> #include <string>
First thing we put at the top of our header is the SDL header files. SDL.h contains the SDL datatypes and functions we need and SDL_main defines how the main function is going to work. When you have to deal with multiple
platforms like Windows and iOS, having an abstraction layer for this is very useful.
We're also including the C++ string library because we are going to be using STL string and some string functions.
We're also including the C++ string library because we are going to be using STL string and some string functions.
/* Constants */ //Screen dimension constants constexpr int kScreenWidth{ 640 }; constexpr int kScreenHeight{ 480 };
Here we're initializing the screen width/height as constants.
If you're unfamiliar with more modern C++, constexpr (constant expression) is a way to define constants so their evaluated when the code is compiling as opposed to when it's running which can give a performance boost. Initializing with braces is the generally preferred method of initialization because of type safety.
If you're wondering why we aren't using auto and hard typing these constants as integers, it because you generally want to avoid using auto in game engine environments. I'll go into why in the addendums below.
If you're unfamiliar with more modern C++, constexpr (constant expression) is a way to define constants so their evaluated when the code is compiling as opposed to when it's running which can give a performance boost. Initializing with braces is the generally preferred method of initialization because of type safety.
If you're wondering why we aren't using auto and hard typing these constants as integers, it because you generally want to avoid using auto in game engine environments. I'll go into why in the addendums below.
/* Function Prototypes */ //Starts up SDL and creates window bool init(); //Loads media bool loadMedia(); //Frees media and shuts down SDL void close();
Our application is going to initialize SDL for use first. Whether you're using SDL, Vulkan, or some other library, it's not uncommon to have to initialize it before it's ready to use. After SDL is initialized, we're going to
load the image, render it to the screen, wait for the user to quit the application, and then clean up and close the application.
Here we're declaing functions to intialize, load the media, and close/clean up the application. The image rendering and input handling will be done in the main function.
Also, please don't email me about the close function being a name collision in C. This is a C++ tutorial and will be using the C++ function overloading that isn't available in pure C.
Here we're declaing functions to intialize, load the media, and close/clean up the application. The image rendering and input handling will be done in the main function.
Also, please don't email me about the close function being a name collision in C. This is a C++ tutorial and will be using the C++ function overloading that isn't available in pure C.
/* Global Variables */ //The window we'll be rendering to SDL_Window* gWindow{ nullptr }; //The surface contained by the window SDL_Surface* gScreenSurface{ nullptr }; //The image we will load and show on the screen SDL_Surface* gHelloWorld{ nullptr };
Here are some global variables we'll be needing (which is why they start with g). We're going to make a window pop up so we need an SDL_Window to represent it. Then we need an
SDL_Surface for the screen and the bmp image we're loading.
If you're new to modern C++, using nullptr is the standard way to have a null pointer and using NULL is deprecated.
SDL_Surface
is a data representation of an image, whether it's an image you're loading from a file or a
representation of what you're seeing on screen.If you're new to modern C++, using nullptr is the standard way to have a null pointer and using NULL is deprecated.
/* Function Implementations */ bool init() { //Initialization flag bool success{ true }; //Initialize SDL if( !SDL_Init( SDL_INIT_VIDEO ) ) { SDL_Log( "SDL could not initialize! SDL error: %s\n", SDL_GetError() ); success = false; }
In our initalization function, first we declare a success flag. Then we call SDL_Init to initialize SDL so we can start using it. We specifically initialize the SDL video
subsystem because we are going to need the rendering functions. You should generally only initialize the subsystems you need.
If initialization fails, we write out an error to the console with SDL_Log. If you're wondering why we aren't using iostream/cout it's because of the lack of thread safety (a lesson I learned the hard way). The reason we're not using printf is because certain platforms (like Android) can get wonky with it and it's best to just have SDL handle it for you.
When logging an error, we write out what the error was and hopefully get more info about the error using SDL_GetError. We also set the success flag to failure.
If initialization fails, we write out an error to the console with SDL_Log. If you're wondering why we aren't using iostream/cout it's because of the lack of thread safety (a lesson I learned the hard way). The reason we're not using printf is because certain platforms (like Android) can get wonky with it and it's best to just have SDL handle it for you.
When logging an error, we write out what the error was and hopefully get more info about the error using SDL_GetError. We also set the success flag to failure.
else { //Create window if( gWindow = SDL_CreateWindow( "SDL3 Tutorial: Hello SDL3", kScreenWidth, kScreenHeight, 0 ); gWindow == nullptr ) { SDL_Log( "Window could not be created! SDL error: %s\n", SDL_GetError() ); success = false; } else { //Get window surface gScreenSurface = SDL_GetWindowSurface( gWindow ); } } return success; }
If SDL initialized successfully, we continue initialization be creating a window with SDL_CreateWindow. The first argument is the caption which is this part of
the window:
The second/third arguments are the screen dimensions. The last argument is an SDL_WindowFlags which we're just setting to 0 so it uses the default behavior because we don't need anything special for this window.
If the window fails to create, we log the error and set the success flag to failure. If it succeeds, we get the window's surface (which contains what we actually can see on screen) using SDL_GetWindowSurface.
After we're done with initialization, we return the success flag so we know if initialization worked or not.
Oh and if you're not familiar with modern C++, it is possible in if conditions to assign a variable and check it in the same if parens as you can see we did with
The second/third arguments are the screen dimensions. The last argument is an SDL_WindowFlags which we're just setting to 0 so it uses the default behavior because we don't need anything special for this window.
If the window fails to create, we log the error and set the success flag to failure. If it succeeds, we get the window's surface (which contains what we actually can see on screen) using SDL_GetWindowSurface.
After we're done with initialization, we return the success flag so we know if initialization worked or not.
Oh and if you're not familiar with modern C++, it is possible in if conditions to assign a variable and check it in the same if parens as you can see we did with
gWindow
. This way we save a line of code.
bool loadMedia() { //File loading flag bool success{ true }; //Load splash image std::string imagePath{ "01-hello-sdl3/hello-sdl3.bmp" }; if( gHelloWorld = SDL_LoadBMP( imagePath.c_str() ); gHelloWorld == nullptr ) { SDL_Log( "Unable to load image %s! SDL Error: %s\n", imagePath.c_str(), SDL_GetError() ); success = false; } return success; }
In our media loading function, we have a success flag like with the initialization function. Then we set the path of the image we want to load and we load it with
SDL_LoadBMP. Like with
After our file is loaded, we return the success flag.
SDL_CreateWindow
, SDL_LoadBMP
returns NULL on an error so when we get a NULL surface back, we report and error to the console and set the success
flag to error.After our file is loaded, we return the success flag.
void close() { //Clean up surface SDL_DestroySurface( gHelloWorld ); gHelloWorld = nullptr; //Destroy window SDL_DestroyWindow( gWindow ); gWindow = nullptr; gScreenSurface = nullptr; //Quit SDL subsystems SDL_Quit(); }
When we're done with our window and the image we loaded, we want to clean them up. We do this by calling SDL_DestroySurface on the image we loaded and
SDL_DestroyWindow on the window we created. Don't forget to set the pointers to null after you destroyed them. When we're done with SDL at the end of the program,
we call SDL_Quit. Oh and don't worry about freeing the screen surface. That's taken care of by
You may be wondering why we aren't using smart pointers from newer versions of C++? That's because they come with a performance penalty and for performance critical applications (like video game engines), they are too slow. One of the biggest problems with students coming out of college is that they never learned manually manage memory and if you don't know how to do that it is going to be a serious skill gap going into your career. While you probably aren't going to be making a AAA game engine anytime soon, and with the projects you are making in the near future with SDL you could probably get away with using smart pointers without a noticable performance issue, you need to learn how to manage your memory as memory management strategies are a very common in interview questions. If you never learn how to code without smart pointers, it's eventually (probably very quickly) going to segfault your career.
SDL_DestroyWindow
.You may be wondering why we aren't using smart pointers from newer versions of C++? That's because they come with a performance penalty and for performance critical applications (like video game engines), they are too slow. One of the biggest problems with students coming out of college is that they never learned manually manage memory and if you don't know how to do that it is going to be a serious skill gap going into your career. While you probably aren't going to be making a AAA game engine anytime soon, and with the projects you are making in the near future with SDL you could probably get away with using smart pointers without a noticable performance issue, you need to learn how to manage your memory as memory management strategies are a very common in interview questions. If you never learn how to code without smart pointers, it's eventually (probably very quickly) going to segfault your career.
int main( int argc, char* args[] ) { //Final exit code int exitCode{ 0 }; //Initialize if( !init() ) { SDL_Log( "Unable to initialize program!\n" ); exitCode = 1; } else { //Load media if( !loadMedia() ) { SDL_Log( "Unable to load media!\n" ); exitCode = 2; }
Ok, now it's time to put everything together.
First we declare an exit code. This will help us quickly tell where something went wrong. Then we call our initialization code and if that goes wrong we log an error and set the exit code to 1 to let the caller now something went wrong in initialization. Then we load our image and if that goes wrong we once again log and set an exit code for image loading error. These codes are semi abritrary so feel free to use whatever error code scheme you like.
First we declare an exit code. This will help us quickly tell where something went wrong. Then we call our initialization code and if that goes wrong we log an error and set the exit code to 1 to let the caller now something went wrong in initialization. Then we load our image and if that goes wrong we once again log and set an exit code for image loading error. These codes are semi abritrary so feel free to use whatever error code scheme you like.
else { //The quit flag bool quit{ false }; //The event data SDL_Event e; SDL_zero( e );
If our initialization and image loading succeeded, we want to declare/initialize some variables before we enter the main loop. First there's a quit flag that keeps track of when we want to exit the application. Then there's
the SDL_Event structure. An
Next we call SDL_zero on the
With our data initialized, it's time to enter the main loop.
SDL_Event
can be a key press, a mouse movement, or (what we care about here) when a user X's out the window.Next we call SDL_zero on the
SDL_Event
to initialize it to all 0 to prevent problems that may occur from uninitialized values.With our data initialized, it's time to enter the main loop.
//The main loop while( quit == false ) { //Get event data while( SDL_PollEvent( &e ) ) { //If event is quit type if( e.type == SDL_EVENT_QUIT ) { //End the main loop quit = true; } }
Here's the top of our main loop. A main loop usually does 3 things:
Our main loop is a simple literal while loop. First thing it does is call SDL_PollEvent over and over. What
The only event type we care about here is the
Once all the events are processed, the event loop finishes and we move onto the next part of the main loop.
- Process user input
- Update the game's logic
- Render the game objects
Our main loop is a simple literal while loop. First thing it does is call SDL_PollEvent over and over. What
SDL_PollEvent
is check if there are any
events to process. If there are events, it will fill the SDL_Event
structure with the data about the event. This loop will keep doing this until there are no events to process. It will process the events in
the order they happened.The only event type we care about here is the
SDL_EVENT_QUIT
type which happens when the user X's out the window. If it does, we set the quit flag to true which will break the main loop after this iteration
of the loop is done.Once all the events are processed, the event loop finishes and we move onto the next part of the main loop.
//Fill the surface white SDL_FillSurfaceRect( gScreenSurface, nullptr, SDL_MapSurfaceRGB( gScreenSurface, 0xFF, 0xFF, 0xFF ) ); //Render image on screen SDL_BlitSurface( gHelloWorld, nullptr, gScreenSurface, nullptr ); //Update the surface SDL_UpdateWindowSurface( gWindow ); }
Now that all the events are processed, its time to draw our image on the screen.
First we fill the screen with white using SDL_FillSurfaceRect. The first argument is the surface you want to fill, which is the screen surface. The second is the region of the screen you want to fill but since we want to fill the whole screen we set it to null. The third argument is the pixel you want to fill with which we calculate using SDL_MapSurfaceRGB. The first argument
After the screen has been cleared white, we blit the image we loaded onto the screen using SDL_BlitSurface. Blit is short for BLock Image Transfer and it basically copies the data from the source surface onto the destination surface like a rubber stamp. The first argument is the source surface and the second argument is the portion of the source surface we want to use and since we want the whole surface we set it to null. The third argument is the destination surface we want to copy onto and the 4th argument says where on the destination you want to blit to. Since we're starting from the corner at x = 0 and y = 0, we can just set this to null.
After we've cleared the screen and blitted our image onto it, it won't become visible until we update the screen surface with SDL_UpdateWindowSurface. This is the last bit of our main loop and it will go back up to the top, check for inputs, and then redraw the image until the user X's out the program.
First we fill the screen with white using SDL_FillSurfaceRect. The first argument is the surface you want to fill, which is the screen surface. The second is the region of the screen you want to fill but since we want to fill the whole screen we set it to null. The third argument is the pixel you want to fill with which we calculate using SDL_MapSurfaceRGB. The first argument
SDL_MapSurfaceRGB
is the surface you want to format to (which the screen, obviously) and the following 3 arguments
are the R, G, and B values of the color you want to map. If you're not familiar with RGB hex colors, red FF green FF blue FF is the hex code for white.After the screen has been cleared white, we blit the image we loaded onto the screen using SDL_BlitSurface. Blit is short for BLock Image Transfer and it basically copies the data from the source surface onto the destination surface like a rubber stamp. The first argument is the source surface and the second argument is the portion of the source surface we want to use and since we want the whole surface we set it to null. The third argument is the destination surface we want to copy onto and the 4th argument says where on the destination you want to blit to. Since we're starting from the corner at x = 0 and y = 0, we can just set this to null.
After we've cleared the screen and blitted our image onto it, it won't become visible until we update the screen surface with SDL_UpdateWindowSurface. This is the last bit of our main loop and it will go back up to the top, check for inputs, and then redraw the image until the user X's out the program.
} } //Clean up close(); return exitCode; }
Once we break out of our main loop, we call our clean up function to free up resources, quit SDL, and return the exit code.
Congratulations on your first graphical program!
Congratulations on your first graphical program!
Download the media and source code for this demo here.
Over the course of these tutorials, I will do things that may technically be considered bad coding but make the code easier to understand. There will be addendums will point that out.
Do be careful if you are a beginner programmer. Making decisions on things like optimization with incomplete knowledge is going to result in bad code. If I tell you to not worry about something, don't worry about it until I tell you it should be a concern. If you haven't even made Tetris with SDL, you probably don't need to be worrying about things like memory allocation strategies.
Back to Index
Over the course of these tutorials, I will do things that may technically be considered bad coding but make the code easier to understand. There will be addendums will point that out.
Do be careful if you are a beginner programmer. Making decisions on things like optimization with incomplete knowledge is going to result in bad code. If I tell you to not worry about something, don't worry about it until I tell you it should be a concern. If you haven't even made Tetris with SDL, you probably don't need to be worrying about things like memory allocation strategies.
Back to Index
Addendum: Why we're using C++17? Why aren't we using auto?
As to why we aren't using C++20 over C++17, it's because of Apple and at the time of writing XCode does not have full C++20 support out of the box. There are ways to get to work, but it's a huge pain and I want to keep these
tutorials beginner friendly as possible. Having to set up C++20 is a lot of hassle for not a lot of payoff. There isn't really anything essential from C++20 I would want to use, just some nice to haves. Google only barely
moved from C++17 to C++20 summer 2024 for their coding standards, so don't event worry about C++23.
Even then, you want to be careful about using the latest version of C++. In the industry, engine programmers are very strict about integrating new C++ features. There are quite a few new C++ features that are convenient to use, but come at significant performance costs. Even if they aren't slow, newer features can be buggier than older features that are tried and tested. This can be huge problem when you're dealing with proprietary console SDKs where you can't just get their source code on github and submit a patch.
Even features that are well established like auto can introduce painful bugs. You generally only want to use auto when you know exactly what the variable type will be. The problem is that you think you do, but you probably don't. This can become a major issue because the compiler will make alterations to your code to optimize it and it will over the course of your career make assumptions about code that are incorrect that cause incorrect runtime behaviour. All non-trivial code has bugs in it and compilers are not trivial to make. It is a reality of professional game development that you will encounter a bug that only happens when compiling for release with optimizations and you or an engine programmer will have to step through assembly to find the bug. When you use auto, that means it's another thing that the compiler can and will mess with which means you don't know how the data will be assembled and that makes having to debug the assembly code that much harder. This is why we prefer strongly typed code.
So don't think that using the latest version of C++ will make your code better, because new features also introduce new problems. If you're new C++ programmer worried about using an outdated version of C++, don't worry about it because the industry prefers tried and true C++ features over the less stable bleeding edge.
This means please don't email me asking when I am going to update these tutorials to the latest version of C++.
Even then, you want to be careful about using the latest version of C++. In the industry, engine programmers are very strict about integrating new C++ features. There are quite a few new C++ features that are convenient to use, but come at significant performance costs. Even if they aren't slow, newer features can be buggier than older features that are tried and tested. This can be huge problem when you're dealing with proprietary console SDKs where you can't just get their source code on github and submit a patch.
Even features that are well established like auto can introduce painful bugs. You generally only want to use auto when you know exactly what the variable type will be. The problem is that you think you do, but you probably don't. This can become a major issue because the compiler will make alterations to your code to optimize it and it will over the course of your career make assumptions about code that are incorrect that cause incorrect runtime behaviour. All non-trivial code has bugs in it and compilers are not trivial to make. It is a reality of professional game development that you will encounter a bug that only happens when compiling for release with optimizations and you or an engine programmer will have to step through assembly to find the bug. When you use auto, that means it's another thing that the compiler can and will mess with which means you don't know how the data will be assembled and that makes having to debug the assembly code that much harder. This is why we prefer strongly typed code.
So don't think that using the latest version of C++ will make your code better, because new features also introduce new problems. If you're new C++ programmer worried about using an outdated version of C++, don't worry about it because the industry prefers tried and true C++ features over the less stable bleeding edge.
This means please don't email me asking when I am going to update these tutorials to the latest version of C++.
Addendum: Global variables and the global namespace.
You probably heard that it's a bad idea to use global variables. That's because it is.
If this was a real application, I would have no global variables and every variable would be tied to a class or a namespace because you want to avoid polluting the global namespace. However, these tutorials are more teaching tools rather than real software, and global variables are easier to keep track of at a small scale (but horrible to keep track of for any medium sized code base). If given the choice between code readibility are code optimization, I will take readibility to make things easier to comprehend.
So for a real application you should avoid them but for quick and dirty demos you'll be fine.
If this was a real application, I would have no global variables and every variable would be tied to a class or a namespace because you want to avoid polluting the global namespace. However, these tutorials are more teaching tools rather than real software, and global variables are easier to keep track of at a small scale (but horrible to keep track of for any medium sized code base). If given the choice between code readibility are code optimization, I will take readibility to make things easier to comprehend.
So for a real application you should avoid them but for quick and dirty demos you'll be fine.
Addendum: Yes, I know this is causing 100% CPU use.
This application is processing inputs and drawing the screen as fast as it can. It's going to eat up as much CPU as it can muster.
I am going to write an article about it in the future, but trying to make every bit of your code as efficient as possible is actually a bad idea. It order to make smart decisions about optimization, you need to know:
I am going to write an article about it in the future, but trying to make every bit of your code as efficient as possible is actually a bad idea. It order to make smart decisions about optimization, you need to know:
- Operating System Architecture
- Computer Hardware Architecture
- Compiler Architecture
- Game Engine Architecture
- Don't do it.
- (This is for experts only) Do it later.
Addendum: Using bools.
You may have heard that it's a bad idea to use bools. Bad idea may be a strong word but using 8 bits of memory when you only need one is wasteful. If this were production code, I probably use bitfields to store groups
of boolean values, but bitfields can make code less readable. Even then, wasting 7 bits here and there isn't the end of the world, it's only when you have tens of thousands of objects wasting dozens of bytes does it become a
problem. A single bool a function call certain isn't going to cause your application to slow to a crawl. As I said before, I will take readibility to make things easier to comprehend if given the choice.