C++ Module 22 – C++ for game development
Collision detection and physics in C++
Detect overlapping boxes and circles in C++, find touching pairs with a spatial hash grid instead of testing every pair, and move bodies stably.
What you will learn
- Test boxes and circles for overlap, edge cases included
- Use a spatial grid so that only pairs which can touch are tested
- Move bodies with semi-implicit Euler and tell when a step can tunnel
- Size a grid's cells and choose a broad phase from measured counts
- Prevent tunnelling with substeps or a swept test, and resolve a contact
Before you start
What you’ll learn · Before you start
What you will learn
- Test boxes and circles for overlap, edge cases included
- Use a spatial grid so that only pairs which can touch are tested
- Move bodies with semi-implicit Euler and tell when a step can tunnel
- Size a grid's cells and choose a broad phase from measured counts
- Prevent tunnelling with substeps or a swept test, and resolve a contact
Before you start
On this page
Testing every pair of 10,000 moving circles for contact takes n(n − 1)/2 = 49,995,000 tests a frame, about 61 ms in this lesson’s run, over three frames at 60 frames a second. So collision detection works in two phases. The broad phase finds the pairs that might touch, cheaply, and the narrow phase tests each of those pairs exactly. A spatial hash grid, which sorts objects into cells and tests only neighbours, cut the 10,000 circles to 16,000 tests a frame and found the same pairs.
Simple physics in C++ adds two short steps around the tests: each body moves with semi-implicit Euler, velocity first and then position, and a pair that touches is pushed apart and bounced. You will write the overlap tests, build the grid and move the bodies.
Boxes and circles overlap after a few comparisons
Two boxes whose sides are parallel to the axes overlap when their ranges overlap on both axes. Two circles overlap when the squared distance between their centres is at most the squared sum of their radii, which needs no square root:
// Overlap tests for the shapes most 2D games collide: axis-aligned boxes and circles.
#pragma once
struct Vec2 {
float x, y;
};
// A box whose sides are parallel to the x and y axes, from its smallest corner to its largest.
struct Box {
Vec2 min, max;
};
struct Circle {
Vec2 centre;
float r;
};
// Two boxes overlap when their ranges overlap on both axes. Touching edges count as a collision.
constexpr bool overlaps(const Box& a, const Box& b) {
return a.min.x <= b.max.x && b.min.x <= a.max.x && a.min.y <= b.max.y && b.min.y <= a.max.y;
}
// Two circles overlap when their centres are no further apart than the sum of the radii. Comparing the squares
// gives the same answer without a square root.
constexpr bool overlaps(const Circle& a, const Circle& b) {
const float dx = a.centre.x - b.centre.x;
const float dy = a.centre.y - b.centre.y;
const float reach = a.r + b.r;
return dx * dx + dy * dy <= reach * reach;
} Here touching edges count as a collision; whichever rule you pick, use it everywhere, or a box resting on the floor will touch it in one test and float above it in another. The tests below cover the cases that usually go wrong:
// The two overlap tests on the cases that usually go wrong: touching, just apart, and one shape inside the other.
#include <cstdio>
#include <string_view>
#include "shapes.hpp"
namespace {
int checks = 0;
int failures = 0;
void check(bool ok, std::string_view what) {
++checks;
if (!ok) {
++failures;
std::printf("FAIL %.*s\n", int(what.size()), what.data());
}
}
} // namespace
int main() {
const Box wall{{100, 0}, {110, 200}};
check(overlaps(wall, Box{{110, 50}, {130, 70}}), "boxes that share an edge collide");
check(!overlaps(wall, Box{{111, 50}, {130, 70}}), "boxes 1 pixel apart do not");
check(!overlaps(wall, Box{{105, 201}, {120, 220}}), "boxes that overlap on x but not on y do not");
check(overlaps(wall, Box{{102, 10}, {104, 20}}), "a box inside another collides");
const Circle ball{{0, 0}, 5};
check(overlaps(ball, Circle{{8, 6}, 5}), "circles whose centres are exactly 10 apart touch (6-8-10 triangle)");
check(!overlaps(ball, Circle{{8, 6.5f}, 5}), "circles a little further apart do not");
check(overlaps(ball, Circle{{1, 1}, 1}), "a small circle inside a large one collides");
check(!overlaps(Circle{{0, 0}, 1}, Circle{{1.5f, 1.5f}, 1}), "circles inside overlapping bounding boxes may still miss");
std::printf("%d of %d checks passed\n", checks - failures, checks);
return failures == 0 ? 0 : 1;
} from session import run
# The overlap tests, built with each compiler and the sanitizers switched on.
run("""
clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined shapes_tests.cpp -o shapes_tests && ./shapes_tests
g++-16 -std=c++23 -Wall -Wextra -fsanitize=address,undefined shapes_tests.cpp -o shapes_tests-gcc && ./shapes_tests-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined shapes_tests.cpp -o shapes_tests && ./shapes_tests 8 of 8 checks passed $ g++-16 -std=c++23 -Wall -Wextra -fsanitize=address,undefined shapes_tests.cpp -o shapes_tests-gcc && ./shapes_tests-gcc 8 of 8 checks passed
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 shapes_session.py
Step through seven of the cases:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Boxes that share an edge. The wall spans x = 100 to 110 and the box x = 110 to 130. 100 ≤ 130 and 110 ≤ 110, so the x ranges overlap, and the y ranges overlap too. The boxes collide, because touching counts here.
- One pixel apart. Start the box at x = 111 instead. 111 ≤ 110 is false, so the x ranges miss, and no other comparison can change that. The boxes are apart.
- Apart on one axis. A box from x = 105 to 120 and y = 201 to 220 overlaps the wall's x range, but 201 ≤ 200 is false. One axis apart is enough for two boxes to be apart.
- Circles that just touch. Circles of radius 5 at (0, 0) and (8, 6): dx² + dy² = 64 + 36 = 100, and (5 + 5)² = 100. 100 ≤ 100, so they touch, and no square root was needed.
- Half a pixel further. Move the second centre to (8, 6.5). 64 + 42.25 = 106.25, more than 100, so the circles are apart.
- One inside the other. A circle of radius 1 at (1, 1) lies inside the first one. 1 + 1 = 2 against (5 + 1)² = 36, and 2 ≤ 36, so the same test reports a collision for a circle inside another.
- Boxes overlap, circles do not. Circles of radius 1 at (0, 0) and (1.5, 1.5) have bounding boxes from −1 to 1 and from 0.5 to 2.5 on each axis, which overlap. But 2.25 + 2.25 = 4.5 is more than (1 + 1)² = 4, so the circles are apart. The corners of a box hold no circle.
The last case is the trap of every circle test: circles whose The smallest box with sides parallel to the axes that contains a shape. A circle of radius r centred at (x, y) has the box from (x − r, y − r) to (x + r, y + r). overlap can still be apart.
Checkpoint
Check: Overlap tests for boxes and circles
A grid finds the same pairs with 3,100 times fewer tests
Every-pair testing grows with the square of the count: twice the objects, four times the tests. A uniform grid fixes that by cutting the world into square cells at least as wide as the largest circle. Each circle is stored in the cell that holds its centre, and two circles can only touch when their cells are the same or neighbours, so each circle is tested against the 3 × 3 block of cells around its own:
A spatial grid with cells as wide as the largest circle
Text description of the diagram
The picture shows a grid of 6 by 5 square cells, each as wide as the largest circle's diameter, with circles scattered over it.
- Circle A has its centre in a cell near the middle. The 3 × 3 block of cells around that cell is shaded and outlined.
- Circle B has its centre in the next cell to the right, inside the block, and overlaps A: the grid tests the pair and finds a contact.
- Circle C sits in the block's top-left cell: the grid tests it against A, and the exact test says they are apart.
- Three grey circles lie in cells outside the block. The grid never tests them against A, because no circle that far away can touch it.
Because no circle is wider than a cell, two circles that touch always have their centres in the same cell or in neighbouring cells, so searching the 3 × 3 block finds every contact of A.
A spatial hash grid stores only the cells that hold something, in a hash table keyed by the cell’s coordinates, so the world needs no fixed size. This program moves 10,000 circles for 10 frames and finds the touching pairs both ways:
world.hpp
// The world of the collision examples: 10,000 circles that move and bounce, every-pair testing, and a spatial
// hash grid. Shared by broadphase.cpp and grid_tuning.cpp.
#pragma once
#include <algorithm>
#include <cstdint>
#include <unordered_map>
#include <utility>
#include <vector>
#include "shapes.hpp"
namespace sim {
constexpr int count = 10'000;
constexpr float world = 2000.0f; // a square world, 2000 x 2000 pixels
constexpr float max_r = 6.0f;
constexpr int frames = 10;
// splitmix64: the same numbers with every compiler and standard library (std:: distributions differ between them).
struct Random {
std::uint64_t state;
float next() { // in [0, 1)
std::uint64_t z = (state += 0x9e3779b97f4a7c15u);
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9u;
z = (z ^ (z >> 27)) * 0x94d049bb133111ebu;
return float((z ^ (z >> 31)) >> 40) / float(1u << 24);
}
};
struct Body {
Circle shape;
Vec2 velocity; // pixels per second
};
std::vector<Body> make_world(std::uint64_t seed) {
Random random{seed};
std::vector<Body> bodies(count);
for (Body& b : bodies) {
b.shape = {{random.next() * world, random.next() * world}, 2.0f + random.next() * (max_r - 2.0f)};
b.velocity = {random.next() * 200.0f - 100.0f, random.next() * 200.0f - 100.0f};
}
return bodies;
}
// Moves every body for one step and bounces it off the edges of the world.
void move(std::vector<Body>& bodies, float dt) {
for (Body& b : bodies) {
Vec2& p = b.shape.centre;
p.x += b.velocity.x * dt;
p.y += b.velocity.y * dt;
if (p.x < 0.0f || p.x > world) {
b.velocity.x = -b.velocity.x;
p.x = std::clamp(p.x, 0.0f, world);
}
if (p.y < 0.0f || p.y > world) {
b.velocity.y = -b.velocity.y;
p.y = std::clamp(p.y, 0.0f, world);
}
}
}
using Pairs = std::vector<std::pair<int, int>>;
// Every pair: n (n - 1) / 2 tests.
Pairs every_pair(const std::vector<Body>& bodies, long long& tests) {
Pairs found;
const int n = int(bodies.size());
for (int i = 0; i < n; ++i)
for (int j = i + 1; j < n; ++j) {
++tests;
if (overlaps(bodies[i].shape, bodies[j].shape)) found.emplace_back(i, j);
}
return found;
}
// A spatial hash grid: square cells, found by hashing the cell's coordinates. A cell at least as wide as the largest
// diameter means that two circles that touch are in the same cell or in neighbouring ones.
struct Grid {
float cell;
std::unordered_map<std::uint64_t, std::vector<int>> cells;
static std::uint64_t key(int cx, int cy) { return std::uint64_t(std::uint32_t(cx)) << 32 | std::uint32_t(cy); }
int index(float v) const { return int(v / cell); }
void rebuild(const std::vector<Body>& bodies) {
for (auto& entry : cells) entry.second.clear(); // keep each cell's memory for the next frame
for (int i = 0; i < int(bodies.size()); ++i) {
const Vec2 p = bodies[i].shape.centre;
cells[key(index(p.x), index(p.y))].push_back(i);
}
}
Pairs pairs(const std::vector<Body>& bodies, long long& tests) const {
Pairs found;
for (int i = 0; i < int(bodies.size()); ++i) {
const int cx = index(bodies[i].shape.centre.x);
const int cy = index(bodies[i].shape.centre.y);
for (int dy = -1; dy <= 1; ++dy)
for (int dx = -1; dx <= 1; ++dx) {
const auto it = cells.find(key(cx + dx, cy + dy));
if (it == cells.end()) continue;
for (int j : it->second) {
if (j <= i) continue; // each pair once
++tests;
if (overlaps(bodies[i].shape, bodies[j].shape)) found.emplace_back(i, j);
}
}
}
std::sort(found.begin(), found.end());
return found;
}
};
} // namespace sim broadphase.cpp
// 10,000 moving circles, two ways to find the ones that touch: test every pair, or test only the circles in
// neighbouring cells of a spatial hash grid (world.hpp). Both must find the same pairs in every frame.
#include <cstdio>
#include <ctime>
#include "world.hpp"
using namespace sim;
// Processor time since `start`, in milliseconds: other programs running on the same computer disturb it far less
// than a stopwatch would.
double ms_since(std::clock_t start) { return 1000.0 * double(std::clock() - start) / CLOCKS_PER_SEC; }
int main() {
std::vector<Body> bodies = make_world(42);
Grid grid{2 * max_r, {}};
long long brute_tests = 0, grid_tests = 0, pairs = 0;
double brute_ms = 0, grid_ms = 0;
bool same = true;
for (int frame = 0; frame < frames; ++frame) {
move(bodies, 1.0f / 60);
std::clock_t t0 = std::clock();
const Pairs a = every_pair(bodies, brute_tests);
brute_ms += ms_since(t0);
t0 = std::clock();
grid.rebuild(bodies);
const Pairs b = grid.pairs(bodies, grid_tests);
grid_ms += ms_since(t0);
same = same && a == b;
pairs += static_cast<long long>(a.size());
}
std::printf("%d circles, radius 2 to 6, in a %.0f x %.0f world, %d frames\n", count, world, world, frames);
std::printf("every pair: %9lld tests a frame, %lld touching pairs a frame, %6.2f ms a frame\n", brute_tests / frames,
pairs / frames, brute_ms / frames);
std::printf("grid of 12: %9lld tests a frame, %lld touching pairs a frame, %6.2f ms a frame\n", grid_tests / frames,
pairs / frames, grid_ms / frames);
std::printf("same pairs in every frame: %s\n", same ? "yes" : "NO");
} from session import run
# Optimised builds, as a game ships. -ffp-contract=off keeps each compiler from fusing a multiply and an add into one
# instruction, which rounds differently, so both find exactly the same pairs.
run("""
clang++ -std=c++23 -O2 -Wall -Wextra -ffp-contract=off broadphase.cpp -o broadphase && ./broadphase
g++-16 -std=c++23 -O2 -Wall -Wextra -ffp-contract=off broadphase.cpp -o broadphase-gcc && ./broadphase-gcc
""") Output
$ clang++ -std=c++23 -O2 -Wall -Wextra -ffp-contract=off broadphase.cpp -o broadphase && ./broadphase 10000 circles, radius 2 to 6, in a 2000 x 2000 world, 10 frames every pair: 49995000 tests a frame, 2622 touching pairs a frame, 61.19 ms a frame grid of 12: 16000 tests a frame, 2622 touching pairs a frame, 3.06 ms a frame same pairs in every frame: yes $ g++-16 -std=c++23 -O2 -Wall -Wextra -ffp-contract=off broadphase.cpp -o broadphase-gcc && ./broadphase-gcc 10000 circles, radius 2 to 6, in a 2000 x 2000 world, 10 frames every pair: 49995000 tests a frame, 2622 touching pairs a frame, 66.98 ms a frame grid of 12: 16000 tests a frame, 2622 touching pairs a frame, 3.59 ms a frame same pairs in every frame: yes
This output changes from run to run: Processor time changes a little from run to run; the counts of tests and pairs are fixed.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 broadphase_session.py
The grid made 16,000 tests a frame instead of 49,995,000, about 3,100 times fewer, and found the same 2,622 touching pairs a frame on average, pair for pair, in all 10 frames. In processor time the gain was smaller, about 20 times with clang and 19 with GCC, because hashing cell coordinates and refilling the cells every frame cost time too. Other broad phases, in one line each:
| Broad phase | What it does |
|---|---|
| Uniform grid | Equal cells; each object is tested against the neighbouring cells. |
| Spatial hash grid | A uniform grid that keeps only occupied cells, in a hash table. |
| Sweep and prune | Sorts objects along one axis and tests pairs that overlap there. |
| Bounding volume hierarchy | Nests objects in a tree of boxes, walked down only where boxes overlap. |
| Quadtree | Splits space into four squares, again where objects crowd. |
Checkpoint
Check: A grid as the broad phase
Each step changes the velocity first, then the position
Each step, semi-implicit Euler first adds acceleration * dt to the velocity and then adds velocity * dt to the
position, using the new velocity. Explicit Euler does the same two lines the other way round, and that small change
makes a spring gain energy on every step. This program moves a thrown ball both ways for three steps:
// A ball thrown straight up at 10 m/s, moved for three steps of 1/60 s by semi-implicit Euler (velocity first, then
// position) and by explicit Euler (position from the old velocity), next to the exact height 10t - 9.81t²/2.
#include <cstdio>
int main() {
constexpr float dt = 1.0f / 60;
constexpr float g = -9.81f; // m/s², the only acceleration
float semi_y = 0, semi_v = 10;
float expl_y = 0, expl_v = 10;
std::puts("step semi-implicit Euler explicit Euler exact");
std::puts(" v (m/s) y (m) v (m/s) y (m) y (m)");
std::printf("%4d %7.4f %6.4f %7.4f %6.4f %6.4f\n", 0, double(semi_v), double(semi_y), double(expl_v),
double(expl_y), 0.0);
for (int step = 1; step <= 3; ++step) {
semi_v += g * dt; // semi-implicit Euler: the velocity first,
semi_y += semi_v * dt; // then the position, with the new velocity
expl_y += expl_v * dt; // explicit Euler: the position with the old velocity,
expl_v += g * dt; // then the velocity
const double t = step / 60.0;
std::printf("%4d %7.4f %6.4f %7.4f %6.4f %6.4f\n", step, double(semi_v), double(semi_y),
double(expl_v), double(expl_y), 10 * t - 9.81 * t * t / 2);
}
} from session import run
# one_step.cpp with Apple clang, then GCC 16's build, whose output is compared with clang's.
run("""
clang++ -std=c++23 -O2 -Wall -Wextra -ffp-contract=off one_step.cpp -o one_step && ./one_step
g++-16 -std=c++23 -O2 -Wall -Wextra -ffp-contract=off one_step.cpp -o one_step-gcc
./one_step > clang.txt && ./one_step-gcc > gcc.txt && cmp clang.txt gcc.txt && echo "GCC 16 printed the same"
""") Output
$ clang++ -std=c++23 -O2 -Wall -Wextra -ffp-contract=off one_step.cpp -o one_step && ./one_step
step semi-implicit Euler explicit Euler exact
v (m/s) y (m) v (m/s) y (m) y (m)
0 10.0000 0.0000 10.0000 0.0000 0.0000
1 9.8365 0.1639 9.8365 0.1667 0.1653
2 9.6730 0.3252 9.6730 0.3306 0.3279
3 9.5095 0.4837 9.5095 0.4918 0.4877
$ g++-16 -std=c++23 -O2 -Wall -Wextra -ffp-contract=off one_step.cpp -o one_step-gcc
$ ./one_step > clang.txt && ./one_step-gcc > gcc.txt && cmp clang.txt gcc.txt && echo "GCC 16 printed the same"
GCC 16 printed the same
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 one_step_session.py
Step through them:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The throw. The ball leaves the ground at 10 m/s, straight up. Gravity changes its velocity by −9.81 m/s² × 1/60 s = −0.1635 m/s in every step, whichever method moves it.
- Semi-implicit: velocity first. Semi-implicit Euler changes the velocity first, from 10 to 9.8365 m/s, and then moves the ball by the new velocity, 9.8365 m/s × 1/60 s = 0.1639 m.
- Explicit: position first. Explicit Euler does the same two lines the other way round. It moves the ball by the old velocity, 10 m/s × 1/60 s = 0.1667 m, and only then changes the velocity to 9.8365 m/s.
- Against the exact height. The exact height after one step is 10t − 9.81t²/2 = 0.1653 m. Semi-implicit Euler is about 1.4 mm short, and explicit Euler about 1.4 mm too high.
- Three steps. After three steps both velocities are 9.5095 m/s, and the heights are 0.4837 m and 0.4918 m against the exact 0.4877 m. The two methods differ only in which velocity moves the ball.
Velocity Verlet averages the accelerations at the start and end of each step. Semi-implicit steps and overlap tests are all the physics in C++ a small 2D game needs, with two hazards. Tunnelling happens when a body moves further in one step than a wall’s thickness plus its own width, so no single position overlaps the wall. Substeps split each step into shorter ones, and a swept test finds the first moment of contact along the path, which physics engines call continuous collision detection. A pair that touches is pushed apart along the line between the centres, and its velocities are reflected with a factor called restitution.
Checkpoint
Check: Moving bodies one step at a time
Practise: circles near a box
near_box.hpp has two gaps:
overlaps(Circle, Box): true when the circle touches or overlaps the box, including a circle inside it.CircleGrid::near(area): the ids of every circle that could touch a box, each once and in increasing order, from the cells around the box.
C++ header · exercise/near_box.hpp
// The exercise: a circle-against-box test, and a grid that finds the circles near a box.
#pragma once
#include <cmath>
#include <cstdint>
#include <unordered_map>
#include <vector>
#include "shapes.hpp"
// TODO 1: true when the circle touches or overlaps the box (a circle inside the box counts).
constexpr bool overlaps(const Circle& c, const Box& b) {
(void)c;
(void)b;
return false;
}
// A grid of square cells. Each circle is stored in the one cell that holds its centre.
struct CircleGrid {
float cell;
float max_r; // the largest radius of any circle stored
std::unordered_map<std::uint64_t, std::vector<int>> cells;
static std::uint64_t key(int cx, int cy) { return std::uint64_t(std::uint32_t(cx)) << 32 | std::uint32_t(cy); }
int index(float v) const { return int(std::floor(v / cell)); }
void insert(int id, const Circle& c) { cells[key(index(c.centre.x), index(c.centre.y))].push_back(id); }
// TODO 2: the ids of every circle that could touch `area`, each id once, in increasing order. Remember that a
// circle can touch the box while its centre lies in a cell outside it.
std::vector<int> near(const Box& area) const {
(void)area;
return {};
}
};
// The circles that touch `area`: candidates from the grid, then the exact test.
inline std::vector<int> touching(const std::vector<Circle>& circles, const CircleGrid& grid, const Box& area) {
std::vector<int> found;
for (int id : grid.near(area))
if (overlaps(circles[std::size_t(id)], area)) found.push_back(id);
return found;
} C++ · exercise/near_box_tests.cpp
// Sample tests of the exercise. All of them pass once both TODOs of near_box.hpp are done.
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <string_view>
#include <vector>
#include "near_box.hpp"
namespace {
int checks = 0;
int failures = 0;
void check(bool ok, std::string_view what) {
++checks;
if (!ok) {
++failures;
std::printf("FAIL %.*s\n", int(what.size()), what.data());
}
}
// A small generator with the same numbers everywhere: the next value in [0, 1).
float next(std::uint32_t& s) {
s = s * 1664525u + 1013904223u;
return float(s >> 8) / float(1u << 24);
}
} // namespace
int main() {
const Box box{{100, 0}, {110, 200}};
check(overlaps(Circle{{115, 50}, 5}, box), "a circle that touches the box's right side");
check(overlaps(Circle{{105, 100}, 2}, box), "a circle inside the box");
check(!overlaps(Circle{{113.5f, -3.5f}, 4.5f}, box), "a circle by the corner, inside the box's reach on both axes, but not touching");
check(overlaps(Circle{{113, -3}, 4.5f}, box), "a circle that just reaches the corner");
// 300 circles in a 400 x 400 square, and 20 boxes. Testing every circle against every box with a correct
// overlaps() finds 36 touching pairs of a circle and a box, whose circle ids add up to 5395.
std::uint32_t seed = 7;
std::vector<Circle> circles;
CircleGrid grid{16, 8, {}};
for (int i = 0; i < 300; ++i) {
circles.push_back({{next(seed) * 400, next(seed) * 400}, 1 + next(seed) * 7});
grid.insert(i, circles.back());
}
int found = 0;
long long id_sum = 0;
bool once = true;
for (int k = 0; k < 20; ++k) {
const float x = next(seed) * 360, y = next(seed) * 360;
const Box area{{x, y}, {x + 5 + next(seed) * 35, y + 5 + next(seed) * 35}};
const std::vector<int> candidates = grid.near(area);
once = once && std::is_sorted(candidates.begin(), candidates.end()) &&
std::adjacent_find(candidates.begin(), candidates.end()) == candidates.end();
for (int id : touching(circles, grid, area)) {
++found;
id_sum += id;
}
}
check(found == 36 && id_sum == 5395, "the grid finds all 36 contacts between a circle and one of the 20 boxes");
check(once, "near() lists each circle once, in increasing order");
CircleGrid edge{16, 8, {}};
const std::vector<Circle> one{{{38, 10}, 7}}; // its centre is in cell (2, 0); it reaches x = 31
edge.insert(0, one[0]);
check(touching(one, edge, Box{{20, 0}, {31, 15}}) == std::vector<int>{0}, "a circle whose centre lies in a cell outside the box");
std::printf("%d of %d checks passed\n", checks - failures, checks);
return failures == 0 ? 0 : 1;
} from session import run
# The sample tests against the starter code, before either TODO is done, with the sanitizers switched on.
run("""
clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined exercise/near_box_tests.cpp -o near_box_tests && ./near_box_tests
g++-16 -std=c++23 -Wall -Wextra -fsanitize=address,undefined exercise/near_box_tests.cpp -o near_box_tests-gcc && ./near_box_tests-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined exercise/near_box_tests.cpp -o near_box_tests && ./near_box_tests FAIL a circle that touches the box's right side FAIL a circle inside the box FAIL a circle that just reaches the corner FAIL the grid finds all 36 contacts between a circle and one of the 20 boxes FAIL a circle whose centre lies in a cell outside the box 2 of 7 checks passed [exit status 1] $ g++-16 -std=c++23 -Wall -Wextra -fsanitize=address,undefined exercise/near_box_tests.cpp -o near_box_tests-gcc && ./near_box_tests-gcc FAIL a circle that touches the box's right side FAIL a circle inside the box FAIL a circle that just reaches the corner FAIL the grid finds all 36 contacts between a circle and one of the 20 boxes FAIL a circle whose centre lies in a cell outside the box 2 of 7 checks passed [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exercise_session.py
You are done when both compilers print 7 of 7 checks passed. First hint: the point of the box closest to the
circle’s centre is the centre with each coordinate clamped into the box’s range, and a circle stored in a cell beside
the box can still reach it, so search the cells of the box grown by max_r on every side.
Then practise overlaps, grids and steps.
Practice that adapts to you
Practise this lesson
9 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Tuning the grid, with measured numbers Pro: opens with a Pro, Premium or Ultimate pass
A grid is only as good as its cell size and its memory layout. This part measures five cell sizes and two layouts on the same 10,000 circles, proves why a cell must be at least as wide as the largest circle, and reproduces the crowd that turns any grid back into testing every pair.
In this part:
- Five cell sizes, measured
- Why the cell must be at least the largest diameter: the proof
- A dense grid instead of a hash map, measured
- The crowd that defeats every grid
- Grids, sweep and prune, and trees: the trade-offs
2 runnable examples (C++, Python) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Stable motion and no tunnelling, reproduced Pro: opens with a Pro, Premium or Ultimate pass
Collision tests only see where bodies are at the end of each step, and a careless integrator puts them in the wrong places before any test runs. This part measures three integrators against exact answers, makes a fast ball pass through a wall, and stops it two ways.
In this part:
- Three integrators against exact answers, with numbers
- A ball that passes through a wall, reproduced
- Substeps: how many are enough
- A swept test: the moment of first contact
- The response: push apart, then bounce
1 diagram · 2 runnable examples (Python, integrators.cpp, tunnelling.cpp) · about 7 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
What are the broad phase and the narrow phase of collision detection, and why do games split the work in two?
The narrow phase answers "do these two shapes overlap?" exactly, with a test per pair of shapes: compare the ranges of two boxes on each axis, or the squared distance between two circles with the squared sum of their radii. Running it on every pair would cost n(n − 1)/2 tests, 49,995,000 a frame for 10,000 objects, and almost all of them would say no. The broad phase cheaply finds the pairs that might overlap, for example with a grid whose cells are at least as wide as the largest object, so only objects in neighbouring cells become candidates; in the lesson's run that left 16,000 tests a frame. The split lets each phase be good at one thing: the broad phase must never miss a pair that touches, and the narrow phase must be exact.
-
Mid to senior
A top-down shooter has 5,000 bullets of radius 2, 200 enemies of radius 30 and a 4,000 × 4,000 pixel arena. Bullets never hit each other. Design the broad phase, choose the cell size, and estimate the tests per frame.
-
Fresher to mid
In a 2D game, bullets of radius 2 fired at 3,000 pixels a second sometimes pass through walls 8 pixels thick. The physics runs 60 steps a second. Explain why with numbers, and compare two fixes.
-
Mid to senior
A camera follows the player on a spring. Over a minute it swings wider and wider until it flies off. Each step, the code works out the spring force at the camera's position, moves the camera by its velocity, then changes the velocity by that force. What is wrong, and what do you change?
Key takeaways
- A broad phase finds the pairs that might touch, and a narrow phase tests each of them exactly.
- Boxes overlap when their ranges overlap on both axes, circles when the squared distance is at most the squared sum of the radii.
- A grid with cells as wide as the largest circle tested 16,000 pairs instead of 49,995,000, and found the same ones.
- Semi-implicit Euler changes the velocity first, and a body tunnels when one step carries it past a wall’s thickness plus its own width.
References
- Working Draft, Programming Languages: C++ (N4950), unordered associative containers (ISO/IEC JTC1/SC22/WG21)
- std::unordered_map (cppreference.com)
- Box2D documentation: Simulation (bullets, continuous collision and sub-steps) (Box2D)
- Integration Basics (Glenn Fiedler (Gaffer On Games))
- Geometric numerical integration illustrated by the Störmer–Verlet method (Acta Numerica 12) (Cambridge University Press)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress