Scrolling
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:
And all you have to do is draw everything relative to the camera, making sure to clip out everything not seen by the camera:
And all you have to do is draw everything relative to the camera, making sure to clip out everything not seen by the camera:
/* 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.
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
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.
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.