summaryrefslogtreecommitdiff
path: root/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests
diff options
context:
space:
mode:
Diffstat (limited to 'Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests')
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/CollisionComponentTests.cs416
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/Implementation/BasicActor.cs33
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/Implementation/BasicWall.cs20
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/MonoGame.Extended.Collisions.Tests.csproj7
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/QuadTreeTests.cs446
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/SpatialHashTests.cs39
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/packages.config6
7 files changed, 967 insertions, 0 deletions
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/CollisionComponentTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/CollisionComponentTests.cs
new file mode 100644
index 0000000..39ac707
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/CollisionComponentTests.cs
@@ -0,0 +1,416 @@
+using System;
+using Microsoft.Xna.Framework;
+using Xunit;
+
+namespace MonoGame.Extended.Collisions.Tests
+{
+ using MonoGame.Extended.Collisions.Layers;
+
+ /// <summary>
+ /// Test collision of actors with various shapes.
+ /// </summary>
+ /// <remarks>
+ /// Uses the fact that <see cref="BasicActor"/> moves itself away from
+ /// <see cref="BasicWall"/> on collision.
+ /// </remarks>
+ public class CollisionComponentTests
+ {
+ 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)));
+ }
+
+ #region Circle Circle
+
+ [Fact]
+ public void PenetrationVectorSameCircleTest()
+ {
+ Point2 pos1 = Point2.Zero;
+ Point2 pos2 = Point2.Zero;
+
+ IShapeF shape1 = new CircleF(pos1, 2.0f);
+ IShapeF shape2 = new CircleF(pos2, 2.0f);
+
+ var actor1 = new BasicActor()
+ {
+ Position = pos1,
+ Bounds = shape1
+ };
+ var actor2 = new BasicWall()
+ {
+ Position = pos2,
+ Bounds = shape2
+ };
+
+ Assert.True(shape1.Intersects(shape2));
+ _collisionComponent.Insert(actor1);
+ _collisionComponent.Insert(actor2);
+ _collisionComponent.Update(_gameTime);
+ Assert.True(Math.Abs(actor1.Position.Y - -4f) < float.Epsilon);
+ }
+
+ [Fact]
+ public void PenetrationVectorSlightlyOverlappingCircleTest()
+ {
+ Point2 pos1 = new Point2(0, 1.5f);
+ Point2 pos2 = Point2.Zero;
+
+ IShapeF shape1 = new CircleF(pos1, 2.0f);
+ IShapeF shape2 = new CircleF(pos2, 2.0f);
+
+ var actor1 = new BasicActor()
+ {
+ Position = pos1,
+ Bounds = shape1
+ };
+ var actor2 = new BasicWall()
+ {
+ Position = pos2,
+ Bounds = shape2
+ };
+
+ Assert.True(shape1.Intersects(shape2));
+ _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
+ Assert.True(Math.Abs(actor1.Position.Y - 4.0f) < float.Epsilon);
+ }
+
+ [Fact]
+ public void PenetrationVectorSlightlyOverlappingOffAxisTest()
+ {
+ Point2 pos1 = new Point2(2, 2.5f);
+ Point2 pos2 = new Point2(2, 1);
+
+ IShapeF shape1 = new CircleF(pos1, 2.0f);
+ IShapeF shape2 = new CircleF(pos2, 2.0f);
+
+ var actor1 = new BasicActor()
+ {
+ Position = pos1,
+ Bounds = shape1
+ };
+ var actor2 = new BasicWall()
+ {
+ Position = pos2,
+ Bounds = shape2
+ };
+
+ Assert.True(shape1.Intersects(shape2));
+ _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
+ Assert.True(Math.Abs(actor1.Position.Y - 5.0f) < float.Epsilon);
+ Assert.True(Math.Abs(actor1.Position.X - 2.0f) < float.Epsilon);
+ }
+
+ [Fact]
+ public void PenetrationZeroRadiusCircleCircleTest()
+ {
+ Point2 pos1 = new Point2(0, 1.5f);
+ Point2 pos2 = Point2.Zero;
+
+ IShapeF shape1 = new CircleF(pos1, 0);
+ IShapeF shape2 = new CircleF(pos2, 2.0f);
+
+ var actor1 = new BasicActor()
+ {
+ Position = pos1,
+ Bounds = shape1
+ };
+ var actor2 = new BasicWall()
+ {
+ Position = pos2,
+ Bounds = shape2
+ };
+
+ Assert.True(shape1.Intersects(shape2));
+ _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
+ // Assert.True(Math.Abs(actor1.Position.Y - 2.0f) < float.Epsilon);
+ }
+
+ #endregion
+
+ #region Circle Rectangle
+
+ [Fact]
+ public void PenetrationVectorCircleRectangleTest()
+ {
+ Point2 pos1 = new Point2(0, 1);
+ Point2 pos2 = new Point2(-2, -1);
+
+ IShapeF shape1 = new CircleF(pos1, 2.0f);
+ IShapeF shape2 = new RectangleF(pos2, new Size2(4, 2));
+
+
+ var actor1 = new BasicActor()
+ {
+ Position = pos1,
+ Bounds = shape1
+ };
+ var actor2 = new BasicWall()
+ {
+ Position = pos2,
+ Bounds = shape2
+ };
+
+ Assert.True(shape1.Intersects(shape2));
+ _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);
+ }
+
+ [Fact]
+ public void PenetrationVectorCircleContainedInRectangleTest()
+ {
+ Point2 pos1 = new Point2(0, 0);
+ Point2 pos2 = new Point2(-2, -1);
+
+ IShapeF shape1 = new CircleF(pos1, 1.0f);
+ IShapeF shape2 = new RectangleF(pos2, new Size2(4, 2));
+
+
+ var actor1 = new BasicActor()
+ {
+ Position = pos1,
+ Bounds = shape1
+ };
+ var actor2 = new BasicWall()
+ {
+ Position = pos2,
+ Bounds = shape2
+ };
+
+ Assert.True(shape1.Intersects(shape2));
+ _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);
+ }
+
+ [Fact]
+ public void PenetrationVectorCircleOffAxisRectangleTest()
+ {
+ Point2 pos1 = new Point2(2, 1);
+ Point2 pos2 = new Point2(-2, -1);
+
+ IShapeF shape1 = new CircleF(pos1, 2.0f);
+ IShapeF shape2 = new RectangleF(pos2, new Size2(4, 2));
+
+
+ var actor1 = new BasicActor()
+ {
+ Position = pos1,
+ Bounds = shape1
+ };
+ var actor2 = new BasicWall()
+ {
+ Position = pos2,
+ Bounds = shape2
+ };
+
+ Assert.True(shape1.Intersects(shape2));
+ _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);
+ }
+
+ #endregion
+
+ #region Rectangle Rectangle
+
+ [Fact]
+ public void PenetrationVectorRectangleRectangleTest()
+ {
+ Point2 pos1 = new Point2(0, 0);
+ Point2 pos2 = new Point2(-2, -1);
+
+ IShapeF shape1 = new RectangleF(pos1, new Size2(4, 2));
+ IShapeF shape2 = new RectangleF(pos2, new Size2(4, 2));
+
+
+ var actor1 = new BasicActor()
+ {
+ Position = pos1,
+ Bounds = shape1
+ };
+ var actor2 = new BasicWall()
+ {
+ Position = pos2,
+ Bounds = shape2
+ };
+
+ Assert.True(shape1.Intersects(shape2));
+ _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);
+ }
+
+ [Fact]
+ public void PenetrationVectorRectangleRectangleOffAxisTest()
+ {
+ Point2 pos1 = new Point2(4, 2);
+ Point2 pos2 = new Point2(3, 1);
+
+ IShapeF shape1 = new RectangleF(pos1, new Size2(4, 2));
+ IShapeF shape2 = new RectangleF(pos2, new Size2(4, 2));
+
+
+ var actor1 = new BasicActor()
+ {
+ Position = pos1,
+ Bounds = shape1
+ };
+ var actor2 = new BasicWall()
+ {
+ Position = pos2,
+ Bounds = shape2
+ };
+
+ Assert.True(shape1.Intersects(shape2));
+ _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_after_update()
+ {
+ var staticBounds = new RectangleF(new Point2(0, 0), new Size2(1, 1));
+ var staticActor = new CollisionIndicatingActor(staticBounds);
+ _collisionComponent.Insert(staticActor);
+ for (int i = 0; i < QuadTree.QuadTree.DefaultMaxObjectsPerNode; i++)
+ {
+ var fillerBounds = new RectangleF(new Point2(0, 2), new Size2(.1f, .1f));
+ var fillerActor = new CollisionIndicatingActor(fillerBounds);
+ _collisionComponent.Insert(fillerActor);
+ }
+
+ var dynamicBounds = new RectangleF(new Point2(2, 2), new Size2(1, 1));
+ var dynamicActor = new CollisionIndicatingActor(dynamicBounds);
+ _collisionComponent.Insert(dynamicActor);
+
+ _collisionComponent.Update(_gameTime);
+ Assert.False(staticActor.IsColliding);
+ Assert.False(dynamicActor.IsColliding);
+
+ dynamicActor.MoveTo(new Point2(0, 0));
+
+ _collisionComponent.Update(_gameTime);
+ Assert.True(dynamicActor.IsColliding);
+ Assert.True(staticActor.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);
+ }
+
+ [Fact]
+ public void InsertActor_ThrowsUndefinedLayerException_IfThereIsNoLayerDefined()
+ {
+ var sut = new CollisionComponent();
+
+ var act = () => sut.Insert(new CollisionIndicatingActor(RectangleF.Empty));
+
+ Assert.Throws<UndefinedLayerException>(act);
+ }
+
+ 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);
+ }
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/Implementation/BasicActor.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/Implementation/BasicActor.cs
new file mode 100644
index 0000000..fe56fdc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/Implementation/BasicActor.cs
@@ -0,0 +1,33 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Collisions.Tests
+{
+ public class BasicActor : ICollisionActor
+ {
+ public Vector2 Position { get; set; }
+ public IShapeF Bounds { get; set; }
+ public Vector2 Velocity { get; set; }
+
+ public BasicActor()
+ {
+ Bounds = new RectangleF(0f, 0f, 1f, 1f);
+ }
+ public void OnCollision(CollisionEventArgs collisionInfo)
+ {
+ Bounds.Position -= collisionInfo.PenetrationVector;
+ Position -= collisionInfo.PenetrationVector;
+
+ if (collisionInfo.Other is BasicActor)
+ {
+ CollisionCount++;
+ }
+ else
+ {
+ Console.WriteLine(collisionInfo.Other.GetType().Name);
+ }
+ }
+
+ public int CollisionCount { get; set; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/Implementation/BasicWall.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/Implementation/BasicWall.cs
new file mode 100644
index 0000000..3492735
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/Implementation/BasicWall.cs
@@ -0,0 +1,20 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Collisions.Tests
+{
+ public class BasicWall : ICollisionActor
+ {
+ public Vector2 Position { get; set; }
+ public IShapeF Bounds { get; set; }
+ public Vector2 Velocity { get; set; }
+
+ public BasicWall()
+ {
+ Bounds = new RectangleF(0f, 0f, 1f, 1f);
+ }
+ public void OnCollision(CollisionEventArgs collisionInfo)
+ {
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/MonoGame.Extended.Collisions.Tests.csproj b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/MonoGame.Extended.Collisions.Tests.csproj
new file mode 100644
index 0000000..c62c1cd
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/MonoGame.Extended.Collisions.Tests.csproj
@@ -0,0 +1,7 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\source\MonoGame.Extended.Collisions\MonoGame.Extended.Collisions.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/QuadTreeTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/QuadTreeTests.cs
new file mode 100644
index 0000000..1013288
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/QuadTreeTests.cs
@@ -0,0 +1,446 @@
+using System.Collections.Generic;
+using MonoGame.Extended.Collisions.QuadTree;
+using Xunit;
+
+namespace MonoGame.Extended.Collisions.Tests
+{
+ public class QuadTreeTests
+ {
+ private QuadTree.QuadTree MakeTree()
+ {
+ // Bounds set to ensure actors will fit inside the tree with default bounds.
+ var bounds = _quadTreeArea;
+ var tree = new QuadTree.QuadTree(bounds);
+
+ return tree;
+ }
+
+ private RectangleF _quadTreeArea = new RectangleF(-10f, -15, 20.0f, 30.0f);
+
+ [Fact]
+ public void ConstructorTest()
+ {
+ var bounds = new RectangleF(-10f, -15, 20.0f, 30.0f);
+ var tree = new QuadTree.QuadTree(bounds);
+
+ Assert.Equal(bounds, tree.NodeBounds);
+ Assert.True(tree.IsLeaf);
+ }
+
+ [Fact]
+ public void NumTargetsEmptyTest()
+ {
+ var tree = MakeTree();
+
+ Assert.Equal(0, tree.NumTargets());
+ }
+
+ [Fact]
+ public void NumTargetsOneTest()
+ {
+ var tree = MakeTree();
+ var actor = new BasicActor();
+
+ tree.Insert(new QuadtreeData(actor));
+
+ Assert.Equal(1, tree.NumTargets());
+ }
+
+
+ [Fact]
+ public void NumTargetsMultipleTest()
+ {
+ var tree = MakeTree();
+ for (int i = 0; i < 5; i++)
+ {
+ tree.Insert(new QuadtreeData(new BasicActor()));
+ }
+
+ Assert.Equal(5, tree.NumTargets());
+ }
+
+ [Fact]
+ public void NumTargetsManyTest()
+ {
+ var tree = MakeTree();
+ for (int i = 0; i < 1000; i++)
+ {
+ tree.Insert(new QuadtreeData(new BasicActor()));
+ Assert.Equal(i + 1, tree.NumTargets());
+ }
+
+ Assert.Equal(1000, tree.NumTargets());
+ }
+
+ [Fact]
+ public void InsertOneTest()
+ {
+ var tree = MakeTree();
+ var actor = new BasicActor();
+
+ tree.Insert(new QuadtreeData(actor));
+
+ Assert.Equal(1, tree.NumTargets());
+ }
+
+ [Fact]
+ public void InsertOneOverlappingQuadrantsTest()
+ {
+ var tree = MakeTree();
+ var actor = new BasicActor
+ {
+ Bounds = new RectangleF(-2.5f, -2.5f, 5f, 5f)
+ };
+
+ tree.Insert(new QuadtreeData(actor));
+
+ Assert.Equal(1, tree.NumTargets());
+ }
+
+ [Fact]
+ public void InsertMultipleTest()
+ {
+ var tree = MakeTree();
+
+ for (int i = 0; i < 10; i++)
+ {
+ tree.Insert(new QuadtreeData(new BasicActor()
+ {
+ Bounds = new RectangleF(0, 0, 1, 1)
+ }));
+ }
+
+ Assert.Equal(10, tree.NumTargets());
+ }
+
+ [Fact]
+ public void InsertManyTest()
+ {
+ var tree = MakeTree();
+
+ for (int i = 0; i < 1000; i++)
+ {
+ tree.Insert(new QuadtreeData(new BasicActor()
+ {
+ Bounds = new RectangleF(0, 0, 1, 1)
+ }));
+ }
+
+ Assert.Equal(1000, tree.NumTargets());
+ }
+
+ [Fact]
+ public void InsertMultipleOverlappingQuadrantsTest()
+ {
+ var tree = MakeTree();
+
+ for (int i = 0; i < 10; i++)
+ {
+ var actor = new BasicActor()
+ {
+ Bounds = new RectangleF(-10f, -15, 20.0f, 30.0f)
+ };
+ tree.Insert(new QuadtreeData(actor));
+ }
+
+ Assert.Equal(10, tree.NumTargets());
+ }
+
+ [Fact]
+ public void RemoveToEmptyTest()
+ {
+ var actor = new BasicActor()
+ {
+ Bounds = new RectangleF(-5f, -7f, 10.0f, 15.0f)
+ };
+ var data = new QuadtreeData(actor);
+
+ var tree = MakeTree();
+ tree.Insert(data);
+
+ tree.Remove(data);
+
+ Assert.Equal(0, tree.NumTargets());
+ }
+
+ [Fact]
+ public void RemoveTwoTest()
+ {
+ var tree = MakeTree();
+ var inserted = new List<QuadtreeData>();
+ var numTargets = 2;
+
+ for (int i = 0; i < numTargets; i++)
+ {
+ var data = new QuadtreeData(new BasicActor()
+ {
+ Bounds = new RectangleF(0, 0, 1, 1)
+ });
+ tree.Insert(data);
+ inserted.Add(data);
+ }
+
+
+ var inTree = numTargets;
+ Assert.Equal(inTree, tree.NumTargets());
+
+ foreach (var data in inserted)
+ {
+ tree.Remove(data);
+ Assert.Equal(--inTree, tree.NumTargets());
+ }
+ }
+
+ [Fact]
+ public void RemoveThreeTest()
+ {
+ var tree = MakeTree();
+ var inserted = new List<QuadtreeData>();
+ var numTargets = 3;
+
+ for (int i = 0; i < numTargets; i++)
+ {
+ var data = new QuadtreeData(new BasicActor()
+ {
+ Bounds = new RectangleF(0, 0, 1, 1)
+ });
+ tree.Insert(data);
+ inserted.Add(data);
+ }
+
+
+ var inTree = numTargets;
+ Assert.Equal(inTree, tree.NumTargets());
+
+ foreach (var data in inserted)
+ {
+ tree.Remove(data);
+ Assert.Equal(--inTree, tree.NumTargets());
+ }
+ }
+
+ [Fact]
+ public void RemoveManyTest()
+ {
+ var tree = MakeTree();
+ var inserted = new List<QuadtreeData>();
+ var numTargets = 1000;
+
+ for (int i = 0; i < numTargets; i++)
+ {
+ var data = new QuadtreeData(new BasicActor()
+ {
+ Bounds = new RectangleF(0, 0, 1, 1)
+ });
+ tree.Insert(data);
+ inserted.Add(data);
+ }
+
+
+ var inTree = numTargets;
+ Assert.Equal(inTree, tree.NumTargets());
+
+ foreach (var data in inserted)
+ {
+ data.RemoveFromAllParents();
+ Assert.Equal(--inTree, tree.NumTargets());
+ }
+ }
+
+
+ [Fact]
+ public void ShakeWhenEmptyTest()
+ {
+ var tree = MakeTree();
+ tree.Shake();
+
+ Assert.Equal(0, tree.NumTargets());
+ }
+
+ [Fact]
+ public void ShakeAfterSplittingWhenEmptyTest()
+ {
+ var tree = MakeTree();
+
+ tree.Split();
+ tree.Shake();
+ Assert.Equal(0, tree.NumTargets());
+ }
+
+ [Fact]
+ public void ShakeAfterSplittingNotEmptyTest()
+ {
+ var tree = MakeTree();
+
+ tree.Split();
+ var data = new QuadtreeData(new BasicActor());
+ tree.Insert(data);
+ tree.Shake();
+ Assert.Equal(1, tree.NumTargets());
+ }
+
+ [Fact]
+ public void ShakeWhenContainingOneTest()
+ {
+ var tree = MakeTree();
+ var numTargets = 1;
+
+ for (int i = 0; i < numTargets; i++)
+ {
+ var data = new QuadtreeData(new BasicActor());
+ tree.Insert(data);
+ }
+
+ tree.Shake();
+ Assert.Equal(numTargets, tree.NumTargets());
+ }
+
+ [Fact]
+ public void ShakeWhenContainingTwoTest()
+ {
+ var tree = MakeTree();
+ var numTargets = 2;
+
+ for (int i = 0; i < numTargets; i++)
+ {
+ var data = new QuadtreeData(new BasicActor());
+ tree.Insert(data);
+ }
+
+ tree.Shake();
+ Assert.Equal(numTargets, tree.NumTargets());
+ }
+
+ [Fact]
+ public void ShakeWhenContainingThreeTest()
+ {
+ var tree = MakeTree();
+ var numTargets = 3;
+
+ for (int i = 0; i < numTargets; i++)
+ {
+ var data = new QuadtreeData(new BasicActor());
+ tree.Insert(data);
+ }
+
+ tree.Shake();
+ Assert.Equal(numTargets, tree.NumTargets());
+ }
+
+ [Fact]
+ public void ShakeWhenContainingManyTest()
+ {
+ var tree = MakeTree();
+ var numTargets = QuadTree.QuadTree.DefaultMaxObjectsPerNode + 1;
+
+ for (int i = 0; i < numTargets; i++)
+ {
+ var data = new QuadtreeData(new BasicActor());
+ tree.Insert(data);
+ }
+
+ tree.Shake();
+ Assert.Equal(numTargets, tree.NumTargets());
+ }
+
+ [Fact]
+ public void QueryWhenEmptyTest()
+ {
+ var tree = MakeTree();
+
+ var query = tree.Query(ref _quadTreeArea);
+
+ Assert.Empty(query);
+ Assert.Equal(0, tree.NumTargets());
+ }
+
+ [Fact]
+ public void QueryNotOverlappingTest()
+ {
+ var tree = MakeTree();
+
+ var area = new RectangleF(100f, 100f, 1f, 1f);
+ var query = tree.Query(ref area);
+
+ Assert.Empty(query);
+ Assert.Equal(0, tree.NumTargets());
+ }
+
+ [Fact]
+ public void QueryLeafNodeNotEmptyTest()
+ {
+ var tree = MakeTree();
+ var actor = new BasicActor();
+ tree.Insert(new QuadtreeData(actor));
+
+ var query = tree.Query(ref _quadTreeArea);
+ Assert.Single(query);
+ Assert.Equal(tree.NumTargets(), query.Count);
+ }
+
+ [Fact]
+ public void QueryLeafNodeNoOverlapTest()
+ {
+ var tree = MakeTree();
+ var actor = new BasicActor();
+ tree.Insert(new QuadtreeData(actor));
+
+ var area = new RectangleF(100f, 100f, 1f, 1f);
+ var query = tree.Query(ref area);
+ Assert.Empty(query);
+ }
+
+ [Fact]
+ public void QueryLeafNodeMultipleTest()
+ {
+ var tree = MakeTree();
+ var numTargets = QuadTree.QuadTree.DefaultMaxObjectsPerNode;
+ for (int i = 0; i < numTargets; i++)
+ {
+ var data = new QuadtreeData(new BasicActor());
+ tree.Insert(data);
+ }
+
+
+ var query = tree.Query(ref _quadTreeArea);
+ Assert.Equal(numTargets, query.Count);
+ Assert.Equal(tree.NumTargets(), query.Count);
+ }
+
+ [Fact]
+ public void QueryNonLeafManyTest()
+ {
+ var tree = MakeTree();
+ var numTargets = 2*QuadTree.QuadTree.DefaultMaxObjectsPerNode;
+ for (int i = 0; i < numTargets; i++)
+ {
+ var data = new QuadtreeData(new BasicActor());
+ tree.Insert(data);
+ }
+
+
+ var query = tree.Query(ref _quadTreeArea);
+ Assert.Equal(numTargets, query.Count);
+ Assert.Equal(tree.NumTargets(), query.Count);
+ }
+
+ [Fact]
+ public void QueryTwiceConsecutiveTest()
+ {
+ var tree = MakeTree();
+ var numTargets = 2 * QuadTree.QuadTree.DefaultMaxObjectsPerNode;
+ for (int i = 0; i < numTargets; i++)
+ {
+ var data = new QuadtreeData(new BasicActor());
+ tree.Insert(data);
+ }
+
+
+ var query1 = tree.Query(ref _quadTreeArea);
+ var query2 = tree.Query(ref _quadTreeArea);
+ Assert.Equal(numTargets, query1.Count);
+ Assert.Equal(tree.NumTargets(), query1.Count);
+ Assert.Equal(query1.Count, query2.Count);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/SpatialHashTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/SpatialHashTests.cs
new file mode 100644
index 0000000..7c09a9e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/SpatialHashTests.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace MonoGame.Extended.Collisions.Tests;
+
+public class SpatialHashTests
+{
+ private SpatialHash generateSpatialHash() => new SpatialHash(new Size2(64, 64));
+ private readonly RectangleF RECT = new RectangleF(10, 10, 20, 20);
+
+ [Fact]
+ public void CollisionOneTrueTest()
+ {
+ var hash = generateSpatialHash();
+ hash.Insert(new BasicActor()
+ {
+ Bounds = RECT,
+ });
+ var collisions = hash.Query(RECT);
+ Assert.Equal(1, collisions.Count());
+ }
+
+ [Fact]
+ public void CollisionTwoTest()
+ {
+ var hash = generateSpatialHash();
+ hash.Insert(new BasicActor
+ {
+ Bounds = RECT,
+ });
+ hash.Insert(new BasicActor
+ {
+ Bounds = RECT,
+ });
+ var collisions = hash.Query(RECT);
+ Assert.Equal(2, collisions.Count());
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/packages.config b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/packages.config
new file mode 100644
index 0000000..fa95cfc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/packages.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="MonoGame.Framework.WindowsDX" version="3.6.0.1625" targetFramework="net452" />
+ <package id="NSubstitute" version="1.10.0.0" targetFramework="net452" />
+ <package id="NUnit" version="2.6.4" targetFramework="net452" />
+</packages> \ No newline at end of file