Collisions

Up to now I had collision detection implemented the brute-force way, meaning that essentially every object in the game was checked against every other object. I thought the number of objects would be small enough that this would be fine for the initial prototype. However, the number of objects might not be so small after all, so I thought about making collision detection more efficient by using a spatial hash map. And I wondered, how these two approaches compare in terms of performance. Let’s run a quick benchmark.

Note

You can find the full source code of the benchmark on GitHub.

Prerequisites

For simplicity’s sake, we’re looking at collisions between axis aligned bounding boxes (AABB) of objects in a 2D space only. Also, every game object has the same size and hence the same bounding box. Furthermore, every game object can collide with every other game object, we’re excluding things like collision layers, hitboxes and hurtboxes, and things like that. Let’s now look at the two ways of implementing collision detection we’re going to compare in our little benchmark.

The Brute-Force Way

Let’s say we have n n objects in a scene and we want to know which objects collide with each other. To do this the naive way, we check every object against every other object. This means that we need to perform n(n1)2 \frac{n \cdot (n-1)}{2} comparisons, which is O(n2) O(n^2) in terms of time complexity. This means quadratic growth in run time.

public class CollisionDetector
{
    public void DetectCollisions()
    {
        var gameObjects = [ ... ];

        for (var i = 0; i < gameObjects.Count; i++) {
            var a = gameObjects[i];
            for (var j = i + 1; j < gameObjects.Count; j++) {
                var b = gameObjects[j];

                // Check for collision between a and b
            }
        }
    }
}

The Spatial Hash Map Way

In order to reduce the number of comparisons, we will use a simple spatial partitioning technique that uses a uniform grid. This means, we divide the 2D space into a grid of equally sized cells. Each cell is then assigned the objects that overlap that cell. We then only need to check the objects in the same cells as the object we are checking against.

A uniform grid with some game objects indicated by their red-colored AABBs

Then, instead of storing an actual grid of objects, we will use a hash table to store the objects using the cell coordinates as the key, and a list of objects that overlap that cell as the value.

For brevity, we’ll omit the implementation of the spatial partitioning grid here. For now, let’s assume we have it available via a variable called grid, and that is has a method GetCells() which returns a collection of all its cells, each consisting of a list of objects that overlap that cell. We can then iterate over the cells and check the objects in each cell against each other. That’s similar to the brute-force approach, but we’re narrowing down the number of objects we need to check by looking at only the objects in the same cell.

The collision detection code from earlier would then look something like this:

public class CollisionDetector
{
    public void DetectCollisions()
    {
        var cells = spatialHashGrid.GetCells();

        foreach (var gameObjects in cells) {
            for (var j = 0; j < gameObjects.Count; j++) {
                var a = gameObjects[j];
                for (var k = j + 1; k < gameObjects.Count; k++) {
                    var b = gameObjects[k];
                    
                    // If not checked yet, check for collision between a and b
                }
            }
        }    
    }
}

Now let’s look at some numbers.

Benchmarking

Let’s run a benchmark to compare the spatial partitioning approach to the brute-force method. We’ll randomly place various numbers of objects in 2D space. The parameters are:

  • 640x360 pixels of 2D space (that’s the design resolution of the game)
  • 32x32 pixels object axis aligned bounding boxes
  • 64x64 pixels grid size
  • Two benchmarks, brute-force vs. spatial hash grid:
    • 10 to 100 objects, in steps of 10
    • 100 to 1000 objects, in steps of 100

The benchmarks are done using BenchmarkDotNet.

Here are the results, graphs are created using https://chartbenchmark.net/:

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26100.6899/24H2/2024Update/HudsonValley)
12th Gen Intel Core i7-12700H 2.70GHz, 1 CPU, 20 logical and 14 physical cores
.NET SDK 10.0.103
  [Host]     : .NET 10.0.3 (10.0.3, 10.0.326.7603), X64 RyuJIT x86-64-v3
  Job-ZSBTUS : .NET 10.0.3 (10.0.3, 10.0.326.7603), X64 RyuJIT x86-64-v3

Object Count from 10 to 100:

