Getting started with 2D graphics in C++ (SDL2)


Reading time: 40 minutes | Coding time: 20 minutes

Yes! You can do graphics using C++ and also create animation. Today in this article I will introduce you all to a C++ graphic library called SDL2. I find this library as a super fun for the demonstration of my algorithms. I hope that it might me useful to you some day, who knows! Very well then, lets get started.

We will start by implementing a basic class design which initialize various parameters of SDL with a clear black window. From there, we will implement drawing different shapes and then we will create a simple animation of those shapes! This will be the scope of this tutorial.

Feeling excited already? Lets get moving...

Installation

Assuming you are using ubuntu 16 or 18, you can simply install by using apt

sudo apt install libsdl2-dev libsdl2-2.0-0 -y;

For other operating system, you can use the google to search for the installation. If you are having difficulty installing then mention it in the comments. I will do my best to help you out.

Anyways, lets move on..

First, we will start by creating an empty directory. For that you can use the gui or terminal, your choice. Lets call that directory "sdl_intro".

Below is the CMakeLists.txt file which will define the build instruction for this project. Be SUPER SUPER SURE, that you name it as "CMakeLists.txt" only (without the quotes) and nothing else.

cmake_minimum_required (VERSION 3.0)
set(CMAKE_BUILD_TYPE Debug)

# Below line is for using C++14 standard
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14")

# Our project name
project (framework)

add_executable(framework main.cpp)

# For finding our SDL2 package!
INCLUDE(FindPkgConfig)

# Searching SDL2 package.
PKG_SEARCH_MODULE(SDL2 REQUIRED sdl2)

# Including our external library's directory for header files!
INCLUDE_DIRECTORIES(${SDL2_INCLUDE_DIRS})

# Linking SDL2 library with our project.
target_link_libraries(framework ${SDL2_LIBRARIES})

Below is the main file for this project which has to be in the same directory as the CMakeLists.txt file.

#include <SDL.h>

class Framework{
public:
    // Contructor which initialize the parameters.
    Framework(int height_, int width_): height(height_), width(width_){
        SDL_Init(SDL_INIT_VIDEO);       // Initializing SDL as Video
        SDL_CreateWindowAndRenderer(width, height, 0, &window, &renderer);
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);      // setting draw color
        SDL_RenderClear(renderer);      // Clear the newly created window
        SDL_RenderPresent(renderer);    // Reflects the changes done in the
                                        //  window.
    }

    // Destructor
    ~Framework(){
        SDL_DestroyRenderer(renderer);
        SDL_DestroyWindow(window);
        SDL_Quit();
    }

private:
    int height;     // Height of the window
    int width;      // Width of the window
    SDL_Renderer *renderer = NULL;      // Pointer for the renderer
    SDL_Window *window = NULL;      // Pointer for the window
};

int main(int argc, char * argv[]){

    // Creating the object by passing Height and Width value.
    Framework fw(200, 400);

    SDL_Event event;    // Event variable

    // Below while loop checks if the window has terminated using close in the
    //  corner.
    while(!(event.type == SDL_QUIT)){
        SDL_Delay(10);  // setting some Delay
        SDL_PollEvent(&event);  // Catching the poll event.
    }
}

Now, make sure you are in the "sdl_intro" directory. Create a new folder or directory called "build". I will demonstrate using terminal commands below.

mkdir build/
cd build/
cmake ../
make

The above commands will create a directory called build and then build the project. You terminal might look like this

your@computer:~/sdl_intro/build$ cmake ../
-- The C compiler identification is GNU 7.4.0
-- The CXX compiler identification is GNU 7.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found PkgConfig: /usr/bin/pkg-config (found version "0.29.1") 
-- Checking for one of the modules 'sdl2'
-- Configuring done
-- Generating done
-- Build files have been written to: /home/shivang/sdl_intro/build
your@computer:~/sdl_intro/build$ make
Scanning dependencies of target framework
[ 50%] Building CXX object CMakeFiles/framework.dir/main.cpp.o
[100%] Linking CXX executable framework
[100%] Built target framework

If you see this, it means that your project has been build successfully. Now an executable has generated in your build/ directory called framework. Just type following in the terminal

./framework

And you can see a black window on your computer screen like this

Screenshot-from-2019-09-29-18-52-51

You can click on cross to terminate the window. Congrats! You have crossed the first checkpoint of this tutorial.

Lets understand the code

So, we have created a class called Framework. The constructor of Framework initializes two important SDL initializers, SDL_Init() and SDL_CreateWindowAndRenderer(). The former initializes SDL library with some flag for example, we have passed SDL_INIT_VIDEO flag. That means we are telling SDL to be initialized in a video mode. There many different modes for SDL. But we won't be learning or discussing those in this article.

