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

Collision Detection

Collision Detection screenshot

Last Updated: Oct 20th, 2024

If you want to tell if two objects are touching, using collision boxes is a common way to do so. Here we'll be checking collision between two axis aligned bounding boxes.
Checking collisions between two convex polygons (and boxes are convex polygons) involves checking how they are separated against the axes of their sides.

Here we have two boxes that are not collided. As you can see, their x projections are on the bottom and their y projections are on the left:
x/y separation


Here you see the boxes have collided along the y axis but they are separated on the x axis:
x separation


Here the boxes are collided on the x axis but they are separated on the y axis:
y separation


When there is no separation on any of the axes there is a collision:
collision detected


This is called the separating axis test where we try to find an axis where the polygons are separated. If there is no separating axis, then the objects are colliding.

In this tutorial we're doing the most basic type of separating axis test: between to axis aligned bounding boxes (knowns a AABBs). Bounding boxes are boxes that encapsulate an object and they are axis aligned because their side go along the x and y axis (as opposed to oriented bounding boxes which are rotated).
class Square
{
    public:
        //The dimensions of the square
        static constexpr int kSquareWidth = 20;
        static constexpr int kSquareHeight = 20;

        //Maximum axis velocity of the square
        static constexpr int kSquareVel = 10;

        //Initializes the variables
        Square();

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

        //Moves the square
        void move( SDL_Rect collider );

        //Shows the square on the screen
        void render();

    private:
        //The collision box
        SDL_Rect mCollisionBox;

        //The velocity of the square
        int mVelX, mVelY;
};
We have our Square class that functions a lot like our Dot class only its movement function takes in a collision box to check collision against when moving and it has an SDL_Rect collision box that also holds its position.
/* Function Prototypes */
//Starts up SDL and creates window
bool init();

//Loads media
bool loadMedia();

//Frees media and shuts down SDL
void close();

//Check collision between two AABBs
bool checkCollision( SDL_Rect a, SDL_Rect b );
We have a new function we're using to check collisions between two collision boxes.

SDL3 does have functions that can help with collision detection, but you should know how to do it manually as it is a very common interview question.
//Square Implementation
Square::Square():
    mCollisionBox{ 0, 0, kSquareWidth, kSquareHeight },
    mVelX{ 0 },
    mVelY{ 0 }
{

}

void Square::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 -= kSquareVel; break;
            case SDLK_DOWN: mVelY += kSquareVel; break;
            case SDLK_LEFT: mVelX -= kSquareVel; break;
            case SDLK_RIGHT: mVelX += kSquareVel; 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 += kSquareVel; break;
            case SDLK_DOWN: mVelY -= kSquareVel; break;
            case SDLK_LEFT: mVelX += kSquareVel; break;
            case SDLK_RIGHT: mVelX -= kSquareVel; break;
        }
    }
}
As you can see the constructor and event handling is mostly unchanged from the Dot to the Square.
void Square::move( SDL_Rect collider )
{
    //Move the square left or right
    mCollisionBox.x += mVelX;

    //If the square went off screen or hit the wall
    if( ( mCollisionBox.x < 0 ) || ( mCollisionBox.x + kSquareWidth > kScreenWidth ) || checkCollision( mCollisionBox, collider ) )
    {
        //Move back
        mCollisionBox.x -= mVelX;
    }

    //Move the square up or down
    mCollisionBox.y += mVelY;

    //If the square went off screen or hit the wall
    if( ( mCollisionBox.y < 0 ) || ( mCollisionBox.y + kSquareHeight > kScreenHeight ) || checkCollision( mCollisionBox, collider ) )
    {
        //Move back
        mCollisionBox.y -= mVelY;
    }
}
When we move, we check if the box went out of bounds and we also check if it collided with the wall. If it does, we undo the motion.
void Square::render()
{
    //Show the square
    SDL_FRect drawingRect{ static_cast<float>( mCollisionBox.x ), static_cast<float>( mCollisionBox.y ), static_cast<float>( mCollisionBox.w ), static_cast<float>( mCollisionBox.h ) };
    SDL_SetRenderDrawColor( gRenderer, 0x00, 0x00, 0x00, 0xFF );   
    SDL_RenderRect( gRenderer, &drawingRect );
}
To draw our square, we call SDL_RenderRect. If you're wondering we convert the SDL_Rect to an SDL_FRect for rendering as opposed to just using an SDL_FRect for everything, the answer is floating point errors. float datatypes are not precise, and if you were to add 0.1f ten times, checked if it was equal 1.f, you would get false. To not deal with that (because dealing with floating point errors is a another can of worms), we're using an integer rectangle for motion.
bool checkCollision( SDL_Rect a, SDL_Rect b )
{
    //Calculate the sides of rect A
    int aMinX = a.x;
    int aMaxX = a.x + a.w;
    int aMinY = a.y;
    int aMaxY = a.y + a.h;

    //Calculate the sides of rect B
    int bMinX = b.x;
    int bMaxX = b.x + b.w;
    int bMinY = b.y;
    int bMaxY = b.y + b.h;
To do the separating axis test we first have to find the minimum x value (the left side), the maximum x value (the right side), the minimum y value (the top side because remember the y axis is inverted), and the maximum y value (the bottom side) for both of the collision boxes.
    //If left side of A is the the right of B
    if( aMinX >= bMaxX )
    {
        return false;
    }

    //If the right side of A to the left of B
    if( aMaxX <= bMinX )
    {
        return false;
    }
}
Here we're checking for separation along the x axis. If the left side of A is to the right of B or the right side of A is to the left of B it means they're are separated along the x axis.
    //If the top side of A is below B
    if( aMinY >= bMaxY )
    {
        return false;
    }

    //If the bottom side of A is above B
    if( aMaxY <= bMinY )
    {
        return false;
    }

    //If none of the sides from A are outside B
    return true;
}
Now we're checking for separation along the y axis. If the top side of A is below B or the bottom side of A is above B it means they're are separated along the y axis.

