Glusoft

Implementing SAT Collision Detection in SDL3

SAT Collision Detection in SDL3

The Separating Axis Theorem (SAT)

The Separating Axis Theorem (SAT) says:

Two convex shapes do not collide if there exists at least one axis onto which their projections do not overlap.

To check this:

The Vector struct

We need to have struct to represent a vector and functions to compute the dot product and the normal:

struct Vector2 {
    float x, y;
    Vector2 operator-(const Vector2& o) const { return {x - o.x, y - o.y}; }
    float dot(const Vector2& o) const { return x * o.x + y * o.y; }
    Vector2 perpendicular() const { return {-y, x}; }
};

Handle OBB (oriented bounding boxes)

We need a function to rotate the point around a center:

Vector2 rotate_point(Vector2 point, Vector2 center, float angleRad) {
    float s = sin(angleRad), c = cos(angleRad);
    point.x -= center.x;
    point.y -= center.y;
    float xnew = point.x * c - point.y * s;
    float ynew = point.x * s + point.y * c;
    return { xnew + center.x, ynew + center.y };
}

Get and rotate the corners

std::vector<Vector2> get_corners(SDL_FRect rect, float angleRad) {
    Vector2 center = {rect.x + rect.w / 2, rect.y + rect.h / 2};
    std::vector<Vector2> corners = {
        {rect.x, rect.y},
        {rect.x + rect.w, rect.y},
        {rect.x + rect.w, rect.y + rect.h},
        {rect.x, rect.y + rect.h}
    };
    for (auto& corner : corners) {
        corner = rotate_point(corner, center, angleRad);
    }
    return corners;
}

Projection onto an axis

Here we will use the dot product to project the polygon onto an axis.

void project(const std::vector<Vector2>& corners, Vector2 axis, float& min, float& max) {
    min = max = corners[0].dot(axis);
    for (size_t i = 1; i < corners.size(); ++i) {
        float p = corners[i].dot(axis);
        if (p < min) min = p;
        if (p > max) max = p;
    }
}

Overlap an axis

We need a function that returns false if there is a gap → separating axis found → no collision.

bool overlap_on_axis(const std::vector<Vector2>& a, const std::vector<Vector2>& b, Vector2 axis) {
    float minA, maxA, minB, maxB;
    project(a, axis, minA, maxA);
    project(b, axis, minB, maxB);
    return !(maxA < minB || maxB < minA);
}

SAT Collision detection function

Here is a recap of the SAT collision algorithm:

Minimum Translation Vector (MTV)

When all projections on the SAT axes overlap, we know there’s a collision. But:

bool SAT_collision(SDL_FRect aRect, float aAngle, SDL_FRect bRect, float bAngle, Vector2& outMTV) {
    auto aCorners = get_corners(aRect, aAngle);
    auto bCorners = get_corners(bRect, bAngle);

    std::vector<Vector2> axes;
    axes.reserve(8);
    
    for (int i = 0; i < 4; ++i) {
        Vector2 edge = aCorners[(i + 1) % 4] - aCorners[i];
        axes.emplace_back(edge.perpendicular());
    }
    for (int i = 0; i < 4; ++i) {
        Vector2 edge = bCorners[(i + 1) % 4] - bCorners[i];
        axes.emplace_back(edge.perpendicular());
    }

    float minOverlap = INFINITY;
    Vector2 smallestAxis = {0, 0};

    for (auto axis : axes) {
        // Normalize axis to get accurate MTV magnitude
        float length = sqrt(axis.x * axis.x + axis.y * axis.y);
        if (length == 0.0f) continue; // skip invalid axis
        axis = {axis.x / length, axis.y / length};

        float minA, maxA, minB, maxB;
        project(aCorners, axis, minA, maxA);
        project(bCorners, axis, minB, maxB);

        if (maxA < minB || maxB < minA) {
            // No overlap on this axis → no collision
            return false;
        }

        float overlap = fmin(maxA, maxB) - fmax(minA, minB);
        if (overlap < minOverlap) {
            minOverlap = overlap;
            smallestAxis = axis;

            // Flip MTV direction to push shape A out of B
            Vector2 d = {aRect.x + aRect.w/2 - (bRect.x + bRect.w/2),
                         aRect.y + aRect.h/2 - (bRect.y + bRect.h/2)};
            if (d.dot(smallestAxis) < 0)
                smallestAxis = {-smallestAxis.x, -smallestAxis.y};
        }
    }

    outMTV = {smallestAxis.x * minOverlap, smallestAxis.y * minOverlap};
    return true;
}

The main function

Initializations

The window and the renderer

SDL_Init(SDL_INIT_VIDEO);
SDL_Window* win = SDL_CreateWindow("SAT Collision - SDL3", 800, 600, SDL_WINDOW_RESIZABLE);
SDL_Renderer* renderer = SDL_CreateRenderer(win, nullptr);

The square A and B

SDL_FRect rectA = {200, 200, 100, 100};
SDL_FRect rectB = {400, 300, 100, 100};
float angleA = 0.0f;
float angleB = 0.0f;

The last time

Uint64 lastTime = SDL_GetTicks();

The main loop

The time elapsed: delta time

Uint64 currentTime = SDL_GetTicks();
float delta = (currentTime - lastTime) / 1000.0f;
lastTime = currentTime;

The event loop

In the event loop only the quit event the player movement is handled after.

while (SDL_PollEvent(&e)) {
    if (e.type == SDL_EVENT_QUIT)
        running = false;
}

The player movement and rotation

const bool* keys = SDL_GetKeyboardState(nullptr);
if (keys[SDL_SCANCODE_LEFT])  rectA.x -= 200 * delta;
if (keys[SDL_SCANCODE_RIGHT]) rectA.x += 200 * delta;
if (keys[SDL_SCANCODE_UP])    rectA.y -= 200 * delta;
if (keys[SDL_SCANCODE_DOWN])  rectA.y += 200 * delta;
if (keys[SDL_SCANCODE_Q])     angleA -= 1.5f * delta;
if (keys[SDL_SCANCODE_E])     angleA += 1.5f * delta;

The collision detection

Vector2 mtv;
bool collides = SAT_collision(rectA, angleA, rectB, angleB, mtv);

The rendering

Clear the screen
SDL_SetRenderDrawColor(renderer, 20, 20, 20, 255);
SDL_RenderClear(renderer);
Draw the MTV
if (collides) {
    printf("Collision! MTV = (%f, %f)\n", mtv.x, mtv.y);
    draw_mtv(renderer, rectA, mtv, {255, 255, 0});
}
Draw the rotated square

For that we use two functions raw_rotated_rect and draw_mtv:

void draw_rotated_rect(SDL_Renderer* renderer, SDL_FRect rect, float angleRad, SDL_Color color) {
    auto corners = get_corners(rect, angleRad);
    SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, 255);
    for (int i = 0; i < 4; ++i) {
        SDL_RenderLine(renderer,
            corners[i].x, corners[i].y,
            corners[(i + 1) % 4].x, corners[(i + 1) % 4].y);
    }
}

void draw_mtv(SDL_Renderer* renderer, SDL_FRect rect, Vector2 mtv, SDL_Color color) {
    Vector2 center = {rect.x + rect.w / 2, rect.y + rect.h / 2};
    Vector2 end = {center.x + mtv.x, center.y + mtv.y};

    SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, 255);
    SDL_RenderLine(renderer, center.x, center.y, end.x, end.y);
}

And then we can call the functions in the loop:

draw_rotated_rect(renderer, rectB, angleB, {100, 100, 255});
draw_rotated_rect(renderer, rectA, angleA, collides ? SDL_Color{255, 50, 50} : SDL_Color{50, 255, 50});

Download the full project : Implementing SAT Collision Detection in SDL3

Need another OS ? => Windows, Mac, Linux