DEV Community

loading...

Using SDL2: Moving an image

Noah11012
Updated on ・8 min read

Before we start this next part, we need to have a little talk about resource management in SDL2. As you know, SDL2 is a C library. This means it heavily relies on pointers and passing pointers around. Typically in C, when a function returns a pointer, it usually means there was something dynamically allocated and the pointer to it was returned back to you. There is no automatic clean up in C. Everything is manually done including resource management.

And what's your point?

What I'm trying to get at is we've been leaking memory all this time.

Gasp! What?! And you've never told this to us? How could you!

I know, I know. We didn't free the resources because the programs were short-lived and it would have cluttered the code. And besides, we weren't technically leaking memory since it was claimed back by the OS after our window closed.

Sorry about overreacting.

Apology accepted. Now with that over with, let's do some clean up first.

We can't put everything in the main() function so we'll have to break up the code into different sections. You can use functions, however, since I'm using C++ I'll be using classes.

I'll be creating a class called Application and, for now, will have two methods:

  1. update()
  2. draw()

The method update() will handle any events and draw to the back buffer.
draw() will simply call SDL_UpdateWindowSurface() to show the back buffer.

main.cpp:

#include <iostream>
#include "application.hpp"

int main()
{
    Application app;

    app.update();
    app.draw();
}
Enter fullscreen mode Exit fullscreen mode

Looking nice isn't, it? All the real work is now hidden behind the Application class leaving our main() function all tidied up.

Application.hpp:

#pragma once

#include <SDL2/SDL.h>
#include <iostream>

class Application
{
public:
    Application();
    ~Application();

    void update();
    void draw();
private:
    SDL_Window  *m_window;
    SDL_Surface *m_window_surface;
    SDL_Event    m_window_event;
};
Enter fullscreen mode Exit fullscreen mode

Application.cpp:

#include "application.hpp"

Application::Application()
{
    m_window = SDL_CreateWindow("SDL2 Window",
                                SDL_WINDOWPOS_CENTERED,
                                SDL_WINDOWPOS_CENTERED,
                                680, 480,
                                0);

    if(!m_window)
    {
        std::cout << "Failed to create window\n";
        std::cout << "SDL2 Error: " << SDL_GetError() << "\n";
        return;
    }

    m_window_surface = SDL_GetWindowSurface(m_window);

    if(!m_window_surface)
    {
        std::cout << "Failed to get window's surface\n";
        std::cout << "SDL2 Error: " << SDL_GetError() << "\n";
        return;
    }
}

Application::~Application()
{
    SDL_FreeSurface(m_window_surface);
    SDL_DestroyWindow(m_window);
}

void Application::update()
{
    bool keep_window_open = true;
    while(keep_window_open)
    {
        while(SDL_PollEvent(&m_window_event) > 0)
        {
            switch(m_window_event.type)
            {
                case SDL_QUIT:
                    keep_window_open = false;
                    break;
            }
        }

        draw();
    }
}

void Application::draw()
{
    SDL_UpdateWindowSurface(m_window);
}
Enter fullscreen mode Exit fullscreen mode

Pretty much the same stuff from last time except now it's separated into two methods. update() checks if any events need to be processed and if there is none/if they all have been processed, call draw().

What I want to bring to your attention is to the deconstructor of the class:

Application::~Application()
{
    SDL_FreeSurface(m_window_surface);
    SDL_DestroyWindow(m_window);
}
Enter fullscreen mode Exit fullscreen mode

This time we free the resources using SDL_FreeSurface() and SDL_DestroyWindow().

Hopefully, this design will help with maintainability.

SDL_Rect

Last time when we were blitting an image onto the window's surface we were using SDL_BlitSurface() and two of the four arguments that we can pass in were pointers to SDL_Rects. We'll be utilizing SDL_Rect to position and scale our image.

First, we'll need an image to test on. I created a small stick figure in my favorite image manipulation program, GIMP. As you will soon see, I am no artist by any stretch of the imagination.

I've decided to create a helper function to load BMP images called load_surface(). I put it into the Application.cpp file but ideally, you should make a file for your helper functions unless they are closely related to the class, which in this case, they aren't.

