Remove dependency on IWorldObject by introducing callbacks, and change terminology from "world object" to "(world) entity"
This commit is contained in:
parent
3bea3f5a9b
commit
83141aeb14
@ -1,13 +0,0 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace SpatialCollections
|
||||
{
|
||||
public interface IWorldObject
|
||||
{
|
||||
Vector2 Position { get; }
|
||||
|
||||
float BoundingRadius { get; }
|
||||
|
||||
Rectangle BoundingBox { get; }
|
||||
}
|
||||
}
|
@ -3,15 +3,16 @@
|
||||
namespace SpatialCollections
|
||||
{
|
||||
/// <summary>
|
||||
/// <para>Quadtree for fast add, remove, and query of world objects. The world size does not have to be known in
|
||||
/// advance, instead the boundaries will be determined dynamically as objects are added.</para>
|
||||
/// <para>Objects are expected to have similar size, and to be somewhat small in relation to the distances in
|
||||
/// between them. This allows simple queries. If very large objects would have to be stored, it could become
|
||||
/// necessary to store objects not in a single leaf, but in each leaf they intersect with.</para>
|
||||
/// <para>Quadtree for fast add, remove, and query of world entities. The world size does not have to be known in
|
||||
/// advance, instead the boundaries will be determined dynamically as entities are added.</para>
|
||||
/// <para>Entities are expected to have similar size, and to be somewhat small in relation to the distances in
|
||||
/// between them. This allows simple queries. If very large entities would have to be stored, it could become
|
||||
/// necessary to store entities not in a single leaf, but in each leaf they intersect with.</para>
|
||||
/// </summary>
|
||||
// TODO: Support for moving objects without having to remove and re-add them.
|
||||
// TODO: Add method to prune empty leaves. An empty leaf is not removed automatically when its last object is removed.
|
||||
public class Quadtree
|
||||
/// <typeparam name="T">Type of the managed entities.</typeparam>
|
||||
// TODO: Support for moving entites without having to remove and re-add them.
|
||||
// TODO: Add method to prune empty leaves. An empty leaf is not removed automatically when its last entity is removed.
|
||||
public class Quadtree<T>
|
||||
{
|
||||
// TODO: It should be possible to calculate whether average query time will be better with or without split, without resorting to max leaf size and max tree depth.
|
||||
/// <summary>
|
||||
@ -26,6 +27,16 @@ namespace SpatialCollections
|
||||
/// </summary>
|
||||
private readonly int _maxTreeDepth;
|
||||
|
||||
/// <summary>
|
||||
/// Callback to determine the position of an entity.
|
||||
/// </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.
|
||||
@ -35,14 +46,14 @@ namespace SpatialCollections
|
||||
/// <summary>
|
||||
/// The list of all items stored in the quadtree.
|
||||
/// </summary>
|
||||
private readonly List<QuadtreeItem<IWorldObject>> _items;
|
||||
private readonly List<QuadtreeItem<T>> _items;
|
||||
|
||||
private Rectangle _rootBoundingBox;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum bounding radius of all stored objects, used to inflate the bounding box for queries.
|
||||
/// The maximum bounding radius of all stored entities, used to inflate the bounding box for queries.
|
||||
/// </summary>
|
||||
private float _maxObjectRadius;
|
||||
private float _maxEntityBoundingRadius;
|
||||
|
||||
/// <summary>
|
||||
/// Stack of vertex indices with their bounding boxes. It is reused for each query.
|
||||
@ -54,27 +65,30 @@ namespace SpatialCollections
|
||||
/// </summary>
|
||||
private readonly Stack<int> _vertexStack;
|
||||
|
||||
public Quadtree(int maxLeafSize, int maxTreeDepth)
|
||||
public Quadtree(int maxLeafSize, int maxTreeDepth, Func<T, Vector2> getPositionCallback,
|
||||
Func<T, Rectangle> getBoundingBoxCallback)
|
||||
{
|
||||
_maxLeafSize = maxLeafSize;
|
||||
_maxTreeDepth = maxTreeDepth;
|
||||
_getPositionCallback = getPositionCallback;
|
||||
_getBoundingBoxCallback = getBoundingBoxCallback;
|
||||
_vertices = new List<QuadtreeVertex>() { new(-1, 0) };
|
||||
_items = new List<QuadtreeItem<IWorldObject>>();
|
||||
_items = new List<QuadtreeItem<T>>();
|
||||
_rootBoundingBox = new Rectangle(Vector2.Zero, Vector2.Zero);
|
||||
_maxObjectRadius = 0f;
|
||||
_maxEntityBoundingRadius = 0f;
|
||||
_queryStack = new Stack<QuadtreeQuery>();
|
||||
_vertexStack = new Stack<int>();
|
||||
}
|
||||
|
||||
public int Count => _items.Count;
|
||||
|
||||
public void Add(IWorldObject obj)
|
||||
public void Add(T entity, float boundingRadius)
|
||||
{
|
||||
var itemIndex = _items.Count;
|
||||
_items.Add(new QuadtreeItem<IWorldObject>(obj));
|
||||
if (_maxObjectRadius < obj.BoundingRadius)
|
||||
_items.Add(new QuadtreeItem<T>(entity));
|
||||
if (_maxEntityBoundingRadius < boundingRadius)
|
||||
{
|
||||
_maxObjectRadius = obj.BoundingRadius;
|
||||
_maxEntityBoundingRadius = boundingRadius;
|
||||
}
|
||||
|
||||
if (_vertices.Count == 1 && _vertices[0].ChildCount < _maxLeafSize)
|
||||
@ -86,42 +100,49 @@ namespace SpatialCollections
|
||||
}
|
||||
else
|
||||
{
|
||||
// Expands the root until the new world object fits.
|
||||
while (!_rootBoundingBox.Contains(obj.Position))
|
||||
var position = _getPositionCallback(entity);
|
||||
|
||||
// Expands the root until the new entity fits.
|
||||
while (!_rootBoundingBox.Contains(position))
|
||||
{
|
||||
ExpandRoot(obj.Position);
|
||||
ExpandRoot(position);
|
||||
}
|
||||
|
||||
(int leafIndex, int depth, Rectangle bounds) = FindLeaf(0, _rootBoundingBox, 0, obj.Position);
|
||||
(int leafIndex, int depth, Rectangle bounds) = FindLeaf(0, _rootBoundingBox, 0, position);
|
||||
while (_vertices[leafIndex].ChildCount >= _maxLeafSize && depth < _maxTreeDepth)
|
||||
{
|
||||
// Splits the vertex and decends into one of the new leaves.
|
||||
SplitLeaf(leafIndex, bounds);
|
||||
(leafIndex, depth, bounds) = FindLeaf(leafIndex, bounds, depth, obj.Position);
|
||||
(leafIndex, depth, bounds) = FindLeaf(leafIndex, bounds, depth, position);
|
||||
}
|
||||
AddToLeaf(leafIndex, itemIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(IWorldObject obj)
|
||||
public bool Remove(T entity)
|
||||
{
|
||||
// Finds the leaf that should contain the world object.
|
||||
(int leafIndex, _, _) = FindLeaf(0, _rootBoundingBox, 0, obj.Position);
|
||||
if (entity == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tries to find the item of the world object.
|
||||
// Finds the leaf that should contain the entity.
|
||||
(int leafIndex, _, _) = FindLeaf(0, _rootBoundingBox, 0, _getPositionCallback(entity));
|
||||
|
||||
// Tries to find the item referencing the entity.
|
||||
int previous = -1;
|
||||
int current = _vertices[leafIndex].FirstChildIndex;
|
||||
while (current != -1)
|
||||
{
|
||||
QuadtreeItem<IWorldObject> item = _items[current];
|
||||
if (item.WorldObject == obj)
|
||||
QuadtreeItem<T> item = _items[current];
|
||||
if (entity.Equals(item.Entity))
|
||||
{
|
||||
// Removes the found item from the leaf vertex.
|
||||
QuadtreeVertex leaf = _vertices[leafIndex];
|
||||
leaf.ChildCount--;
|
||||
if (previous != -1)
|
||||
{
|
||||
_items[previous] = new QuadtreeItem<IWorldObject>(_items[previous].WorldObject, item.Next);
|
||||
_items[previous] = new QuadtreeItem<T>(_items[previous].Entity, item.Next);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -138,14 +159,14 @@ namespace SpatialCollections
|
||||
current = item.Next;
|
||||
}
|
||||
|
||||
// World object was not found.
|
||||
// Entity was not found.
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Query(Rectangle box, List<IWorldObject> resultList)
|
||||
public void Query(Rectangle box, List<T> resultList)
|
||||
{
|
||||
Rectangle inflatedBox = box;
|
||||
inflatedBox.Expand(_maxObjectRadius);
|
||||
inflatedBox.Expand(_maxEntityBoundingRadius);
|
||||
|
||||
QueryProcessChildVertex(inflatedBox, box, 0, _rootBoundingBox, resultList, _queryStack);
|
||||
|
||||
@ -175,7 +196,7 @@ namespace SpatialCollections
|
||||
_vertices.Add(new QuadtreeVertex(-1, 0));
|
||||
_items.Clear();
|
||||
_rootBoundingBox = new Rectangle(Vector2.Zero, Vector2.Zero);
|
||||
_maxObjectRadius = 0f;
|
||||
_maxEntityBoundingRadius = 0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -223,7 +244,7 @@ namespace SpatialCollections
|
||||
|
||||
private void AddToRootLeaf(int itemIndex)
|
||||
{
|
||||
Vector2 position = _items[itemIndex].WorldObject.Position;
|
||||
Vector2 position = _getPositionCallback(_items[itemIndex].Entity);
|
||||
if (_vertices[0].ChildCount == 0)
|
||||
{
|
||||
_vertices[0] = new QuadtreeVertex(itemIndex, 1);
|
||||
@ -252,7 +273,7 @@ namespace SpatialCollections
|
||||
next = _vertices[leafIndex].FirstChildIndex;
|
||||
_vertices[leafIndex] = new QuadtreeVertex(itemIndex, _vertices[leafIndex].ChildCount + 1);
|
||||
}
|
||||
_items[itemIndex] = new QuadtreeItem<IWorldObject>(_items[itemIndex].WorldObject, next);
|
||||
_items[itemIndex] = new QuadtreeItem<T>(_items[itemIndex].Entity, next);
|
||||
}
|
||||
|
||||
private void SplitLeaf(int leafIndex, Rectangle leafBounds)
|
||||
@ -271,7 +292,7 @@ namespace SpatialCollections
|
||||
while (next != -1)
|
||||
{
|
||||
var item = _items[next];
|
||||
int newLeafIndex = FindNextVertex(leafIndex, center, item.WorldObject.Position);
|
||||
int newLeafIndex = FindNextVertex(leafIndex, center, _getPositionCallback(item.Entity));
|
||||
AddToLeaf(newLeafIndex, next);
|
||||
next = item.Next;
|
||||
}
|
||||
@ -322,7 +343,7 @@ namespace SpatialCollections
|
||||
}
|
||||
|
||||
private void QueryProcessChildVertex(Rectangle inflatedBox, Rectangle box, int vertexIndex,
|
||||
Rectangle vertexBounds, List<IWorldObject> resultList, Stack<QuadtreeQuery> stack)
|
||||
Rectangle vertexBounds, List<T> resultList, Stack<QuadtreeQuery> stack)
|
||||
{
|
||||
switch (inflatedBox.Intersects(vertexBounds))
|
||||
{
|
||||
@ -353,7 +374,7 @@ namespace SpatialCollections
|
||||
}
|
||||
}
|
||||
|
||||
private void QueryAppendContainedItems(int vertexIndex, Rectangle box, List<IWorldObject> resultList)
|
||||
private void QueryAppendContainedItems(int vertexIndex, Rectangle box, List<T> resultList)
|
||||
{
|
||||
_vertexStack.Push(vertexIndex);
|
||||
|
||||
@ -377,15 +398,15 @@ namespace SpatialCollections
|
||||
}
|
||||
}
|
||||
|
||||
private void QueryAppendContainedLeafItems(int vertexIndex, Rectangle box, List<IWorldObject> resultList)
|
||||
private void QueryAppendContainedLeafItems(int vertexIndex, Rectangle box, List<T> resultList)
|
||||
{
|
||||
int index = _vertices[vertexIndex].FirstChildIndex;
|
||||
while (index != -1)
|
||||
{
|
||||
IWorldObject obj = _items[index].WorldObject;
|
||||
if (box.Intersects(obj.BoundingBox) != IntersectionType.Disjoint)
|
||||
T entity = _items[index].Entity;
|
||||
if (box.Intersects(_getBoundingBoxCallback(entity)) != IntersectionType.Disjoint)
|
||||
{
|
||||
resultList.Add(obj);
|
||||
resultList.Add(entity);
|
||||
}
|
||||
index = _items[index].Next;
|
||||
}
|
||||
@ -401,7 +422,7 @@ namespace SpatialCollections
|
||||
_items.RemoveUnorderedAt(index);
|
||||
if (index != _items.Count)
|
||||
{
|
||||
(int leafIndex, _, _) = FindLeaf(0, _rootBoundingBox, 0, _items[index].WorldObject.Position);
|
||||
(int leafIndex, _, _) = FindLeaf(0, _rootBoundingBox, 0, _getPositionCallback(_items[index].Entity));
|
||||
int current = _vertices[leafIndex].FirstChildIndex;
|
||||
if (current == _items.Count)
|
||||
{
|
||||
@ -413,7 +434,7 @@ namespace SpatialCollections
|
||||
{
|
||||
current = _items[current].Next;
|
||||
}
|
||||
_items[current] = new QuadtreeItem<IWorldObject>(_items[current].WorldObject, index);
|
||||
_items[current] = new QuadtreeItem<T>(_items[current].Entity, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,16 +7,16 @@
|
||||
/// </summary>
|
||||
public int Next;
|
||||
|
||||
public T WorldObject;
|
||||
public T Entity;
|
||||
|
||||
public QuadtreeItem(T worldObject) : this(worldObject, -1)
|
||||
public QuadtreeItem(T entity) : this(entity, -1)
|
||||
{
|
||||
}
|
||||
|
||||
public QuadtreeItem(T worldObject, int next)
|
||||
public QuadtreeItem(T entity, int next)
|
||||
{
|
||||
Next = next;
|
||||
WorldObject = worldObject;
|
||||
Entity = entity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,32 +6,32 @@ namespace QuadtreeTests
|
||||
{
|
||||
public class Tests
|
||||
{
|
||||
private class TestWorldObject(Vector2 position) : IWorldObject
|
||||
private const float BoundingRadius = 1.0f;
|
||||
|
||||
private class TestEntity(Vector2 position)
|
||||
{
|
||||
public Vector2 Position { get; } = position;
|
||||
|
||||
public float BoundingRadius { get; } = 1.0f;
|
||||
|
||||
public Rectangle BoundingBox { get; } = new Rectangle(position - Vector2.One, position + Vector2.One);
|
||||
}
|
||||
|
||||
private List<Vector2> _positions;
|
||||
|
||||
private List<IWorldObject> _objects;
|
||||
private List<TestEntity> _entities;
|
||||
|
||||
private Quadtree _quadtree;
|
||||
private Quadtree<TestEntity> _quadtree;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_positions = [new Vector2(1.0f, 1.0f), new Vector2(3.0f, 3.0f), new Vector2(5.0f, 8.0f)];
|
||||
_objects = new();
|
||||
_quadtree = new(20, 4);
|
||||
_entities = new();
|
||||
_quadtree = new(20, 4, e => e.Position, e => e.BoundingBox);
|
||||
|
||||
for (int i = 0; i < _positions.Count; i++)
|
||||
{
|
||||
var obj = new TestWorldObject(_positions[i]);
|
||||
_objects.Add(obj);
|
||||
var obj = new TestEntity(_positions[i]);
|
||||
_entities.Add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,10 +46,10 @@ namespace QuadtreeTests
|
||||
{
|
||||
AddObjectsAndAssertCount();
|
||||
|
||||
for (int i = 0; i < _objects.Count; i++)
|
||||
for (int i = 0; i < _entities.Count; i++)
|
||||
{
|
||||
_quadtree.Remove(_objects[i]);
|
||||
Assert.That(_quadtree.Count, Is.EqualTo(_objects.Count - i - 1));
|
||||
_quadtree.Remove(_entities[i]);
|
||||
Assert.That(_quadtree.Count, Is.EqualTo(_entities.Count - i - 1));
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,16 +57,16 @@ namespace QuadtreeTests
|
||||
public void TestQuery()
|
||||
{
|
||||
AddObjectsAndAssertCount();
|
||||
List<IWorldObject> result = new();
|
||||
List<TestEntity> result = new();
|
||||
_quadtree.Query(new Rectangle(new Vector2(3.5f, 3.5f), new Vector2(10.0f, 10.0f)), result);
|
||||
Assert.That(result.Count, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
private void AddObjectsAndAssertCount()
|
||||
{
|
||||
for (int i = 0; i < _objects.Count; i++)
|
||||
for (int i = 0; i < _entities.Count; i++)
|
||||
{
|
||||
_quadtree.Add(_objects[i]);
|
||||
_quadtree.Add(_entities[i], BoundingRadius);
|
||||
Assert.That(_quadtree.Count, Is.EqualTo(i + 1));
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
# Quadtree
|
||||
|
||||
Quadtree for fast add, remove, and query of world objects. The world size does not have to be known in advance, instead the boundaries will be determined dynamically as objects are added.
|
||||
Quadtree for fast add, remove, and query of world entities. The world size does not have to be known in advance, instead the boundaries will be determined dynamically as entities are added.
|
Loading…
x
Reference in New Issue
Block a user