For strategy games, with potentially hundreds of units running about at once, nearest neighbour searching is a non-trivial problem. This is when a unit must check who its neighbours are as efficiently as possible to make decisions about movement and combat.
If there are only ever going to be a handful of units in the game at any one time, then a linear search is probably fine. A linear search is when code examines a list of items, one by one, until it finds what its looking for. This is fine for a shopping list, but not necessarily for checking hundreds of units multiple times a second. If 100 units are checking 100 units every time, that’s a lot of checks!
To investigate, I created 300 units, noting frame rate as a heuristic for code performance. When collision detection was switched off the simulation fluctuated around 90 Frames Per Second (FPS). Not bad. However, when collision detection was activated, using a linear search, this reduced the frame rate to 10. Clearly something had to be done.
The ideal solution is spatial partitioning, using an octree. This means that the game world is encompassed by a huge box (octant), and that box contains eight smaller boxes, and each of those boxes is divided recursively into eight more, etc. Each octant logs which units are contained within, adding units when they enter the box, and removing them when they exit.
This solution allows recursive searching up and down the tree, to find the boxes relevant to the space being searched. This is especially powerful with a dynamic octree, meaning octants only exist where units exist, significantly reducing the number of octants that need to be searched. Instead of checking a three hundred item list, we just check the contents of a dozen or so boxes.
In the below image octants are visualised using cyan and magenta. The dynamic octree must prune itself, and so when an octant has no units inside it is placed on a list of vacant octants. Vacant octants have a timer, and after a few seconds of disuse are deleted. This ensures the octree never gets too big, and allows octants to be reused where units are most likely to move again.
The proof is in the pudding. But in this case, the pudding is code, and the metaphor is broken. Regardless, it shouldn’t be hard to improve upon a measly 10 FPS. The octree was applied to all unit movement and combat neighbour checks. The latest results are as follows:
1 unit 270-280 FPS
10 units 250-260 FPS
100 units 130-160 FPS
200 units 90-110 FPS
300 units 70-80 FPS
400 units 50-60 FPS
500 units 30-40 FPS
These figures can undoubtedly still be improved, and they demonstrate the efficacy of spatial partitioning, compared to a naive solution like linear search. The octree also provides the foundations upon which a deterministic raycaster can be built.
One optimisation has been to sort units inside octants by team, and to push those lists up the tree recursively. What this means is that units searching for nearby enemies may only have to check up the tree, until they find the octant which encloses their maximum range. If there are no units belonging to a hostile team inside that octant, then the search is resolved. The alternative is suboptimal: to move up and then down the octree, searching every unit in every octant for whether they belong to a hostile team.