using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Microsoft.Xna.Framework; namespace MonoGame.Extended.Collisions { /// /// Handles basic collision between actors. /// When two actors collide, their OnCollision method is called. /// public class CollisionComponent : SimpleGameComponent { private readonly Dictionary _targetDataDictionary = new Dictionary(); private readonly Quadtree _collisionTree; /// /// Creates a collision tree covering the specified area. /// /// Boundary of the collision tree. public CollisionComponent(RectangleF boundary) { _collisionTree = new Quadtree(boundary); } /// /// Update the collision tree and process collisions. /// /// /// Boundary shapes are updated if they were changed since the last /// update. /// /// public override void Update(GameTime gameTime) { // Detect collisions foreach (var value in _targetDataDictionary.Values) { _collisionTree.Remove(value); var target = value.Target; var collisions =_collisionTree.Query(target.Bounds); // Generate list of collision Infos foreach (var other in collisions) { var collisionInfo = new CollisionEventArgs() { Other = other.Target, PenetrationVector = CalculatePenetrationVector(value.Bounds, other.Bounds) }; target.OnCollision(collisionInfo); value.Bounds = value.Target.Bounds; } _collisionTree.Insert(value); } } /// /// Inserts the target into the collision tree. /// The target will have its OnCollision called when collisions occur. /// /// Target to insert. public void Insert(ICollisionActor target) { if (!_targetDataDictionary.ContainsKey(target)) { var data = new QuadtreeData(target); _targetDataDictionary.Add(target, data); _collisionTree.Insert(data); } } /// /// Removes the target from the collision tree. /// /// Target to remove. public void Remove(ICollisionActor target) { if (_targetDataDictionary.ContainsKey(target)) { var data = _targetDataDictionary[target]; _collisionTree.Remove(data); _targetDataDictionary.Remove(target); } } /// /// Gets if the target is inserted in the collision tree. /// /// Actor to check if contained /// True if the target is contained in the collision tree. public bool Contains(ICollisionActor target) { return _targetDataDictionary.ContainsKey(target); } #region Penetration Vectors /// /// Calculate a's penetration into b /// /// The penetrating shape. /// The shape being penetrated. /// The distance vector from the edge of b to a's Position private static Vector2 CalculatePenetrationVector(IShapeF a, IShapeF b) { switch (a) { case RectangleF rectA when b is RectangleF rectB: return PenetrationVector(rectA, rectB); case CircleF circA when b is CircleF circB: return PenetrationVector(circA, circB); case CircleF circA when b is RectangleF rectB: return PenetrationVector(circA, rectB); case RectangleF rectA when b is CircleF circB: return PenetrationVector(rectA, circB); } throw new NotSupportedException("Shapes must be either a CircleF or RectangleF"); } 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(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(RectangleF rect, CircleF circ) { return -PenetrationVector(circ, rect); } #endregion } }