SDL_Surface *load_surface(char const *path)
{
    SDL_Surface *image_surface = SDL_LoadBMP(path);

    if(!image_surface)
        return 0;

    return image_surface;
}
Enter fullscreen mode Exit fullscreen mode

Next, we will add two other member variables to our class called m_image and m_image_position.

private:
    SDL_Surface *m_image;
    SDL_Rect     m_image_position;

    SDL_Window  *m_window;
    SDL_Surface *m_window_surface;
    SDL_Event    m_window_event;
Enter fullscreen mode Exit fullscreen mode

If you were making a proper application, you would most likely (if you are using C++) wrap the SDL_Surface and SDL_Rect together and put the new class somewhere more sensible like a Window class for holding the contents of a window. We're just making a program to show a particular feature in SDL2 so this doesn't matter as much.

Next, we load the image and display it on the screen like last time. The image I'm using is 22 x 43.

In the constructor we add the following line of code:

m_image = load_surface("stick_figure.bmp");
Enter fullscreen mode Exit fullscreen mode

We'll skip the error checking for simplicity sake.

And in the draw() method I added the line of code to copy the pixels onto the screen.

SDL_BlitSurface(m_image, NULL, m_window_surface, NULL);
Enter fullscreen mode Exit fullscreen mode

Compile and run.

Our stick figure is in the top left corner of the window. Now let's animate him going to the right.

Okay, so how do we accomplish that?

Remember about SDL_Rect? That's what we're going to be using to move the image.

An SDL_Rect is made up of four members:

  1. An x position
  2. A y position
  3. A width
  4. A height

Before we blit the image, we can create an SDL_Rect, change some of its member fields and then pass it to the SDL_BlitSurface() function.*

*Okay, so I wasn't being entirely honest with you again. SDL_BlitSurface() is actually a macro that expands out to SDL_UpperBlit(). Why did they do this? I'm not sure.

In the Application class we added a member called m_image_position. This is going to be the SDL_Rect that controls how the image is positioned and scaled.

In the constructor, we add the code to set the member variables of m_image_position to the appropriate values.

m_image_position.x = 0;
m_image_position.y = 0;
m_image_position.w = 22;
m_image_position.h = 43;
Enter fullscreen mode Exit fullscreen mode

Hold up! This is a bad design! Not all images are 22 x 43. There needs to be something more dynamic!

Again, I'm trying to keep the code as simple as possible without sacrificing too many good practices. For now, just substitute the dimensions of your image.

SDL_BlitSurface(m_image, NULL, m_window_surface, &m_image_position);
Enter fullscreen mode Exit fullscreen mode

The last argument in SDL_BlitSurface() takes a pointer to an SDL_Rect and its the argument that decides what region of the window the image will be copied to and how it will be scaled.

Compile and run.

Some as last time.

Can we please get to the animation?

Yes, now that everything is in place, we can now animate the image.

In the update() method, we'll increment the image's x position by one every frame.

m_image_position.x += 1;
Enter fullscreen mode Exit fullscreen mode

Compile and run.

Wow. That stick figure flew by.

For some of you, you may not even see the stick figure. This is because your computer is running at maximum effort to move the image across the window.

Okay, makes sense. How do we fix it?

Easy, we need to scale the image's increment by how much time has passed since the last frame.

So, how do we do that in SDL2?

There is no SDL_AmountOfTimeThatHasPassedSinceTheLastFrame()* function. We'll have to come up with something.

*Can you imagine typing that lengthy function name?

But before we can do anything, we have to do some refactoring. Doesn't everyone love that?

We'll introduce a new method called loop() and now update() will take a double.

In main.cpp:

Application app;

app.loop();
Enter fullscreen mode Exit fullscreen mode

In Application.hpp:

void loop();
void update(double delta_time);
Enter fullscreen mode Exit fullscreen mode

With this new method in our class, we can move the event processing loop into it and now update() has only one job.

For now, I capped the frame rate to 60 frames per second by passing in 1.0/60.0 or approximately 0.01667 to update().

