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;
}
}