Continuous Collision Detection in Video Games using Binary Search

Published in Algorithms on

Fast-moving objects passing through walls is a well-known problem in video games. How can we solve it without compromising efficiency or precision?

Preface: Collision Detection in Video Games

This section gives a brief overview of how collision detection works in video games and introduces some terminology, so that we’re on the same page.

Collision Masks

Objects in video games usually have invisible shapes attached to them for the purpose of detecting collisions. These shapes are known as collision masks (or collision boxes).

We say two objects are in collision when their collision masks overlap. Rectangles, circles, or other simple convex shapes are preferred to be used as masks because they are faster to check for overlaps.

In the following image, imagine the filled shapes are game objects, and the dotted lines are their collision masks. The square object has a rounded square collision mask, and the triangle object has a circular collision mask.

Collision masks overlap indicates a collision
There is a collision only when collision masks overlap

Frame-by-frame Collision Detection

Imagine you are playing a game. You shoot a bullet parallel to the xx-axis (in the positive xx-direction) at a speed of 10 units per frame. If the bullet is at (x,y)(x, y) in the current frame, it’s going to be at (x+10,y)(x+10, y) in the next frame. The bullet does not move in a continuous path from (x,y)(x, y) to (x+10,y)(x+10, y), rather it teleports to the new position.

Frame-by-frame movement of a bullet

During each frame of the game, after updating the position of an object depending on the inputs and factors in the game world, its new position is compared with other objects to see if there is a collision. This frame-by-frame collision detection method is widely used because it’s simple, efficient, and goes well with the game loop pattern used commonly by game engines.

What is a Game Loop?

A game loop runs continuously during gameplay. Each turn of the loop, it processes user input without blocking, updates the game state, and renders the game. It tracks the passage of time to control the rate of gameplay.1

The key takeaway here is that a moving object passes through discrete points during its movement.

Tunneling

What if there is a wall between (x,y)(x, y) and (x+10,y)(x+10, y), and the collision masks of both the bullet and the wall are small enough that they do not collide? The bullet simply passes through the wall! This incident is known as tunneling.

The bullet tunnels through the wall

Hence, checking only for collision mask overlap at discrete points is not enough. Moving objects should check the volume covered in the continuous motion from one point to another, known as the swept volume2. This approach is known as Continuous Collision Detection (CCD).

No more tunneling!

Checking collision against swept volume

The Problem

With tunneling solved, we now have a working collision system. But there is one issue - precision.

Notice in the video that the bullet appears to be destroyed slightly before it actually touches the wall. This happens because our current collision detection only tells us if a collision occurred during the frame, but not exactly where.

Hence, we can’t perfectly time a “destroy” effect (like the “boom” in the video), or apply an effect on the wall. This is the problem we’re trying to solve - finding exactly where the bullet collides with the wall.

Formally, the problem can be stated as follows:

Let t0t_0 be the starting time of the current frame and t1t_1 be the starting time of the next frame. Assuming two objects collide in the period [t0,t1][t_0, t_1], determine the exact moment t[t0,t1]t\in [t_0, t_1] when the collision first happens.

Suppose the bullet object has a function, check_collision(t0, t1, obstacle), that returns whether there is a collision between the object obstacle and the swept volume of the bullet from time t0 to time t1. Somewhere in the bullet’s update cycle, you might see something like:

if (!check_collision(t0, t1, obstacle)) {
  move_forward();
} else {
  double t = find_collision_time(t0, t1, obstacle);
  double collision_x = get_x_at_time(t);
  double collision_y = get_y_at_time(t);
  apply_destroy_effect(collision_x, collision_y);

  destroy_self();
}

We are going to implement the find_collision_time function - which returns the exact time of collision.

While our example focuses on detecting the collision between a bullet and a wall, the same principle applies to any object that requires precise collision detection.

One simple approach is to check for collisions in small time steps between t0t_0 and t1t_1. If we choose a step size Δt\Delta t, the algorithm requires O(N)O(N) collision checks where N(t1t0)/ΔtN \approx \lceil (t_1 - t_0)/\Delta t\rceil. This means the time complexity of the linear search is O(S)O(S), as the number of checks (NN) scales linearly with speed.

For maximum accuracy, we need to set Δt=(t1t0)/S\Delta t = (t_1 - t_0) / S, where SS units per frame is the speed of the bullet.

double find_collision_time(double t0, double t1, Object obstacle) {
  const double dt = (t1 - t0) / this->speed;
  for (double t = t0; t <= t1; t += dt) {
    if (check_collision(t0, t, obstacle)) {
      return t;
    }
  }
  return t1;
}

While this approach works, it becomes inefficient for high-precision collision detection with many objects. The accuracy of the result depends on the step count. Higher step count produces more accurate results, but at the cost of more collision checks. Lower step count performs fewer collision checks, at the cost of reduced accuracy.

