
Implementing SAT Collision Detection in SDL3
Learn how to implement the Separating Axis Theorem (SAT) collision detection in SDL3. This tutorial covers the logic, code example, and integration in a 2D game environment.
- SDL3

The Separating Axis Theorem (SAT)
The Separating Axis Theorem (SAT) states:
Two convex shapes do not collide if there exists at least one axis onto which their projections do not overlap.
To check this:
- Generate axes (normals to edges) from both shapes.
- Project both shapes onto each axis.
- If projections don’t overlap on any axis → no collision.
- If projections overlap on all axes → collision!
The Vector struct
We need a struct to represent a 2D vector along with helper methods to compute the dot product and the perpendicular vector:
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 a point around a center point:
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 use the dot product to project the polygon vertices onto an axis and compute the scalar minimum and maximum range:
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 on an axis
We need a function that returns false if there is a gap (a separating axis is found, meaning 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:
- Gets rotated corners of both rectangles.
- Extracts 4 axes per shape from edges (8 total).
- For each axis, calls
overlap_on_axis(...). - If any axis fails, returns
false. - If all axes overlap, returns
true+ computes the Minimum Translation Vector (MTV) → collision!
Minimum Translation Vector (MTV)
When all projections on the SAT axes overlap, we know there’s a collision. But:
- For each axis, we can compute how much the projections overlap.
- The axis with the smallest overlap is the one we should move along to separate them.
- The MTV = this axis, scaled by the minimum overlap.
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) {
return false; // Separating axis found -> no collision
}
float overlap = std::min(maxA, maxB) - std::max(minA, minB);
if (overlap < minOverlap) {
minOverlap = overlap;
smallestAxis = axis;
}
}
outMTV = { smallestAxis.x * minOverlap, smallestAxis.y * minOverlap };
return true;
}Download the full project : Implementing SAT Collision Detection in SDL3
- For macOS:
SDL3_SATCollision_mac.7z - For Windows:
SDL3_SATCollision_win.7z - For Linux:
SDL3_SATCollision_linux.7z