If there is no separation along either the x or y axis, we return true because they must be colliding.
            //The quit flag
            bool quit{ false };

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

            //Timer to cap frame rate
            LTimer capTimer;

            //Square we will be moving around on the screen
            Square square;

            //The wall we will be colliding with
            constexpr int kWallWidth = Square::kSquareWidth;
            constexpr int kWallHeight = kScreenHeight - Square::kSquareHeight * 2;
            SDL_Rect wall{ ( kScreenWidth - kWallWidth ) / 2, ( kScreenHeight - kWallHeight ) / 2, kWallWidth, kWallHeight };
            
Before going into the main loop, we declare our square and place the wall we'll be colliding against.
            //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 square events
                    square.handleEvent( e );
                }
                
                //Update square
                square.move( wall );

                //Fill the background
                SDL_SetRenderDrawColor( gRenderer, 0xFF, 0xFF, 0xFF,  0xFF );
                SDL_RenderClear( gRenderer );

                //Render wall
                SDL_FRect drawingRect{ static_cast( wall.x ), static_cast( wall.y ),  static_cast( wall.w ),  static_cast( wall.h ) };
                SDL_SetRenderDrawColor( gRenderer, 0x00, 0x00, 0x00, 0xFF );   
                SDL_RenderRect( gRenderer, &drawingRect );
                
                //Render square
                square.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 );
                }
            } 
As you can see, we put the event handler in the event loop, update the square's motion before beginning rendering, and then clear the screen, draw the wall, and then draw the square.
You maybe asking "well what if I want to check collisions with objects that aren't square". There are way to check collisions with shapes that aren't rectanges (some of which we'll be covering in future tutorials), and there are volumes written on the subject of checking collisions with different shapes, but you can get pretty far with just AABBs.

Collision volumes do not have to perfectly match the objects they're representing. The recently released Marvel vs. Capcom Fighting Collection has the option to view collision boxes:
It's Marvel Baby


As you can see, the collision volumes just have to be close enough. Besides, the more complex the collision geometry, the more computationally expensive it is to handle it. You have to ask yourself if the additional accuracy is worth the cost.

Oh, and it wouldn't be a bad idea to take a look at the game loops article because as your game loops start becoming more complex, knowing how to order your logic properly is important.

Addendum: Generic polygon separating axis test

You maybe be asking "What if I want to rotate my boxes?". The fact is, the separating axis test works mostly the same for rotated boxes (known as oriented bounding boxes or OBBs).

Instead of finding the min/max x/y values for each of the boxes, you find the min/max i/j values for each of the boxes. You will have a set of min/max i/j values for the A box and min/max i/j values for the B box. You figure out these values by projecting the center point of each side onto the i/j axis for both box A and B. Then you check of the boxes are separating on the Ai, Aj, Bi, or Bj axis. If there is no separation, the boxes are collided. For any convex polygon, it's basically the same story. Each side of each polygon is an axis the separating axis needs to be checked against.

If all of this is going over your head, it's because this all involves vector math which they will teach you in your first semester physics class or multivariable calculus class. As mentioned before, this is beyond the scope of this tutorial set.

Addendum: This code is badly structured and why you need to learn to how to write bad code

I have to say that this code is noticably bad. Let's go over what's wrong with it.

1) The class structure. If this was a real game engine, it would be structured something like this:
class GameObject
{
public:
/*yada yada*/
private:
Transform mTransform; //Holds position, rotation, scale
RigidBody mRigidBody; //Hold mass, velocity, etc
Collider mCollider; //Holds the collision box
Graphic mGraphic; //Holds the info for the square we'll be rendering
};
It's a good idea to decouple physics/collision detection/rendering code because getting them tangled can quickly cause problems.

2) How the collisions are handled. Here we're checking collision and then immediately responding. In a real engine, we would check all the collisions to get all contact data. A contact has information like which two objects are colliding, how much they are penetrating, what direction the collision is in, etc. Only after all the collisions have been detected do you want to start processing them.

3) How the physics are handled. Yes, there is motion so there is technically some form of physics here. Physics/collision logic is usually handled something like this:
applyAllForcesToObjects(); //Gravity, wind, propulsion, etc
moveAllObjects(); //Apply acceleration/velocity to position
checkForCollisions(); //Generate all contacts/collisions
processCollisions(); //Resolves all contacts/collisions
The main take away is that you generally want to separate motion from collision detection and collision detection from collision processing. The thing is in order to get this working properly, you have to know physics and vector math which is beyond the scope of this tutorial.

However, if you're making a Nasty Tetris Project, this motion/collision detection/collision processing in a single step is just fine. The only object interaction you're really going to have is the player controlled piece against the resting pieces. Just putting all this logic together will let you finish the project sooner and finishing the project quickly is one of your main goals of the project. The fact is as a professional game programmer you will be put in a situations where you can do things right or you can do things quickly.

Let me let you in a big secret: professional game code (yes, even games that won GOTY) is often terrible and filled with issues that would make a first semester computer science student gag. Good code takes time to make and the stories you heard about crunch are true. YAGNI is a very important principle in game development and you will need make decisions on having to trade code quality for finishing things faster. A lot of the time, you just need to get things working as fast as possible.

A lot of the time doing things "properly" involves making the code more flexible but it is possible that you will not need the flexibility. Learning when to do things properly and when to do them quickly is a skill you will need to master. So, yes, to become a professional game programmer you will not only need to know how to make good code you will also need to learn when to make bad code.