The trade-off between accuracy and performance is the limitation of linear search. This can quickly become impractical for object-heavy games or games that require precise collision detection.

Can we make this more efficient without sacrificing precision? Turns out, we can!

Let’s denote check_collision(t0, t, obstacle) as f(t)f(t). So f(t)f(t) is a function which returns 11 if the swept volume of the bullet in the time interval [t0,t][t_0, t] collides with an obstacle, and 00 otherwise.

Claim: If there is a collision in the swept volume during [t0,t][t_0, t], there will also be a collision in any later time interval [t0,t][t_0, t'] where ttt' \geq t. This can be proven formally, but the intuition is simple.

Proof

Let’s define the swept volume of an object over the time interval [T1,T2][T_1, T_2] as

V([T1,T2])=t[T1,T2]C(t)V([T_1, T_2]) = \bigcup_{t\in [T_1, T_2]}C(t)

where C(t)C(t) is the object’s collision shape at time tt.

If ttt'\geq t, then

V([t0,t])V([t0,t])V([t_0, t])\sube V([t_0, t'])

Therefore, if V([t0,t])WV([t_0, t])\cap W\neq \empty for an obstacle WW, then V([t0,t])WV([t_0, t'])\cap W\neq \empty for all ttt'\geq t.

In other words, f(t)f(t) is monotonically non-decreasing with respect to tt. This monotonicity allows us to use binary search on f(t)f(t). We can take the f(t)f(t) vs tt graph from our example to help with visualization:

Swept volume vs time graph is monotonically non-decreasing
f(t)f(t) is monotonically non-decreasing w.r.t. tt

The problem can thus be restated as the following:

Find the minimum t[t0,t1]t\in [t_0, t_1] such that f(t)=1f(t) = 1, where f(t0)=0f(t_0) = 0 and f(t1)=1f(t_1) = 1.

While we could mathematically calculate tt for simple shapes (like a line vs a rectangle), binary search is powerful because it works for any collision shape. We don’t need to derive new methods if we change the player’s mask from a rectangle to a complex polygon; we can run the same search.

The implementation uses a precision ϵ\epsilon to stop searching. I like to use a maximum cap of iterations in these scenarios because I don’t trust doubles.

Choosing ϵ\epsilon

One option is to use a constant value for ϵ\epsilon, like ϵ=104\epsilon = 10^{-4}.

Another option is to make ϵ\epsilon tied to the speed so that faster motion gets higher precision, like ϵ=(t1t0)/S\epsilon = (t_1 - t_0) / S.

I like to use a combination of both! The precision scales with speed, but clamped so that the value doesn’t get too low or too high.

ϵ=clamp((t1t0)/S,106,103)\epsilon = clamp((t_1 - t_0) / S, 10^{-6}, 10^{-3})

double find_collision_time(double t0, double t1, Object obstacle) {
  // Handle t0 separately - in case we are already colliding
  if (check_collision(t0, t0, obstacle)) return t0;

  // Handle (t0, t1]
  const double eps = clamp((t1 - t0) / this->speed, 1e-6, 1e-3);
  const int maxIterations = 1 + ceil(log2((t1 - t0) / eps));
  for (int i = 0; (i < maxIterations) && ((t1 - t0) > eps); ++i) {
    double mid = (t0 + t1) / 2.;
    if (check_collision(t0, mid, obstacle)) {
      t1 = mid;
    } else {
      t0 = mid;
    }
  }
  return t1;
}

The time complexity is O(log((t1t0)/ϵ))O(\log ((t_1 - t_0) / \epsilon)) which becomes O(logS)O(\log S) when ϵ1/S\epsilon \approx 1 / S.

This way we get high precision (determined by the epsilon), with minimal number of collision checks. The algorithm gives us a very precise result in O(logS)O(\log S) instead of O(S)O(S) in linear search.

Finding the point of collision using binary search

Bonus

  1. The swept volume is rarely required in practice. Most of the time, we approximate the movement as a ray (or multiple rays) instead of the full swept volume to check for collisions. This becomes a line segment intersection problem, which can be solved in O(1)O(1) for most needs. This method is known as raycasting.
    In some cases, raycasting can make you think hard about edge cases! You need to carefully consider the position and number of raycasts an object has to do. One such example is shown in the following image.
The limitation of simple raycasting: A center ray (red) misses the wall, causing the object to clip inside. Checking corners (green) or using a swept volume is required to detect this collision.
The bullet tunnels through the wall with only one raycast from the center
  1. The system can be expanded to a scenario where both colliding objects are in motion.
    Make one object the reference frame relative to which the other object is in motion. Then apply the same technique discussed above.

References

  1. “Game loop · Sequencing Patterns · Game programming Patterns.” https://gameprogrammingpatterns.com/game-loop.html (accessed Feb. 04, 2026).

  2. C. Ericson, Real-Time Collision Detection, 0 ed. CRC Press, 2004. doi: 10.1201/b14581.


Algorithms Game Dev