Moving forward, we encounter another SDL function called SDL_SetRenderDrawColor() which requires renderer pointers and four more values. It sets the drawing color for the renderer, in our case black because we are passing all the values as 0. SDL uses 8-bit color values in Red-Green-Blue-Alpha order where alpha indicating transparency.

Next function we encounter is SDL_RenderClear() which clears the entire screen to which the passing renderer is pointing with the previously set renderer draw-color which in this case is black. The changes done to the screen won't be reflected unless you call SDL_RenderPresent(). Now you might ask, why not reflect the screen every time we update the renderer? If you do that you might experience the lag on the screen as changing the pixels every time would increase the complexity by O(height x width). So, make sure you only show what is important.

Now, I will uncover the mystery behind the SDL_Events event line and below line of the code.

// Below while loop checks if the window has terminated using close in the
    //  corner.
    while(!(event.type == SDL_QUIT)){
        SDL_Delay(10);  // setting some Delay
        SDL_PollEvent(&event);  // Catching the poll event.
    }

So, SDL_Events is just a SDL custom data type which stores the event on the SDL window. Such as closing, mouse hovering, mouse click etc. We will focus on event when we press the terminate button or "cross" button. Lets just say that we won't use these lines, what will happen? The object is created in the main function that means the window will be created but after that the program ends that means destructor will be called and SDL window will be destroyed. This all will happen in a split second. So in order to let it open, we have to create a loop that won't allow the program to exit from the main function. That's why I have created a while loop which will only exit when we click the close button on the window. And this close-button-click even is capture using SDL_PollEvent() function.

Drawing shapes.

Lets start with a circle! Copy and replace the main code with the below code which is updated to draw circle.

#include <SDL.h>
#include <cmath>

class Framework{
public:
    // Contructor which initialize the parameters.
    Framework(int height_, int width_): height(height_), width(width_){
        SDL_Init(SDL_INIT_VIDEO);       // Initializing SDL as Video
        SDL_CreateWindowAndRenderer(width, height, 0, &window, &renderer);
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);      // setting draw color
        SDL_RenderClear(renderer);      // Clear the newly created window
        SDL_RenderPresent(renderer);    // Reflects the changes done in the
                                        //  window.
    }

    // Destructor
    ~Framework(){
        SDL_DestroyRenderer(renderer);
        SDL_DestroyWindow(window);
        SDL_Quit();
    }

    void draw_circle(int center_x, int center_y, int radius_){
        // Setting the color to be RED with 100% opaque (0% trasparent).
        SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);

        // Drawing circle
        for(int x=center_x-radius_; x<=center_x+radius_; x++){
            for(int y=center_y-radius_; y<=center_y+radius_; y++){
                if((std::pow(center_y-y,2)+std::pow(center_x-x,2)) <= 
                    std::pow(radius_,2)){
                    SDL_RenderDrawPoint(renderer, x, y);
                }
            }
        }

        // Show the change on the screen
        SDL_RenderPresent(renderer);
    }

private:
    int height;     // Height of the window
    int width;      // Width of the window
    SDL_Renderer *renderer = NULL;      // Pointer for the renderer
    SDL_Window *window = NULL;      // Pointer for the window
};

int main(int argc, char * argv[]){

    // Creating the object by passing Height and Width value.
    Framework fw(200, 400);

    // Calling the function that draws circle.
    fw.draw_circle(200, 100, 50);

    SDL_Event event;    // Event variable

    // Below while loop checks if the window has terminated using close in the
    //  corner.
    while(!(event.type == SDL_QUIT)){
        SDL_Delay(10);  // setting some Delay
        SDL_PollEvent(&event);  // Catching the poll event.
    }
}

After changing the main.cpp code with the above, you can try building the projet again. For that, repeat the previously explained step where you have to go into build/ directory and type make command in the terminal. You can get the below result when executing framework file.

Screenshot-from-2019-09-30-14-29-38

You can see a red circle, centered at the middle of the screen. Let me explain the changes in the code.

So, I started by implementing a new method called draw_circle() which takes three parameters, x & y coordinate of the circle's center and radius of the circle.

In this method, I am changing the renderer's drawing color to red by passing second argument as 255, and also I want it 100% opaque that's why I am passing 255 to the last argument.

After color, lets discuss how I created a circle. There is one method in SDL which can draw pixel on the screen, its called SDL_RenderDrawPoint(). I am passing the renderer and x-y coordinates of the screen to this function.

So, you might be wondering that what would be the logic behind drawing the circle. But before that let's revise the math definition of circle.

MATH

All the points on the circle follows the following equation

img

