Lazy Foo' Productions

SDL Forums external link SDL Tutorials 🐰SDL3 Tutorials🥚 Articles OpenGL Tutorials OpenGL Forums external link
Follow BlueSky Follow Facebook Follow Twitter Follow Threads
Donate
News FAQs Contact Bugs

Scrolling

Scrolling screenshot

Last Updated: Nov 2nd, 2024

Up until now we've only be able to draw things as large as the screen. Here we're going to be drawing a level larger than the screen and scrolling through it with a camera.
To have a camera to scroll a level larger than the screen you need to have a rectangle to define the position and dimensions of the camera:
full level

And all you have to do is draw everything relative to the camera, making sure to clip out everything not seen by the camera:
screen
/* Constants */
//Screen dimension constants
constexpr int kScreenWidth{ 640 };
constexpr int kScreenHeight{ 480 };
constexpr int kScreenFps{ 60 };

//Level dimensions
constexpr int kLevelWidth{ 1280 };
constexpr int kLevelHeight{ 960 };
Since we're going to be moving around in a space larger than the screen, we need to define the level dimensions separately.
class Dot
{
    public:
        //The dimensions of the dot
        static constexpr int kDotWidth = 20;
        static constexpr int kDotHeight = 20;

        //Maximum axis velocity of the dot
        static constexpr int kDotVel = 10;

        //Initializes the variables
        Dot();

        //Takes key presses and adjusts the dot's velocity
        void handleEvent( SDL_Event& e );

        //Moves the dot
        void move();

        //Shows the dot on the screen
        void render( SDL_FRect camera );

        //Position accessors
        int getPosX();
        int getPosY();

    private:
        //The X and Y offsets of the dot
        int mPosX, mPosY;

        //The velocity of the dot
        int mVelX, mVelY;
};
Our dot class now takes a camera for rendering and has accessor functions to get the dot's position.
void Dot::move()
{
    //Move the dot left or right
    mPosX += mVelX;

    //If the dot went too far to the left or right
    if( ( mPosX < 0 ) || ( mPosX + kDotWidth > kLevelWidth ) )
    {
        //Move back
        mPosX -= mVelX;
    }

    //Move the dot up or down
    mPosY += mVelY;

    //If the dot went too far up or down
    if( ( mPosY < 0 ) || ( mPosY + kDotHeight > kLevelHeight ) )
    {
        //Move back
        mPosY -= mVelY;
    }
}

void Dot::render( SDL_FRect camera )
{
    //Show the dot
    gDotTexture.render( static_cast( mPosX ) - camera.x, static_cast( mPosY ) - camera.y );
}

int Dot::getPosX()
{
    return mPosX;
}

int Dot::getPosY()
{
    return mPosY;
}
Our changes to the dot are fairly simple. When moving, instead of bounding the motion within the screen, we check if the dot has moved outside of the level. When rendering, we render the dot relative to the position of the camera, which we do by subtracting the position of the dot by the position of the camera.

Lastly, we have some functions to get the dot position.
            //The quit flag
            bool quit{ false };

            //The event data
            SDL_Event e;
            SDL_zero( e );

            //Timer to cap frame rate
            LTimer capTimer;

            //Dot we will be moving around on the screen
            Dot dot;

            //Defines camera area
            SDL_FRect camera{ 0.f, 0.f, kScreenWidth, kScreenHeight };
Before going into our main loop, we declare an SDL_FRect to define the camera.
                //Update dot
                dot.move();

                //Center camera over dot
                camera.x = static_cast( dot.getPosX() + Dot::kDotWidth / 2 - kScreenWidth / 2 );
                camera.y = static_cast( dot.getPosY() + Dot::kDotHeight / 2 - kScreenHeight / 2 );

                //Bound the camera
                if( camera.x < 0 )
                {
                    camera.x = 0;
                }
                else if( camera.x + camera.w > kLevelWidth )
                {
                    camera.x = kLevelWidth - camera.w;
                }
                if( camera.y < 0 )
                {
                    camera.y = 0;
                }
                else if( camera.y + camera.h > kLevelHeight )
                {
                    camera.y = kLevelHeight - camera.h;
                }
When the dot moves, we want to center it over the dot. We also don't want the camera to show anything that's outside of the bounds of the level so after it moves we keep it inside the level.
                //Fill the background
                SDL_SetRenderDrawColor( gRenderer, 0xFF, 0xFF, 0xFF,  0xFF );
                SDL_RenderClear( gRenderer );

                //Show background
                gBgTexture.render( 0.f, 0.f, &camera );

                //Render dot
                dot.render( camera );

                //Update screen
                SDL_RenderPresent( gRenderer );
                
Lastly, we make sure to only render the part of the background shown by the camera and we show the dot relative to the camera.

Addendum: Typical camera functionality

There is some things most cameras do that we didn't do here for the sake of brevity but we'll cover them here.

In most game engines, we typically make sure objects are in the camera's field of view before rendering them. With 3D engines especially, this is an important optimization but it is definitely useful in 2D engines also. The way you do it is by checking if the object collides with the AABB of the 2D camera. As you would imagine, this does add some overhead which is worth it if you have a lot of objects but wouldn't help much if you're only moving a single dot around a level.

We only used an SDL_FRect for our camera but we probably should have created a separate camera class to keep the camera data and the code that moves the camera together. Camera behavior can get real complex with things like camera animation and the tracking logic can also warrant dedicated class. However, as always remember YAGNI.

Addendum: Decoupling rendering data from rendering

As mentioned in previous tutorials, it's considered good practice to separate the data of what needs to be rendered from the code that actually renders it if you want code that scales. This tutorial is a perfect example of why.

Right now we only have two objects that need to be rendered relative to the camera: the dot and screen. However, when you have dozens if not hundreds of objects that need to be rendered relative to the camera, having to repeatedly code the code to render objects relative to the camera for each type of object will quickly become tedious. And what happens if you have multiple cameras?

As always though, remember YAGNI. Scalability comes at a cost and if you're going to just throw your code out after the project is done for a Nasty Tetris Project, just do it the fastest way possible.