using System.Collections.Generic;
namespace MonoGame.Extended.Collisions
{
///
/// Class for doing collision handling with a quad tree.
///
public class Quadtree
{
public const int DefaultMaxDepth = 7;
public const int DefaultMaxObjectsPerNode = 25;
protected List Children = new List();
protected List Contents = new List();
///
/// Creates a quad tree with the given bounds.
///
/// The bounds of the new quad tree.
public Quadtree(RectangleF bounds)
{
CurrentDepth = 0;
NodeBounds = bounds;
}
protected int CurrentDepth { get; set; }
protected int MaxDepth { get; set; } = DefaultMaxDepth;
protected int MaxObjectsPerNode { get; set; } = DefaultMaxObjectsPerNode;
///
/// Gets the bounds of the area contained in this quad tree.
///
public RectangleF NodeBounds { get; protected set; }
///
/// Gets whether the current node is a leaf node.
///
public bool IsLeaf => Children.Count == 0;
///
/// Counts the number of unique targets in the current Quadtree.
///
/// Returns the targets of objects found.
public int NumTargets()
{
Reset();
var objectCount = 0;
// Do BFS on nodes to count children.
var process = new Queue();
process.Enqueue(this);
while (process.Count > 0)
{
var processing = process.Dequeue();
if (!processing.IsLeaf)
{
foreach (var child in processing.Children) process.Enqueue(child);
}
else
{
var contents = processing.Contents;
foreach (var data in contents)
{
if (!data.Flag)
{
objectCount++;
data.Flag = true;
}
}
}
}
Reset();
return objectCount;
}
///
/// Inserts the data into the tree.
///
/// Data being inserted.
public void Insert(QuadtreeData data)
{
var actorBounds = data.Target.Bounds;
// Object doesn't fit into this node.
if (!NodeBounds.Intersects(actorBounds))
{
return;
}
if (IsLeaf && Contents.Count >= MaxObjectsPerNode) Split();
if (IsLeaf)
{
Contents.Add(data);
}
else
{
foreach (var child in Children)
{
child.Insert(data);
}
}
}
///
/// Removes data from the Quadtree
///
/// The data to be removed.
public void Remove(QuadtreeData data)
{
if (IsLeaf)
{
var removeIndex = -1;
for (int i = 0, size = Contents.Count; i < size; i++)
{
if (Contents[i].Target == data.Target)
{
removeIndex = i;
break;
}
}
if (removeIndex != -1)
{
Contents.RemoveAt(removeIndex);
}
}
else
{
foreach (var quadTree in Children)
{
quadTree.Remove(data);
}
}
Shake();
}
///
/// Resets all QuadtreeData.Flag to false.
///
///
/// Used internally to query and count contents without duplicates.
///
public void Reset()
{
if (IsLeaf)
for (int i = 0, size = Contents.Count; i < size; i++)
{
var quadTreeData = Contents[i];
quadTreeData.Flag = false;
Contents[i] = quadTreeData;
}
else
{
for (int i = 0, size = Children.Count; i < size; i++)
Children[i].Reset();
}
}
///
/// Removes unneccesary leaf nodes and simplifies the quad tree.
///
public void Shake()
{
if (!IsLeaf)
{
var numObjects = NumTargets();
if (numObjects == 0)
{
Children.Clear();
}
else if (numObjects < MaxObjectsPerNode)
{
var process = new Queue();
process.Enqueue(this);
while (process.Count > 0)
{
var processing = process.Dequeue();
if (!processing.IsLeaf)
foreach (var subTree in processing.Children)
{
process.Enqueue(subTree);
}
else
{
foreach (var data in processing.Contents)
{
if (!data.Flag)
{
Contents.Add(data);
data.Flag = true;
}
}
}
}
Children.Clear();
}
}
}
///
/// Splits a quadtree into quadrants.
///
public void Split()
{
if (CurrentDepth + 1 >= MaxDepth) return;
var min = NodeBounds.TopLeft;
var max = NodeBounds.BottomRight;
var center = NodeBounds.Center;
RectangleF[] childAreas =
{
RectangleF.CreateFrom(min, center),
RectangleF.CreateFrom(new Point2(center.X, min.Y), new Point2(max.X, center.Y)),
RectangleF.CreateFrom(center, max),
RectangleF.CreateFrom(new Point2(min.X, center.Y), new Point2(center.X, max.Y))
};
for (var i = 0; i < childAreas.Length; ++i)
{
var node = new Quadtree(childAreas[i]);
Children.Add(node);
Children[i].CurrentDepth = CurrentDepth + 1;
}
for (int i = 0, size = Contents.Count; i < size; ++i)
{
for (int j = 0; j < Children.Count; j++)
{
Children[j].Insert(Contents[i]);
}
}
Contents.Clear();
}
///
/// Queries the quadtree for targets that intersect with the given area.
///
/// The area to query for overlapping targets
/// A unique list of targets intersected by area.
public List Query(IShapeF area)
{
Reset();
return QueryWithoutReset(area);
}
private List QueryWithoutReset(IShapeF area)
{
var result = new List();
if (!NodeBounds.Intersects(area)) return result;
if (IsLeaf)
{
for (int i = 0, size = Contents.Count; i < size; i++)
{
if (Contents[i].Bounds.Intersects(area)
&& !Contents[i].Flag)
{
result.Add(Contents[i]);
Contents[i].Flag = true;
}
}
}
else
{
for (int i = 0, size = Children.Count; i < size; i++)
{
var recurse = Children[i].QueryWithoutReset(area);
result.AddRange(recurse);
}
}
return result;
}
}
}