using Quadtree.Items; using System.Collections.Generic; using System.Linq; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif namespace Quadtree { /// /// Base quadtree node implementation. /// public abstract class NodeBase : INode where TItem : IItem where TNode : NodeBase, new() { public Bounds Bounds { get; set; } public TNode ParentNode { get; set; } public IList SubNodes { get; set; } public IQuadtreeRoot TreeRoot { get; set; } /// /// List of inserted items. /// private readonly HashSet _items; public NodeBase() { SubNodes = new List(4); _items = new HashSet(); } /// /// Verifies whether provided boundaries () are fully contained within the boundaries of the node. /// /// /// Boundaries of an object /// True if object is fully contained within the node, False otherwise public bool Contains(Bounds bounds) => bounds.min.x >= Bounds.min.x && bounds.min.z >= Bounds.min.z && bounds.max.x < Bounds.max.x && bounds.max.z < Bounds.max.z; public IntraLocation Location(Bounds bounds) { if (bounds.min.z >= Bounds.center.z) { // items are located in top sub-nodes if (bounds.max.x < Bounds.center.x) { // items are located in top left sub-node return IntraLocation.UPPER_LEFT; } else if (bounds.min.x >= Bounds.center.x) { // items are located in top right sub-node return IntraLocation.UPPER_RIGHT; } else { // item does not fit to either one, but is top // (max.x is right, min.x is left) return IntraLocation.SPANNING_UPPER; } } else if (bounds.max.z < Bounds.center.z) { // items are located in bottom sub-nodes if (bounds.max.x < Bounds.center.x) { // items are located in bottom left sub-node return IntraLocation.LOWER_LEFT; } else if (bounds.min.x >= Bounds.center.x) { // items are located in bottom right sub-node return IntraLocation.LOWER_RIGHT; } else { // item does not fit to either one, but is bottom // (max.x is right, min.x is left) return IntraLocation.SPANNING_LOWER; } } else { // item does not fit to any sub-node // (max.z is top, min.z is bottom) if (bounds.min.x >= Bounds.center.x) { // bounds span over top right and bottom right nodes return IntraLocation.SPANNING_RIGHT; } else if (bounds.max.x < Bounds.center.x) { // bounds span over top left and bottom left nodes return IntraLocation.SPANNING_LEFT; } else { // bounds span over all sub-nodes return IntraLocation.SPANNING; } } } public void Insert(TItem item) { // create new sub-nodes if (SubNodes.Count == 0) CreateSubNodes(); // sub-nodes can not be created anymore if (SubNodes.Count == 0) { // insert item into this node _items.Add(item); // and designate this node its parent item.ParentNode = (TNode)this; return; } var itemBounds = item.GetBounds(); var itemBoundsLocation = Location(itemBounds); switch (itemBoundsLocation) { // boundaries are contained within one of the subnodes case IntraLocation.UPPER_LEFT: case IntraLocation.UPPER_RIGHT: case IntraLocation.LOWER_RIGHT: case IntraLocation.LOWER_LEFT: SubNodes[(int)itemBoundsLocation].Insert(item); break; // boundaries are spanning over 2 or more subnodes default: _items.Add(item); item.ParentNode = (TNode)this; break; } } public void Remove(TItem item) { var itemBounds = item.GetBounds(); var itemBoundsLocation = Location(itemBounds); switch (itemBoundsLocation) { // boundaries are contained within one of the subnodes case IntraLocation.UPPER_LEFT: case IntraLocation.UPPER_RIGHT: case IntraLocation.LOWER_RIGHT: case IntraLocation.LOWER_LEFT: SubNodes[(int)itemBoundsLocation].Remove(item); break; // boundaries are spanning over 2 or more subnodes default: RemoveOwnItem(item); break; } } /// /// Removes provided item () from the node. /// /// /// Item to be removed from the node /// /// protected internal void RemoveOwnItem(TItem item) { // remove the item from the node _items.Remove(item); // update its parent node item.ParentNode = null; if (IsEmpty()) { // remove subnodes if subtree of this node is empty SubNodes.Clear(); } } public bool IsEmpty() { if (_items.Count > 0) return false; foreach (var subNode in SubNodes) if (!subNode.IsEmpty()) return false; return true; } public void Update(TItem item, bool forceInsertionEvaluation = true, bool hasOriginallyContainedItem = true) { if (Contains(item.GetBounds())) { // item is contained by this node if (hasOriginallyContainedItem) { // ...and this node has originally contained the item if (forceInsertionEvaluation) { // ...and insertion evaluation is forced // this checks whether the item hasn't moved into any of the subnodes RemoveOwnItem(item); Insert(item); } // item is still contained by its original node, no action necessary return; } // ...but this node is not its original container // insert item either to this node or any of its children Insert(item); // update has been successful return; } // the item is not contained by this node if (ParentNode == null) { // ...and this node does not have any parent - the tree must be expanded TreeRoot.Expand(); if (ParentNode == null) { // the expansion has failed for some reason Debug.LogError("Tree root expansion failed for item " + item.ToString()); return; } } // the item is not contained by this node if (hasOriginallyContainedItem) { // ...and this node has originally contained the item - it must be removed RemoveOwnItem(item); } // parent is (now) available ParentNode.Update(item, forceInsertionEvaluation, false); // the item is now contained by another node, update has been successful } /// /// Creates sub-nodes for the node. /// protected internal void CreateSubNodes() { var subBoundsSize = Bounds.size * .5f; if (subBoundsSize.x < TreeRoot.MinimumPossibleNodeSize || subBoundsSize.z < TreeRoot.MinimumPossibleNodeSize) { // new sub-node bounds are too small return; } var centerOffset = subBoundsSize * .5f; // top left node [-x +z] centerOffset.x *= -1f; SubNodes.Insert((int)IntraLocation.UPPER_LEFT, new TNode() { TreeRoot = TreeRoot, ParentNode = (TNode)this, Bounds = new Bounds(Bounds.center + centerOffset, subBoundsSize), }); // top right node [+x +z] centerOffset.x *= -1f; SubNodes.Insert((int)IntraLocation.UPPER_RIGHT, new TNode() { TreeRoot = TreeRoot, ParentNode = (TNode)this, Bounds = new Bounds(Bounds.center + centerOffset, subBoundsSize), }); // bottom right node [+x -z] centerOffset.z *= -1f; SubNodes.Insert((int)IntraLocation.LOWER_RIGHT, new TNode() { TreeRoot = TreeRoot, ParentNode = (TNode)this, Bounds = new Bounds(Bounds.center + centerOffset, subBoundsSize), }); // bottom left node [-x -z] centerOffset.x *= -1f; SubNodes.Insert((int)IntraLocation.LOWER_LEFT, new TNode() { TreeRoot = TreeRoot, ParentNode = (TNode)this, Bounds = new Bounds(Bounds.center + centerOffset, subBoundsSize), }); } public void FindAndAddItems(Bounds bounds, ref IList items) { if (SubNodes.Count == 0) { // no sub-nodes exist AddOwnItems(ref items); return; } // always add any items in this node intersecting with the boundaries AddOwnItems(ref items, bounds); var boundsLocation = Location(bounds); switch (boundsLocation) { // boundaries are contained within one of the subnodes case IntraLocation.UPPER_LEFT: case IntraLocation.UPPER_RIGHT: case IntraLocation.LOWER_RIGHT: case IntraLocation.LOWER_LEFT: SubNodes[(int)boundsLocation].FindAndAddItems(bounds, ref items); break; // boundaries are spanning over left subnodes case IntraLocation.SPANNING_LEFT: SubNodes[(int)IntraLocation.UPPER_LEFT].AddItems(ref items, bounds); SubNodes[(int)IntraLocation.LOWER_LEFT].AddItems(ref items, bounds); break; // boundaries are spanning over right subnodes case IntraLocation.SPANNING_RIGHT: SubNodes[(int)IntraLocation.UPPER_RIGHT].AddItems(ref items, bounds); SubNodes[(int)IntraLocation.LOWER_RIGHT].AddItems(ref items, bounds); break; // boundaries are spanning over upper subnodes case IntraLocation.SPANNING_UPPER: SubNodes[(int)IntraLocation.UPPER_LEFT].AddItems(ref items, bounds); SubNodes[(int)IntraLocation.UPPER_RIGHT].AddItems(ref items, bounds); break; // boundaries are spanning over lower subnodes case IntraLocation.SPANNING_LOWER: SubNodes[(int)IntraLocation.LOWER_LEFT].AddItems(ref items, bounds); SubNodes[(int)IntraLocation.LOWER_RIGHT].AddItems(ref items, bounds); break; // boundaries are spanning over all subnodes case IntraLocation.SPANNING: default: AddSubNodeItems(ref items, bounds); break; } } public void AddItems(ref IList items, Bounds? bounds = null) { AddOwnItems(ref items, bounds); AddSubNodeItems(ref items, bounds); } /// /// Adds all items belonging to this node (ignoring sub-nodes) to the provided list of items (). /// If boundaries () are provided then only items intersecting with them will be added. /// /// /// Output list for found items /// Boundaries to look for items within protected internal void AddOwnItems(ref IList items, Bounds? bounds = null) { var itemSource = bounds != null ? _items.Where(item => item.GetBounds().Intersects((Bounds)bounds)) : _items; foreach (var item in itemSource) { items.Add(item); } } /// /// Adds all items belonging to sub-nodes (ignoring own items) to the provided list of items (). /// If boundaries () are provided then only items intersecting with them will be added. /// /// /// Output list for found items /// Boundaries to look for items within protected internal void AddSubNodeItems(ref IList items, Bounds? bounds = null) { foreach (var subNode in SubNodes) subNode.AddItems(ref items, bounds); } public void Clear() { _items.Clear(); SubNodes.Clear(); } public void DrawBounds(bool displayNumberOfItems = false) { #if UNITY_EDITOR if (displayNumberOfItems) Handles.Label(Bounds.center, _items.Count.ToString()); Gizmos.DrawWireCube(Bounds.center, Bounds.size); #endif foreach (var subNode in SubNodes) subNode.DrawBounds(displayNumberOfItems); } } }