void Application::loop()
{
    bool keep_window_open = true;
    while(keep_window_open)
    {
        while(SDL_PollEvent(&m_window_event) > 0)
        {
            switch(m_window_event.type)
            {
                case SDL_QUIT:
                    keep_window_open = false;
                    break;
            }
        }

        update(1.0/60.0);
        draw();
    }
}

void Application::update(double delta_time)
{
    m_image_position.x = m_image_position.x + (1 * delta_time);
}
Enter fullscreen mode Exit fullscreen mode

We have another problem.

What is it this time?

If we manually calculate how much the stick figure will move per frame and place the result in m_image_position.x we get the value of zero. Why? It has to do something with how C++ work with different types with the same operator. C++ only uses the same types when using an operator so of one the operands will have to give. This is called the promotion rules*. For example, if one operand is a double then other will be converted to a double.

*At least that's what I call it.

So, (1 * delta_time) is converted to (1.0 * delta_time).
After the subexpression is computed, the expression m_image_position.x + result_of_subexpression is calculated.

Because m_image_position.x is an int and result_of_subexpression is a double for reasons we saw earlier, the value in m_image_position.x will be promoted to a double.

Remember, I capped the frame rate to 60 fps by passing in 1.0/60.0 or approximately 0.01667 to update(). When we calculate the expression m_image_position.x + (1 * 0.01667) we get 0.01667. I'll walk you though the steps. If m_image_position.x starts at 0:

0 + (1 + 0.01667)
0 + (1.0 + 0.01667
0 + 0.01667
0.01667

Well, why isn't the stick figure moving about 0.01667 pixels per frame then?

Simple: when you have a variable of type int and you try to put something in it with a type of double the fractional part gets truncated and only the whole number is placed into the value.

When we put 0.01667 into m_image_position.x it gets converted to an int because m_image_position.x is an int.

Effectively, we're moving zero pixels per second.

Any ideas to fix this?

Yup. We add two new variables of type double to our class.

double       m_image_x;
double       m_image_y;
Enter fullscreen mode Exit fullscreen mode

Ugh, we'll have to refactor this soon. This is getting out of hand.

In the constructor, we initialize both to 0.0.

m_image_x = 0.0;
m_image_y = 0.0;
Enter fullscreen mode Exit fullscreen mode
void Application::update(double delta_time)
{
    m_image_x = m_image_x + (5 * delta_time);
    m_image_position.x = m_image_x;
}
Enter fullscreen mode Exit fullscreen mode

In this implementation, m_image_x keeps the fractional part of the calculation.

Compile and run.

Now there's some weirdness going on.

It's because we're not clearing the screen before we draw leaving previously drawn items on the screen.

The simple fix is to add this line of code in draw() before the blitting:

SDL_FillRect(m_window_surface, NULL, SDL_MapRGB(m_window_surface->format, 0, 0, 0));
Enter fullscreen mode Exit fullscreen mode

I know it's a lot of new stuff that we haven't talked about, but let's step through this slowly.

SDL_FillRect fills a rectangular region on a surface with any color we want. We can choose to fill a region of the surface if we want. The last argument is the color that we want the filled region to be. It's a Uint32 or simply an unsigned int that is 32 bits wide or 4 bytes. Fortunately, SDL2 provides a function called SDL_MapRGB() that takes four arguments.

  1. The format of a surface.
  2. The red value
  3. The green value
  4. The blue value

I understand the last three arguments, but what is the first?

In SDL_Surface there is a member variable which is a pointer to an SDL_PixelFormat. This contains information such as bit per pixel, masks for each component of a pixel, and has a member called format. It has the type of SDL_PixelFormatEnum and this is all the formats that SDL2 can work with. If you view the documentation, you will see a familiar acronym in some of the values like SDL_PIXELFORMAT_RGB332 and some that you probably are not familiar with like SDL_PIXELFORMAT_YVYU. When we supply the format to SDL_MapRGB(), we are letting it know what version of the format we want to use.

Compile and run.

Now our stick figure moves across the screen without any weirdness.

What's next?

In the next article, we'll gain control over the stick figure and move it however we like.

This part is probably the more information packed of the articles I've written so far. If you have any questions I'll be happy to answer them.

All code for this series can be found at my Github repository: https://github.com/Noah11012/sdl2-tutorial-code

Discussion (0)