Wednesday, March 27, 2013

Yet Another Quad Tree Tutorial

There was nothing to show for Screenshot Saturday this week, but that doesn't mean there hasn't been any progress! I spent the weekend writing a quad-tree implementation and integrating it into the existing engine. It was not a very hard process, and there are already dozens of tutorials available online for this, but this guide may still come in handy for someone.

How Do Quad Trees Work

You may be familiar with a binary tree. Essentially there's a "root" structure (known as a "node") that branches out into two nodes (binary = two). Then, each of these nodes splits into two of their own. This continues as long as necessary. The nodes that aren't split are called "leaf" nodes.
Quad trees work exactly the same way, except the nodes are split into four parts rather than two. This is useful for video games because the screen is a rectangle, and can thus easily be split into smaller subsequent rectangles. Here's a screenshot of a stress-test quad tree in progress:


The small, 8x8 quads represent particles with physics. Each quad you see can contain a maximum of 32 (an arbitrary value) particles before it automatically splits into 4 nodes. The tree cannot go more than 6 levels deep, though, because that would eventually cause a stack overflow due to the recursion inherent to quad trees.

You can see this live, in-action by downloading a demo here (pardon the dependencies, I used my old wrapper classes to write this). Hold keys to generate particles, or use the mouse to set them in certain locations. Right-click the mouse to test for a collision with the large blue quad. The result will show up in the console window as a 1 (true) or 0 (false). You can notice that the operation is performed incredibly quickly, even with an insane amount of particles on the screen. That is the power of a quad tree.

Note: If you get DLL errors, you may need to install the VC++ redistributable from here.



Nodes

At the core of a quad tree is each individual quad itself, called a node. This node should contain information about the collide-able objects it holds, its dimensions, its depth, its child nodes (if any), its parent node (if any). Here is a sample bare-bones quad tree node structure:


This is the structure we'll be using in this tutorial. For a more full-featured node class, you can see the actual implementation I use in IronClad here.

Recursion

Quad trees rely heavily on recursion. If you want to insert an object into the tree, you have to check the root node for children, then check which child the object belongs in, and then repeat that for every child in the child until you reach the leaf node, which has no children. This is easily modeled with recursion. One of the issues that arises with this is a stack overflow, which happens when you are too nested in the tree. This is why a depth limit is necessary; I use a depth limit of 6.

The CQuadTree Class

This class will essentially act as a wrapper around a single node, the root node. It will also provide the recursive functions for insertion, deletion, and collision detection. Here is the class definition we will be using in this tutorial. For the sake of brevity and not splitting this into separate files per method, here is the entire class with explanations as comments within it. The implementation, though, will be split up into parts so I can explain each part in full.


Step 1: Splitting

The primary appeal of a quad tree is the ability to split into sub-nodes as needed, so this needs to be done first, for insertion to work properly. The process looks complicated, but is actually fairly simple. All nodes have a QTNode** member, which is a pointer to QTNode*. This is used to create an array of 4 QTNode*'s, each of which is then used to create a new QTNode. It's much cleaner to use an std::vector<QTNode*>, but for simplicity sake we'll do this.
So, after we've created the nodes, we set some default parameters (depth, children, etc) then give each one its respective dimensions, based on the parent node. The position is offset properly, width and height are divided in half, then made 1 pixel larger than usual to account for integer division errors that occur as depth increases. Each child node then gets the objects from the parent node that belong to it.
Comments in the code should further explain what's going on:


Step 2: Insertion

First and fore-most is the necessity of being able to insert objects into the quad tree.To do this, we first check to see if a node has any children. If it does, it checks to see if the object "fits" into any of the children, via collision detection on the node. On the child that it does fit into, the method calls itself, but using the child as the starting node (recursion!). If the node has no children, the object is added to the nodes list of objects, but only if there is space. If the node is full, it calls Split() on the node and then calls RInsert again, using itself as the starting node since it now has children.

That's the English explanation, and you're probably thinking "what." But let's take a look at the code.


Step 3: Removal

Often times objects get destroyed, whether that be a particle whose time has expired, or an enemy tank that has blow up. In that case, we would want to remove this object from the quad tree, so collisions with it no longer occur (though not necessarily for a blown up tank, if it doesn't disappear, but that's another matter entirely). The algorithm basically follows the same pattern as insertion.


Step 4: Collision Detection

Now we can finally implement the actual thing quad trees are used for, collision! Once again, this is done in a recursive fashion similar to that of RRemove() and RInsert().


Step 5: Updating the Tree

Of course, objects aren't static. Most will move around, be it an enemy tracking the player, bullets flying, or simply the camera panning through the scene. Thus, it's necessary to update the quad tree as objects move. The simplest, and least efficient way, would be to just remove all of the objects, and then insert them again. A much more effective method would be to have the objects themselves keep track of whether or not they have moved since the last frame. In IronClad, this is done with the entity marking a flag (m_update) if CRigidBody::Move() is called with values that are different than the current position, or the m_Force vector before the CRigidBody::Update() call is non-zero. Then, in CQuadTree::Update(), the tree will RInsert() updated objects again by moving up the tree from the old node (CRigidBody::mp_CurrentNode).

A further optimization would be not only to update objects that have moved, but to check the likelier nodes first, rather than starting from the root again. This is done by calling RInsert() on the object with the starting node being the parent of the current node. For example, if a particle moves 5px per frame, and it's exited the current node, the likeliest place it would now be would be another one of the child nodes of the parent.

This is probably the slowest operation in the quad tree, due to the fact that as more objects move (and the farther they move from their original place), the more RInsert() calls have to be performed. I haven't tested this extensively, so I am not sure if a stack overflow could occur due to excessive recursion. It's likely that it won't, since each object needs 6 calls (QT_MAX_DEPTH) at the most to find a proper node, and will usually need just 1 call.

Here is the code, with further explanation:



Using the Quad Tree

Now that we've implemented the quad tree and all of it's functions, it's very easy and simple to use efficiently. For example, if you are loading a level, it might look something like this:

CQuadTree QT;
CLevel Level1("Level1.dat");
const std::vector<CEntity*>& objs = Level1.GetPhysicalObjects();
for(size_t i = 0; i < objs.size(); ++i)
    QT.Insert(objs[i]);

Later, if you wanted to, for example, not allow the player to move if he's colliding with any objects in the level, you could just do


CEntity* pResult = QT.Collides(Player);
if(pResult == nullptr) Player.Move(requested_position);

Simple as that!
You don't necessarily need a single quad tree for the entire game world, either. You could have a separate one containing level objects, another containing enemies, another for particles, etc. That way it'd be even more efficient in terms of what you are trying to test collision for.

That's all for this tutorial, feel free to include comments and questions below! If you'd like to check out the official implementation of a quad tree in IronClad you can check out the full header here and the source code here.

4 comments:

  1. Maybe you have your quadtree testing app with time mesurements in ms (lets say 10000 objects). I woul like to compare results with my algorithm.

    ReplyDelete
    Replies
    1. Excellent idea :)

      I compiled my engine into a DLL library in release mode and linked it to my testing executable, also in release mode.

      The test creates 1000000 entities, plus one more for collision, and moves them to a random place in the "field," which is 800x600. It makes them have 32x32 bounding-box rects. Then, timing starts. Each object is inserted into the quad tree invidually. Then, a single collision request is made on an object.

      The test code can be found here.

      My machine specs:
      Windows 7 Ultimate 64-bit
      Intel i5-3570K @ 3.40GHz
      8GB DDR3 Memory
      nVidia GeForce 450GTS 1GB GDDR5

      Average execution time of 10 tests on my machine: 758ms.

      See here for a screenshot of the output.

      Delete
    2. I'm curious about why your collision function returns only a single colliding entity. It seems to me that you'd frequently need to perform collision between a single moving entity and multiple other entities (for instance the player entity could bump into two trees at once).

      Delete
    3. Hey Sparky, sorry it took me so long to approve your comment.

      I haven't integrated this project into any of my games, so that issue hasn't really risen. But you're absolutely right; it should probably return a set of entities that received a collision.

      Delete