
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:
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}; }
};
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 };
}
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;
}
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;
}
}
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);
}
Here is a recap of the SAT collision algorithm:
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;
}
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);
SDL_FRect rectA = {200, 200, 100, 100};
SDL_FRect rectB = {400, 300, 100, 100};
float angleA = 0.0f;
float angleB = 0.0f;
Uint64 lastTime = SDL_GetTicks();
Uint64 currentTime = SDL_GetTicks();
float delta = (currentTime - lastTime) / 1000.0f;
lastTime = currentTime;
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;
}
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;
Vector2 mtv;
bool collides = SAT_collision(rectA, angleA, rectB, angleB, mtv);
SDL_SetRenderDrawColor(renderer, 20, 20, 20, 255);
SDL_RenderClear(renderer);
if (collides) {
printf("Collision! MTV = (%f, %f)\n", mtv.x, mtv.y);
draw_mtv(renderer, rectA, mtv, {255, 255, 0});
}
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});
Need another OS ? => Windows, Mac, Linux