Remove entity boundaries from Quadtree

The Quadtree did no meaningful management of entity boundaries. All it did was store the maximum and inflate the query rectangle accordingly. The caller can easily take over this responsibility. Consequently, this simplifies queries.
This commit is contained in:
Stefan Müller 2025-09-27 23:23:38 +02:00
parent 157fea7761
commit 85fcd2358f
2 changed files with 33 additions and 47 deletions

View File

@ -32,11 +32,6 @@ namespace SpatialCollections
/// </summary>
private readonly Func<T, Vector2> _getPositionCallback;
/// <summary>
/// Callback to determine the bounding box of an entity.
/// </summary>
private readonly Func<T, Rectangle> _getBoundingBoxCallback;
/// <summary>
/// The list of quadtree vertices. The root vertex is always the first item. The four child vertices of a branch
/// are always created together and stored contiguously.
@ -50,11 +45,6 @@ namespace SpatialCollections
private Rectangle _rootBoundingBox;
/// <summary>
/// The maximum bounding radius of all stored entities, used to inflate the bounding box for queries.
/// </summary>
private float _maxEntityBoundingRadius;
/// <summary>
/// Stack of vertex indices with their bounding boxes. It is reused for each query.
/// </summary>
@ -65,31 +55,24 @@ namespace SpatialCollections
/// </summary>
private readonly Stack<int> _vertexStack;
public Quadtree(int maxLeafSize, int maxTreeDepth, Func<T, Vector2> getPositionCallback,
Func<T, Rectangle> getBoundingBoxCallback)
public Quadtree(int maxLeafSize, int maxTreeDepth, Func<T, Vector2> getPositionCallback)
{
_maxLeafSize = maxLeafSize;
_maxTreeDepth = maxTreeDepth;
_getPositionCallback = getPositionCallback;
_getBoundingBoxCallback = getBoundingBoxCallback;
_vertices = new List<QuadtreeVertex>() { new(-1, 0) };
_items = new List<QuadtreeItem<T>>();
_rootBoundingBox = new Rectangle(Vector2.Zero, Vector2.Zero);
_maxEntityBoundingRadius = 0f;
_queryStack = new Stack<QuadtreeQuery>();
_vertexStack = new Stack<int>();
}
public int Count => _items.Count;
public void Add(T entity, float boundingRadius)
public void Add(T entity)
{
var itemIndex = _items.Count;
_items.Add(new QuadtreeItem<T>(entity));
if (_maxEntityBoundingRadius < boundingRadius)
{
_maxEntityBoundingRadius = boundingRadius;
}
if (_vertices.Count == 1 && _vertices[0].ChildCount < _maxLeafSize)
{
@ -165,10 +148,7 @@ namespace SpatialCollections
public void Query(Rectangle box, List<T> resultList)
{
Rectangle inflatedBox = box;
inflatedBox.Expand(_maxEntityBoundingRadius);
QueryProcessChildVertex(inflatedBox, box, 0, _rootBoundingBox, resultList, _queryStack);
QueryProcessChildVertex(box, 0, _rootBoundingBox, resultList, _queryStack);
while (_queryStack.Count > 0)
{
@ -177,16 +157,16 @@ namespace SpatialCollections
int childIndex = _vertices[query.VertexIndex].FirstChildIndex;
Rectangle halfBounds = new(query.VertexBounds.Center, query.VertexBounds.Max);
Vector2 halfSize = halfBounds.Size;
QueryProcessChildVertex(inflatedBox, box, childIndex++, halfBounds, resultList, _queryStack);
QueryProcessChildVertex(box, childIndex++, halfBounds, resultList, _queryStack);
halfBounds.Translate(new Vector2(-halfSize.X, 0f));
QueryProcessChildVertex(inflatedBox, box, childIndex++, halfBounds, resultList, _queryStack);
QueryProcessChildVertex(box, childIndex++, halfBounds, resultList, _queryStack);
halfBounds.Translate(new Vector2(halfSize.X, -halfSize.Y));
QueryProcessChildVertex(inflatedBox, box, childIndex++, halfBounds, resultList, _queryStack);
QueryProcessChildVertex(box, childIndex++, halfBounds, resultList, _queryStack);
halfBounds.Translate(new Vector2(-halfSize.X, 0f));
QueryProcessChildVertex(inflatedBox, box, childIndex, halfBounds, resultList, _queryStack);
QueryProcessChildVertex(box, childIndex, halfBounds, resultList, _queryStack);
}
}
@ -196,7 +176,6 @@ namespace SpatialCollections
_vertices.Add(new QuadtreeVertex(-1, 0));
_items.Clear();
_rootBoundingBox = new Rectangle(Vector2.Zero, Vector2.Zero);
_maxEntityBoundingRadius = 0f;
}
/// <summary>
@ -342,21 +321,21 @@ namespace SpatialCollections
return (vertexIndex, depth, bounds);
}
private void QueryProcessChildVertex(Rectangle inflatedBox, Rectangle box, int vertexIndex,
Rectangle vertexBounds, List<T> resultList, Stack<QuadtreeQuery> stack)
private void QueryProcessChildVertex(Rectangle box, int vertexIndex, Rectangle vertexBounds,
List<T> resultList, Stack<QuadtreeQuery> stack)
{
switch (inflatedBox.Intersects(vertexBounds))
switch (box.Intersects(vertexBounds))
{
case IntersectionType.Contains:
if (_vertices[vertexIndex].ChildCount == -1)
{
// Found contained vertex, appends items of all child vertices.
QueryAppendContainedItems(vertexIndex, box, resultList);
QueryAppendItems(vertexIndex, resultList);
}
else
{
// Found leaf, appends items.
QueryAppendContainedLeafItems(vertexIndex, box, resultList);
QueryAppendLeafItems(vertexIndex, resultList);
}
break;
case IntersectionType.Intersects:
@ -367,14 +346,14 @@ namespace SpatialCollections
}
else
{
// Found leaf, appends items.
// Found leaf, appends contained items.
QueryAppendContainedLeafItems(vertexIndex, box, resultList);
}
break;
}
}
private void QueryAppendContainedItems(int vertexIndex, Rectangle box, List<T> resultList)
private void QueryAppendItems(int vertexIndex, List<T> resultList)
{
_vertexStack.Push(vertexIndex);
@ -393,22 +372,34 @@ namespace SpatialCollections
else
{
// Found leaf, appends items.
QueryAppendContainedLeafItems(current, box, resultList);
QueryAppendLeafItems(current, resultList);
}
}
}
private void QueryAppendLeafItems(int vertexIndex, List<T> resultList)
{
int index = _vertices[vertexIndex].FirstChildIndex;
while (index != -1)
{
var item = _items[index];
resultList.Add(item.Entity);
index = item.Next;
}
}
private void QueryAppendContainedLeafItems(int vertexIndex, Rectangle box, List<T> resultList)
{
int index = _vertices[vertexIndex].FirstChildIndex;
while (index != -1)
{
T entity = _items[index].Entity;
if (box.Intersects(_getBoundingBoxCallback(entity)) != IntersectionType.Disjoint)
var item = _items[index];
T entity = item.Entity;
if (box.Contains(_getPositionCallback(entity)))
{
resultList.Add(entity);
}
index = _items[index].Next;
index = item.Next;
}
}

