using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Chernobyl.Collections.Generic; using Chernobyl.Collections.Generic.Hierarchy; using Chernobyl.Event; using Chernobyl.Mathematics.Geometry; using Chernobyl.Mathematics.Movement; using Enumerable = System.Linq.Enumerable; namespace Chernobyl.Mathematics.Collision { /// /// An /// that builds a 2D collision graph using 2D quads that are organized into /// a tree structure. /// /// The type of /// being held within this /// . /// This type must implement /// and /// /// The type that will /// be passed in to check for collisions against the /// s contained within this /// . This type must implement /// public class QuadTree : ShapeDecorator, ICollidableCollection, ITreeEnumerable> where TContainedCollidable : IExtendedCollidable where TCollider : IExtendedCollidable { /// /// Constructor. /// /// The area this /// covers. /// This is the area where /// can be placed for collision detection. This /// can not /// handle collision detection outside of this area. public QuadTree(Rectangle area) : this(area, DefaultMaxCollidablesPerNode) {} /// /// Constructor. /// /// The area this /// covers. /// This is the area where /// can be placed for collision detection. This /// can not /// handle collision detection outside of this area. /// The maximum number of /// that can be included in /// a single node of a /// before that /// node is split and gains children nodes. By default, this value is /// specified by the field . /// Thrown when the value /// of is zero. Each node can /// hold 1 or more s per /// node. public QuadTree(Rectangle area, uint maxCollidablesPerNode) { _volume = area; MaxCollidablesPerNode = maxCollidablesPerNode; Collidables = new List((int)MaxCollidablesPerNode); DirtyCollidables = new List(); Children = new QuadTree[ChildrenCount]; } /// /// Adds an object to this /// . /// The object added to this list will be sorted into the collision /// graph of this /// . /// /// The /// object to add. /// Thrown if the add failed because the /// item could not be completely contained within this /// . public void Add(TContainedCollidable item) { if (TryAdd(item) == false) throw new ArgumentException("Failed to add the item \"" + item + "\" to the QuadTree \"" + this + "\". The item could not be entirely contained within " + "the QuadTree. Please expand " + "the size of the QuadTree " + "to allow the item to fit.", "item"); } /// /// Attempts to add the specified /// object to this /// . /// If the add is successful then the object will have been sorted into /// the collision graph of this /// . /// /// The /// object to add. /// True if the add was successful, false if the add could not /// be completed because the item could not be completely contained /// within this . public bool TryAdd(TContainedCollidable item) { bool result = false; if(_volume.Encloses(item) == true) { // either we will take the new item, or one of our children will result = true; ++_count; // if we haven't reached our max number of collidables, then // we can just add this to our collidables list if (Collidables.Count < MaxCollidablesPerNode) { Collidables.Add(item); item.TransformDirtied += OnCollidableTransformDirtied; } else { // if we don't have children, we will have to create them // and attempt to give them our children nodes if(HasChildren == false) { uint childrenCount = 0; Children[childrenCount++] = new QuadTree(_volume.NorthWestQuadrant, MaxCollidablesPerNode); Children[childrenCount++] = new QuadTree(_volume.NorthEastQuadrant, MaxCollidablesPerNode); Children[childrenCount++] = new QuadTree(_volume.SouthWestQuadrant, MaxCollidablesPerNode); Children[childrenCount++] = new QuadTree(_volume.SouthEastQuadrant, MaxCollidablesPerNode); // we create only four children because that's all we can // with a Rectangle, but if the ChildrenCount says // otherwise, we've got problems. if (ChildrenCount != childrenCount) throw new Exception("The number of children specified " + "in ChildrenCount was greater than the number of children " + "created. Please contact Lokas Gate if you see this exception." + "Include the exception information in your correspondence."); foreach (QuadTree child in Children) { // subscribe to this child's OnCollision event child.OnCollisionStarted += OnChildQuadTreeCollision; child.TreeDirtied += OnChildTreeDirtied; child.CollidableMovedOut += OnChildCollidableMovedOut; // attempt to add our Collidables to our new children, // if we succeed for any one of them, than that collidable // now belongs to our children for (int i = 0; i < Collidables.Count; i++) { TContainedCollidable collidable = Collidables[i]; if (child.TryAdd(collidable) == true) { collidable.TransformDirtied -= OnCollidableTransformDirtied; Collidables.RemoveAt(i); } } } } // try to add the new item to one of our children, if none // of our children will take it, we will. if (Children.Any(quad => quad.TryAdd(item)) == false) { Collidables.Add(item); item.TransformDirtied += OnCollidableTransformDirtied; } } } return result; } /// /// Removes the specified /// object from this /// . /// /// The /// object to remove from this /// . /// True if was successfully removed /// from this , /// otherwise false. This method also returns false if /// could not be found within this /// . public bool Remove(TContainedCollidable item) { bool result = false; // make sure we've resorted dirty collidables first Clean(); // ensure the collidable is inside the volume if (_count != 0 && _volume.Encloses(item) == true) { result = Collidables.Remove(item); if (result == false) { uint numChildrenWithoutCollidables = 0; // the item is probably in a child, if we have one if (HasChildren == true) { foreach (QuadTree child in Children) { // if our children no longer have collidables, they // will be pruned to save memory. if (result == false) { if (child.Remove(item) == true) { result = true; --_count; } } // if our children no longer have collidables, they // will be pruned to save memory. if (child.Count == 0) ++numChildrenWithoutCollidables; } // prune our children if they contain no collidables if (numChildrenWithoutCollidables == Children.Length) ClearChildren(); } } else { item.TransformDirtied -= OnCollidableTransformDirtied; --_count; } } return result; } /// /// Removes all objects /// from this quad tree and all children quad trees. /// public void Clear() { Collidables.Clear(); ClearChildren(); _count = 0; } /// /// Checks if the item passed in is located within the quad tree. /// /// The /// object to look for. /// True if the is contained within this ///quad tree, false if otherwise. public bool Contains(TContainedCollidable item) { // make sure we've resorted dirty collidables first Clean(); bool result = false; if(_volume.Encloses(item) == true) { result = Collidables.Contains(item) || (HasChildren == true && Children.Any(child => child.Contains(item))); } return result; } /// /// Copies the contents of this quad tree into an array of /// objects. /// /// The array that is the destination for all of the /// objects copied from this quad tree. /// The zero based index at which copying begins. public void CopyTo(TContainedCollidable[] array, int arrayIndex) { // make sure we've resorted dirty collidables first Clean(); if(array == null) throw new ArgumentNullException("array", "The array to copy into " + "is null. It must be set to a valid array."); if (arrayIndex < 0) throw new ArgumentOutOfRangeException("arrayIndex", "index at " + "which to begin copying is less than 0. Cannot begin copying " + "at a negative number."); if(_count > (array.Length - arrayIndex)) throw new ArgumentException("The number of elements in this " + "ICollection is greater than the available space from " + "arrayIndex to the end of the destination array", "arrayIndex"); if(Collidables.Count != 0) { Collidables.CopyTo(array, arrayIndex); arrayIndex += Collidables.Count; } if (HasChildren == true) { for (int i = 0; i < Children.Length && arrayIndex < array.Length; i++) { QuadTree child = Children[i]; child.CopyTo(array, arrayIndex); arrayIndex += child.Count; } } } /// /// Returns the number of /// instances within this /// and all children s. /// public int Count { get { // make sure we've resorted dirty collidables first Clean(); return _count; } private set { _count = value; } } /// /// True if this quad tree cannot be modified, false if otherwise. /// public bool IsReadOnly { get { return false; } } /// /// Returns an that can be iterated /// over to collect the objects that are colliding with the passed in /// . Note this method should never /// pre-compute the collisions. It should return an /// that returns an /// that checks for collisions as it /// iterates. /// /// The to check /// against for collisions. /// The that can be iterated over /// to get the s that are being collided /// against. public IEnumerable GetCollidables(TCollider collider) { // TODO: see if this method can be made faster // make sure we've resorted dirty collidables first Clean(); // make sure this quad tree collides with our volume first if (collider.IsColliding(_volume) == true) { // check our collidables for potential collisions foreach (TContainedCollidable collidable in Collidables.Where(collidable => collidable.IsColliding(collider) == true)) { yield return collidable; } // ask the children for their collidables if (HasChildren == true) { foreach (TContainedCollidable collidable in Children.SelectMany(child => child.GetCollidables(collider))) { yield return collidable; } } } } /// /// Returns an enumerator that iterates through the /// objects contained within this quad tree. /// /// The enumerator to the held /// objects. public IEnumerator GetEnumerator() { return ((IEnumerable>)this).SelectMany(quadTrees => quadTrees.Collidables).GetEnumerator(); } /// /// Returns an enumerator that iterates through the /// objects contained within this quad tree. /// /// The enumerator to the held /// objects. IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } /// /// Returns an enumerator that iterates through the collection. /// /// A that can be used to iterate /// through the collection. IEnumerator> IEnumerable>.GetEnumerator() { return BreadthFirst.GetEnumerator(); } /// /// An event that is raised when a collidable has started colliding with /// this collidable. When the collision has ended, /// will be raised. /// public event EventHandler OnCollisionStarted; /// /// An event that is raised when the collision with a collidable has /// ended. This will event will be raised after the invocation of /// . /// public event EventHandler OnCollisionEnded; /// /// Checks for a collision between this quad tree and the /// passed in. /// /// The to check /// for a collision against. /// True if there was a collision against a /// within this /// , false if /// otherwise. public bool IsColliding(TCollider collider) { bool result = false; // make sure we've resorted dirty collidables first Clean(); if (collider.IsColliding(_volume) == true) { // first check against our collidables if we have collidables result = Collidables.Any(coll => coll.IsColliding(collider)); // now check against our children, if we have children and // if necessary if (result == false && HasChildren == true) result = Children.Any(child => child.IsColliding(collider)); } return result; } /// /// Called when this collidable has collided with another collidable. /// This QuadTree just invokes its OnCollision instance when this method /// is invoked. /// /// The object that was just hit. public void HandleCollisionStarted(ICollidable collider) { if (OnCollisionStarted != null) OnCollisionStarted(this, EventArgs.Empty); } /// /// Called when this collidable has stopped colliding with another /// collidable. /// /// The object that was just hit. public void HandleCollisionEnded(ICollidable collider) { if (OnCollisionEnded != null) OnCollisionEnded(this, EventArgs.Empty); } /// /// The maximum number of /// that can be included in a single node of a /// before that /// node is split and gains children nodes. By default, this value is /// specified by the field . /// /// Thrown when the value /// set on this property is zero. Each node can hold 1 or more /// s per node. public uint MaxCollidablesPerNode { get { return _maxCollidablesPerNode; } private set { if(value == 0) throw new ArgumentOutOfRangeException("value", value, "The " + "value of MaxCollidablesPerNode cannot be zero. Each " + "node can only hold 1 or more collidables per node."); _maxCollidablesPerNode = value; } } /// /// The default value for the /// property. The value of this field is 4. /// public const uint DefaultMaxCollidablesPerNode = 4; /// /// An that iterates over an /// in depth first order (see /// http://en.wikipedia.org/wiki/Depth-first_search for more information). /// /// public IEnumerable> DepthFirst { get { return new EnumeratorFactory>( () => new DepthFirstEnumerator>(this, GetChildQuadTreeEnumerator)); } } /// /// An that iterates over an /// in breadth first order (see /// http://en.wikipedia.org/wiki/Breadth-first_search for more information). /// /// public IEnumerable> BreadthFirst { get { return new EnumeratorFactory>( () => new BreadthFirstEnumerator>(this, GetChildQuadTreeEnumerator)); } } /// /// Returns a that represents this instance. /// /// /// A that represents this instance. /// public override string ToString() { return "{Name=\"" + Name + "\", Volume:" + _volume + "}"; } /// /// Cleans the instance by re-adding any dirty (translated, scaled, /// rotated, etc) s, if /// possible. If not possible, the /// is removed and the /// event is raised. This method also /// invokes on the children /// of this /// . Note that /// it is not required that you call this method. The /// will call it /// whenever it is necessary (such as when you access the number of /// s contained within this /// using the /// property). You may need it, however, in cases /// where you want the event called /// early (if it needs to be called). /// public void Clean() { if (IsTreeDirty == true) { if (DirtyCollidables.Count != 0) { // attempt to re-add the dirty collidables to this QuadTree. // If they cannot be re-added, those who need to know will be // notified and the collidable will be removed from this QuadTree. foreach (TContainedCollidable collidable in DirtyCollidables) { // we'll assume we are removing all of the dirty // collidables from this QuadTree. If a re-add is successful // the TryAdd method we'll increment the Count as appropriate _count--; if (TryAdd(collidable) == false) { if (CollidableMovedOut != null) CollidableMovedOut(this, new EventArgs(collidable)); } } DirtyCollidables.Clear(); } // clean the rest of the tree below this one if (HasChildren == true) { foreach (QuadTree child in Children) child.Clean(); } IsTreeDirty = false; } } /// /// The children of this ; /// each has the /// number of children specified by . The /// children (items in the array not the array itself) may be set to a /// NULL value if this /// has no children. /// public QuadTree[] Children { get; private set; } /// /// The number of children each /// is given. /// The value of this field is 4. /// public const uint ChildrenCount = 4; /// /// An event that is raised after a /// contained within this /// or a /// descendant /// has had its transform dirtied. In this case, the /// may no longer fit in this /// and will /// need to be resorted upwards in the tree. Note that this event is /// only invoked once . After /// it has been invoked, the /// must be /// cleaned by calling for it to be invoked again. /// public event EventHandler TreeDirtied; /// /// An event that is raised after a /// that was previously /// contained within this /// has been /// removed because it has translated, rotated, and/or scaled and can no /// longer be contained within this /// . Note that /// this event is only invoked if the /// is cleaned /// using the method. /// public event EventHandler> CollidableMovedOut; /// /// True if this /// contains s that need to /// be resorted upwards in the tree because their /// s were dirtied (they moved or scaled). If /// the value of the property is false and true is set on this property /// the will be invoked (provided there are /// subscribers). /// protected bool IsTreeDirty { get { return _isTreeDirty; } private set { bool previousValue = _isTreeDirty; _isTreeDirty = value; if (value == true && previousValue == false && TreeDirtied != null) TreeDirtied(this, EventArgs.Empty); } } /// /// The instance that implements the /// functionality. /// protected override IShape DecoratedShape { get { return _volume; } } /// /// The transform that provides this transforms capabilities. /// protected override ITransform DecoratedTransform { get { return _volume; } } /// /// A event handler that invokes the /// event when a child /// 's /// is collided against. /// /// The sender of the event. /// The arguments to the event. void OnChildQuadTreeCollision(object sender, EventArgs e) { if (OnCollisionStarted != null) OnCollisionStarted(this, e); } /// /// An event handler that is invoked when a child /// has been /// dirtied (it's event has been invoked). /// /// The instance that generated the event. /// The instance containing the event data. void OnChildTreeDirtied(object sender, EventArgs e) { IsTreeDirty = true; } /// /// An event that is raised when a /// contained within this /// is dirtied /// (translated, scaled, etc) and needs to be resorted up into the tree. /// /// The source of the event. /// The instance containing /// the event data. void OnCollidableTransformDirtied(object sender, EventArgs e) { // we remove by index for performance reasons: to prevent having // to cast the sender and to prevent having to search the entire // list with the List.RemoveAll(Predicate) method. int index = Collidables.FindIndex(coll => ReferenceEquals(coll, sender)); if (index != -1) { DirtyCollidables.Add(Collidables[index]); Collidables.RemoveAt(index); IsTreeDirty = true; } } /// /// An event handler that is invoked by a child when it can no longer /// contain a (the child /// invoked it's event). This method /// will attempt to re-add the collidable. If the re-add fails, this /// 's /// event will be raised. /// /// The instance that generated the event. /// The /// instance containing the event data. void OnChildCollidableMovedOut(object sender, EventArgs e) { // one of our children has rejected a collidable because the // collidable no longer fit within its bounds. We are going to // reduce the count because of this and attempt to re-add the // collidable. If the add succeeds then the TryAdd will increment // the count back up. _count--; if (TryAdd(e.Value) == false && CollidableMovedOut != null) CollidableMovedOut(this, e); } /// /// A helper method that gets rid of our children. /// void ClearChildren() { for (int i = 0; i < Children.Length; i++) { // in case someone else has a reference to our child // we will remove ourself from the OnCollisionStarted // event even though we get rid of the child. Children[i].OnCollisionStarted -= OnChildQuadTreeCollision; Children[i].TreeDirtied -= OnChildTreeDirtied; Children[i].CollidableMovedOut -= OnChildCollidableMovedOut; Children[i] = null; } } /// /// True if this /// has children, false if otherwise. /// bool HasChildren { get { return Children[0] != null; } } /// /// Retrieves the for the Children list of /// a . This /// method is special in that it checks to see if the Children list /// is empty (i.e., all the children are set to null) and, if so, /// returns an from an empty /// . /// /// The /// whose /// children is to be retrieved. /// The to the /// list or an from /// an empty . static IEnumerator> GetChildQuadTreeEnumerator(QuadTree quadTree) { return quadTree.HasChildren == true ? ((IEnumerable>)quadTree.Children).GetEnumerator() : Enumerable.Empty>().GetEnumerator(); } /// /// The collidables that are located in this quad tree. If this quad /// tree has children, then the collidables will be moved to them, if /// possible. /// List Collidables { get; set; } /// /// The s whose transforms /// were dirtied because they had been translated, rotated, scaled, etc. /// These s need to be /// resorted upwards in the tree. /// List DirtyCollidables { get; set; } /// /// The bounding volume for the quad tree. /// Rectangle _volume; /// /// The backing field to /// uint _maxCollidablesPerNode; /// /// The backing field to . /// bool _isTreeDirty; /// /// The backing field to . /// int _count; } }