Quadtree for moving objects

This is my first Quadtree implementation and I’m not too impressed with it :frowning:

I started out because I wanted to speed up my Boids application (http://www.red3d.com/cwr/boids/).

  1. Boids move around
  2. Boids adapt their movement based on their closest neigbours

Without any form of spatial organisation I had a O(n^2) complexity for each update

Enter the StaticQuadtree…

My StaticQuadtree generates all lower levels of the tree on instantiation.
A StaticQuadtree with 1 level is basically the same as my original Vector.
After some performance tests I noticed that generating more than 5 levels starts to decrease performance. Especially when removing Boids from the Quadtree when they move and when searching in an area that contains several sub levels.

Is that a normal number of levels for a Quadtree, or can this code be improved by multitudes after a few simple optimizations? :slight_smile:

Also, am I correct in asuming that I have to remove and re-add all boids during update?
Wouldn’t it be easier (quicker even) to regenerate the Quadtree before each update?

My first implementation:


import java.util.Collection;
import java.util.Vector;

public class StaticQuadtree implements Quadtree {
	private final static int DEFAULT_LEVELS = 5;
	private final Collection<Entity> entities = new Vector<Entity>();
	private final Node head;

	public StaticQuadtree() {
		this(DEFAULT_LEVELS);
	}
	public StaticQuadtree(int levels) {
		if (levels < 0) throw new IllegalArgumentException();
		head = new Node(levels, new BoundingBox(-256, -256, 256, 256));
	}

	@Override
	public synchronized int size() {
		return entities.size();
	}
	@Override
	public synchronized Collection<Entity> get() {
		return new Vector<Entity>(entities);
	}
	@Override
	public synchronized void get(BoundingBox bb, Collection<Entity> result) {
		head.get(bb, result);
	}
	@Override
	public synchronized void add(Entity entity) {
		if (entities.contains(entity)) throw new RuntimeException();

		entities.add(entity);
		head.add(entity);
	}
	@Override
	public synchronized void remove(Entity entity) {
		if (!entities.contains(entity)) throw new RuntimeException();

		entities.remove(entity);
		head.remove(entity);
	}

	@Override
	public String toString() {
		return "[QT "+ head +"]";
	}

	private final class Node {
		private final int level;
		private final BoundingBox bb;
		private final Collection<Entity> entities;
		private final Node minmin, minmax, maxmin, maxmax;

		public Node(final int level, BoundingBox bb) {
			if (level < 0) throw new IllegalArgumentException();
			this.level = level;
			this.bb = bb;

			if (level > 0) {
				entities = null;
				final double dx = (bb.maxX - bb.minX)/2;
				final double dy = (bb.maxY - bb.minY)/2;
				minmin = new Node(level-1, new BoundingBox(bb.minX,    bb.minY,    bb.maxX-dx, bb.maxY-dy));
				minmax = new Node(level-1, new BoundingBox(bb.minX,    bb.minY+dy, bb.maxX-dx, bb.maxY));
				maxmin = new Node(level-1, new BoundingBox(bb.minX+dx, bb.minY,    bb.maxX,    bb.maxY-dy));
				maxmax = new Node(level-1, new BoundingBox(bb.minX+dx, bb.minY+dy, bb.maxX,    bb.maxY));
			} else {
				entities = new Vector<Entity>();
				minmin = null;
				minmax = null;
				maxmin = null;
				maxmax = null;
			}
		}

		public synchronized int size() {
			if (level == 0) {
				return entities.size();
			}
			int sum = 0;
			sum += minmin.size();
			sum += minmax.size();
			sum += maxmin.size();
			sum += maxmax.size();
			return sum;
		}
		public synchronized void get(Collection<Entity> result) {
			if (level == 0) {
				result.addAll(entities);
			} else {
				minmin.get(result);
				minmax.get(result);
				maxmin.get(result);
				maxmax.get(result);
			}
		}
		public synchronized void get(BoundingBox bb, Collection<Entity> result) {
			if (!this.bb.intersects(bb)) throw new RuntimeException();

			if (level == 0) {
				for (Entity entity : entities) {
					if (bb.contains(entity.pos)) {
						result.add(entity);
					}
				}
			} else {
				if (minmin.bb.intersects(bb)) minmin.get(bb, result);
				if (minmax.bb.intersects(bb)) minmax.get(bb, result);
				if (maxmin.bb.intersects(bb)) maxmin.get(bb, result);
				if (maxmax.bb.intersects(bb)) maxmax.get(bb, result);
			}
		}
		public synchronized void add(Entity entity) {
			if (!this.bb.contains(entity.pos)) throw new RuntimeException();

			if (level == 0) {
				entities.add(entity);
			} else {
				if (minmin.bb.contains(entity.pos)) minmin.add(entity);
				if (minmax.bb.contains(entity.pos)) minmax.add(entity);
				if (maxmin.bb.contains(entity.pos)) maxmin.add(entity);
				if (maxmax.bb.contains(entity.pos)) maxmax.add(entity);
			}
		}
		public synchronized void remove(Entity entity) {
			if (!this.bb.contains(entity.pos)) throw new RuntimeException();

			if (level == 0) {
				entities.remove(entity);
			} else {
				if (minmin.bb.contains(entity.pos)) minmin.remove(entity);
				if (minmax.bb.contains(entity.pos)) minmax.remove(entity);
				if (maxmin.bb.contains(entity.pos)) maxmin.remove(entity);
				if (maxmax.bb.contains(entity.pos)) maxmax.remove(entity);
			}
		}

		@Override
		public String toString() {
			StringBuffer sb = new StringBuffer();
			if (entities != null) sb.append(entities);
			if (minmin != null) sb.append(minmin);
			if (minmax != null) sb.append(minmax);
			if (maxmin != null) sb.append(maxmin);
			if (maxmax != null) sb.append(maxmax);
			return "[Node "+ sb.toString() +"]";
		}
	}
}

final class Entity {
	public Point pos;
	public Point vel;

	public Entity(double x, double y) {
		this.pos = new Point(x, y);
		this.vel = new Point(0, 0);
	}

	@Override
	public String toString() {
		return "[Entity "+ pos.x +":"+ pos.y +"]";
	}
}

final class Point {
	public final double x, y;

	public Point(double x, double y) {
		this.x = x;
		this.y = y;
	}
}

final class BoundingBox {
	public final double minX, minY;
	public final double maxX, maxY;

	public BoundingBox(double minX, double minY, double maxX, double maxY) {
		if (minX >= maxX) throw new IllegalArgumentException();
		if (minY >= maxY) throw new IllegalArgumentException();

		this.minX = minX;
		this.minY = minY;
		this.maxX = maxX;
		this.maxY = maxY;
	}

	public boolean contains(double x, double y) {
		return (minX < x && x <= maxX &&
				minY < y && y <= maxY);
	}
	public boolean contains(Point that) {
		return contains(that.x, that.y);
	}
	public boolean intersects(BoundingBox that) {
		return (minX < that.maxX && that.minX <= maxX &&
				minY < that.maxY && that.minY <= maxY);
	}
}

Stay tuned for my DynamicQuadtree update… :slight_smile:

Those both sound like bad ideas to me. I’d suggest you update the quad tree incrementally - when an object moves first check if it’s still within the same node, if it is then you can leave it where it is. If it’s changed then you can search back up the tree until you find a node that does fit, then go down again to find the leaf node to insert it into. For the typical case where an object crosses a boundary you’ll only have to search a few nodes rather than doing a complete insertion.

Because my Boids fly in flocks and doesn’t spread out evenly I think an evenly distributed Quadtree would be a waste.

Enter the DynamicQuadtree:

My DynamicQuadtree is in many ways identical to the StaticQuadtree. However it uses lazy-load to generate the lower layers. A new layer is generated when more then a specified number of Entities has been added to a Node.

Many things improved directly:

  • Inital creation of a Quadtree is now very quick
  • Searching in an empty tree is now much quicker
    However:
  • Add and remove operations are basically unaffected :frowning:
    64k Entities take ~20 sec to add and ~10sek to remove

Can anyone direct me in the way to improve this?
Are there more parameters than bucket size and tree depth?


import java.util.Collection;
import java.util.Vector;

public class DynamicQuadtree implements Quadtree {
	private final static int DEFAULT_SPLIT_SIZE = 5;
	private final static int DEFAULT_LEVELS = 5;
	private final Collection<Entity> entities = new Vector<Entity>();
	private final Node head;

	public DynamicQuadtree() {
		this(DEFAULT_SPLIT_SIZE, DEFAULT_LEVELS);
	}
	public DynamicQuadtree(int splitSize, int levels) {
		if (splitSize < 1) throw new IllegalArgumentException();
		if (levels < 0) throw new IllegalArgumentException();
		this.head = new Node(splitSize, levels, new BoundingBox(-256, -256, 256, 256));
	}

	@Override
	public synchronized int size() {
		return head.size();
	}
	@Override
	public synchronized Collection<Entity> get() {
		return new Vector<Entity>(entities);
	}
	@Override
	public synchronized void get(BoundingBox bb, Collection<Entity> result) {
		head.get(bb, result);
	}
	@Override
	public synchronized void add(Entity entity) {
		if (entities.contains(entity)) throw new RuntimeException();

		entities.add(entity);
		head.add(entity);
	}
	@Override
	public synchronized void remove(Entity entity) {
		if (!entities.contains(entity)) throw new RuntimeException();

		entities.remove(entity);
		head.remove(entity);
	}

	@Override
	public String toString() {
		return "[QT "+ head +"]";
	}

	private final class Node {
		private final int splitSize;
		private final int joinSize;
		private final int level;
		private final BoundingBox bb;
		private Collection<Entity> entities;
		private Node minmin, minmax, maxmin, maxmax;

		public Node(int splitSize, int level, BoundingBox bb) {
			if (level < 0) throw new IllegalArgumentException();

			this.splitSize = splitSize;
			this.joinSize = splitSize*2;
			this.level = level;
			this.bb = bb;
			this.entities = new Vector<Entity>();
		}

		public int size() {
			if (entities != null) {
				return entities.size();
			} else { // is split
				int sum = 0;
				sum += minmin.size();
				sum += minmax.size();
				sum += maxmin.size();
				sum += maxmax.size();
				return sum;
			}
		}
		public void get(Collection<Entity> result) {
			if (entities != null) {
				result.addAll(entities);
			} else { // is split
				minmin.get(result);
				minmax.get(result);
				maxmin.get(result);
				maxmax.get(result);
			}
		}
		public void get(BoundingBox bb, Collection<Entity> result) {
			if (!this.bb.intersects(bb)) throw new RuntimeException();

			if (entities != null) {
				for (Entity entity : entities) {
					if (bb.contains(entity.pos)) result.add(entity);
				}
			} else { // is split
				if (minmin.bb.intersects(bb)) minmin.get(bb, result);
				if (minmax.bb.intersects(bb)) minmax.get(bb, result);
				if (maxmin.bb.intersects(bb)) maxmin.get(bb, result);
				if (maxmax.bb.intersects(bb)) maxmax.get(bb, result);
			}
		}
		public void add(Entity entity) {
			if (!this.bb.contains(entity.pos)) throw new RuntimeException();

			if (entities != null) {
				entities.add(entity);

				if (entities.size() >= splitSize && level > 0) { // split
					final Collection<Entity> tmpEntities = new Vector<Entity>(entities);

					entities = null;
					final double dx = (bb.maxX - bb.minX)/2;
					final double dy = (bb.maxY - bb.minY)/2;
					minmin = new Node(splitSize, level-1, new BoundingBox(bb.minX, bb.minY, bb.maxX-dx, bb.maxY-dy));
					minmax = new Node(splitSize, level-1, new BoundingBox(bb.minX, bb.minY+dy, bb.maxX-dx, bb.maxY));
					maxmin = new Node(splitSize, level-1, new BoundingBox(bb.minX+dx, bb.minY, bb.maxX, bb.maxY-dy));
					maxmax = new Node(splitSize, level-1, new BoundingBox(bb.minX+dx, bb.minY+dy, bb.maxX, bb.maxY));

					for (Entity tmpEntity : tmpEntities) {
						add(tmpEntity);
					}
				}
			} else { // is split
				if (minmin.bb.contains(entity.pos)) minmin.add(entity);
				if (minmax.bb.contains(entity.pos)) minmax.add(entity);
				if (maxmin.bb.contains(entity.pos)) maxmin.add(entity);
				if (maxmax.bb.contains(entity.pos)) maxmax.add(entity);
			}
		}
		public void remove(Entity entity) {
			if (!this.bb.contains(entity.pos)) throw new RuntimeException();

			if (entities != null) {
				entities.remove(entity);
			} else { // is split
				if (minmin.bb.contains(entity.pos)) minmin.remove(entity);
				if (minmax.bb.contains(entity.pos)) minmax.remove(entity);
				if (maxmin.bb.contains(entity.pos)) maxmin.remove(entity);
				if (maxmax.bb.contains(entity.pos)) maxmax.remove(entity);

				int sum = 0;
				sum += minmin.size();
				sum += minmax.size();
				sum += maxmin.size();
				sum += maxmax.size();
				if (sum < joinSize) {
					entities = new Vector<Entity>();
					minmin.get(entities); minmin = null;
					minmax.get(entities); minmax = null;
					maxmin.get(entities); maxmin = null;
					maxmax.get(entities); maxmax = null;
				}
			}
		}

		public String toString() {
			StringBuffer sb = new StringBuffer();
			if (entities != null) sb.append(entities);
			if (minmin != null) sb.append(minmin);
			if (minmax != null) sb.append(minmax);
			if (maxmin != null) sb.append(maxmin);
			if (maxmax != null) sb.append(maxmax);
			return "[Node "+ sb.toString() +"]";
		}
	}
}

Not sure what you mean by “evenly distributed”, but the whole point of a quadtree is to localize queries. Skimming your code, it looks way over complex with lots of redundent data.

I tried to say that if most of my Boids fly around in the top right corner. It would be better to have a high resolution in that corner and a low resolution in the other corners. No need to generate the nodes where there are no Boids.

Ah! But that’s the whole point. Otherwise you just have a 2D array with an overly complex interface (and memory requirements). Surely there is some existing Java code you could look at.

That is exactly how a quadtree normally works - if you rebuild the quadtree from scratch every frame based on the data you’re attempting to classify, which is all you have to do. So do that. You’ll then find that the quadtree nodes end up being quite a lot of garbage, so you might want to implement a quadtree node factory that uses a pool.

Cas :slight_smile:

How did my “on-demand tree” become a 2D array?
Exactly why is there a point in having n levels of populated Quadtree if the parent can handle the request?

Please try to be more specific about the parts that are “way over complex with lots of redundent data”.
I’ve tried to write this as straightforward as possible so that others can read it and help me improve it.

This sounds like something that will help during move (remove/add).
Now I get odd profiling results. One of my tests (add 64k of Entities, remove them) generally executes in 30 sec. However every once in a while it takes almost a minute.

WRT: Being a 2D array that was in reference to “even distributed” not the code, I should have been more clear.

WRT: Being overcomplex. Examples: Nodes is taking on two rolls, that of an interior node and a leaf. Since your only storing data in leaves the code and data size can be reduced by breaking in two. ‘splitSize’, ‘joinSize’ and ‘bb’ could be replaced by a reference to the tree and a chosen coordinate. (The extent is implicit from the overall size and the depth). This should speed up construction time.

The reason your remove is slow, is that you remove from a Vector.

First you have to search the vector (on average you have to check 50% of the items), and them you have to remove the item (on average you have to shift 50% of the remaining items).

If you replace the Vector by a HashSet, you’d get far better results.

…adding will be a tad slower though…

Also, your code will gloriously fail when you add N items (where N >= splitSize) at the exact same location.

It will split until a StackOverflow occurs.

In fact remove the check for contains() as well - it’s not actually necessary for the correct implementation of the quadtree.

Cas :slight_smile:

Nah, it does a contains() on the entity position, which is a normal optimisation.

splitSize and joinSize are now moved to the Quadtree class. Still available to the inner class. Thanks! :slight_smile:
I will try to change the BB class into a set of static methods that will work with the “level” and one point. Not sure how readable the code will be however…

Whoa!

After changing Vector to HashSet the add time was reduced from ~17sec to ~200ms
Remove time is reduced from ~13sec to ~4sec
Search times are approximately doubled

Odd… :slight_smile:

Regarding the performance loss on ‘search times’…

You can turn your QuadTree in two-state datastructure.

While adding/removing, use a HashSet.
Once everything has settled, copy all items in your HashSet into an ArrayList.
While searching, use the ArrayList.
Once you need to add/remove again, convert it to a HashSet again.

The conversion shouldn’t be too much of a performance-drain. At least it is increasing in nearly linear time with more entities, while the savings are exponential.

Hi again,

I made a reference implementation of my Quadtree : FlatQuadtree. Basically a wrapper for a single HashSet.
The performance gain compared to my other Quadtrees doesn’t seem to be as great as I had hoped. :frowning:

With FlatQuadtree: 150 Boids @ 60hz
With either Static/DynamicQuadtree: 190 Boids @ 60hz


import java.util.Collection;
import java.util.HashSet;

public class FlatQuadtree implements Quadtree {
	private final Collection<Entity> boids = new HashSet<Entity>();

	@Override
	public void add(Entity entity) {
		boids.add(entity);
	}

	@Override
	public Collection<Entity> get() {
		final Collection<Entity> result = new HashSet<Entity>();
		get(result);
		return result;
	}

	@Override
	public void get(Collection<Entity> result) {
		result.addAll(boids);
	}

	@Override
	public Collection<Entity> get(BoundingSquare bs) {
		final Collection<Entity> result = new HashSet<Entity>();
		get(bs, result);
		return result;
	}

	@Override
	public void get(BoundingSquare bs, Collection<Entity> result) {
		for (Entity boid : boids) {
			if (bs.contains(boid.getPos())) {
				result.add(boid);
			}
		}
	}

	@Override
	public Collection<Entity> get(BoundingCircle bc) {
		final Collection<Entity> result = new HashSet<Entity>();
		get(bc, result);
		return result;
	}

	@Override
	public void get(BoundingCircle bc, Collection<Entity> result) {
		for (Entity boid : boids) {
			if (bc.contains(boid.getPos())) {
				result.add(boid);
			}
		}
	}

	@Override
	public void lock(boolean lock) {}

	@Override
	public int maxLevel() {
		return 1;
	}

	@Override
	public void moveDoit(Entity entity, Point pos) {
		throw new RuntimeException();
	}

	@Override
	public boolean moveTest(Entity entity, Point pos) {
		return true;
	}

	@Override
	public void remove(Entity entity) {
		boids.remove(entity);
	}

	@Override
	public int size() {
		return boids.size();
	}
}

I’ve also got a new question: Should it be possible for an object to be located in more than one BB?
Right now all objects are limited to one BB. The add method return after the 1st successful add operation.
But if I want to use Quadtrees for colission detection I assume that their location in the Quadtree should be based on their position and size and not just their position. Right?

Right. The rule is:

For any object you attempt to add to the tree:
If the quadtree node has child nodes:
Offer the object to the child node. If it is accepted, you’re done

Otherwise, if there are no child nodes or none of the child nodes accept the object:
If the object’s bounds fits entirely within this node, add it to this node’s list of contents (ie. accept the object)
Otherwise, don’t accept the object.

This means that objects with fall partially outside the absolute outer bounds of the quadtree will not be accepted into the quadtree at all and will need to be kept in a boundless master node, which should have an API like the quadtree, and should be the point of entry from the rest of your code.

Cas :slight_smile:

I’m gonna jump around. First some high level choices:

(Should be noted that we’re talking about region (equal subdivision) quad-trees…well at least I am)

A) Data only in leaf nodes
B) Data in any node

  1. Data stored in single node (leaf or otherwise)
  2. Data (potentially) stored in multiple.

A+1: Is only really reasonable if your maximum depth is restricted so you never need to visit more than ‘n’ siblings. So if you limit the depth such that the largest object will just fit within that leaf, you have to check up to 3 siblings. Of course any B generally requires some walking. I personally use A+2.

Logically I treat the universe as normalized on range [0,1] for both dimensions and only consider split values. This way any node which is outside the universe will map to the leaf with the closest Manhattan distance. (Shrug) There’s no one right or wrong way to do this stuff and the best choice (when clear) will depend on a lot of details of exact system.