Where, h & k are the x and y coordinate of the center, respectively. Also, x and y are the corresponding coordinate point on the circle. Now, lets manipulate the circle's formula a bit. Lets replace the equal sign with less than equal sign. What would that represent? All the points bounding the circle.

Now coming back to our draw_circle method, you can see the if condition in the for loop. I am checking the circle inequality in the if condition, and if the point is satisfy then draw the point.

For the points, I start with by square which ranges from (center point - radius) to (center point + radius). We check each of these points if they satisfy the inequality they will be change using draw_circle method.

Congrats! you have completed 2nd milestone of the article!

Animation in SDL

For animation, we will take the circle and move it up and down the screen. Let's get started! First of all you have to copy the code below and replace it with what you already have.

#include <SDL.h>
#include <cmath>
#include <vector>

class Framework{
public:
    // Contructor which initialize the parameters.
    Framework(int height_, int width_): height(height_), width(width_){
        SDL_Init(SDL_INIT_VIDEO);       // Initializing SDL as Video
        SDL_CreateWindowAndRenderer(width, height, 0, &window, &renderer);
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);      // setting draw color
        SDL_RenderClear(renderer);      // Clear the newly created window
        SDL_RenderPresent(renderer);    // Reflects the changes done in the
                                        //  window.
    }

    // Destructor
    ~Framework(){
        SDL_DestroyRenderer(renderer);
        SDL_DestroyWindow(window);
        SDL_Quit();
    }

    void draw_circle(int center_x, int center_y, int radius_){
        // Setting the color to be RED with 100% opaque (0% trasparent).
        SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);

        // Drawing circle
        for(int x=center_x-radius_; x<=center_x+radius_; x++){
            for(int y=center_y-radius_; y<=center_y+radius_; y++){
                if((std::pow(center_y-y,2)+std::pow(center_x-x,2)) <= 
                    std::pow(radius_,2)){
                    SDL_RenderDrawPoint(renderer, x, y);
                }
            }
        }

        // Show the change on the screen
        SDL_RenderPresent(renderer);
    }

    void move_circle(){
        // Setting the color to be RED with 100% opaque (0% trasparent).
        
        SDL_Event event;    // Event variable
        while(!(event.type == SDL_QUIT)){

            // Circle will go down!
            for(int i=0; i<height; i++){
                SDL_Delay(10);  // setting some Delay
                SDL_PollEvent(&event);  // Catching the poll event.
                if(event.type == SDL_QUIT) return;
                SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
                SDL_RenderClear(renderer);
                draw_circle(width/2, i, 25);
            }

            // Circle will go up!
            for(int i=height; i>0; i--){
                SDL_Delay(10);  // setting some Delay
                SDL_PollEvent(&event);  // Catching the poll event.
                if(event.type == SDL_QUIT) return;
                SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
                SDL_RenderClear(renderer);
                draw_circle(width/2, i, 25);
            }
        }
        
    }

private:
    int height;     // Height of the window
    int width;      // Width of the window
    SDL_Renderer *renderer = NULL;      // Pointer for the renderer
    SDL_Window *window = NULL;      // Pointer for the window
};

int main(int argc, char * argv[]){

    // Creating the object by passing Height and Width value.
    Framework fw(200, 400);

    // Starting the animation
    fw.move_circle();
    
    return 1;
}

Now build the project and run the executable. Now you can see the red circle moving up and down the screen. As always, you can terminate the window by clicking the cross button. Did you noticed any change to the code?

You guessed it right! I have added a new member function called move_circle(). If you can see it carefully I have logically designed the for loop in such a way that it will go up and down continuously. Let me explain in detail about what happens in the for loop.

So the for loop starts with i=0 and increments by 1 every loop till max height of the screen. In the loop, first we are checking if the close or cross button has been hit. If it's been hit then exit the function and return to the main function. Now assuming it's not been hit yet, we set the renderer's color as black in the next line. Then after that we clear the screen with black color. Then we are calling the draw_circle method which draws the circle and calls SDL_RenderPresent() method which will reflect all changes that happen since last call.

Note here that I am calling SDL_RenderPresent() when I want to see the change. Also remember that not every change is work showing on the screen. For e.g. I am clearing the screen with black color but I am not showing it on the screen. What will happen if I do that? I think you capable to play with it by yourself to know the answer. Try it and let me know in the comments.

Some untouched topics in SDL2

  • You can play around with textures.
  • You can access audio to make it an interactive application.
  • A fun and real world application of SDL would be using it to make a arcade games.
  • Also, possibilities are endless.

Happy Coding...

Question

How would you change the equation of the circle to draw just the border?

Using both less than and greater than comparision
greater than comparison only
with just an equal to sign
None of the above
If you use equal sign only then you won't be able to see the border quite circular. That's why you have to use option A, because little threshold helps to visualize the border!