Three Collision-Detection Ideas

Often, in games, you want to know if an object is touching another object, or if a laser is bouncing off a mirror, or if the player character is standing on something. When writing functions to check the collisions between objects, you probably want to keep in mind a few basic ideas.

Early Out

The first rule of collision detection is to avoid doing collision detection. This means eliminating potential collisions as soon as possible.

Broad Phase

Consider this very naive bit of collision-check code:

//No broad phase; does N*(N-1) checks:
for (Object &a : objects) {
	for (Object &b : objects) {
		if (&a == &b) break;
		collision_check(a,b);
	}
}

This code is fine -- it's not going to miss collisions -- but it's also expensive.

Instead, we'd rather have something like this:

//Add objects to a spatial grid:
Grid grid;
for (Object const &a : objects) {
	grid.add(a);
}
//Read out all pairs of objects that appear in the same cell:
std::unordered_set< Object *, Object * > potential_pairs;
for (Cell const &cell : grid.nonempty_cells) {
	for (Object *a : cell.objects) {
		for (Object *b : cell.objects) {
			if (a == b) break;
			potential_pairs.insert(std::make_pair(a,b));
		}
	}
}
for (std::pair< Object *, Object * > p : potential_pairs) {
	collision_check(*p.first,*p.second);
}

In the worst case, this code performs similarly to the code above. But in most game situations objects are spread out in the world, and a grid can really cut down on the number of collision checks required.

One subtlety: object size vs grid cell size. Bit objects need to be referenced in lots of grid cells, while lots of small objects might leave more

In fact, there's no reason to re-build the grid every frame. One could instead update it as objects are moved. This can be a big savings for, e.g., static level geometry.

Indeed, a simpler but still valid idea might be to precompute a grid of all static objects. E.g., in a tile-based platformer, the tilemap is already such a grid.

Temporal Continuity

But one can go further with temporal continuity. If your collision check function can give a bound on the time until it needs to check again, you can maintain a list of objects whose checks can be skipped:

static std::unordered_map< std::pair< Object *, Object * >, float > to_skip;
for (std::pair< Object *, Object * > p : potential_pairs) {
	auto f = to_skip.find(p);
	if (f != to_skip.end()) {
		if (f->second < now) {
			to_skip.erase(f);
		} else {
			continue;
		}
	}
	float time_to_collision = collision_check(*p.first,*p.second);
	if (time_to_collision > 0.0f) {
		to_skip[p] = now + time_to_collision;
	}
}

Of course, there's a trade-off here: you need to make your collision function more complicated (including reasoning about maximum possible accelerations), and your code is paying the hash lookup and maintenance time penalties as well.

Overall, think of broad phases as something you add when you need them (or when they are very easy -- e.g. tilemaps) Because, in certain scenarios, they can result in irregular memory access and computational overhead that end up slowing down collision checking. E.g., according to a talk by AMD folks at Eurographics some years ago, if you are doing AABB tests on the GPU, a broad phase can actually slow things down until you have more than 1000 objects!

Simple First

Say you have a triangle mesh with thousands of elements. It makes no sense to check each of these triangles if you can eliminate a possible collision by first checking a bounding box (or sphere).

void collision_check(Object &a, Object &b) {
	if (aabb_vs_aabb(a,b)) {
		detailed_check(a,b);
	}
}

Modern games also take this one step further, using proxy objects that are lower fidelity than the displayed meshes, for all collision checks. That is, the very high-detail render meshes are never checked against at all.

This idea even carries into the checks themselves -- if there are easier and harder cases to check, check the easy ones first.

Mathematical Underpinnings

Convex sets are sets such that any line segment between two points in the set is contained in the set. Convex sets are really useful to think about for collision detection because they are mathematically nice objects.

Most objects in a game can be decomposed into convex sets.

Minkowski Sum

Define "$+$" on sets $A$ and $B$ as $$A + B \equiv \{x + y \;|\; x \in A, y \in B\}$$ This is the "Minkowski Sum".

This is very useful when thinking about collision problems, as it is often easier to think about whether $A-B$ contains the origin than if $A$ intersects $B$.

In addition, the Minkowski sum of two convex sets is convex, which is the basis of the GJK algorithm.

Separating Axis

If convex objects don't intersect, there is some axis such that (when projected to this axis) the objects don't intersect. That is, given bounded, closed, convex sets $A$, $B$ that don't intersect, there must be some direction $d$ such that $$\forall x\in A, y \in B:\; x\cdot d < y \cdot d$$.

Many collision checking algorithms boil down to figuring out which directions are reasonable to test and then testing them. For instance, AABB vs AABB tests check the x,y, and z axes.

Continuous Collision Detection

Sweeping a convex set along a line produces a convex set. You can use this as the basis for continuous collision detection functions. (See also: GJK algorithm.)

Voronoi Regions

When developing a collision check, it can help to think about the space around an object as being divided into regions based on what part of the object is nearest. For instance, in checking a sphere vs a box, it makes sense to think about faces, edges, and vertices on the cube.

Distance Fields

If you can come up with a mathematical function that gives the signed distance to the surface of your object, you can immediately perform a collision check against a point or sphere.

Consider using tabulated distance fields as proxies for otherwise hard-to-check-against goemetry that doesn't need to be represented perfectly.

Inside instead of Outside

In some situations, it actually makes more sense to think about tracking a player or object inside another object, rather than trying to keep it outside the object.

This can turn out to be a very powerful view, because one only needs to track the current thing the player is inside, and -- if the player leaves -- figure out which thing to move them to.

Walk Meshes provide a good example of objects it makes sense to think about in this sense.