Motion
Last Updated: Oct 19th, 2024
Now that we can have a consistent frame rate, we can start moving things around the screen.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(); private: //The X and Y offsets of the dot int mPosX, mPosY; //The velocity of the dot int mVelX, mVelY; };
Here is the class for the dot we will be moving around the screen. First we have some constants the size of the dot and it velocity. Then we have functions to construct the dot, handle input, move it, and render it. For variables, we have an x/y position and an x/y velocity.
//Dot Implementation Dot::Dot(): mPosX{ 0 }, mPosY{ 0 }, mVelX{ 0 }, mVelY{ 0 } { }
Our constructor does basic variable initialization.
void Dot::handleEvent( SDL_Event& e ) { //If a key was pressed if( e.type == SDL_EVENT_KEY_DOWN && e.key.repeat == 0 ) { //Adjust the velocity switch( e.key.key ) { case SDLK_UP: mVelY -= kDotVel; break; case SDLK_DOWN: mVelY += kDotVel; break; case SDLK_LEFT: mVelX -= kDotVel; break; case SDLK_RIGHT: mVelX += kDotVel; break; } } //If a key was released else if( e.type == SDL_EVENT_KEY_UP && e.key.repeat == 0 ) { //Adjust the velocity switch( e.key.key ) { case SDLK_UP: mVelY += kDotVel; break; case SDLK_DOWN: mVelY -= kDotVel; break; case SDLK_LEFT: mVelX += kDotVel; break; case SDLK_RIGHT: mVelX -= kDotVel; break; } } }
Our event handler adjusts the dots velocity when the user presses or releases keys. We check to make sure to only handle the first key press (when repeat equals 0) in case the user has key repeat on.
Now you may be wondering why we just aren't incrementing the position every time the user presses a key. If you were to move the x position every time the user pressed the key, you would have to repeatedly press the key to keep the dot moving. This is why we use velocity instead.
Velocity is the speed and direction of the movement. A positive x velocity moves right, a negative x velocity moves left, and a negative/positive y velocity moves up/down (remember that -y is up in SDL). So when we press right, the dot's velocity becomes positive 10, and it will move 10 pixels every frame until right key is released and the velocity returns to 0.
Now you may be wondering why we just aren't incrementing the position every time the user presses a key. If you were to move the x position every time the user pressed the key, you would have to repeatedly press the key to keep the dot moving. This is why we use velocity instead.
Velocity is the speed and direction of the movement. A positive x velocity moves right, a negative x velocity moves left, and a negative/positive y velocity moves up/down (remember that -y is up in SDL). So when we press right, the dot's velocity becomes positive 10, and it will move 10 pixels every frame until right key is released and the velocity returns to 0.
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 > kScreenWidth ) ) { //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 > kScreenHeight ) ) { //Move back mPosY -= mVelY; } }
When we move our dot, we apply the velocity to the position, then we check if the dot hit the edge of screen. If it did, we undo the motion. We do this for the x axis first, then the y axis.
This method of collision response is a bit janky, but since the dot only moves in increments of 10 and the screen is 640 by 480 (both multiples of 10), it will work.
This method of collision response is a bit janky, but since the dot only moves in increments of 10 and the screen is 640 by 480 (both multiples of 10), it will work.
void Dot::render() { //Show the dot gDotTexture.render( static_cast<float>( mPosX ), static_cast<float>( mPosY ) ); }
And in the rendering function we just draw the dot texture.
//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;
Before we go into the main loop, we declare the frame rate regulating timer and the dot.
//The main loop while( quit == false ) { //Start frame time capTimer.start(); //Get event data while( SDL_PollEvent( &e ) ) { //If event is quit type if( e.type == SDL_EVENT_QUIT ) { //End the main loop quit = true; } //Process dot events dot.handleEvent( e ); } //Update dot dot.move(); //Fill the background SDL_SetRenderDrawColor( gRenderer, 0xFF, 0xFF, 0xFF, 0xFF ); SDL_RenderClear( gRenderer ); //Render dot dot.render(); //Update screen SDL_RenderPresent( gRenderer ); //Cap frame rate constexpr Uint64 nsPerFrame = 1000000000 / kScreenFps; Uint64 frameNs = capTimer.getTicksNS(); if( frameNs < nsPerFrame ) { SDL_DelayNS( nsPerFrame - frameNs ); } }
Here we are inserting our dot into the main loop. We put the event handler in the event loop, the updater right before rendering, and the rendering function in the rendering portion of the main loop.
As you can also see, we're capping the frame rate to get a consistent frame rate which you need for frame based motion. Since we run at 60 FPS and the dot moves at 10 pixels per frame along each axis, it moves at 600 pixels per second along each axis.
And for the love of pointers, don't email me asking me to up the frame rate (yes, there are people that obsessed with frame rates). 60 FPS is fine quick demos like this.
As you can also see, we're capping the frame rate to get a consistent frame rate which you need for frame based motion. Since we run at 60 FPS and the dot moves at 10 pixels per frame along each axis, it moves at 600 pixels per second along each axis.
And for the love of pointers, don't email me asking me to up the frame rate (yes, there are people that obsessed with frame rates). 60 FPS is fine quick demos like this.
Addendum: Per frame movement vs time based movement
The vast majority of games made within the last few decades don't use frame based movement. Frame based movement was more common in the pre 3D era of games where resources were more limited like how Super Mario Bros 3 on the NES was around 385kb where SDL3.dll (which is
fairly thin wrapper library) is over 6 times that.
While time based movement is objectively better, it is slightly more complicated. You have to know a few physics fundamentals (like only a single semester's worth) to get it working right. For the sake of simplicity, we will be using frame based movement mostly. I also recommend using frame based movement for your Nasty Tetris Project as it will make things faster to complete and finishing as quickly as you can is one of the main goals of the project.
While time based movement is objectively better, it is slightly more complicated. You have to know a few physics fundamentals (like only a single semester's worth) to get it working right. For the sake of simplicity, we will be using frame based movement mostly. I also recommend using frame based movement for your Nasty Tetris Project as it will make things faster to complete and finishing as quickly as you can is one of the main goals of the project.