| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 |
- 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(layer, layer);
- 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 = 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 = 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 = Matrix3x2.CreateRotationZ(orientedRectangleB.Orientation.Rotation);
- var circleCenterInRectangleSpace = rotation.Transform(circleA.Center - orientedRectangleB.Center);
- var circleInRectangleSpace = new CircleF(circleCenterInRectangleSpace, circleA.Radius);
- var boundingRectangle = new BoundingRectangle(new Vector2(), orientedRectangleB.Radii);
- var penetrationVector = PenetrationVector(circleInRectangleSpace, boundingRectangle);
- var inverseRotation = Matrix3x2.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.Intersect(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
- }
- }
|