summaryrefslogtreecommitdiff
path: root/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionComponent.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionComponent.cs')
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionComponent.cs341
1 files changed, 341 insertions, 0 deletions
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionComponent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionComponent.cs
new file mode 100644
index 0000000..51467d9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionComponent.cs
@@ -0,0 +1,341 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Diagnostics;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Collisions.Layers;
+using MonoGame.Extended.Collisions.QuadTree;
+
+namespace MonoGame.Extended.Collisions
+{
+ /// <summary>
+ /// Handles basic collision between actors.
+ /// When two actors collide, their OnCollision method is called.
+ /// </summary>
+ public class CollisionComponent : SimpleGameComponent
+ {
+ public const string DEFAULT_LAYER_NAME = "default";
+
+ private Dictionary<string, Layer> _layers = new();
+
+ /// <summary>
+ /// List of collision's layers
+ /// </summary>
+ public IReadOnlyDictionary<string, Layer> Layers => _layers;
+
+ private HashSet<(Layer, Layer)> _layerCollision = new();
+
+ /// <summary>
+ /// Creates component with default layer, which is a collision tree covering the specified area (using <see cref="QuadTree"/>.
+ /// </summary>
+ /// <param name="boundary">Boundary of the collision tree.</param>
+ public CollisionComponent(RectangleF boundary)
+ {
+ SetDefaultLayer(new Layer(new QuadTreeSpace(boundary)));
+ }
+
+ /// <summary>
+ /// Creates component with specifies default layer.
+ /// If layer is null, method creates component without default layer.
+ /// </summary>
+ /// <param name="layer">Default layer</param>
+ public CollisionComponent(Layer layer = null)
+ {
+ if (layer is not null)
+ SetDefaultLayer(layer);
+ }
+
+ /// <summary>
+ /// The main layer has the name from <see cref="DEFAULT_LAYER_NAME"/>.
+ /// The main layer collision with itself and all other layers.
+ /// </summary>
+ /// <param name="layer">Layer to set default</param>
+ public void SetDefaultLayer(Layer layer)
+ {
+ if (_layers.ContainsKey(DEFAULT_LAYER_NAME))
+ Remove(DEFAULT_LAYER_NAME);
+ Add(DEFAULT_LAYER_NAME, layer);
+ foreach (var otherLayer in _layers.Values)
+ AddCollisionBetweenLayer(layer, otherLayer);
+ }
+
+ /// <summary>
+ /// Update the collision tree and process collisions.
+ /// </summary>
+ /// <remarks>
+ /// Boundary shapes are updated if they were changed since the last
+ /// update.
+ /// </remarks>
+ /// <param name="gameTime"></param>
+ public override void Update(GameTime gameTime)
+ {
+ foreach (var layer in _layers.Values)
+ layer.Reset();
+
+ foreach (var (firstLayer, secondLayer) in _layerCollision)
+ foreach (var actor in firstLayer.Space)
+ {
+ var collisions = secondLayer.Space.Query(actor.Bounds.BoundingRectangle);
+ foreach (var other in collisions)
+ if (actor != other && actor.Bounds.Intersects(other.Bounds))
+ {
+ var penetrationVector = CalculatePenetrationVector(actor.Bounds, other.Bounds);
+
+ actor.OnCollision(new CollisionEventArgs
+ {
+ Other = other,
+ PenetrationVector = penetrationVector
+ });
+ other.OnCollision(new CollisionEventArgs
+ {
+ Other = actor,
+ PenetrationVector = -penetrationVector
+ });
+ }
+
+ }
+ }
+
+ /// <summary>
+ /// Inserts the target into the collision tree.
+ /// The target will have its OnCollision called when collisions occur.
+ /// </summary>
+ /// <param name="target">Target to insert.</param>
+ public void Insert(ICollisionActor target)
+ {
+ var layerName = target.LayerName ?? DEFAULT_LAYER_NAME;
+ if (!_layers.TryGetValue(layerName, out var layer))
+ {
+ throw new UndefinedLayerException(layerName);
+ }
+
+ layer.Space.Insert(target);
+ }
+
+ /// <summary>
+ /// Removes the target from the collision tree.
+ /// </summary>
+ /// <param name="target">Target to remove.</param>
+ public void Remove(ICollisionActor target)
+ {
+ if (target.LayerName is not null)
+ _layers[target.LayerName].Space.Remove(target);
+ else
+ foreach (var layer in _layers.Values)
+ if (layer.Space.Remove(target))
+ return;
+ }
+
+ #region Layers
+
+ /// <summary>
+ /// Add the new layer. The name of layer must be unique.
+ /// </summary>
+ /// <param name="name">Name of layer</param>
+ /// <param name="layer">The new layer</param>
+ /// <exception cref="ArgumentNullException"><paramref name="name"/> is null</exception>
+ public void Add(string name, Layer layer)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ throw new ArgumentNullException(nameof(name));
+
+ if (!_layers.TryAdd(name, layer))
+ throw new DuplicateNameException(name);
+
+ if (name != DEFAULT_LAYER_NAME)
+ AddCollisionBetweenLayer(_layers[DEFAULT_LAYER_NAME], layer);
+ }
+
+ /// <summary>
+ /// Remove the layer and all layer's collisions.
+ /// </summary>
+ /// <param name="name">The name of the layer to delete</param>
+ /// <param name="layer">The layer to delete</param>
+ public void Remove(string name = null, Layer layer = null)
+ {
+ name ??= _layers.First(x => x.Value == layer).Key;
+ _layers.Remove(name, out layer);
+ _layerCollision.RemoveWhere(tuple => tuple.Item1 == layer || tuple.Item2 == layer);
+ }
+
+ public void AddCollisionBetweenLayer(Layer a, Layer b)
+ {
+ _layerCollision.Add((a, b));
+ }
+
+ public void AddCollisionBetweenLayer(string nameA, string nameB)
+ {
+ _layerCollision.Add((_layers[nameA], _layers[nameB]));
+ }
+
+ #endregion
+
+ #region Penetration Vectors
+
+ /// <summary>
+ /// Calculate a's penetration into b
+ /// </summary>
+ /// <param name="a">The penetrating shape.</param>
+ /// <param name="b">The shape being penetrated.</param>
+ /// <returns>The distance vector from the edge of b to a's Position</returns>
+ private static Vector2 CalculatePenetrationVector(IShapeF a, IShapeF b)
+ {
+ return a switch
+ {
+ CircleF circleA when b is CircleF circleB => PenetrationVector(circleA, circleB),
+ CircleF circleA when b is RectangleF rectangleB => PenetrationVector(circleA, rectangleB),
+ CircleF circleA when b is OrientedRectangle orientedRectangleB => PenetrationVector(circleA, orientedRectangleB),
+
+ RectangleF rectangleA when b is CircleF circleB => PenetrationVector(rectangleA, circleB),
+ RectangleF rectangleA when b is RectangleF rectangleB => PenetrationVector(rectangleA, rectangleB),
+ RectangleF rectangleA when b is OrientedRectangle orientedRectangleB => PenetrationVector(rectangleA, orientedRectangleB),
+
+ OrientedRectangle orientedRectangleA when b is CircleF circleB => PenetrationVector(orientedRectangleA, circleB),
+ OrientedRectangle orientedRectangleA when b is RectangleF rectangleB => PenetrationVector(orientedRectangleA, rectangleB),
+ OrientedRectangle orientedRectangleA when b is OrientedRectangle orientedRectangleB => PenetrationVector(orientedRectangleA, orientedRectangleB),
+
+ _ => throw new ArgumentOutOfRangeException(nameof(a))
+ };
+ }
+
+ private static Vector2 PenetrationVector(CircleF circ1, CircleF circ2)
+ {
+ if (!circ1.Intersects(circ2))
+ {
+ return Vector2.Zero;
+ }
+
+ var displacement = Point2.Displacement(circ1.Center, circ2.Center);
+
+ Vector2 desiredDisplacement;
+ if (displacement != Vector2.Zero)
+ {
+ desiredDisplacement = displacement.NormalizedCopy() * (circ1.Radius + circ2.Radius);
+ }
+ else
+ {
+ desiredDisplacement = -Vector2.UnitY * (circ1.Radius + circ2.Radius);
+ }
+
+
+ var penetration = displacement - desiredDisplacement;
+ return penetration;
+ }
+
+ private static Vector2 PenetrationVector(CircleF circ, RectangleF rect)
+ {
+ var collisionPoint = rect.ClosestPointTo(circ.Center);
+ var cToCollPoint = collisionPoint - circ.Center;
+
+ if (rect.Contains(circ.Center) || cToCollPoint.Equals(Vector2.Zero))
+ {
+ var displacement = Point2.Displacement(circ.Center, rect.Center);
+
+ Vector2 desiredDisplacement;
+ if (displacement != Vector2.Zero)
+ {
+ // Calculate penetration as only in X or Y direction.
+ // Whichever is lower.
+ var dispx = new Vector2(displacement.X, 0);
+ var dispy = new Vector2(0, displacement.Y);
+ dispx.Normalize();
+ dispy.Normalize();
+
+ dispx *= (circ.Radius + rect.Width / 2);
+ dispy *= (circ.Radius + rect.Height / 2);
+
+ if (dispx.LengthSquared() < dispy.LengthSquared())
+ {
+ desiredDisplacement = dispx;
+ displacement.Y = 0;
+ }
+ else
+ {
+ desiredDisplacement = dispy;
+ displacement.X = 0;
+ }
+ }
+ else
+ {
+ desiredDisplacement = -Vector2.UnitY * (circ.Radius + rect.Height / 2);
+ }
+
+ var penetration = displacement - desiredDisplacement;
+ return penetration;
+ }
+ else
+ {
+ var penetration = circ.Radius * cToCollPoint.NormalizedCopy() - cToCollPoint;
+ return penetration;
+ }
+ }
+
+ private static Vector2 PenetrationVector(CircleF circleA, OrientedRectangle orientedRectangleB)
+ {
+ var rotation = Matrix2.CreateRotationZ(orientedRectangleB.Orientation.Rotation);
+ var circleCenterInRectangleSpace = rotation.Transform(circleA.Center - orientedRectangleB.Center);
+ var circleInRectangleSpace = new CircleF(circleCenterInRectangleSpace, circleA.Radius);
+ var boundingRectangle = new BoundingRectangle(new Point2(), orientedRectangleB.Radii);
+
+ var penetrationVector = PenetrationVector(circleInRectangleSpace, boundingRectangle);
+ var inverseRotation = Matrix2.CreateRotationZ(-orientedRectangleB.Orientation.Rotation);
+ var transformedPenetration = inverseRotation.Transform(penetrationVector);
+
+ return transformedPenetration;
+ }
+
+ private static Vector2 PenetrationVector(RectangleF rect, CircleF circ)
+ {
+ return -PenetrationVector(circ, rect);
+ }
+
+ private static Vector2 PenetrationVector(RectangleF rect1, RectangleF rect2)
+ {
+ var intersectingRectangle = RectangleF.Intersection(rect1, rect2);
+ Debug.Assert(!intersectingRectangle.IsEmpty,
+ "Violation of: !intersect.IsEmpty; Rectangles must intersect to calculate a penetration vector.");
+
+ Vector2 penetration;
+ if (intersectingRectangle.Width < intersectingRectangle.Height)
+ {
+ var d = rect1.Center.X < rect2.Center.X
+ ? intersectingRectangle.Width
+ : -intersectingRectangle.Width;
+ penetration = new Vector2(d, 0);
+ }
+ else
+ {
+ var d = rect1.Center.Y < rect2.Center.Y
+ ? intersectingRectangle.Height
+ : -intersectingRectangle.Height;
+ penetration = new Vector2(0, d);
+ }
+
+ return penetration;
+ }
+
+ private static Vector2 PenetrationVector(RectangleF rectangleA, OrientedRectangle orientedRectangleB)
+ {
+ return PenetrationVector((OrientedRectangle)rectangleA, orientedRectangleB);
+ }
+
+ private static Vector2 PenetrationVector(OrientedRectangle orientedRectangleA, CircleF circleB)
+ {
+ return -PenetrationVector(circleB, orientedRectangleA);
+ }
+
+ private static Vector2 PenetrationVector(OrientedRectangle orientedRectangleA, RectangleF rectangleB)
+ {
+ return -PenetrationVector(rectangleB, orientedRectangleA);
+ }
+
+ private static Vector2 PenetrationVector(OrientedRectangle orientedRectangleA, OrientedRectangle orientedRectangleB)
+ {
+ return OrientedRectangle.Intersects(orientedRectangleA, orientedRectangleB)
+ .MinimumTranslationVector;
+ }
+
+ #endregion
+ }
+}