MethodObject CountMeanErrorStd Dev
‘Brute Force’10412.0 ns1.69 ns1.58 ns
‘Spatial Hash’10301.0 ns0.56 ns0.50 ns
‘Brute Force’201,280.2 ns7.76 ns6.87 ns
‘Spatial Hash’20712.8 ns2.33 ns2.07 ns
‘Brute Force’302,542.2 ns7.13 ns6.67 ns
‘Spatial Hash’301,171.0 ns6.07 ns4.74 ns
‘Brute Force’404,223.2 ns17.77 ns16.62 ns
‘Spatial Hash’401,669.5 ns14.82 ns13.86 ns
‘Brute Force’506,280.6 ns40.63 ns38.00 ns
‘Spatial Hash’502,214.4 ns13.24 ns12.39 ns
‘Brute Force’608,725.4 ns44.31 ns41.45 ns
‘Spatial Hash’602,865.4 ns20.96 ns18.58 ns
‘Brute Force’7011,605.5 ns117.24 ns109.67 ns
‘Spatial Hash’703,575.1 ns8.66 ns7.68 ns
‘Brute Force’8014,580.3 ns31.25 ns29.23 ns
‘Spatial Hash’804,344.4 ns17.80 ns15.78 ns
‘Brute Force’9018,122.7 ns42.93 ns40.15 ns
‘Spatial Hash’905,241.1 ns28.21 ns26.39 ns
‘Brute Force’10022,102.9 ns76.83 ns71.87 ns
‘Spatial Hash’1005,765.8 ns28.37 ns25.15 ns
Brute Force vs. Spatial Hash Grid Duration, 10 to 100 Objects

Brute Force vs Spatial Hash Grid Duration, 10 to 100 Objects

As expected, the brute-force approach is growing much faster than the spatial hash approach. Increasing the number of objects from 10 to 100 (a 10x increase) costs roughly 54x more time (412 ns to 22,103 ns). In contrast, the spatial hash approach grows much more slowly: the same 10x increase in objects only costs about 19x more time (301 ns to 5,766 ns). At n=10 the spatial hash is 1.4x faster, by n=100 it’s already 3.8x faster.

Now, let’s tighten the screws and increase the object count.

Object Count from 100 to 1000:

MethodObject CountMeanErrorStd Dev
‘Brute Force’10022.366 us0.0523 us0.0463 us
‘Spatial Hash’1005.689 us0.0243 us0.0215 us
‘Brute Force’20081.588 us0.3415 us0.3027 us
‘Spatial Hash’20018.732 us0.0579 us0.0484 us
‘Brute Force’300179.117 us0.5575 us0.4942 us
‘Spatial Hash’30044.896 us0.5860 us0.5481 us
‘Brute Force’400309.608 us0.5617 us0.4979 us
‘Spatial Hash’40068.248 us0.3117 us0.2916 us
‘Brute Force’500469.737 us0.6178 us0.5159 us
‘Spatial Hash’500118.365 us0.3170 us0.2966 us
‘Brute Force’600682.300 us1.9441 us1.7234 us
‘Spatial Hash’600143.733 us0.6471 us0.6053 us
‘Brute Force’700914.185 us3.2993 us2.9247 us
‘Spatial Hash’700208.231 us0.5437 us0.5085 us
‘Brute Force’8001,186.063 us4.5536 us3.8024 us
‘Spatial Hash’800294.344 us1.1251 us1.0524 us
‘Brute Force’9001,525.599 us9.7191 us7.5880 us
‘Spatial Hash’900308.287 us1.4430 us1.2792 us
‘Brute Force’10001,880.231 us18.4161 us16.3254 us
‘Spatial Hash’1000394.605 us1.2528 us1.1718 us
Brute Force vs. Spatial Hash Grid Duration, 100 to 1000 Objects

Brute Force vs Spatial Hash Grid Duration, 100 to 1000 Objects

From n=100 to n=1000 (10x more objects), the spatial hash time grows from ~5.7 µs to ~395 µs which is a ~69x increase for a 10x larger input. The brute force approach grows ~84x. At n=100, the spatial hash is 4x faster than the brute force approach, at n=1000 it’s ~4.7x faster.

While the spatial hash approach is still faster for larger input sizes, its advantage seems to be plateauing.

In my game, I’m expecting the number of game objects being simultaneously on the screen to be around between 100 and 200 tops. So, the spatial hash approach should be sufficient for my needs.

Other Optimization Approaches

The uniform grid is probably the easiest spatial partitioning technique to implement, but it has some limitations. There are other, more sophisticated techniques like Quadtree, or k-d trees.

Conclusion

We looked at two different collision detection approaches and compared their performance. The spatial hash approach seems a good choice for my game where I expect a moderate number of objects (100 to 200 max). However, for very large number of objects, other techniques might be more suitable.

Resources