The quadtree (revisited)
The 2D game that I’m making is like a top-down view of a maze. The player can walk through the maze and shoot monsters. As for the implementation, I started out with a grid. At first, grid cells were either a wall or a floor, and things were incredibly simple. Then I scaled up the size of the grid cells, so that a single grid cell would contain multiple objects; items for the player to pick up and monsters to fight. Using a grid for spatial partitioning is quite alright, but it’s inefficient.
Some concerns I have regarding the use of grids:
- requires storage space regardless of whether the grid cell is empty or actually contains objects. In practice, lots of space is wasted
- the player can be on a grid cell boundary, meaning that you have to check multiple grid cells for collisions, which is a hassle
- player might hit a large object that spans multiple grid cells
A much better solution exists, namely the quadtree. The quadtree is a brilliant, academic, high performance solution for doing spatial partitioning in game codes. There is a lot to be read about the quadtree on the interwebs, but somehow it’s not enough.
The basics of the quadtree spatial partitioning algorithm are simple. You have a 2D space (the map) and there are objects that live within the bounds of that 2D space.
- if there are more than N objects in the 2D space, split the space into 4 quadrants
- reinsert objects into the quadrants
- recurse as needed
Objects that are in the same quadrant might collide. N is a small constant, typically between 3 and 8. For collision testing, we only need to select the right quadrant and do at most N/2 tests. This holds true regardless of how many objects exist in the game world.
Caveat #1 : Only leafs contain objects
As a consequence of the rules of the partitioning algorithm, objects always reside in leafs of the quadtree. The intermittent nodes do not contain any objects. There are plenty of bad examples to be found on the internet where people get this wrong. If you get this wrong, the code will not perform optimally because it will falsely detect collisions in objects that could never collide. And thus you are wasting cycles doing excess collision tests, and missing the point of why we are using a quadtree in the first place.
Caveat #2 : Objects have size
It’s rather logical that objects in a 2D space have size (ie. width and height). Consequently objects may cross quadrant boundaries, and thus objects may reside in multiple quadrants at the same time. This requires an implementation with pointers to objects, where multiple pointers may be referencing the same object. If you get this wrong, certain collisions will go undetected, ruining the game play.
Caveat #3 : Objects in motion
A quadtree can only hold objects at a fixed position. For objects that are in motion, this means that the quadtree represents a snapshot of a situation, a point in time. Consider a game running at 60 frames per second. Each frame is a snapshot in itself. When an object moves from left to right, the object will be in a different position in every single frame, creating the illusion of movement. Because the position changes, we have to update the quadtree on every frame. If you get this wrong, collision detection won’t work. Updating the quadtree involves removing the object and re-inserting it. These are relatively expensive operations, so in some cases you might as well simply recreate the entire quadtree from scratch for every frame.
Caveat #4 : Fast-moving objects
Related to the last caveat, things are getting tricky at high velocities.
Consider a fast-moving object at position
(x0, y0) at the start of the
frame. It’s traveling fast, and will be at
(x1, y1) by the end of the
frame. During this time step the object really is in two places at the
same time—and in all other positions in between as well. During the
time step, the dimensions of the object stretch from
(x0, y0) to
(x1, y1). Implementation-wise, an object now has two kinds of dimensions:
bounds used for drawing, and stretched bounds used for detecting collisions
by means of a quadtree.
If you get this wrong, fast-moving objects may well teleport right through
Caveat #5 : Wrapping worlds
I like the game world to wrap around (ie. keep walking left, and emerge on the right side of the map). Wrapping in all directions is called a toroidal topology. The quadtree has no provisioning for this. The best thing to do is to transform the object’s coordinates from world space to camera space (or “arena space”) and work with that. Now there are two kinds of object positions: world coordinates and the transformed coordinates. If you get this wrong, the world will not correctly wrap and you may get things as crazy as objects spontaneously popping in and out of view.
I’m not trying to scare you. The quadtree is a high performance partitioning device and well-worth the coding effort, but it’s also quite complicated to get right.