Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Advanced
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: Boxes and circles overlap after a few comparisons · A grid finds the same pairs with 3,100 times fewer tests · Each step changes the velocity first, then the position · Practise: circles near a box. With a pass: 2 in-depth parts and 3 interview answers, about 13 minutes.

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:

shapes.hpp C++ header · shapes.hpp
// 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:

shapes_tests.cpp C++ · shapes_tests.cpp
// 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;
}
With each compiler Python · shapes_session.py
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:

Interactive · step by step Seven overlap tests from shapes_tests.cpp, worked through Step through the boxes first, then the circles. Each step shows the comparison that decides and what it says.
Seven overlap tests from shapes_tests.cpp, worked through, step 7 of 7. Showing Shape a (circle (0, 0), radius 1), Shape b (circle (1.5, 1.5), radius 1), The comparison (4.5 > 4), Verdict (apart, boxes overlap). In focus: Shape a, Shape b, The comparison, Verdict.Shape acircle (0, 0), radius 1Shape bcircle (1.5, 1.5), radius 1The comparison4.5 > 4Verdictapart, boxes overlap

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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

  1. Question 1 of 2 Two circles have radii 3 and 5. Their centres are 7 apart along x and 6 apart along y. Do they touch?

    Choose one answer.

    A hint

    Compare dx² + dy² with (r₁ + r₂)².

    Show the answer: Question 1 of 2

    Answer: No, the squared distance 85 is more than 64, the squared sum of the radii

    Compare squares: 7² + 6² = 85 and (3 + 5)² = 64. Since 85 > 64, the centres are further apart than the sum of the radii (about 9.2 against 8). Overlapping bounding boxes, or small distances on each axis, do not make circles touch.

  2. Question 2 of 2 shapes.hpp counts touching edges as a collision. What matters most about that rule?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Using the same rule in every test, so that a box resting on the floor never touches it in one test and floats in another

    With <=, boxes that share an edge collide; with <, they do not. Either is fine as long as every test uses the same one, or a resting box flickers between touching the floor and floating above it.

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 grid of square cells: circle A is tested only against circles in the 3 × 3 block of cells around it.A: the circle being tested, in the middle cellB: in the 3 × 3 block and touching A: a contactC: in the block, tested, but apart from AGrey circles: outside the block, never tested against AABC

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.

  1. Circle A has its centre in a cell near the middle. The 3 × 3 block of cells around that cell is shaded and outlined.
  2. 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.
  3. Circle C sits in the block's top-left cell: the grid tests it against A, and the exact test says they are apart.
  4. 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:

Every pair against a spatial hash grid

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");
}
10,000 circles, 10 frames, each compiler Python · broadphase_session.py
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

  1. Question 1 of 2 How many pair tests does testing every pair of 2,000 objects take in one frame?

    Type a number.

    A hint

    Every object against every other, each pair once.

    Show the answer: Question 1 of 2

    Answer: 1999000 tests

    n (n − 1) / 2 = 2,000 × 1,999 / 2 = 1,999,000. With five times as many objects, 10,000, it is 49,995,000: twenty-five times as many tests.

  2. Question 2 of 2 The largest circle in a game is 12 pixels across. Which cell width lets a search of the 3 × 3 block of cells around each circle find every touching pair?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Any width of 12 pixels or more

    Touching circles have centres at most one diameter apart, so with cells at least that wide their cells are the same or neighbours. That is why the lesson's grid uses cells of 12 pixels for circles of radius 2 to 6.

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:

one_step.cpp C++ · one_step.cpp
// 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);
    }
}
Three steps of a thrown ball, each compiler Python · one_step_session.py
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:

Interactive · step by step Three steps of a thrown ball, in two orders Step through one_step.cpp. Both methods add the same velocity change; they differ in which velocity moves the ball.
Three steps of a thrown ball, in two orders, step 5 of 5. Showing Semi-implicit: velocity (9.5095 m/s), Explicit: velocity (9.5095 m/s), Semi-implicit: height (0.4837 m), Explicit: height (0.4918 m), Exact height (0.4877 m after three). In focus: Semi-implicit: height, Explicit: height, Exact height.new velocityold velocitySemi-implicit: velocity9.5095 m/sExplicit: velocity9.5095 m/sSemi-implicit: height0.4837 mExplicit: height0.4918 mExact height0.4877 m after three

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

  1. Question 1 of 2 Which order does semi-implicit Euler use in each step?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The velocity first, from the acceleration, then the position, from the new velocity

    Semi-implicit Euler adds acceleration × dt to the velocity, then velocity × dt to the position, with the new velocity. Explicit Euler swaps the two lines, and that small change makes a spring gain energy on every step.

  2. Question 2 of 2 A crate 8 pixels wide moves 20 pixels per step towards a wall 6 pixels thick. Can it pass through the wall without any tested position overlapping it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Yes, because 20 pixels is more than the wall's 6 pixels plus the crate's 8

    Tunnelling needs a step longer than the obstacle's thickness plus the body's own width: 20 > 6 + 8 = 14, so some starting positions put one step's end before the wall and the next one beyond it.

