diff --git a/src/cs/MonoGame.Extended.Collisions/QuadTree/CollisionComponent.cs b/src/cs/MonoGame.Extended.Collisions/QuadTree/CollisionComponent.cs index 8e31d347..1caf96ac 100644 --- a/src/cs/MonoGame.Extended.Collisions/QuadTree/CollisionComponent.cs +++ b/src/cs/MonoGame.Extended.Collisions/QuadTree/CollisionComponent.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using Microsoft.Xna.Framework; namespace MonoGame.Extended.Collisions @@ -12,8 +11,7 @@ namespace MonoGame.Extended.Collisions /// public class CollisionComponent : SimpleGameComponent { - private readonly Dictionary _targetDataDictionary = - new Dictionary(); + private readonly Dictionary _targetDataDictionary = new(); private readonly Quadtree _collisionTree; @@ -47,14 +45,13 @@ namespace MonoGame.Extended.Collisions // Generate list of collision Infos foreach (var other in collisions) { - var collisionInfo = new CollisionEventArgs() - { - Other = other.Target, - PenetrationVector = CalculatePenetrationVector(value.Bounds, other.Bounds) - }; + var collisionInfo = new CollisionEventArgs + { + Other = other.Target, + PenetrationVector = CalculatePenetrationVector(value.Bounds, other.Bounds) + }; target.OnCollision(collisionInfo); - value.Bounds = value.Target.Bounds; } _collisionTree.Insert(value); } @@ -230,4 +227,4 @@ namespace MonoGame.Extended.Collisions #endregion } -} \ No newline at end of file +} diff --git a/src/cs/MonoGame.Extended.Collisions/QuadTree/QuadTree.cs b/src/cs/MonoGame.Extended.Collisions/QuadTree/QuadTree.cs index df4df180..1bf7bdab 100644 --- a/src/cs/MonoGame.Extended.Collisions/QuadTree/QuadTree.cs +++ b/src/cs/MonoGame.Extended.Collisions/QuadTree/QuadTree.cs @@ -4,18 +4,32 @@ using System.Collections.Generic; namespace MonoGame.Extended.Collisions { /// - /// Class for doing collision handling with a quad tree. + /// Class for doing collision handling with a quad tree. /// public class Quadtree { + /// + /// The default maximum depth. + /// public const int DefaultMaxDepth = 7; + + /// + /// The default maximum objects per node. + /// public const int DefaultMaxObjectsPerNode = 25; + /// + /// Contains the children of this node. + /// protected List Children = new List(); + + /// + /// Contains the data for this node in the quadtree. + /// protected HashSet Contents = new HashSet(); /// - /// Creates a quad tree with the given bounds. + /// Creates a quad tree with the given bounds. /// /// The bounds of the new quad tree. public Quadtree(RectangleF bounds) @@ -24,23 +38,31 @@ namespace MonoGame.Extended.Collisions NodeBounds = bounds; } + /// + /// Gets or sets the current depth for this node in the quadtree. + /// protected int CurrentDepth { get; set; } + /// + /// Gets or sets the maximum depth of the quadtree. + /// protected int MaxDepth { get; set; } = DefaultMaxDepth; - + /// + /// Gets or sets the maximum objects per node in this quadtree. + /// protected int MaxObjectsPerNode { get; set; } = DefaultMaxObjectsPerNode; /// - /// Gets the bounds of the area contained in this quad tree. + /// 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. + /// 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. + /// Counts the number of unique targets in the current Quadtree. /// /// Returns the targets of objects found. public int NumTargets() @@ -74,21 +96,21 @@ namespace MonoGame.Extended.Collisions } } } - for (var i = 0; i < dirtyItems.Count; i++) + foreach (var quadtreeData in dirtyItems) { - dirtyItems[i].MarkClean(); + quadtreeData.MarkClean(); } return objectCount; } /// - /// Inserts the data into the tree. + /// Inserts the data into the tree. /// /// Data being inserted. public void Insert(QuadtreeData data) { - var actorBounds = data.Target.Bounds; - + var actorBounds = data.Bounds; + // Object doesn't fit into this node. if (!NodeBounds.Intersects(actorBounds)) { @@ -114,7 +136,7 @@ namespace MonoGame.Extended.Collisions } /// - /// Removes data from the Quadtree + /// Removes data from the Quadtree /// /// The data to be removed. public void Remove(QuadtreeData data) @@ -126,58 +148,60 @@ namespace MonoGame.Extended.Collisions } else { - throw new InvalidOperationException($"Cannot remove from a non leaf {nameof(Quadtree)}"); + throw new InvalidOperationException($"Cannot remove from a non leaf {nameof(Quadtree)}"); } } /// - /// Removes unneccesary leaf nodes and simplifies the quad tree. + /// Removes unnecessary leaf nodes and simplifies the quad tree. /// public void Shake() { - if (!IsLeaf) + if (IsLeaf) { - List dirtyItems = new List(); + return; + } - var numObjects = NumTargets(); - if (numObjects == 0) + List dirtyItems = new List(); + + var numObjects = NumTargets(); + if (numObjects == 0) + { + Children.Clear(); + } + else if (numObjects < MaxObjectsPerNode) + { + var process = new Queue(); + process.Enqueue(this); + while (process.Count > 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) { - var processing = process.Dequeue(); - if (!processing.IsLeaf) + foreach (var subTree in processing.Children) { - foreach (var subTree in processing.Children) - { - process.Enqueue(subTree); - } + process.Enqueue(subTree); } - else + } + else + { + foreach (var data in processing.Contents) { - foreach (var data in processing.Contents) + if (data.Dirty == false) { - if (data.Dirty == false) - { - AddToLeaf(data); - data.MarkDirty(); - dirtyItems.Add(data); - } + AddToLeaf(data); + data.MarkDirty(); + dirtyItems.Add(data); } } } - Children.Clear(); } + Children.Clear(); + } - for (var i = 0; i < dirtyItems.Count; i++) - { - dirtyItems[i].MarkClean(); - } + foreach (var quadtreeData in dirtyItems) + { + quadtreeData.MarkClean(); } } @@ -188,7 +212,7 @@ namespace MonoGame.Extended.Collisions } /// - /// Splits a quadtree into quadrants. + /// Splits a quadtree into quadrants. /// public void Split() { @@ -233,7 +257,7 @@ namespace MonoGame.Extended.Collisions } /// - /// Queries the quadtree for targets that intersect with the given area. + /// 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. @@ -241,16 +265,16 @@ namespace MonoGame.Extended.Collisions { var recursiveResult = new List(); QueryWithoutReset(area, recursiveResult); - for (var i = 0; i < recursiveResult.Count; i++) + foreach (var quadtreeData in recursiveResult) { - recursiveResult[i].MarkClean(); + quadtreeData.MarkClean(); } return recursiveResult; } private void QueryWithoutReset(IShapeF area, List recursiveResult) { - if (!NodeBounds.Intersects(area)) + if (!NodeBounds.Intersects(area)) return; if (IsLeaf) @@ -273,4 +297,4 @@ namespace MonoGame.Extended.Collisions } } } -} \ No newline at end of file +} diff --git a/src/cs/MonoGame.Extended.Collisions/QuadTree/QuadTreeData.cs b/src/cs/MonoGame.Extended.Collisions/QuadTree/QuadTreeData.cs index d7046d5c..cb50d3b5 100644 --- a/src/cs/MonoGame.Extended.Collisions/QuadTree/QuadTreeData.cs +++ b/src/cs/MonoGame.Extended.Collisions/QuadTree/QuadTreeData.cs @@ -1,65 +1,86 @@ using System.Collections.Generic; using System.Linq; -namespace MonoGame.Extended.Collisions +namespace MonoGame.Extended.Collisions; + +/// +/// Data structure for the quad tree. +/// Holds the entity and collision data for it. +/// +public class QuadtreeData { + private readonly ICollisionActor _target; + private readonly HashSet _parents = new(); + /// - /// Data structure for the quad tree. - /// Holds the entity and collision data for it. + /// Initialize a new instance of QuadTreeData. /// - public class QuadtreeData + /// + public QuadtreeData(ICollisionActor target) { - public QuadtreeData(ICollisionActor target) - { - Target = target; - Bounds = target.Bounds; - } - - public void RemoveParent(Quadtree parent) - { - Parents.Remove(parent); - } - - public void AddParent(Quadtree parent) - { - Parents.Add(parent); - } - - public void RemoveFromAllParents() - { - foreach (Quadtree parent in Parents.ToList()) - { - parent.Remove(this); - } - Parents.Clear(); - } - - public HashSet Parents = new HashSet(); - - /// - /// Gets or sets the Target for collision. - /// - public ICollisionActor Target { get; set; } - - /// - /// Gets or sets whether Target has had its collision handled this - /// iteration. - /// - public bool Dirty { get; private set; } - - public void MarkDirty() - { - Dirty = true; - } - - public void MarkClean() - { - Dirty = false; - } - - /// - /// Gets or sets the bounding box for collision detection. - /// - public IShapeF Bounds { get; set; } + _target = target; } -} \ No newline at end of file + + /// + /// Remove a parent node. + /// + /// + public void RemoveParent(Quadtree parent) + { + _parents.Remove(parent); + } + + /// + /// Add a parent node. + /// + /// + public void AddParent(Quadtree parent) + { + _parents.Add(parent); + } + + /// + /// Remove all parent nodes from this node. + /// + public void RemoveFromAllParents() + { + foreach (var parent in _parents.ToList()) + { + parent.Remove(this); + } + + _parents.Clear(); + } + + /// + /// Gets the bounding box for collision detection. + /// + public IShapeF Bounds => _target.Bounds; + + /// + /// Gets the collision actor target. + /// + public ICollisionActor Target => _target; + + /// + /// Gets or sets whether Target has had its collision handled this + /// iteration. + /// + public bool Dirty { get; private set; } + + /// + /// Mark node as dirty. + /// + public void MarkDirty() + { + Dirty = true; + } + + /// + /// Mark node as clean, i.e. not dirty. + /// + public void MarkClean() + { + Dirty = false; + } +} diff --git a/src/cs/Tests/MonoGame.Extended.Collisions.Tests/CollisionComponentTests.cs b/src/cs/Tests/MonoGame.Extended.Collisions.Tests/CollisionComponentTests.cs index 77f3c3c9..60c1b09d 100644 --- a/src/cs/Tests/MonoGame.Extended.Collisions.Tests/CollisionComponentTests.cs +++ b/src/cs/Tests/MonoGame.Extended.Collisions.Tests/CollisionComponentTests.cs @@ -13,16 +13,14 @@ namespace MonoGame.Extended.Collisions.Tests /// public class CollisionComponentTests { - private CollisionComponent collisionComponent; - - private GameTime _gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromMilliseconds(16)); + private readonly CollisionComponent _collisionComponent; + private readonly GameTime _gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromMilliseconds(16)); public CollisionComponentTests() { - collisionComponent = new CollisionComponent(new RectangleF(Point2.Zero, new Point2(10, 10))); + _collisionComponent = new CollisionComponent(new RectangleF(Point2.Zero, new Point2(10, 10))); } - #region Circle Circle [Fact] @@ -46,9 +44,9 @@ namespace MonoGame.Extended.Collisions.Tests }; Assert.True(shape1.Intersects(shape2)); - collisionComponent.Insert(actor1); - collisionComponent.Insert(actor2); - collisionComponent.Update(_gameTime); + _collisionComponent.Insert(actor1); + _collisionComponent.Insert(actor2); + _collisionComponent.Update(_gameTime); Assert.True(Math.Abs(actor1.Position.Y - -4f) < float.Epsilon); } @@ -73,9 +71,9 @@ namespace MonoGame.Extended.Collisions.Tests }; Assert.True(shape1.Intersects(shape2)); - collisionComponent.Insert(actor1); - collisionComponent.Insert(actor2); - collisionComponent.Update(_gameTime); + _collisionComponent.Insert(actor1); + _collisionComponent.Insert(actor2); + _collisionComponent.Update(_gameTime); // Actor should have moved up because the distance is shorter. Assert.True(actor1.Position.Y > actor2.Position.Y); // The circle centers should be about 4 units away after moving @@ -103,9 +101,9 @@ namespace MonoGame.Extended.Collisions.Tests }; Assert.True(shape1.Intersects(shape2)); - collisionComponent.Insert(actor1); - collisionComponent.Insert(actor2); - collisionComponent.Update(_gameTime); + _collisionComponent.Insert(actor1); + _collisionComponent.Insert(actor2); + _collisionComponent.Update(_gameTime); // Actor should have moved up because the distance is shorter. Assert.True(actor1.Position.Y > actor2.Position.Y); // The circle centers should be about 4 units away after moving @@ -134,9 +132,9 @@ namespace MonoGame.Extended.Collisions.Tests }; Assert.True(shape1.Intersects(shape2)); - collisionComponent.Insert(actor1); - collisionComponent.Insert(actor2); - collisionComponent.Update(_gameTime); + _collisionComponent.Insert(actor1); + _collisionComponent.Insert(actor2); + _collisionComponent.Update(_gameTime); // Actor should have moved up because the distance is shorter. Assert.True(actor1.Position.Y > actor2.Position.Y); // The circle centers should be about 4 units away after moving @@ -169,9 +167,9 @@ namespace MonoGame.Extended.Collisions.Tests }; Assert.True(shape1.Intersects(shape2)); - collisionComponent.Insert(actor1); - collisionComponent.Insert(actor2); - collisionComponent.Update(_gameTime); + _collisionComponent.Insert(actor1); + _collisionComponent.Insert(actor2); + _collisionComponent.Update(_gameTime); Assert.True(Math.Abs(actor1.Position.X - 0.0f) < float.Epsilon); Assert.True(Math.Abs(actor1.Position.Y - 3.0f) < float.Epsilon); } @@ -198,9 +196,9 @@ namespace MonoGame.Extended.Collisions.Tests }; Assert.True(shape1.Intersects(shape2)); - collisionComponent.Insert(actor1); - collisionComponent.Insert(actor2); - collisionComponent.Update(_gameTime); + _collisionComponent.Insert(actor1); + _collisionComponent.Insert(actor2); + _collisionComponent.Update(_gameTime); Assert.True(Math.Abs(actor1.Position.X - 0.0f) < float.Epsilon); Assert.True(Math.Abs(actor1.Position.Y - -2.0f) < float.Epsilon); } @@ -227,9 +225,9 @@ namespace MonoGame.Extended.Collisions.Tests }; Assert.True(shape1.Intersects(shape2)); - collisionComponent.Insert(actor1); - collisionComponent.Insert(actor2); - collisionComponent.Update(_gameTime); + _collisionComponent.Insert(actor1); + _collisionComponent.Insert(actor2); + _collisionComponent.Update(_gameTime); Assert.True(Math.Abs(actor1.Position.X - 2.0f) < float.Epsilon); Assert.True(Math.Abs(actor1.Position.Y - 3.0f) < float.Epsilon); } @@ -260,9 +258,9 @@ namespace MonoGame.Extended.Collisions.Tests }; Assert.True(shape1.Intersects(shape2)); - collisionComponent.Insert(actor1); - collisionComponent.Insert(actor2); - collisionComponent.Update(_gameTime); + _collisionComponent.Insert(actor1); + _collisionComponent.Insert(actor2); + _collisionComponent.Update(_gameTime); Assert.True(Math.Abs(actor1.Position.X - 0.0f) < float.Epsilon); Assert.True(Math.Abs(actor1.Position.Y - 1.0f) < float.Epsilon); } @@ -289,15 +287,90 @@ namespace MonoGame.Extended.Collisions.Tests }; Assert.True(shape1.Intersects(shape2)); - collisionComponent.Insert(actor1); - collisionComponent.Insert(actor2); - collisionComponent.Update(_gameTime); + _collisionComponent.Insert(actor1); + _collisionComponent.Insert(actor2); + _collisionComponent.Update(_gameTime); Assert.True(Math.Abs(actor1.Position.X - 4.0f) < float.Epsilon); Assert.True(Math.Abs(actor1.Position.Y - 3.0f) < float.Epsilon); } - #endregion + public class Behaviours : CollisionComponentTests + { + [Fact] + public void Actors_is_colliding() + { + var staticBounds = new RectangleF(new Point2(0, 0), new Size2(1, 1)); + var anotherStaticBounds = new RectangleF(new Point2(0, 0), new Size2(1, 1)); + var staticActor = new CollisionIndicatingActor(staticBounds); + var anotherStaticActor = new CollisionIndicatingActor(anotherStaticBounds); + _collisionComponent.Insert(staticActor); + _collisionComponent.Insert(anotherStaticActor); + + _collisionComponent.Update(_gameTime); + + Assert.True(staticActor.IsColliding); + Assert.True(anotherStaticActor.IsColliding); + } + + [Fact] + public void Actors_is_not_colliding_when_dynamic_actor_is_moved_out_of_collision_bounds() + { + var staticBounds = new RectangleF(new Point2(0, 0), new Size2(1, 1)); + var dynamicBounds = new RectangleF(new Point2(0, 0), new Size2(1, 1)); + var staticActor = new CollisionIndicatingActor(staticBounds); + var dynamicActor = new CollisionIndicatingActor(dynamicBounds); + _collisionComponent.Insert(staticActor); + _collisionComponent.Insert(dynamicActor); + dynamicActor.MoveTo(new Point2(2, 2)); + + _collisionComponent.Update(_gameTime); + + Assert.False(staticActor.IsColliding); + Assert.False(dynamicActor.IsColliding); + } + + [Fact] + public void Actors_is_colliding_when_dynamic_actor_is_moved_into_collision_bounds() + { + var staticBounds = new RectangleF(new Point2(0, 0), new Size2(1, 1)); + var dynamicBounds = new RectangleF(new Point2(2, 2), new Size2(1, 1)); + var staticActor = new CollisionIndicatingActor(staticBounds); + var dynamicActor = new CollisionIndicatingActor(dynamicBounds); + _collisionComponent.Insert(staticActor); + _collisionComponent.Insert(dynamicActor); + dynamicActor.MoveTo(new Point2(0, 0)); + + _collisionComponent.Update(_gameTime); + + Assert.True(staticActor.IsColliding); + Assert.True(dynamicActor.IsColliding); + } + + private class CollisionIndicatingActor : ICollisionActor + { + private RectangleF _bounds; + + public CollisionIndicatingActor(RectangleF bounds) + { + _bounds = bounds; + } + + public IShapeF Bounds => _bounds; + + public void OnCollision(CollisionEventArgs collisionInfo) + { + IsColliding = true; + } + + public bool IsColliding { get; private set; } + + public void MoveTo(Point2 position) + { + _bounds = new RectangleF(position, _bounds.Size); + } + } + } } -} \ No newline at end of file +}