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.BoundingBox; // Object doesn't fit into this node. if (!RectangleF.Intersects(NodeBounds, 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(RectangleF area) { Reset(); return QueryWithoutReset(area); } private List QueryWithoutReset(RectangleF area) { var result = new List(); if (!RectangleF.Intersects(area, NodeBounds)) return result; if (IsLeaf) { for (int i = 0, size = Contents.Count; i < size; i++) { if (RectangleF.Intersects(Contents[i].BoundingBox, 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; } } }