Practise: circles near a box

near_box.hpp has two gaps:

  1. overlaps(Circle, Box): true when the circle touches or overlaps the box, including a circle inside it.
  2. 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.
The starter code and its sample tests

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;
}
The sample tests against the starter code Python · exercise_session.py
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.

  1. Question 1 of 9 When do two boxes whose sides are parallel to the axes overlap?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: When their ranges overlap on the x axis and on the y axis

    overlaps(Box, Box) checks a.min ≤ b.max and b.min ≤ a.max on x and on y, and all four comparisons must hold.

  2. Question 2 of 9 Why does shapes.hpp compare dx² + dy² with (r₁ + r₂)² instead of the distance with r₁ + r₂?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: Both sides are never negative, so their squares compare the same way, and no square root is needed

    A distance and a sum of radii are both at least 0, so comparing their squares gives the same answer as comparing them. The test becomes a few multiplications and one comparison.

  3. Question 3 of 9 To save time, a game decides that two circles collide whenever their bounding boxes overlap. Which pair does it get wrong?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: Circles of radius 1 at (0, 0) and (1.5, 1.5), which are apart although their boxes overlap

    A bounding box is a fine first filter, but the corners of a box hold no circle: diagonal neighbours can have overlapping boxes and no contact. Keep the exact squared-distance test as the narrow phase.

  4. Question 4 of 9 Which phase may report a pair of circles that turns out not to touch?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: The broad phase, whose pairs the narrow phase then tests exactly

    The broad phase may be generous but never stingy: a pair it drops is a collision lost, while an extra pair only costs one exact test in the narrow phase.

  5. Question 5 of 9 Every-pair testing makes 49,995,000 tests for 10,000 objects. About how many times as many does it make for 20,000 objects?

    Type a number.

    Show the answer: Question 5 of 9

    Answer: 4 times (anything from 3.99 to 4.01 counts)

    n(n − 1)/2 grows with the square of n: 20,000 × 19,999 / 2 = 199,990,000, almost exactly 4 times 49,995,000. Twice the objects, four times the tests.

  6. Question 6 of 9 The grid made about 3,100 times fewer tests than every pair, but its frames were only about 20 times faster. Why?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: Hashing cell coordinates and refilling the cells every frame cost time too

    Counting tests measures one part of the work. The grid also hashes each circle's cell and rebuilds the cells every frame, which is why 3,100 times fewer tests gave about 20 times less time with clang and 19 with GCC.

  7. Question 7 of 9 A fast ball sometimes passes through a thin wall. Which changes stop it?

    Choose every answer that is right.

    Show the answer: Question 7 of 9

    Answer:

    • Splitting each step into substeps that move less than the wall's thickness plus the ball's width
    • A swept test that finds the first moment of contact along the step

    Tunnelling happens because no tested position overlaps the wall. Shorter moves or a test along the whole path fix that. A longer step makes it worse, and the integrator changes where the ball goes, not whether a jump is tested.

  8. Question 8 of 9 A ball is thrown up at 12 m/s, with steps of 1/60 s and gravity −9.81 m/s². How high does semi-implicit Euler put it after the first step, in metres?

    Type a number.

    A hint

    Velocity first, 12 − 9.81/60, then the position from that velocity.

    Show the answer: Question 8 of 9

    Answer: 0.1973 m (anything from 0.1968 to 0.1978 counts)

    The velocity becomes 12 − 0.1635 = 11.8365 m/s, and the ball moves 11.8365 × 1/60 ≈ 0.1973 m. Explicit Euler would move it by the old velocity, 12 × 1/60 = 0.2 m.

  9. Question 9 of 9 What does a swept test find?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: The first moment along a step's path at which two shapes touch

    A swept test follows the path from the start of a step to its end and finds the first moment of contact, so a thin wall cannot be skipped. Substeps reach the same goal by making each move shorter.

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.

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.

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

  1. 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.

  2. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. 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.

    Model answer, Pro. First step and follow-ups

  4. 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?

    Model answer, Pro. First step and follow-ups

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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.