View File

@ -1,4 +1,3 @@
using NSubstitute;
using SpatialCollections;
using System.Numerics;
@ -6,13 +5,9 @@ namespace QuadtreeTests
{
public class Tests
{
private const float BoundingRadius = 1.0f;
private class TestEntity(Vector2 position)
{
public Vector2 Position { get; } = position;
public Rectangle BoundingBox { get; } = new Rectangle(position - Vector2.One, position + Vector2.One);
}
private List<Vector2> _positions;
@ -26,7 +21,7 @@ namespace QuadtreeTests
{
_positions = [new Vector2(1.0f, 1.0f), new Vector2(3.0f, 3.0f), new Vector2(5.0f, 8.0f)];
_entities = new();
_quadtree = new(20, 4, e => e.Position, e => e.BoundingBox);
_quadtree = new(20, 4, e => e.Position);
for (int i = 0; i < _positions.Count; i++)
{
@ -58,7 +53,7 @@ namespace QuadtreeTests
{
AddObjectsAndAssertCount();
List<TestEntity> result = new();
_quadtree.Query(new Rectangle(new Vector2(3.5f, 3.5f), new Vector2(10.0f, 10.0f)), result);
_quadtree.Query(new Rectangle(new Vector2(2.5f, 2.5f), new Vector2(10.0f, 10.0f)), result);
Assert.That(result.Count, Is.EqualTo(2));
}
@ -66,7 +61,7 @@ namespace QuadtreeTests
{
for (int i = 0; i < _entities.Count; i++)
{
_quadtree.Add(_entities[i], BoundingRadius);
_quadtree.Add(_entities[i]);
Assert.That(_quadtree.Count, Is.EqualTo(i + 1));
}
}