commit 3d1aa25a1e92d8b725ba31a4175ca90896aae995 Author: Evan Hemsley Date: Fri Sep 6 01:11:58 2019 -0700 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cbbd0b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ \ No newline at end of file diff --git a/Bonk/AABB.cs b/Bonk/AABB.cs new file mode 100644 index 0000000..46b67a1 --- /dev/null +++ b/Bonk/AABB.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using MoonTools.Core.Structs; + +namespace MoonTools.Core.Bonk +{ + public struct AABB + { + public int MinX { get; private set; } + public int MinY { get; private set; } + public int MaxX { get; private set; } + public int MaxY { get; private set; } + + public int Width { get { return MaxX - MinX; } } + public int Height { get { return MaxY - MinY; } } + + public static AABB FromTransform2DedVertices(IEnumerable vertices, Transform2D transform) + { + var Transform2DedVertices = vertices.Select(vertex => Vector2.Transform(vertex, transform.TransformMatrix)); + + return new AABB + { + MinX = (int)Math.Round(Transform2DedVertices.Min(vertex => vertex.X)), + MinY = (int)Math.Round(Transform2DedVertices.Min(vertex => vertex.Y)), + MaxX = (int)Math.Round(Transform2DedVertices.Max(vertex => vertex.X)), + MaxY = (int)Math.Round(Transform2DedVertices.Max(vertex => vertex.Y)) + }; + } + + public AABB(int minX, int minY, int maxX, int maxY) + { + MinX = minX; + MinY = minY; + MaxX = maxX; + MaxY = maxY; + } + } +} \ No newline at end of file diff --git a/Bonk/Bonk.csproj b/Bonk/Bonk.csproj new file mode 100644 index 0000000..0fc7a33 --- /dev/null +++ b/Bonk/Bonk.csproj @@ -0,0 +1,20 @@ + + + 1.0.0 + netstandard2.0 + .NET Core Collision Detection for MonoGame + MoonTools.Core.Bonk + MoonTools.Core.Bonk + Moonside Games + Evan Hemsley + Evan Hemsley 2019 + MoonTools.Core.Bonk + true + MoonTools.Core.Bonk + LGPL-3.0-only + https://github.com/MoonsideGames/MoonTools.Core.Bonk + + + + + \ No newline at end of file diff --git a/Bonk/Circle.cs b/Bonk/Circle.cs new file mode 100644 index 0000000..26d43dd --- /dev/null +++ b/Bonk/Circle.cs @@ -0,0 +1,41 @@ +using System; +using Microsoft.Xna.Framework; +using MoonTools.Core.Structs; + +namespace MoonTools.Core.Bonk +{ + public struct Circle : IShape2D, IEquatable + { + public int Radius { get; private set; } + + public Circle(int radius) + { + Radius = radius; + } + + public Vector2 Support(Vector2 direction, Transform2D transform) + { + return Vector2.Transform(Vector2.Normalize(direction) * Radius, transform.TransformMatrix); + } + + public AABB AABB(Transform2D Transform2D) + { + return new AABB( + Transform2D.Position.X - Radius, + Transform2D.Position.Y - Radius, + Transform2D.Position.X + Radius, + Transform2D.Position.Y + Radius + ); + } + + public bool Equals(IShape2D other) + { + if (other is Circle circle) + { + return Radius == circle.Radius; + } + + return false; + } + } +} diff --git a/Bonk/EPA2D.cs b/Bonk/EPA2D.cs new file mode 100644 index 0000000..2219f26 --- /dev/null +++ b/Bonk/EPA2D.cs @@ -0,0 +1,102 @@ +/* + * Implementation of the Expanding Polytope Algorithm + * as based on the following blog post: + * https://blog.hamaluik.ca/posts/building-a-collision-engine-part-2-2d-penetration-vectors/ + */ + +using Microsoft.Xna.Framework; +using MoonTools.Core.Structs; +using System; +using System.Collections.Generic; + +namespace MoonTools.Core.Bonk +{ + enum PolygonWinding + { + Clockwise, + CounterClockwise + } + + public static class EPA2D + { + // vector returned gives direction from A to B + public static Vector2 Intersect(IShape2D shapeA, Transform2D Transform2DA, IShape2D shapeB, Transform2D Transform2DB, IEnumerable givenSimplexVertices) + { + var simplexVertices = new SimplexVertices(new Vector2?[36]); + + foreach (var vertex in givenSimplexVertices) + { + simplexVertices.Add(vertex); + } + + var e0 = (simplexVertices[1].X - simplexVertices[0].X) * (simplexVertices[1].Y + simplexVertices[0].Y); + var e1 = (simplexVertices[2].X - simplexVertices[1].X) * (simplexVertices[2].Y + simplexVertices[1].Y); + var e2 = (simplexVertices[0].X - simplexVertices[2].X) * (simplexVertices[0].Y + simplexVertices[2].Y); + var winding = e0 + e1 + e2 >= 0 ? PolygonWinding.Clockwise : PolygonWinding.CounterClockwise; + + Vector2 intersection = default; + + for (int i = 0; i < 32; i++) + { + var edge = FindClosestEdge(winding, simplexVertices); + var support = CalculateSupport(shapeA, Transform2DA, shapeB, Transform2DB, edge.normal); + var distance = Vector2.Dot(support, edge.normal); + + intersection = edge.normal; + intersection *= distance; + + if (Math.Abs(distance - edge.distance) <= float.Epsilon) + { + return intersection; + } + else + { + simplexVertices.Insert(edge.index, support); + } + } + + return intersection; + } + + private static Edge FindClosestEdge(PolygonWinding winding, SimplexVertices simplexVertices) + { + var closestDistance = float.PositiveInfinity; + var closestNormal = Vector2.Zero; + var closestIndex = 0; + + for (int i = 0; i < simplexVertices.Count; i++) + { + var j = i + 1; + if (j >= simplexVertices.Count) { j = 0; } + Vector2 edge = simplexVertices[j] - simplexVertices[i]; + + Vector2 norm; + if (winding == PolygonWinding.Clockwise) + { + norm = new Vector2(edge.Y, -edge.X); + } + else + { + norm = new Vector2(-edge.Y, edge.X); + } + norm.Normalize(); + + var dist = Vector2.Dot(norm, simplexVertices[i]); + + if (dist < closestDistance) + { + closestDistance = dist; + closestNormal = norm; + closestIndex = j; + } + } + + return new Edge(closestDistance, closestNormal, closestIndex); + } + + private static Vector2 CalculateSupport(IShape2D shapeA, Transform2D Transform2DA, IShape2D shapeB, Transform2D Transform2DB, Vector2 direction) + { + return shapeA.Support(direction, Transform2DA) - shapeB.Support(-direction, Transform2DB); + } + } +} diff --git a/Bonk/Edge.cs b/Bonk/Edge.cs new file mode 100644 index 0000000..8ac02b9 --- /dev/null +++ b/Bonk/Edge.cs @@ -0,0 +1,17 @@ +using Microsoft.Xna.Framework; + +namespace MoonTools.Core.Bonk +{ + public struct Edge + { + public float distance; + public Vector2 normal; + public int index; + + public Edge(float distance, Vector2 normal, int index) { + this.distance = distance; + this.normal = normal; + this.index = index; + } + } +} diff --git a/Bonk/GJK2D.cs b/Bonk/GJK2D.cs new file mode 100644 index 0000000..76b23e2 --- /dev/null +++ b/Bonk/GJK2D.cs @@ -0,0 +1,126 @@ +/* + * Implementation of the GJK collision algorithm + * Based on some math blogs + * https://blog.hamaluik.ca/posts/building-a-collision-engine-part-1-2d-gjk-collision-detection/ + * and some code from https://github.com/kroitor/gjk.c + */ + +using Microsoft.Xna.Framework; +using MoonTools.Core.Structs; +using System; + +namespace MoonTools.Core.Bonk +{ + public static class GJK2D + { + private enum SolutionStatus + { + NoIntersection, + Intersection, + StillSolving + } + + public static ValueTuple TestCollision(IShape2D shapeA, Transform2D Transform2DA, IShape2D shapeB, Transform2D Transform2DB) + { + var vertices = new SimplexVertices(new Vector2?[] { null, null, null, null }); + + const SolutionStatus solutionStatus = SolutionStatus.StillSolving; + var direction = Transform2DB.Position - Transform2DA.Position; + + var result = (solutionStatus, direction); + + while (result.solutionStatus == SolutionStatus.StillSolving) + { + result = EvolveSimplex(shapeA, Transform2DA, shapeB, Transform2DB, vertices, result.direction); + } + + return ValueTuple.Create(result.solutionStatus == SolutionStatus.Intersection, vertices); + } + + private static (SolutionStatus, Vector2) EvolveSimplex(IShape2D shapeA, Transform2D Transform2DA, IShape2D shapeB, Transform2D Transform2DB, SimplexVertices vertices, Vector2 direction) + { + switch(vertices.Count) + { + case 0: + if (direction == Vector2.Zero) + { + direction = Vector2.UnitX; + } + break; + + case 1: + direction *= -1; + break; + + case 2: + var ab = vertices[1] - vertices[0]; + var a0 = vertices[0] * -1; + + direction = TripleProduct(ab, a0, ab); + if (direction == Vector2.Zero) + { + direction = Perpendicular(ab); + } + break; + + case 3: + var c0 = vertices[2] * -1; + var bc = vertices[1] - vertices[2]; + var ca = vertices[0] - vertices[2]; + + var bcNorm = TripleProduct(ca, bc, bc); + var caNorm = TripleProduct(bc, ca, ca); + + // the origin is outside line bc + // get rid of a and add a new support in the direction of bcNorm + if (Vector2.Dot(bcNorm, c0) > 0) + { + vertices.RemoveAt(0); + direction = bcNorm; + } + // the origin is outside line ca + // get rid of b and add a new support in the direction of caNorm + else if (Vector2.Dot(caNorm, c0) > 0) + { + vertices.RemoveAt(1); + direction = caNorm; + } + // the origin is inside both ab and ac, + // so it must be inside the triangle! + else + { + return (SolutionStatus.Intersection, direction); + } + break; + } + + return (AddSupport(shapeA, Transform2DA, shapeB, Transform2DB, vertices, direction) ? + SolutionStatus.StillSolving : + SolutionStatus.NoIntersection, direction); + } + + private static bool AddSupport(IShape2D shapeA, Transform2D Transform2DA, IShape2D shapeB, Transform2D Transform2DB, SimplexVertices vertices, Vector2 direction) + { + var newVertex = shapeA.Support(direction, Transform2DA) - shapeB.Support(-direction, Transform2DB); + vertices.Add(newVertex); + return Vector2.Dot(direction, newVertex) >= 0; + } + + private static Vector2 TripleProduct(Vector2 a, Vector2 b, Vector2 c) + { + var A = new Vector3(a.X, a.Y, 0); + var B = new Vector3(b.X, b.Y, 0); + var C = new Vector3(c.X, c.Y, 0); + + var first = Vector3.Cross(A, B); + var second = Vector3.Cross(first, C); + + return new Vector2(second.X, second.Y); + } + + private static Vector2 Perpendicular(Vector2 v) + { + return new Vector2(v.Y, -v.X); + } + } +} diff --git a/Bonk/IShape2D.cs b/Bonk/IShape2D.cs new file mode 100644 index 0000000..cffbfe2 --- /dev/null +++ b/Bonk/IShape2D.cs @@ -0,0 +1,15 @@ +using System; +using Microsoft.Xna.Framework; +using MoonTools.Core.Structs; + +namespace MoonTools.Core.Bonk +{ + public interface IShape2D : IEquatable + { + // A Support function for a Minkowski sum. + // A Support function gives the point on the edge of a shape based on a direction. + Vector2 Support(Vector2 direction, Transform2D transform); + + AABB AABB(Transform2D transform); + } +} diff --git a/Bonk/Line.cs b/Bonk/Line.cs new file mode 100644 index 0000000..0617b07 --- /dev/null +++ b/Bonk/Line.cs @@ -0,0 +1,41 @@ +using System; +using Microsoft.Xna.Framework; +using MoonTools.Core.Structs; + +namespace MoonTools.Core.Bonk +{ + public struct Line : IShape2D, IEquatable + { + private Position2D[] vertices; + + public Line(Position2D start, Position2D end) + { + vertices = new Position2D[2] { start, end }; + } + + public Vector2 Support(Vector2 direction, Transform2D transform) + { + var Transform2DedStart = Vector2.Transform(vertices[0], transform.TransformMatrix); + var Transform2DedEnd = Vector2.Transform(vertices[1], transform.TransformMatrix); + return Vector2.Dot(Transform2DedStart, direction) > Vector2.Dot(Transform2DedEnd, direction) ? + Transform2DedStart : + Transform2DedEnd; + } + + public AABB AABB(Transform2D Transform2D) + { + return Bonk.AABB.FromTransform2DedVertices(vertices, Transform2D); + } + + public bool Equals(IShape2D other) + { + if (other is Line) + { + var otherLine = (Line)other; + return vertices[0].ToVector2() == otherLine.vertices[0].ToVector2() && vertices[1].ToVector2() == otherLine.vertices[1].ToVector2(); + } + + return false; + } + } +} diff --git a/Bonk/Polygon.cs b/Bonk/Polygon.cs new file mode 100644 index 0000000..f7dbd05 --- /dev/null +++ b/Bonk/Polygon.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.Xna.Framework; +using MoonTools.Core.Structs; + +namespace MoonTools.Core.Bonk +{ + public struct Polygon : IShape2D, IEquatable + { + public Position2D[] Vertices { get; private set; } + + // vertices are local to the origin + public Polygon(params Position2D[] vertices) + { + Vertices = vertices; + } + + public Vector2 Support(Vector2 direction, Transform2D transform) + { + var furthest = float.NegativeInfinity; + var furthestVertex = Vector2.Transform(Vertices[0], transform.TransformMatrix); + + foreach (var vertex in Vertices) + { + var Transform2DedVertex = Vector2.Transform(vertex, transform.TransformMatrix); + var distance = Vector2.Dot(Transform2DedVertex, direction); + if (distance > furthest) + { + furthest = distance; + furthestVertex = Transform2DedVertex; + } + } + + return furthestVertex; + } + + public AABB AABB(Transform2D Transform2D) + { + return Bonk.AABB.FromTransform2DedVertices(Vertices, Transform2D); + } + + public bool Equals(IShape2D other) + { + if (other is Polygon) + { + var otherPolygon = (Polygon)other; + + if (Vertices.Length != otherPolygon.Vertices.Length) { return false; } + + for (int i = 0; i < Vertices.Length; i++) + { + if (Vertices[i].ToVector2() != otherPolygon.Vertices[i].ToVector2()) { return false;} + } + + return true; + } + + return false; + } + } +} diff --git a/Bonk/Rectangle.cs b/Bonk/Rectangle.cs new file mode 100644 index 0000000..d533f47 --- /dev/null +++ b/Bonk/Rectangle.cs @@ -0,0 +1,69 @@ +using System; +using Microsoft.Xna.Framework; +using MoonTools.Core.Structs; + +namespace MoonTools.Core.Bonk +{ + public struct Rectangle : IShape2D, IEquatable + { + public int MinX { get; } + public int MinY { get; } + public int MaxX { get; } + public int MaxY { get; } + + private Position2D[] vertices; + + public Rectangle(int minX, int minY, int maxX, int maxY) + { + MinX = minX; + MinY = minY; + MaxX = maxX; + MaxY = maxY; + + vertices = new Position2D[4] + { + new Position2D(minX, minY), + new Position2D(minX, maxY), + new Position2D(maxX, minY), + new Position2D(maxX, maxY) + }; + } + + public Vector2 Support(Vector2 direction, Transform2D transform) + { + var furthestDistance = float.NegativeInfinity; + var furthestVertex = Vector2.Transform(vertices[0], transform.TransformMatrix); + + foreach (var v in vertices) + { + var Transform2DedVertex = Vector2.Transform(v, transform.TransformMatrix); + var distance = Vector2.Dot(Transform2DedVertex, direction); + if (distance > furthestDistance) + { + furthestDistance = distance; + furthestVertex = Transform2DedVertex; + } + } + + return furthestVertex; + } + + public AABB AABB(Transform2D Transform2D) + { + return Bonk.AABB.FromTransform2DedVertices(vertices, Transform2D); + } + + public bool Equals(IShape2D other) + { + if (other is Rectangle rectangle) + { + return MinX == rectangle.MinX && + MinY == rectangle.MinY && + MaxX == rectangle.MaxX && + MaxY == rectangle.MaxY; + } + + return false; + } + } +} diff --git a/Bonk/SimplexVertices.cs b/Bonk/SimplexVertices.cs new file mode 100644 index 0000000..b89b753 --- /dev/null +++ b/Bonk/SimplexVertices.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Xna.Framework; + +namespace MoonTools.Core.Bonk +{ + public struct SimplexVertices : IEnumerable + { + public Vector2?[] vertices; + + /// + /// Make sure to pass in all nulls + /// + public SimplexVertices(Vector2?[] vertices) + { + this.vertices = vertices; + } + + public Vector2 this[int key] + { + get + { + if (!vertices[key].HasValue) { throw new IndexOutOfRangeException(); } + return vertices[key].Value; + } + set + { + vertices[key] = value; + } + } + + public int Count { + get + { + for (int i = 0; i < vertices.Length; i++) + { + if (!vertices[i].HasValue) { return i; } + } + return vertices.Length; + } + } + + public void Add(Vector2 vertex) + { + if (Count > vertices.Length - 1) { throw new IndexOutOfRangeException(); } + + vertices[Count] = vertex; + } + + public void Insert(int index, Vector2 vertex) + { + if (Count >= vertices.Length || index > vertices.Length - 1) { throw new IndexOutOfRangeException(); } + + var currentCount = Count; + + for (int i = currentCount - 1; i >= index; i--) + { + vertices[i + 1] = vertices[i]; + } + + vertices[index] = vertex; + } + + public IEnumerator GetEnumerator() + { + foreach (Vector2? vec in vertices) + { + if (!vec.HasValue) { yield break; } + yield return vec.Value; + } + } + + public void RemoveAt(int index) + { + if (index > vertices.Length - 1 || index > Count) { throw new ArgumentOutOfRangeException(); } + + for (int i = vertices.Length - 2; i >= index; i--) + { + vertices[i] = vertices[i + 1]; + } + + vertices[vertices.Length - 1] = null; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/Bonk/SpatialHash.cs b/Bonk/SpatialHash.cs new file mode 100644 index 0000000..a06b1bc --- /dev/null +++ b/Bonk/SpatialHash.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using MoonTools.Core.Structs; + +namespace MoonTools.Core.Bonk +{ + public class SpatialHash where T : IEquatable + { + private readonly int cellSize; + + private readonly Dictionary>> hashDictionary = new Dictionary>>(); + private readonly Dictionary<(IShape2D, Transform2D), T> IDLookup = new Dictionary<(IShape2D, Transform2D), T>(); + + public SpatialHash(int cellSize) + { + this.cellSize = cellSize; + } + + public (int, int) Hash(int x, int y) + { + return ((int)Math.Floor((float)x / cellSize), (int)Math.Floor((float)y / cellSize)); + } + + public void Insert(T id, IShape2D shape, Transform2D Transform2D) + { + var box = shape.AABB(Transform2D); + var minHash = Hash(box.MinX, box.MinY); + var maxHash = Hash(box.MaxX, box.MaxY); + + for (int i = minHash.Item1; i <= maxHash.Item1; i++) + { + for (int j = minHash.Item2; j <= maxHash.Item2; j++) + { + if (!hashDictionary.ContainsKey(i)) + { + hashDictionary.Add(i, new Dictionary>()); + } + + if (!hashDictionary[i].ContainsKey(j)) + { + hashDictionary[i].Add(j, new HashSet<(IShape2D, Transform2D)>()); + } + + hashDictionary[i][j].Add((shape, Transform2D)); + IDLookup[(shape, Transform2D)] = id; + } + } + } + + public IEnumerable<(T, IShape2D, Transform2D)> Retrieve(T id, IShape2D shape, Transform2D Transform2D) + { + var box = shape.AABB(Transform2D); + var minHash = Hash(box.MinX, box.MinY); + var maxHash = Hash(box.MaxX, box.MaxY); + + for (int i = minHash.Item1; i <= maxHash.Item1; i++) + { + for (int j = minHash.Item2; j <= maxHash.Item2; j++) + { + if (hashDictionary.ContainsKey(i) && hashDictionary[i].ContainsKey(j)) + { + foreach (var (otherShape, otherTransform2D) in hashDictionary[i][j]) + { + var otherID = IDLookup[(otherShape, otherTransform2D)]; + if (!id.Equals(otherID)) { yield return (otherID, otherShape, otherTransform2D); } + } + } + } + } + } + + public void Clear() + { + foreach (var innerDict in hashDictionary.Values) + { + foreach (var set in innerDict.Values) + { + set.Clear(); + } + } + + IDLookup.Clear(); + } + } +} \ No newline at end of file diff --git a/Test/EPA2DTest.cs b/Test/EPA2DTest.cs new file mode 100644 index 0000000..df35325 --- /dev/null +++ b/Test/EPA2DTest.cs @@ -0,0 +1,70 @@ +using NUnit.Framework; +using FluentAssertions; + +using Microsoft.Xna.Framework; +using System; +using MoonTools.Core.Structs; +using MoonTools.Core.Bonk; + +namespace Tests +{ + public class EPA2DTest + { + [Test] + public void RectangleOverlap() + { + var squareA = new MoonTools.Core.Bonk.Rectangle(-1, -1, 1, 1); + var transformA = Transform2D.DefaultTransform; + var squareB = new MoonTools.Core.Bonk.Rectangle(-1, -1, 1, 1); + var transformB = new Transform2D(new Vector2(1.5f, 0)); + + var test = GJK2D.TestCollision(squareA, transformA, squareB, transformB); + + Assert.That(test.Item1, Is.True); + + var intersection = EPA2D.Intersect(squareA, transformA, squareB, transformB, test.Item2); + + intersection.X.Should().Be(1f); + intersection.Y.Should().Be(0); + } + + [Test] + public void CircleOverlap() + { + var circleA = new Circle(2); + var transformA = Transform2D.DefaultTransform; + var circleB = new Circle(1); + var transformB = new Transform2D(new Vector2(1, 1)); + + var test = GJK2D.TestCollision(circleA, transformA, circleB, transformB); + + Assert.That(test.Item1, Is.True); + + var intersection = EPA2D.Intersect(circleA, transformA, circleB, transformB, test.Item2); + + var ix = circleA.Radius * (float)Math.Cos(Math.PI / 4) - (circleB.Radius * (float)Math.Cos(5 * Math.PI / 4) + transformB.Position.X); + var iy = circleA.Radius * (float)Math.Sin(Math.PI / 4) - (circleB.Radius * (float)Math.Sin(5 * Math.PI / 4) + transformB.Position.Y); + + intersection.X.Should().BeApproximately(ix, 0.01f); + intersection.Y.Should().BeApproximately(iy, 0.01f); + } + + [Test] + public void LineRectangleOverlap() + { + var line = new Line(new Position2D(-4, -4), new Position2D(4, 4)); + var transformA = Transform2D.DefaultTransform; + var square = new MoonTools.Core.Bonk.Rectangle(-1, -1, 1, 1); + var transformB = Transform2D.DefaultTransform; + + var test = GJK2D.TestCollision(line, transformA, square, transformB); + + Assert.That(test.Item1, Is.True); + + var intersection = EPA2D.Intersect(line, transformA, square, transformB, test.Item2); + + intersection.X.Should().Be(-1); + intersection.Y.Should().Be(1); + } + } +} diff --git a/Test/GJK2DTest.cs b/Test/GJK2DTest.cs new file mode 100644 index 0000000..085b882 --- /dev/null +++ b/Test/GJK2DTest.cs @@ -0,0 +1,201 @@ +using NUnit.Framework; +using MoonTools.Core.Bonk; +using MoonTools.Core.Structs; +using Microsoft.Xna.Framework; + +namespace Tests +{ + public class GJK2DTest + { + [Test] + public void LineLineOverlapping() + { + var lineA = new Line(new Position2D(-1, -1), new Position2D(1, 1)); + var lineB = new Line(new Position2D(-1, 1), new Position2D(1, -1)); + + Assert.IsTrue(GJK2D.TestCollision(lineA, Transform2D.DefaultTransform, lineB, Transform2D.DefaultTransform).Item1); + } + + [Test] + public void LineLineNotOverlapping() + { + var lineA = new Line(new Position2D(0, 1), new Position2D(1, 0)); + var lineB = new Line(new Position2D(-1, -1), new Position2D(-2, -2)); + + Assert.IsFalse(GJK2D.TestCollision(lineA, Transform2D.DefaultTransform, lineB, Transform2D.DefaultTransform).Item1); + } + + [Test] + public void CircleCircleOverlapping() + { + var circleA = new Circle(2); + var transformA = new Transform2D(new Vector2(-1, -1)); + var circleB = new Circle(2); + var transformB = new Transform2D(new Vector2(1, 1)); + + Assert.IsTrue(GJK2D.TestCollision(circleA, transformA, circleB, transformB).Item1); + } + + [Test] + public void CircleCircleNotOverlapping() + { + var circleA = new Circle(2); + var transformA = new Transform2D(new Vector2(-5, -5)); + var circleB = new Circle(2); + var transformB = new Transform2D(new Vector2(5, 5)); + + Assert.IsFalse(GJK2D.TestCollision(circleA, transformA, circleB, transformB).Item1); + } + + [Test] + public void PolygonPolygonOverlapping() + { + var shapeA = new Polygon( + new Position2D(-1, 1), new Position2D(1, 1), + new Position2D(-1, -1), new Position2D(1, -1) + ); + + var transformA = Transform2D.DefaultTransform; + + var shapeB = new Polygon( + new Position2D(-1, 1), new Position2D(1, 1), + new Position2D(-1, -1), new Position2D(1, -1) + ); + + var transformB = new Transform2D(new Vector2(0.5f, 0.5f)); + + Assert.IsTrue(GJK2D.TestCollision(shapeA, transformA, shapeB, transformB).Item1); + } + + [Test] + public void PolygonPolygonNotOverlapping() + { + var shapeA = new Polygon(new Position2D(0, 0), + new Position2D(-1, 1), new Position2D(1, 1), + new Position2D(-1, -1), new Position2D(1, -1) + ); + + var transformA = Transform2D.DefaultTransform; + + var shapeB = new Polygon( + new Position2D(-1, 1), new Position2D(1, 1), + new Position2D(-1, -1), new Position2D(1, -1) + ); + + var transformB = new Transform2D(new Vector2(5, 0)); + + Assert.IsFalse(GJK2D.TestCollision(shapeA, transformA, shapeB, transformB).Item1); + } + + [Test] + public void LinePolygonOverlapping() + { + var line = new Line(new Position2D(-1, -1), new Position2D(1, 1)); + + var transformA = Transform2D.DefaultTransform; + + var polygon = new Polygon( + new Position2D(-1, -1), new Position2D(1, -1), + new Position2D(1, 1), new Position2D(-1, 1) + ); + + var transformB = Transform2D.DefaultTransform; + + Assert.IsTrue(GJK2D.TestCollision(line, transformA, polygon, transformB).Item1); + } + + [Test] + public void LinePolygonNotOverlapping() + { + var line = new Line(new Position2D(-5, 5), new Position2D(-5, 5)); + + var transformA = Transform2D.DefaultTransform; + + var polygon = new Polygon(new Position2D(0, 0), + new Position2D(-1, -1), new Position2D(1, -1), + new Position2D(1, 1), new Position2D(-1, 1) + ); + + var transformB = Transform2D.DefaultTransform; + + Assert.IsFalse(GJK2D.TestCollision(line, transformA, polygon, transformB).Item1); + } + + [Test] + public void LineCircleOverlapping() + { + var line = new Line(new Position2D(-1, -1), new Position2D(1, 1)); + var transformA = Transform2D.DefaultTransform; + var circle = new Circle(1); + var transformB = Transform2D.DefaultTransform; + + Assert.IsTrue(GJK2D.TestCollision(line, transformA, circle, transformB).Item1); + } + + [Test] + public void LineCircleNotOverlapping() + { + var line = new Line(new Position2D(-5, -5), new Position2D(-4, -4)); + var transformA = Transform2D.DefaultTransform; + var circle = new Circle(1); + var transformB = Transform2D.DefaultTransform; + + Assert.IsFalse(GJK2D.TestCollision(line, transformA, circle, transformB).Item1); + } + + [Test] + public void CirclePolygonOverlapping() + { + var circle = new Circle(1); + var transformA = new Transform2D(new Vector2(0.25f, 0)); + + var square = new Polygon( + new Position2D(-1, -1), new Position2D(1, -1), + new Position2D(1, 1), new Position2D(-1, 1) + ); + + var transformB = Transform2D.DefaultTransform; + + Assert.IsTrue(GJK2D.TestCollision(circle, transformA, square, transformB).Item1); + } + + [Test] + public void CirclePolygonNotOverlapping() + { + var circle = new Circle(1); + var circleTransform = new Transform2D(new Vector2(5, 0)); + + var square = new Polygon(new Position2D(0, 0), + new Position2D(-1, -1), new Position2D(1, -1), + new Position2D(1, 1), new Position2D(-1, 1) + ); + var squareTransform = Transform2D.DefaultTransform; + + Assert.IsFalse(GJK2D.TestCollision(circle, circleTransform, square, squareTransform).Item1); + } + + [Test] + public void RotatedRectanglesOverlapping() + { + var rectangleA = new MoonTools.Core.Bonk.Rectangle(-1, -1, 2, 2); + var transformA = new Transform2D(new Vector2(-1, 0), -90f); + + var rectangleB = new MoonTools.Core.Bonk.Rectangle(-1, -1, 1, 1); + var transformB = new Transform2D(new Vector2(1, 0)); + + Assert.IsTrue(GJK2D.TestCollision(rectangleA, transformA, rectangleB, transformB).Item1); + } + + [Test] + public void RectanglesTouching() + { + var rectangleA = new MoonTools.Core.Bonk.Rectangle(-1, -1, 1, 1); + var transformA = new Transform2D(new Position2D(-1, 0)); + + var rectangleB = new MoonTools.Core.Bonk.Rectangle(-1, -1, 1, 1); + var transformB = new Transform2D(new Vector2(1, 0)); + + Assert.IsTrue(GJK2D.TestCollision(rectangleA, transformA, rectangleB, transformB).Item1); + } + } +} diff --git a/Test/SpatialHashTest.cs b/Test/SpatialHashTest.cs new file mode 100644 index 0000000..62ce8a7 --- /dev/null +++ b/Test/SpatialHashTest.cs @@ -0,0 +1,75 @@ +using FluentAssertions; +using Microsoft.Xna.Framework; +using NUnit.Framework; +using MoonTools.Core.Structs; +using MoonTools.Core.Bonk; + +namespace Tests +{ + public class SpatialHashTest + { + [Test] + public void InsertAndRetrieve() + { + var spatialHash = new SpatialHash(16); + + var rectA = new MoonTools.Core.Bonk.Rectangle(-2, -2, 2, 2); + var rectATransform = new Transform2D(new Vector2(-8, -8)); + + var rectB = new MoonTools.Core.Bonk.Rectangle(-2, -2, 2, 2); + var rectBTransform = new Transform2D(new Vector2(8, 8)); + + var rectC = new MoonTools.Core.Bonk.Rectangle(-2, -2, 2, 2); + var rectCTransform = new Transform2D(new Vector2(24, -4)); + + var rectD = new MoonTools.Core.Bonk.Rectangle(-2, -2, 2, 2); + var rectDTransform = new Transform2D(new Vector2(24, 24)); + + var circleA = new MoonTools.Core.Bonk.Circle(2); + var circleATransform = new Transform2D(new Vector2(24, -8)); + + var circleB = new MoonTools.Core.Bonk.Circle(8); + var circleBTransform = new Transform2D(new Vector2(16, 16)); + + var line = new MoonTools.Core.Bonk.Line(new Position2D(20, -4), new Position2D(22, -12)); + var lineTransform = new Transform2D(new Vector2(0, 0)); + + spatialHash.Insert(0, rectA, rectATransform); + spatialHash.Insert(1, rectB, rectBTransform); + spatialHash.Insert(2, rectC, rectCTransform); + spatialHash.Insert(3, rectD, rectDTransform); + spatialHash.Insert(4, circleA, circleATransform); + spatialHash.Insert(1, circleB, circleBTransform); + spatialHash.Insert(6, line, lineTransform); + + spatialHash.Retrieve(0, rectA, rectATransform).Should().BeEmpty(); + spatialHash.Retrieve(1, rectB, rectBTransform).Should().NotContain((1, circleB, circleBTransform)); + spatialHash.Retrieve(2, rectC, rectCTransform).Should().Contain((6, line, lineTransform)).And.Contain((4, circleA, circleATransform)); + spatialHash.Retrieve(3, rectD, rectDTransform).Should().Contain((1, circleB, circleBTransform)); + + spatialHash.Retrieve(4, circleA, circleATransform).Should().Contain((6, line, lineTransform)).And.Contain((2, rectC, rectCTransform)); + spatialHash.Retrieve(1, circleB, circleBTransform).Should().NotContain((1, rectB, rectBTransform)).And.Contain((3, rectD, rectDTransform)); + + spatialHash.Retrieve(6, line, lineTransform).Should().Contain((4, circleA, circleATransform)).And.Contain((2, rectC, rectCTransform)); + } + + [Test] + public void Clear() + { + var spatialHash = new SpatialHash(16); + + var rectA = new MoonTools.Core.Bonk.Rectangle(-2, -2, 2, 2); + var rectATransform = new Transform2D(new Vector2(-8, -8)); + + var rectB = new MoonTools.Core.Bonk.Rectangle(-2, -2, 2, 2); + var rectBTransform = new Transform2D(new Vector2(8, 8)); + + spatialHash.Insert(0, rectA, rectATransform); + spatialHash.Insert(1, rectB, rectBTransform); + + spatialHash.Clear(); + + spatialHash.Retrieve(0, rectA, rectATransform).Should().HaveCount(0); + } + } +} \ No newline at end of file diff --git a/Test/Test.csproj b/Test/Test.csproj new file mode 100644 index 0000000..31e8e2e --- /dev/null +++ b/Test/Test.csproj @@ -0,0 +1,15 @@ + + + netcoreapp3.0 + false + + + + + + + + + + + \ No newline at end of file diff --git a/bonk.sln b/bonk.sln new file mode 100644 index 0000000..14b504d --- /dev/null +++ b/bonk.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bonk", "Bonk\Bonk.csproj", "{F5349EC2-5BA2-4051-A1BB-48649344739B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{866BDF4C-96C3-4DA9-B461-E01BF2146D19}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F5349EC2-5BA2-4051-A1BB-48649344739B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5349EC2-5BA2-4051-A1BB-48649344739B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5349EC2-5BA2-4051-A1BB-48649344739B}.Debug|x64.ActiveCfg = Debug|Any CPU + {F5349EC2-5BA2-4051-A1BB-48649344739B}.Debug|x64.Build.0 = Debug|Any CPU + {F5349EC2-5BA2-4051-A1BB-48649344739B}.Debug|x86.ActiveCfg = Debug|Any CPU + {F5349EC2-5BA2-4051-A1BB-48649344739B}.Debug|x86.Build.0 = Debug|Any CPU + {F5349EC2-5BA2-4051-A1BB-48649344739B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5349EC2-5BA2-4051-A1BB-48649344739B}.Release|Any CPU.Build.0 = Release|Any CPU + {F5349EC2-5BA2-4051-A1BB-48649344739B}.Release|x64.ActiveCfg = Release|Any CPU + {F5349EC2-5BA2-4051-A1BB-48649344739B}.Release|x64.Build.0 = Release|Any CPU + {F5349EC2-5BA2-4051-A1BB-48649344739B}.Release|x86.ActiveCfg = Release|Any CPU + {F5349EC2-5BA2-4051-A1BB-48649344739B}.Release|x86.Build.0 = Release|Any CPU + {866BDF4C-96C3-4DA9-B461-E01BF2146D19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {866BDF4C-96C3-4DA9-B461-E01BF2146D19}.Debug|Any CPU.Build.0 = Debug|Any CPU + {866BDF4C-96C3-4DA9-B461-E01BF2146D19}.Debug|x64.ActiveCfg = Debug|Any CPU + {866BDF4C-96C3-4DA9-B461-E01BF2146D19}.Debug|x64.Build.0 = Debug|Any CPU + {866BDF4C-96C3-4DA9-B461-E01BF2146D19}.Debug|x86.ActiveCfg = Debug|Any CPU + {866BDF4C-96C3-4DA9-B461-E01BF2146D19}.Debug|x86.Build.0 = Debug|Any CPU + {866BDF4C-96C3-4DA9-B461-E01BF2146D19}.Release|Any CPU.ActiveCfg = Release|Any CPU + {866BDF4C-96C3-4DA9-B461-E01BF2146D19}.Release|Any CPU.Build.0 = Release|Any CPU + {866BDF4C-96C3-4DA9-B461-E01BF2146D19}.Release|x64.ActiveCfg = Release|Any CPU + {866BDF4C-96C3-4DA9-B461-E01BF2146D19}.Release|x64.Build.0 = Release|Any CPU + {866BDF4C-96C3-4DA9-B461-E01BF2146D19}.Release|x86.ActiveCfg = Release|Any CPU + {866BDF4C-96C3-4DA9-B461-E01BF2146D19}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal