Overview
This lab builds a small SFML project that answers a precise question a cheap overlap check cannot: given two candidate shapes, are they actually touching, and if so, by how much and in which direction? Working from an OBB class that spins and drifts around a window, you will build a broad-phase pre-check that flags candidate pairs from their bounding boxes, then a narrow-phase SAT test using the Separating Axis Theorem to confirm or reject each candidate against the shapes' true, rotated corners. By the end, a live window shows a user-controlled box alongside several auto-rotating ones, drawing a thin yellow line between any pair whose bounding boxes overlap but whose true shapes do not touch, a thick red line and a push-direction arrow between any pair SAT confirms are genuinely colliding, and on-screen counts of candidates, confirmed collisions and false positives, updating live as boxes spin and drift into and out of each other.
Learning Objectives
By the end of this lab you will be able to:
- Explain why a rotating box's tight axis-aligned bounding box is not a fixed size, and why that makes an AABB overlap test alone unreliable for rotated shapes
- Implement the Separating Axis Theorem's projection test, checking a pair of oriented boxes against each box's own face-normal axes and exiting early the moment one axis proves separation
- Compute the Minimum Translation Vector for a confirmed collision, the axis and distance that would push the two shapes apart by the smallest amount
- Distinguish a cheap broad-phase candidate check from a precise narrow-phase confirmation, and observe on screen how often the cheap check alone gets the answer wrong
Concept
A bounding box built from a shape's rotated corners is only ever an approximation. Two boxes might have overlapping bounding boxes while their true, rotated outlines do not touch at all, a broad-phase false positive. The bigger the rotation, the worse this gets. At 0 or 90 degrees a box's axis-aligned bounding box (AABB) matches its true outline exactly, but as it rotates toward 45 degrees the AABB has to grow outward to still contain every corner, swelling toward the box's full half-diagonal before shrinking back down as the rotation continues past 45. A cheap AABB overlap test alone cannot tell a genuine collision from two boxes that merely have swollen, overlapping bounds.
The Separating Axis Theorem (SAT) resolves this precisely for convex shapes. Two convex shapes are not overlapping if and only if there exists at least one axis onto which their projections, their shadows cast along that axis, do not overlap. That axis is called a separating axis. For two boxes, only each box's own face-normal directions need testing, two per box, four in total for a pair, since a rectangle has only two unique edge directions. The test projects both boxes' corners onto each candidate axis in turn using a dot product, and compares the resulting min/max ranges. The moment any single axis shows a gap between the two projected ranges, that axis alone proves the boxes cannot be touching, and the test can stop immediately without checking the rest. Only if every one of the four axes shows overlapping projections are the boxes provably colliding.
When a pair is confirmed colliding, the axis with the smallest overlap is the Minimum Translation Vector (MTV), the cheapest single direction and distance that would separate the two shapes. This lab represents an SAT result as a small Result struct holding a bool colliding, a unit-length normal pointing from one box's centre toward the other's, and a penetration distance along that normal. The projection step itself is a dot product between a corner and an axis, tracking a running min and max across all four corners of one box for a given axis.
Tasks
Task 1 - A Box That Spins, Drifts and Bounces
Create a new Console App (C++) project in Visual Studio the same way as in the Environment Setup lab, naming it NarrowPhase. Use Add > New Item to add a new header file named OBB.h, and write the class shown below. An OBB (oriented bounding box) is a rotated rectangle stored as a centre, half-extents, and a rotation angle in radians, together with an angular and linear velocity so it can spin and drift on its own. update advances the rotation and position for one frame and bounces off the window's edges using the box's half-diagonal as a simple circular bound, so the box never visibly pokes through a wall no matter its rotation. getCorners returns the four true, rotated corners in world space, and draw renders exactly that same corner data as an outlined polygon, so the shape on screen always matches the shape any later test will examine.
Success condition: the project compiles with OBB fully defined in OBB.h. There is no window yet, that is added in the next task.
#pragma once
#include <SFML/Graphics.hpp>
#include <algorithm>
#include <array>
#include <cmath>
class OBB {
public:
OBB(sf::Vector2f centerPos, sf::Vector2f halfExtents, float rotationRadians,
float angularVelocityRadPerSec, sf::Vector2f linearVelocity, sf::Color colour)
: center(centerPos), halfExtents(halfExtents), rotation(rotationRadians),
angularVelocity(angularVelocityRadPerSec), velocity(linearVelocity), colour(colour) {}
void update(float deltaTime, float worldWidth, float worldHeight) {
rotation += angularVelocity * deltaTime;
center += velocity * deltaTime;
float boundRadius = boundingRadius();
if (center.x - boundRadius < 0.0f) {
center.x = boundRadius;
velocity.x = -velocity.x;
} else if (center.x + boundRadius > worldWidth) {
center.x = worldWidth - boundRadius;
velocity.x = -velocity.x;
}
if (center.y - boundRadius < 0.0f) {
center.y = boundRadius;
velocity.y = -velocity.y;
} else if (center.y + boundRadius > worldHeight) {
center.y = worldHeight - boundRadius;
velocity.y = -velocity.y;
}
}
std::array<sf::Vector2f, 4> getCorners() const {
float cosR = std::cos(rotation);
float sinR = std::sin(rotation);
std::array<sf::Vector2f, 4> localSigns = {
sf::Vector2f(-halfExtents.x, -halfExtents.y),
sf::Vector2f(halfExtents.x, -halfExtents.y),
sf::Vector2f(halfExtents.x, halfExtents.y),
sf::Vector2f(-halfExtents.x, halfExtents.y)
};
std::array<sf::Vector2f, 4> corners;
for (std::size_t i = 0; i < 4; ++i) {
float localX = localSigns[i].x;
float localY = localSigns[i].y;
corners[i] = sf::Vector2f(
center.x + localX * cosR - localY * sinR,
center.y + localX * sinR + localY * cosR);
}
return corners;
}
std::array<sf::Vector2f, 2> getAxes() const {
float cosR = std::cos(rotation);
float sinR = std::sin(rotation);
return { sf::Vector2f(cosR, sinR), sf::Vector2f(-sinR, cosR) };
}
sf::FloatRect getAABB() const {
auto corners = getCorners();
float minX = corners[0].x, maxX = corners[0].x;
float minY = corners[0].y, maxY = corners[0].y;
for (std::size_t i = 1; i < 4; ++i) {
minX = std::min(minX, corners[i].x);
maxX = std::max(maxX, corners[i].x);
minY = std::min(minY, corners[i].y);
maxY = std::max(maxY, corners[i].y);
}
return sf::FloatRect(sf::Vector2f(minX, minY), sf::Vector2f(maxX - minX, maxY - minY));
}
float boundingRadius() const {
return std::sqrt(halfExtents.x * halfExtents.x + halfExtents.y * halfExtents.y);
}
void draw(sf::RenderWindow& window, sf::Color outlineColour, float outlineThickness = 2.0f) const {
auto corners = getCorners();
sf::ConvexShape shape(4);
for (std::size_t i = 0; i < 4; ++i) {
shape.setPoint(i, corners[i]);
}
shape.setFillColor(sf::Color::Transparent);
shape.setOutlineColor(outlineColour);
shape.setOutlineThickness(outlineThickness);
window.draw(shape);
}
void drawAABB(sf::RenderWindow& window, sf::Color colour) const {
sf::FloatRect bounds = getAABB();
sf::RectangleShape box(bounds.size);
box.setPosition(bounds.position);
box.setFillColor(sf::Color::Transparent);
box.setOutlineColor(colour);
box.setOutlineThickness(1.0f);
window.draw(box);
}
sf::Vector2f center;
sf::Vector2f halfExtents;
float rotation;
float angularVelocity;
sf::Vector2f velocity;
sf::Color colour;
};
Task 2 - A Window With One Box You Control
Use Add > New Item to add a new C++ source file named NarrowPhaseDemo.cpp, and write the main function shown below. It opens a window and creates a single green OBB in the centre, moved with the arrow keys or WASD and rotated with Q and E, using continuous key state rather than key-press events so holding a key moves or rotates it smoothly frame to frame.
Success condition: a window opens showing one green box in the centre. Arrow keys or WASD move it, Q and E rotate it, and it stays fully on screen.
#include <SFML/Graphics.hpp>
#include <algorithm>
#include <optional>
#include "OBB.h"
namespace {
constexpr unsigned int WORLD_WIDTH = 900;
constexpr unsigned int WORLD_HEIGHT = 800;
constexpr float MOVE_SPEED = 220.0f;
constexpr float ROTATE_SPEED = 1.6f;
}
int main() {
sf::VideoMode videoMode(sf::Vector2u(WORLD_WIDTH, WORLD_HEIGHT));
sf::RenderWindow window(videoMode, "Narrow Phase - SAT on Rotating Boxes");
window.setFramerateLimit(60);
OBB userBox(
sf::Vector2f(WORLD_WIDTH * 0.5f, WORLD_HEIGHT * 0.5f),
sf::Vector2f(35.0f, 35.0f),
0.0f, 0.0f,
sf::Vector2f(0.0f, 0.0f),
sf::Color(80, 220, 140));
sf::Clock clock;
while (window.isOpen()) {
float deltaTime = clock.restart().asSeconds();
while (const std::optional event = window.pollEvent()) {
if (event->is<sf::Event::Closed>()) {
window.close();
}
}
sf::Vector2f move(0.0f, 0.0f);
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::Left) || sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::A)) {
move.x -= 1.0f;
}
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::Right) || sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::D)) {
move.x += 1.0f;
}
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::Up) || sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::W)) {
move.y -= 1.0f;
}
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::Down) || sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::S)) {
move.y += 1.0f;
}
userBox.center += move * MOVE_SPEED * deltaTime;
userBox.center.x = std::clamp(userBox.center.x, 0.0f, static_cast<float>(WORLD_WIDTH));
userBox.center.y = std::clamp(userBox.center.y, 0.0f, static_cast<float>(WORLD_HEIGHT));
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::Q)) {
userBox.rotation -= ROTATE_SPEED * deltaTime;
}
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::E)) {
userBox.rotation += ROTATE_SPEED * deltaTime;
}
window.clear(sf::Color(12, 12, 18));
userBox.draw(window, userBox.colour, 3.0f);
window.display();
}
return 0;
}
Task 3 - Filling the Scene With Auto-Rotating Boxes
Open NarrowPhaseDemo.cpp. Add a helper that builds a randomly placed, randomly spinning box, and use it to fill the scene with several boxes alongside the user's green one.
- In the anonymous namespace, add a
std::mt19937 rng(std::random_device{}())and amakeRandomBox(float worldWidth, float worldHeight)function returning anOBBwith a random position, a half-extent between20.0fand45.0f, a random starting rotation, a random angular velocity between0.25fand1.6fradians per second (positive or negative), a random colour, and a linear velocity that is(0, 0)for roughly half the boxes and a small random drift for the rest. - In
main, declare astd::vector<OBB> boxes, pushuserBoxonto it first so it stays index0, then push seven more boxes built frommakeRandomBox. - Replace the single
userBox.center += move * ...block so it only ever touchesboxes[0], and add a loop callingupdateon every box from index1onward. - Replace the single
userBox.draw(...)call with a loop drawing every box inboxes, each withbox.draw(window, box.colour, 2.0f).
Success condition: the window shows the green user box plus several other boxes spinning and drifting independently, all bouncing off the window's edges.
namespace {
constexpr unsigned int WORLD_WIDTH = 900;
constexpr unsigned int WORLD_HEIGHT = 800;
constexpr std::size_t BOX_COUNT = 8; // includes the user-controlled box
constexpr float MOVE_SPEED = 220.0f;
constexpr float ROTATE_SPEED = 1.6f;
// TODO: declare std::mt19937 rng(std::random_device{}())
// TODO: sf::Color randomColour() using a std::uniform_int_distribution<int> c(100, 255)
// TODO: OBB makeRandomBox(float worldWidth, float worldHeight), returning an
// OBB with a random position (60 px margin from the edges), a random
// half-extent between 20.0f and 45.0f, a random rotation, a random
// angular velocity between 0.25f and 1.6f rad/s (flip its sign with a
// std::bernoulli_distribution), a random colour, and velocity (0, 0)
// for roughly half the boxes and a small random drift for the rest
}
// In main(): declare std::vector<OBB> boxes, push userBox first, then push
// seven more from makeRandomBox. Update boxes[1] onward each frame and draw
// every box in the vector.
Task 4 - Watching the Bounding Box Swell and Shrink
Open NarrowPhaseDemo.cpp. Add a toggle so every box's tight bounding box can be shown alongside its true rotated outline, making the problem this lab exists to solve visible directly.
- Declare
bool showAABB = true;before the main loop. - Inside the event-polling loop, check for an
sf::Event::KeyPressedevent withevent->getIf<sf::Event::KeyPressed>(), and flipshowAABBwhenkeyPressed->scancodeissf::Keyboard::Scancode::G. - Just before drawing the boxes themselves, if
showAABBis true, loop over every box and callbox.drawAABB(window, sf::Color(90, 90, 110, 130)).
Success condition: pressing G toggles a translucent rectangle around every box showing its tight axis-aligned bounds. Watch one spinning box closely: its bounding box visibly grows as the box rotates toward 45 degrees and shrinks back down as it continues rotating toward 90.
// Before the main loop:
bool showAABB = true;
// Inside the event-polling loop, alongside the existing sf::Event::Closed check:
if (const auto* keyPressed = event->getIf<sf::Event::KeyPressed>()) {
switch (keyPressed->scancode) {
// TODO: on sf::Keyboard::Scancode::G, flip showAABB
default:
break;
}
}
// Just before the loop that draws every box:
// TODO: if showAABB, loop over every box and call
// box.drawAABB(window, sf::Color(90, 90, 110, 130))
Task 5 - Declaring the SAT Module
In Solution Explorer, use Add > New Item to add a new header file named SAT.h, and write the declarations shown below. A Result struct carries everything a caller needs back from one test: whether the pair collides, the unit-length push direction, and how far apart they overlap along that direction. The two free functions inside the SAT namespace are declared here but not yet defined, that happens over the next two tasks.
Success condition: the project still compiles, nothing calls these functions yet.
#pragma once
#include <SFML/Graphics.hpp>
#include "OBB.h"
namespace SAT {
struct Result {
bool colliding = false;
sf::Vector2f normal{ 0.f, 0.f }; // unit MTV direction, points from A's centre toward B's
float penetration = 0.f; // MTV magnitude along normal; 0 if not colliding
};
// Precise narrow-phase test between two oriented boxes.
Result test(const OBB& a, const OBB& b);
// Cheap broad-phase pre-check: tight AABB vs tight AABB.
bool aabbOverlap(const OBB& a, const OBB& b);
}
Task 6 - Implementing the Broad-Phase Pre-Check
Use Add > New Item to add a new C++ source file named SAT.cpp, and implement aabbOverlap shown below. This is a cheap first pass, exactly the tight AABB drawn by the G toggle in the previous task, used here to decide whether a pair is even worth testing precisely.
- Include
"SAT.h". - Define
SAT::aabbOverlapto returna.getAABB().findIntersection(b.getAABB()).has_value().
Success condition: the project compiles with aabbOverlap defined. SAT::test is declared in SAT.h but still has no definition, that is fine as long as nothing calls it yet.
#include "SAT.h"
// TODO: define bool SAT::aabbOverlap(const OBB& a, const OBB& b), returning
// a.getAABB().findIntersection(b.getAABB()).has_value()
Task 7 - Finding and Drawing Candidate Pairs
Open NarrowPhaseDemo.cpp. Each frame, build the list of candidate pairs using the same brute-force, every-pair-once approach as the Brute-Force Broad Phase lab, this time filtering with SAT::aabbOverlap instead of a circle overlap test, then draw a thin line between the centres of every candidate pair so the broad-phase result is visible on screen before any precise test runs.
- Include
"SAT.h"alongside the existing includes. - Each frame, after updating every box, declare
std::vector<std::pair<std::size_t, std::size_t>> candidatePairs, and fill it with a nested loop over everyi < boxes.size()andjfromi + 1toboxes.size() - 1, pushing(i, j)wheneverSAT::aabbOverlap(boxes[i], boxes[j])is true. - After drawing the AABB overlay and before drawing the boxes themselves, loop over
candidatePairsand draw a line betweenboxes[pair.first].centerandboxes[pair.second].centerusing twosf::Vertexentries colouredsf::Color(255, 220, 60, 140)andsf::PrimitiveType::Lines. - Build an
sf::Textreportingboxes.size()andcandidatePairs.size(), and draw it in the top-left corner.
Success condition: a yellow line connects every pair of boxes whose bounding boxes overlap. Toggle G on and watch closely: some yellow lines connect boxes whose true rotated outlines are clearly not touching, only their swollen bounding boxes are overlapping. That gap is exactly what the next task closes.
// After updating every box, before clearing the window:
// TODO: declare std::vector<std::pair<std::size_t, std::size_t>> candidatePairs
// and fill it with a nested loop (i from 0, j from i + 1) pushing (i, j)
// whenever SAT::aabbOverlap(boxes[i], boxes[j]) is true
// After drawing the AABB overlay, before drawing the boxes:
// TODO: loop over candidatePairs and draw a line between
// boxes[pair.first].center and boxes[pair.second].center using two
// sf::Vertex entries coloured sf::Color(255, 220, 60, 140) and
// sf::PrimitiveType::Lines
// TODO: build and draw an sf::Text in the top-left corner reporting
// boxes.size() and candidatePairs.size()
Task 8 - Projecting a Box Onto an Axis
Open SAT.cpp and add the two small helpers shown below, in an anonymous namespace above aabbOverlap. Every later step of the SAT test relies on these to turn a box's four corners into a single range along one axis.
- Add a
struct Projection { float min; float max; }. - Add
float dot(sf::Vector2f a, sf::Vector2f b)returninga.x * b.x + a.y * b.y. - Add
Projection project(const std::array<sf::Vector2f, 4>& corners, sf::Vector2f axis). Startmnandmxboth atdot(corners[0], axis), then loopifrom1to3, computingp = dot(corners[i], axis)and updatingmnandmxwithstd::minandstd::max. Return{ mn, mx }.
Success condition: the project compiles. Neither helper is called from outside this file yet, that starts in the next task.
#include "SAT.h"
#include <algorithm>
#include <array>
#include <limits>
namespace {
struct Projection {
float min;
float max;
};
float dot(sf::Vector2f a, sf::Vector2f b) {
// TODO: return a.x * b.x + a.y * b.y
return 0.0f;
}
Projection project(const std::array<sf::Vector2f, 4>& corners, sf::Vector2f axis) {
// TODO: start mn and mx both at dot(corners[0], axis), then loop i
// from 1 to 3 updating mn with std::min and mx with std::max against
// dot(corners[i], axis), then return { mn, mx }
return { 0.0f, 0.0f };
}
}
bool SAT::aabbOverlap(const OBB& a, const OBB& b) {
return a.getAABB().findIntersection(b.getAABB()).has_value();
}
Task 9 - Testing All Four Axes
Open SAT.cpp and add SAT::test below aabbOverlap. This is the precise narrow-phase test itself: check the two boxes' projections against each of the four candidate axes, exiting the moment one axis proves they cannot be touching.
- Get both boxes' corners with
a.getCorners()andb.getCorners(), and combine both boxes' axes froma.getAxes()andb.getAxes()into onestd::array<sf::Vector2f, 4> axes. - Declare
float bestOverlap = std::numeric_limits<float>::max()andsf::Vector2f bestAxis(0.f, 0.f)to track the smallest overlap seen so far. - For each
axisinaxes, project both boxes' corners onto it, then computeoverlap = std::min(projA.max, projB.max) - std::max(projA.min, projB.min). - If
overlap <= 0.f, this axis alone proves separation, returnResult{ false, sf::Vector2f(0.f, 0.f), 0.f }immediately without checking the remaining axes. - Otherwise, if
overlap < bestOverlap, updatebestOverlapandbestAxis.
Success condition: the project compiles with a working test, but it is not called from main yet, so nothing changes on screen. That is the next task.
SAT::Result SAT::test(const OBB& a, const OBB& b) {
std::array<sf::Vector2f, 4> cornersA = a.getCorners();
std::array<sf::Vector2f, 4> cornersB = b.getCorners();
// TODO: combine axesA and axesB (from a.getAxes() and b.getAxes()) into
// one std::array<sf::Vector2f, 4> axes
float bestOverlap = std::numeric_limits<float>::max();
sf::Vector2f bestAxis(0.f, 0.f);
// TODO: for each axis in axes, project cornersA and cornersB onto it,
// compute overlap = std::min(projA.max, projB.max) - std::max(projA.min, projB.min).
// If overlap <= 0.f, return Result{ false, sf::Vector2f(0.f, 0.f), 0.f }
// immediately. Otherwise, if overlap < bestOverlap, update bestOverlap
// and bestAxis.
return Result{ false, sf::Vector2f(0.f, 0.f), 0.f };
}
Task 10 - Orienting the Push Direction
Open SAT.cpp and finish SAT::test. Reaching the end of the axis loop without an early return means every axis showed overlap, so the boxes are colliding. bestAxis already holds the smallest overlap direction, but it could point either way, this step makes sure the returned normal always points from a toward b.
- Below the axis loop, compute
sf::Vector2f direction = b.center - a.center. - If
dot(direction, bestAxis) < 0.f, negatebestAxisso it points towardb. - Return
Result{ true, bestAxis, bestOverlap }.
Success condition: the project compiles with a complete SAT::test. It still is not called from main yet, that is the next task.
// At the end of SAT::test, after the axis loop finds no separating axis:
// TODO: compute sf::Vector2f direction = b.center - a.center. If
// dot(direction, bestAxis) < 0.f, negate bestAxis so the normal points from
// a's centre toward b's. Then return Result{ true, bestAxis, bestOverlap }
Task 11 - Confirming Collisions on Screen
Open NarrowPhaseDemo.cpp and replace the flat yellow-line drawing from Task 7 with a real narrow-phase pass, so a candidate pair only stays yellow if SAT rejects it, and turns red once SAT confirms it.
- Right after building
candidatePairs, declarestd::vector<SAT::Result> results(candidatePairs.size()), then loop over every candidate pair callingSAT::test(boxes[pair.first], boxes[pair.second])and storing the result. - Count how many results have
colliding == trueinto aconfirmedCount. - Replace the single-colour line loop from Task 7: for each candidate pair, if its result is colliding, draw a thick line (use a rectangle-shaped line, 4 pixels wide, in
sf::Color(255, 60, 60)) between the two centres. Otherwise, draw the thin yellow line exactly as before. - Update the stats text to also report
confirmedCountandcandidatePairs.size() - confirmedCountas the false-positive count.
Success condition: drive the green box into a spinning neighbour. The connecting line turns red only once their true rotated outlines actually touch, staying yellow the whole time only their bounding boxes overlap.
// Right after building candidatePairs:
// TODO: declare std::vector<SAT::Result> results(candidatePairs.size()),
// then fill it by calling SAT::test(boxes[pair.first], boxes[pair.second])
// for each entry in candidatePairs
// TODO: count how many results have colliding == true into confirmedCount
// Replacing the Task 7 line-drawing loop:
for (const auto& pair : candidatePairs) {
sf::Vector2f centerA = boxes[pair.first].center;
sf::Vector2f centerB = boxes[pair.second].center;
// TODO: if this pair's SAT result is colliding, draw a thick
// (4 pixel) sf::Color(255, 60, 60) line between centerA and centerB.
// Otherwise, draw the thin sf::Color(255, 220, 60, 140) line from
// Task 7 using sf::Vertex and sf::PrimitiveType::Lines.
}
// TODO: extend the stats text to also report confirmedCount and
// (candidatePairs.size() - confirmedCount) as the false-positive count
Task 12 - Drawing the Push-Apart Arrow
Open NarrowPhaseDemo.cpp. Add a small arrow-drawing helper and call it for every confirmed collision, so the direction and (scaled up for visibility) size of the minimum push needed to separate a pair is visible directly, not just implied by the red line.
- In the anonymous namespace, add the
drawThickLineanddrawArrowhelpers shown below.drawArrowdraws a shaft from a start point along a direction for a given length, plus a small two-stroke arrowhead at the tip. - Add
bool showMTV = true;before the main loop, and toggle it onsf::Keyboard::Scancode::Malongside the existingGhandler. - Where a confirmed pair's thick red line is drawn, if
showMTVis true, calldrawArrowstarting at the midpoint between the two centres, using the result'snormalas the direction andresult.penetration * 4.0fas the length (the multiplier exaggerates small penetrations so they stay visible), colouredsf::Color(255, 160, 30).
Success condition: pressing M toggles an orange arrow on every confirmed collision, pointing away from the overlap in the direction SAT::test reported as the cheapest separating push.
void drawThickLine(sf::RenderWindow& window, sf::Vector2f from, sf::Vector2f to, float thickness, sf::Color colour) {
sf::Vector2f delta = to - from;
float length = std::sqrt(delta.x * delta.x + delta.y * delta.y);
if (length < 0.0001f) {
return;
}
sf::RectangleShape line(sf::Vector2f(length, thickness));
line.setOrigin(sf::Vector2f(0.0f, thickness * 0.5f));
line.setPosition(from);
float angleDeg = std::atan2(delta.y, delta.x) * 180.0f / 3.14159265f;
line.setRotation(sf::degrees(angleDeg));
line.setFillColor(colour);
window.draw(line);
}
void drawArrow(sf::RenderWindow& window, sf::Vector2f from, sf::Vector2f direction, float length, sf::Color colour) {
if (length < 0.0001f) {
return;
}
sf::Vector2f tip = from + direction * length;
drawThickLine(window, from, tip, 3.0f, colour);
float angle = std::atan2(direction.y, direction.x);
float headLength = std::min(12.0f, length * 0.4f);
constexpr float headSpreadRadians = 2.6f;
sf::Vector2f left(std::cos(angle + headSpreadRadians), std::sin(angle + headSpreadRadians));
sf::Vector2f right(std::cos(angle - headSpreadRadians), std::sin(angle - headSpreadRadians));
drawThickLine(window, tip, tip + left * headLength, 3.0f, colour);
drawThickLine(window, tip, tip + right * headLength, 3.0f, colour);
}
// bool showMTV = true; before the main loop, toggled on sf::Keyboard::Scancode::M
// Where a confirmed pair's thick red line is drawn:
// if (showMTV) {
// sf::Vector2f midpoint = (centerA + centerB) * 0.5f;
// drawArrow(window, midpoint, result.normal, result.penetration * 4.0f, sf::Color(255, 160, 30));
// }
Solution
Try the tasks above before revealing the solution.