#if DUNGEN_QUADTREE
using System;
using System.Collections.Generic;
using UnityEngine;
namespace DunGen.Collision
{
///
/// A generic quadtree that supports insertion, removal, querying, and automatic expansion.
///
/// The type of objects stored in the quadtree.
public class Quadtree
{
///
/// Internal node class that holds objects and may subdivide into four children.
///
private class Node
{
public Bounds bounds;
public List objects;
public Node[] children; // null if leaf
///
/// True if this node has not been subdivided.
///
public bool IsLeaf => children == null;
public Node(Bounds bounds)
{
this.bounds = bounds;
this.objects = new List();
}
///
/// Subdivides this node into four children along the X and Z axes.
/// Assumes a 2D quadtree (ignoring Y, but preserving its height).
///
public void Subdivide()
{
children = new Node[4];
// Each child has half the size in x and z, but retains the parent's height.
Vector3 childSize = new Vector3(bounds.size.x / 2f, bounds.size.y, bounds.size.z / 2f);
Vector3 center = bounds.center;
float offsetX = childSize.x / 2f;
float offsetZ = childSize.z / 2f;
// Bottom-left quadrant (assuming z increases upward)
children[0] = new Node(new Bounds(center + new Vector3(-offsetX, 0, -offsetZ), childSize));
// Bottom-right quadrant
children[1] = new Node(new Bounds(center + new Vector3(offsetX, 0, -offsetZ), childSize));
// Top-left quadrant
children[2] = new Node(new Bounds(center + new Vector3(-offsetX, 0, offsetZ), childSize));
// Top-right quadrant
children[3] = new Node(new Bounds(center + new Vector3(offsetX, 0, offsetZ), childSize));
}
}
// The current quadtree root.
private Node root;
// Delegate to obtain a Bounds from an object of type T.
private readonly Func getBounds;
// Maximum objects a node can hold before subdividing.
private readonly int maxObjectsPerNode;
// Maximum allowed depth (to prevent infinite subdivision).
private readonly int maxDepth;
///
/// Constructs a quadtree with an initial bounding region.
///
/// The starting bounds of the quadtree.
/// A delegate to extract the Unity Bounds from a T object.
/// Maximum objects per node before subdivision.
/// Maximum subdivision depth.
public Quadtree(Bounds initialBounds, Func getBounds, int maxObjectsPerNode = 4, int maxDepth = 10)
{
root = new Node(initialBounds);
this.getBounds = getBounds;
this.maxObjectsPerNode = maxObjectsPerNode;
this.maxDepth = maxDepth;
}
///
/// Inserts an object into the quadtree.
/// Automatically expands the tree if the object does not fit in the current bounds.
///
/// The object to insert.
public void Insert(T obj)
{
Bounds objBounds = getBounds(obj);
// Expand the tree until the new object fits entirely within the root.
while (!root.bounds.Contains(objBounds.min) || !root.bounds.Contains(objBounds.max))
{
ExpandRoot(objBounds);
}
Insert(root, obj, 0);
}
///
/// Internal recursive insertion method.
///
private void Insert(Node node, T obj, int depth)
{
Bounds objBounds = getBounds(obj);
// If not a leaf, try to delegate insertion into one of the children.
if (!node.IsLeaf)
{
foreach (Node child in node.children)
{
// Insert only if the child completely contains the object's bounds.
if (child.bounds.Contains(objBounds.min) && child.bounds.Contains(objBounds.max))
{
Insert(child, obj, depth + 1);
return;
}
}
}
// Otherwise, store the object in this node.
node.objects.Add(obj);
// Subdivide this node if too many objects have accumulated and if we haven't reached maximum depth.
if (node.IsLeaf && node.objects.Count > maxObjectsPerNode && depth < maxDepth)
{
node.Subdivide();
// Reinsert existing objects into children if they fully fit; otherwise they remain in this node.
for (int i = node.objects.Count - 1; i >= 0; i--)
{
T existingObj = node.objects[i];
Bounds existingBounds = getBounds(existingObj);
foreach (Node child in node.children)
{
if (child.bounds.Contains(existingBounds.min) && child.bounds.Contains(existingBounds.max))
{
Insert(child, existingObj, depth + 1);
node.objects.RemoveAt(i);
break;
}
}
}
}
}
///
/// Removes an object from the quadtree.
///
/// The object to remove.
/// True if the object was removed; otherwise false.
public bool Remove(T obj)
{
return Remove(root, obj);
}
///
/// Internal recursive removal method.
///
private bool Remove(Node node, T obj)
{
bool removed = false;
Bounds objBounds = getBounds(obj);
// Check current node.
if (node.objects.Remove(obj))
{
removed = true;
}
// If there are children, check any that might intersect with the object's bounds.
if (!node.IsLeaf)
{
foreach (Node child in node.children)
{
// Only search children whose bounds intersect the object's bounds.
if (child.bounds.Intersects(objBounds))
{
removed |= Remove(child, obj);
}
}
}
return removed;
}
///
/// Queries the quadtree for all objects whose bounds overlap the specified region.
///
/// The bounds to query.
/// A list of objects overlapping the query bounds.
public List Query(Bounds queryBounds)
{
List results = new List();
Query(root, queryBounds, results);
return results;
}
public void Query(Bounds queryBounds, ref List results)
{
Query(root, queryBounds, results);
}
///
/// Recursively collects objects overlapping the query bounds.
///
private void Query(Node node, Bounds queryBounds, List results)
{
if (!node.bounds.Intersects(queryBounds))
{
return;
}
// Add objects in this node that intersect the query bounds.
foreach (T obj in node.objects)
{
if (getBounds(obj).Intersects(queryBounds))
{
results.Add(obj);
}
}
// If the node is subdivided, query its children.
if (!node.IsLeaf)
{
foreach (Node child in node.children)
{
Query(child, queryBounds, results);
}
}
}
///
/// Draws the quadtree nodes and the objects contained in each node using Unity's Debug.DrawLine API.
///
/// Duration (in seconds) for which the lines should persist.
public void DrawDebug(float duration = 0f)
{
DrawDebug(root, duration);
}
///
/// Recursively draws node boundaries and objects.
///
private void DrawDebug(Node node, float duration)
{
// Draw this node's bounds (white).
DrawBounds(node.bounds, Color.white, duration);
// Draw each object's bounds within this node (green).
foreach (T obj in node.objects)
{
Bounds objBounds = getBounds(obj);
DrawBounds(objBounds, Color.green, duration);
}
// Recursively draw children nodes if they exist.
if (!node.IsLeaf)
{
foreach (Node child in node.children)
{
DrawDebug(child, duration);
}
}
}
///
/// Helper method that draws a rectangle representing the bounds on the XZ plane.
///
private void DrawBounds(Bounds b, Color color, float duration)
{
Vector3 center = b.center;
Vector3 extents = b.extents;
// Calculate corners (on the XZ plane; Y is kept at the center's Y).
Vector3 topLeft = new Vector3(center.x - extents.x, center.y, center.z + extents.z);
Vector3 topRight = new Vector3(center.x + extents.x, center.y, center.z + extents.z);
Vector3 bottomRight = new Vector3(center.x + extents.x, center.y, center.z - extents.z);
Vector3 bottomLeft = new Vector3(center.x - extents.x, center.y, center.z - extents.z);
Debug.DrawLine(topLeft, topRight, color, duration);
Debug.DrawLine(topRight, bottomRight, color, duration);
Debug.DrawLine(bottomRight, bottomLeft, color, duration);
Debug.DrawLine(bottomLeft, topLeft, color, duration);
}
///
/// Expands the root bounds so that it fully contains the specified object bounds.
/// Instead of reinserting every object, the current root is wrapped in a new, larger root.
/// This implementation uses a doubling strategy so that one child quadrant exactly matches the old root.
///
private void ExpandRoot(Bounds objBounds)
{
// Save current root parameters.
Vector3 oldCenter = root.bounds.center;
Vector3 oldSize = root.bounds.size; // Must be nonzero and positive
// Determine whether the new object is to the left/right and down/up of the current center.
// If the new object is to the left, we want the old tree to be in the right quadrant, etc.
bool newObjLeft = objBounds.center.x < oldCenter.x;
bool newObjDown = objBounds.center.z < oldCenter.z;
// Use a multiplier of 2 so that each child quadrant of the new root matches the old root exactly.
Vector3 newSize = new Vector3(oldSize.x * 2f, oldSize.y, oldSize.z * 2f);
// Determine offsets:
// We want the child quadrant's center to equal oldCenter.
// Since each child quadrant is half the new root's size,
// oldCenter = newRoot.center + (dx * oldSize.x/2, 0, dz * oldSize.z/2)
// Solve for newRoot.center:
int dx = newObjLeft ? 1 : -1; // if new object is left, shift right so that old tree ends up in the opposite (right) quadrant
int dz = newObjDown ? 1 : -1; // if new object is down, shift up so that old tree ends up in the opposite (up) quadrant
Vector3 newRootCenter = oldCenter - new Vector3((dx * oldSize.x) / 2f, 0, (dz * oldSize.z) / 2f);
// Create the new root with these computed parameters.
Node newRoot = new Node(new Bounds(newRootCenter, newSize));
newRoot.Subdivide();
// Determine which child quadrant of the new root should exactly match the old root.
// Child quadrant centers (relative to newRoot.center) are:
// 0: bottom-left (-oldSize.x/2, -oldSize.z/2)
// 1: bottom-right (oldSize.x/2, -oldSize.z/2)
// 2: top-left (-oldSize.x/2, oldSize.z/2)
// 3: top-right (oldSize.x/2, oldSize.z/2)
int quadrant;
if (dx == -1 && dz == -1)
quadrant = 0; // new object is to right/up, so old tree goes bottom-left.
else if (dx == 1 && dz == -1)
quadrant = 1; // new object is to left/up, so old tree goes bottom-right.
else if (dx == -1 && dz == 1)
quadrant = 2; // new object is to right/down, so old tree goes top-left.
else // (dx == 1 && dz == 1)
quadrant = 3; // new object is to left/down, so old tree goes top-right.
// Place the existing root into the appropriate quadrant.
newRoot.children[quadrant] = root;
root = newRoot;
}
///
/// Recursively collects all objects stored in the tree.
///
private List CollectAllObjects(Node node)
{
List list = new List(node.objects);
if (!node.IsLeaf)
{
foreach (Node child in node.children)
{
list.AddRange(CollectAllObjects(child));
}
}
return list;
}
}
}
#endif