Overview
This lab builds a small SFML project that measures how expensive collision detection gets as the number of moving objects grows. Working across a Ball class and a bruteForcePairs function, you will represent each moving object as a small struct with just enough state to bounce around a window, write a function that checks every ball against every other ball exactly once, and wire the result into a live SFML window that reports how many candidate pairs were found and how long the check took each frame. By the end, clicking to add balls or pressing a key to add fifty at once visibly drives both the reported pair count and the per-frame timing upward, and the connecting lines drawn between every candidate pair make the sheer number of comparisons something you can see rather than only read as a number.
Learning Objectives
By the end of this lab you will be able to:
- Explain what broad-phase collision detection is and why checking every possible pair of objects is the simplest, but most expensive, way to find them
- Implement a Ball struct that stores enough state to move and bounce inside a window without performing any collision detection itself
- Implement bruteForcePairs so it returns exactly one pair for every combination of two balls, using a nested loop that never revisits a pair or checks a ball against itself
- Measure and report the number of candidate pairs and the time a broad-phase pass takes, and relate the growth in both to the O(n^2) shape of the algorithm as ball count increases
Concept
A game with many moving objects cannot afford to test every object against every other object for a precise, expensive collision check every frame. Broad phase collision detection exists to answer a cheaper, earlier question first: which pairs of objects are even worth checking closely? Objects that pass the broad-phase test become candidate pairs, and only those pairs go on to a more expensive precise check. The simplest possible broad phase does no filtering at all: it treats every single pair of objects as a candidate, which is exactly what makes it the brute-force approach and the baseline every smarter broad-phase structure is measured against.
Generating every pair exactly once, with no repeats and no pair of an object with itself, is a nested loop where the inner loop always starts one position ahead of the outer loop. For an outer index i running from 0 to n-1, the inner index j runs from i + 1 to n-1, so (i, j) is produced but (j, i) never is, and i is never paired with itself. Counting how many pairs that produces reveals the cost. For n objects, the total pair count is n * (n - 1) / 2, a quantity that grows with the square of n. Doubling the number of objects roughly quadruples the number of pairs, which is what the phrase O(n^2) describes: the work grows proportionally to n squared, not to n itself.
The function that does this pairing takes a std::vector<Ball> and returns a std::vector of pairs, using std::pair<const Ball*, const Ball*> so each entry points back at the two balls involved rather than copying them. Timing the function itself, separately from any rendering, uses std::chrono::high_resolution_clock::now() before and after the call, with the difference converted to microseconds through std::chrono::duration<double, std::micro>.
Tasks
Task 1 - A Ball That Moves and Bounces
Create a new Console App (C++) project in Visual Studio the same way as in the Environment Setup lab, naming it BruteForceBroadPhase. Use Add > New Item to add a new header file named Ball.h, and write the Ball class shown below. A ball only needs a position, a velocity, a radius and a colour, enough to move around and overlap other balls. It does not resolve collisions with other balls in this lab, it only bounces off the edges of the window.
- Store
position,velocity,radiusandcolouras public members, set through the constructor's initializer list. - In
update, advancepositionbyvelocity * deltaTime. - After moving, check each axis against the world bounds. If the ball has gone past an edge, clamp its position back to that edge and flip the matching component of
velocity.
Success condition: the project compiles with Ball fully defined in Ball.h. There is no window yet, that is added in the next task.
#pragma once
#include <SFML/Graphics.hpp>
class Ball {
public:
Ball(sf::Vector2f pos, sf::Vector2f vel, float r, sf::Color col)
: position(pos), velocity(vel), radius(r), colour(col) {}
void update(float deltaTime, float worldWidth, float worldHeight) {
// TODO: advance position by velocity * deltaTime, then bounce off
// each edge of the world by clamping position back inside it and
// flipping the matching component of velocity
}
void draw(sf::RenderWindow& window) const {
sf::CircleShape shape(radius);
shape.setOrigin(sf::Vector2f(radius, radius));
shape.setPosition(position);
shape.setFillColor(colour);
window.draw(shape);
}
sf::Vector2f position;
sf::Vector2f velocity;
float radius;
sf::Color colour;
};
Task 2 - Generating Every Candidate Pair
Use Add > New Item to add a new header file named BruteForce.h, and write the bruteForcePairs function shown below. It takes every ball in a std::vector<Ball> and returns every possible pair, exactly once.
- Loop
ifrom0toballs.size() - 1. - Inside that, loop
jfromi + 1toballs.size() - 1, sojalways starts one position ahead ofi. - For each
(i, j), add a pair of pointers,&balls[i]and&balls[j], to the result.
Success condition: the project compiles with bruteForcePairs defined in BruteForce.h, taking a const std::vector<Ball>& and returning a std::vector<std::pair<const Ball*, const Ball*>>.
#pragma once
#include <vector>
#include <utility>
#include "Ball.h"
inline std::vector<std::pair<const Ball*, const Ball*>> bruteForcePairs(const std::vector<Ball>& balls) {
std::vector<std::pair<const Ball*, const Ball*>> pairs;
// TODO: loop i from 0 to balls.size() - 1, and inside that loop j from
// i + 1 to balls.size() - 1, adding a pair of pointers (&balls[i],
// &balls[j]) to pairs for every (i, j) combination
return pairs;
}
Task 3 - A Window Full of Balls
Use Add > New Item to add a new C++ source file named BruteForceBroadPhase.cpp, and write the main function shown below. It opens an 800 by 800 window, fills a std::vector<Ball> with 30 randomly placed and coloured balls, and updates and draws every ball each frame. bruteForcePairs from Task 2 is not called yet, so nothing is measured.
- Inside the main loop, after event polling, call
ball.update(deltaTime, WORLD_WIDTH, WORLD_HEIGHT)for every ball inballs.
Success condition: a window opens showing 30 balls of random colour and size bouncing around inside the window edges, with no pair count or timing displayed yet.
#include <SFML/Graphics.hpp>
#include <vector>
#include <random>
#include "Ball.h"
#include "BruteForce.h"
namespace {
constexpr unsigned int WORLD_WIDTH = 800;
constexpr unsigned int WORLD_HEIGHT = 800;
constexpr std::size_t DEFAULT_BALL_COUNT = 30;
std::mt19937 rng(std::random_device{}());
Ball makeRandomBall(float worldWidth, float worldHeight) {
std::uniform_real_distribution<float> posX(20.0f, worldWidth - 20.0f);
std::uniform_real_distribution<float> posY(20.0f, worldHeight - 20.0f);
std::uniform_real_distribution<float> vel(-120.0f, 120.0f);
std::uniform_real_distribution<float> radiusDist(4.0f, 10.0f);
std::uniform_int_distribution<int> colourDist(100, 255);
return Ball(
sf::Vector2f(posX(rng), posY(rng)),
sf::Vector2f(vel(rng), vel(rng)),
radiusDist(rng),
sf::Color(colourDist(rng), colourDist(rng), colourDist(rng)));
}
}
int main() {
sf::VideoMode videoMode(sf::Vector2u(WORLD_WIDTH, WORLD_HEIGHT));
sf::RenderWindow window(videoMode, "Brute-Force Broad Phase");
window.setFramerateLimit(60);
std::vector<Ball> balls;
for (std::size_t i = 0; i < DEFAULT_BALL_COUNT; ++i) {
balls.push_back(makeRandomBall(static_cast<float>(WORLD_WIDTH), static_cast<float>(WORLD_HEIGHT)));
}
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();
}
}
// TODO: call ball.update(deltaTime, WORLD_WIDTH, WORLD_HEIGHT) for
// every ball in balls
// The pair count and timing are added in the next task.
window.clear(sf::Color(12, 12, 18));
for (const auto& ball : balls) {
ball.draw(window);
}
window.display();
}
return 0;
}
Task 4 - Measuring and Displaying the Broad Phase
Open BruteForceBroadPhase.cpp and return to main(). The ball updates from Task 3 are shown below already solved. Add the broad-phase call directly after them, before the balls are drawn, and display its result on screen.
- Record a start time with
std::chrono::high_resolution_clock::now(), callbruteForcePairs(balls), then record an end time the same way. - Convert the difference between the two times to microseconds with
std::chrono::duration<double, std::micro>(end - start).count(). - Build an
sf::Textthat reportsballs.size()and the pair count returned bybruteForcePairs, and draw it in the top-left corner of the window.
Success condition: the window displays a live ball count, pair count and microsecond timing, and the pair count always matches balls.size() * (balls.size() - 1) / 2 exactly.
while (window.isOpen()) {
float deltaTime = clock.restart().asSeconds();
while (const std::optional event = window.pollEvent()) {
if (event->is<sf::Event::Closed>()) {
window.close();
}
}
// Ball updates (Task 3, already solved).
for (auto& ball : balls) {
ball.update(deltaTime, static_cast<float>(WORLD_WIDTH), static_cast<float>(WORLD_HEIGHT));
}
// TODO: time a call to bruteForcePairs(balls) using
// std::chrono::high_resolution_clock, convert the elapsed time to
// microseconds, then build and draw an sf::Text reporting balls.size(),
// the pair count, and the microsecond timing
window.clear(sf::Color(12, 12, 18));
for (const auto& ball : balls) {
ball.draw(window);
}
window.display();
}
Task 5 - Drawing the Candidate Pairs
Open BruteForceBroadPhase.cpp and return to main(). With the pair count and timing from Task 4 already on screen, draw a line between the two balls in every candidate pair, so the number reported becomes something visible, not only a count.
- After computing
pairs, loop over every entry and draw a two-pointsf::Vertexline betweenpairs[i].first->positionandpairs[i].second->positionusingsf::PrimitiveType::Lines. - Give the line a soft, semi-transparent colour such as
sf::Color(255, 220, 60, 90)so overlapping lines remain readable instead of turning into a solid mass.
Success condition: a thin yellow line connects every pair of balls on screen at once, and the tangle of lines visibly gets denser as more balls are added.
window.clear(sf::Color(12, 12, 18));
for (const auto& ball : balls) {
ball.draw(window);
}
// TODO: loop over pairs and draw a two-point sf::Vertex line between
// pairs[i].first->position and pairs[i].second->position for every pair,
// using sf::PrimitiveType::Lines and a soft translucent colour
window.display();
Task 6 - Adding Balls to Watch the Cost Climb
Open BruteForceBroadPhase.cpp and return to main(). So far the ball count never changes. Add a key handler inside the event-polling loop so pressing A spawns fifty more balls, making the O(n^2) growth from the Concept section something you can trigger and watch happen.
- Inside the event loop, check for an
sf::Event::KeyPressedevent usingevent->getIf<sf::Event::KeyPressed>(). - If
keyPressed->scancodeissf::Keyboard::Scancode::A, callmakeRandomBallfifty times and push each result ontoballs.
Success condition: pressing A repeatedly adds fifty balls at a time, and both the pair count text from Task 4 and the density of lines from Task 5 climb sharply within a few presses, roughly quadrupling the pair count each time the ball count doubles.
while (const std::optional event = window.pollEvent()) {
if (event->is<sf::Event::Closed>()) {
window.close();
}
// TODO: check for an sf::Event::KeyPressed event with getIf, and when
// keyPressed->scancode is sf::Keyboard::Scancode::A, push fifty new
// balls from makeRandomBall onto balls
}
Solution
Try the tasks above before revealing the solution.