CollisionComponent.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Data;
  4. using System.Diagnostics;
  5. using System.Linq;
  6. using Microsoft.Xna.Framework;
  7. using MonoGame.Extended.Collisions.Layers;
  8. using MonoGame.Extended.Collisions.QuadTree;
  9. namespace MonoGame.Extended.Collisions
  10. {
  11. /// <summary>
  12. /// Handles basic collision between actors.
  13. /// When two actors collide, their OnCollision method is called.
  14. /// </summary>
  15. public class CollisionComponent : SimpleGameComponent
  16. {
  17. public const string DEFAULT_LAYER_NAME = "default";
  18. private Dictionary<string, Layer> _layers = new();
  19. /// <summary>
  20. /// List of collision's layers
  21. /// </summary>
  22. public IReadOnlyDictionary<string, Layer> Layers => _layers;
  23. private HashSet<(Layer, Layer)> _layerCollision = new();
  24. /// <summary>
  25. /// Creates component with default layer, which is a collision tree covering the specified area (using <see cref="QuadTree"/>.
  26. /// </summary>
  27. /// <param name="boundary">Boundary of the collision tree.</param>
  28. public CollisionComponent(RectangleF boundary)
  29. {
  30. SetDefaultLayer(new Layer(new QuadTreeSpace(boundary)));
  31. }
  32. /// <summary>
  33. /// Creates component with specifies default layer.
  34. /// If layer is null, method creates component without default layer.
  35. /// </summary>
  36. /// <param name="layer">Default layer</param>
  37. public CollisionComponent(Layer layer = null)
  38. {
  39. if (layer is not null)
  40. SetDefaultLayer(layer);
  41. }
  42. /// <summary>
  43. /// The main layer has the name from <see cref="DEFAULT_LAYER_NAME"/>.
  44. /// The main layer collision with itself and all other layers.
  45. /// </summary>
  46. /// <param name="layer">Layer to set default</param>
  47. public void SetDefaultLayer(Layer layer)
  48. {
  49. if (_layers.ContainsKey(DEFAULT_LAYER_NAME))
  50. Remove(DEFAULT_LAYER_NAME);
  51. Add(DEFAULT_LAYER_NAME, layer);
  52. foreach (var otherLayer in _layers.Values)
  53. AddCollisionBetweenLayer(layer, otherLayer);
  54. }
  55. /// <summary>
  56. /// Update the collision tree and process collisions.
  57. /// </summary>
  58. /// <remarks>
  59. /// Boundary shapes are updated if they were changed since the last
  60. /// update.
  61. /// </remarks>
  62. /// <param name="gameTime"></param>
  63. public override void Update(GameTime gameTime)
  64. {
  65. foreach (var layer in _layers.Values)
  66. layer.Reset();
  67. foreach (var (firstLayer, secondLayer) in _layerCollision)
  68. foreach (var actor in firstLayer.Space)
  69. {
  70. var collisions = secondLayer.Space.Query(actor.Bounds.BoundingRectangle);
  71. foreach (var other in collisions)
  72. if (actor != other && actor.Bounds.Intersects(other.Bounds))
  73. {
  74. var penetrationVector = CalculatePenetrationVector(actor.Bounds, other.Bounds);
  75. actor.OnCollision(new CollisionEventArgs
  76. {
  77. Other = other,
  78. PenetrationVector = penetrationVector
  79. });
  80. other.OnCollision(new CollisionEventArgs
  81. {
  82. Other = actor,
  83. PenetrationVector = -penetrationVector
  84. });
  85. }
  86. }
  87. }
  88. /// <summary>
  89. /// Inserts the target into the collision tree.
  90. /// The target will have its OnCollision called when collisions occur.
  91. /// </summary>
  92. /// <param name="target">Target to insert.</param>
  93. public void Insert(ICollisionActor target)
  94. {
  95. var layerName = target.LayerName ?? DEFAULT_LAYER_NAME;
  96. if (!_layers.TryGetValue(layerName, out var layer))
  97. {
  98. throw new UndefinedLayerException(layerName);
  99. }
  100. layer.Space.Insert(target);
  101. }
  102. /// <summary>
  103. /// Removes the target from the collision tree.
  104. /// </summary>
  105. /// <param name="target">Target to remove.</param>
  106. public void Remove(ICollisionActor target)
  107. {
  108. if (target.LayerName is not null)
  109. _layers[target.LayerName].Space.Remove(target);
  110. else
  111. foreach (var layer in _layers.Values)
  112. if (layer.Space.Remove(target))
  113. return;
  114. }
  115. #region Layers
  116. /// <summary>
  117. /// Add the new layer. The name of layer must be unique.
  118. /// </summary>
  119. /// <param name="name">Name of layer</param>
  120. /// <param name="layer">The new layer</param>
  121. /// <exception cref="ArgumentNullException"><paramref name="name"/> is null</exception>
  122. public void Add(string name, Layer layer)
  123. {
  124. if (string.IsNullOrWhiteSpace(name))
  125. throw new ArgumentNullException(nameof(name));
  126. if (!_layers.TryAdd(name, layer))
  127. throw new DuplicateNameException(name);
  128. if (name != DEFAULT_LAYER_NAME)
  129. {
  130. AddCollisionBetweenLayer(layer, layer);
  131. AddCollisionBetweenLayer(_layers[DEFAULT_LAYER_NAME], layer);
  132. }
  133. }
  134. /// <summary>
  135. /// Remove the layer and all layer's collisions.
  136. /// </summary>
  137. /// <param name="name">The name of the layer to delete</param>
  138. /// <param name="layer">The layer to delete</param>
  139. public void Remove(string name = null, Layer layer = null)
  140. {
  141. name ??= _layers.First(x => x.Value == layer).Key;
  142. _layers.Remove(name, out layer);
  143. _layerCollision.RemoveWhere(tuple => tuple.Item1 == layer || tuple.Item2 == layer);
  144. }
  145. public void AddCollisionBetweenLayer(Layer a, Layer b)
  146. {
  147. _layerCollision.Add((a, b));
  148. }
  149. public void AddCollisionBetweenLayer(string nameA, string nameB)
  150. {
  151. _layerCollision.Add((_layers[nameA], _layers[nameB]));
  152. }
  153. #endregion
  154. #region Penetration Vectors
  155. /// <summary>
  156. /// Calculate a's penetration into b
  157. /// </summary>
  158. /// <param name="a">The penetrating shape.</param>
  159. /// <param name="b">The shape being penetrated.</param>
  160. /// <returns>The distance vector from the edge of b to a's Position</returns>
  161. private static Vector2 CalculatePenetrationVector(IShapeF a, IShapeF b)
  162. {
  163. return a switch
  164. {
  165. CircleF circleA when b is CircleF circleB => PenetrationVector(circleA, circleB),
  166. CircleF circleA when b is RectangleF rectangleB => PenetrationVector(circleA, rectangleB),
  167. CircleF circleA when b is OrientedRectangle orientedRectangleB => PenetrationVector(circleA, orientedRectangleB),
  168. RectangleF rectangleA when b is CircleF circleB => PenetrationVector(rectangleA, circleB),
  169. RectangleF rectangleA when b is RectangleF rectangleB => PenetrationVector(rectangleA, rectangleB),
  170. RectangleF rectangleA when b is OrientedRectangle orientedRectangleB => PenetrationVector(rectangleA, orientedRectangleB),
  171. OrientedRectangle orientedRectangleA when b is CircleF circleB => PenetrationVector(orientedRectangleA, circleB),
  172. OrientedRectangle orientedRectangleA when b is RectangleF rectangleB => PenetrationVector(orientedRectangleA, rectangleB),
  173. OrientedRectangle orientedRectangleA when b is OrientedRectangle orientedRectangleB => PenetrationVector(orientedRectangleA, orientedRectangleB),
  174. _ => throw new ArgumentOutOfRangeException(nameof(a))
  175. };
  176. }
  177. private static Vector2 PenetrationVector(CircleF circ1, CircleF circ2)
  178. {
  179. if (!circ1.Intersects(circ2))
  180. {
  181. return Vector2.Zero;
  182. }
  183. var displacement = circ1.Center - circ2.Center;
  184. Vector2 desiredDisplacement;
  185. if (displacement != Vector2.Zero)
  186. {
  187. desiredDisplacement = displacement.NormalizedCopy() * (circ1.Radius + circ2.Radius);
  188. }
  189. else
  190. {
  191. desiredDisplacement = -Vector2.UnitY * (circ1.Radius + circ2.Radius);
  192. }
  193. var penetration = displacement - desiredDisplacement;
  194. return penetration;
  195. }
  196. private static Vector2 PenetrationVector(CircleF circ, RectangleF rect)
  197. {
  198. var collisionPoint = rect.ClosestPointTo(circ.Center);
  199. var cToCollPoint = collisionPoint - circ.Center;
  200. if (rect.Contains(circ.Center) || cToCollPoint.Equals(Vector2.Zero))
  201. {
  202. var displacement = circ.Center - rect.Center;
  203. Vector2 desiredDisplacement;
  204. if (displacement != Vector2.Zero)
  205. {
  206. // Calculate penetration as only in X or Y direction.
  207. // Whichever is lower.
  208. var dispx = new Vector2(displacement.X, 0);
  209. var dispy = new Vector2(0, displacement.Y);
  210. dispx.Normalize();
  211. dispy.Normalize();
  212. dispx *= (circ.Radius + rect.Width / 2);
  213. dispy *= (circ.Radius + rect.Height / 2);
  214. if (dispx.LengthSquared() < dispy.LengthSquared())
  215. {
  216. desiredDisplacement = dispx;
  217. displacement.Y = 0;
  218. }
  219. else
  220. {
  221. desiredDisplacement = dispy;
  222. displacement.X = 0;
  223. }
  224. }
  225. else
  226. {
  227. desiredDisplacement = -Vector2.UnitY * (circ.Radius + rect.Height / 2);
  228. }
  229. var penetration = displacement - desiredDisplacement;
  230. return penetration;
  231. }
  232. else
  233. {
  234. var penetration = circ.Radius * cToCollPoint.NormalizedCopy() - cToCollPoint;
  235. return penetration;
  236. }
  237. }
  238. private static Vector2 PenetrationVector(CircleF circleA, OrientedRectangle orientedRectangleB)
  239. {
  240. var rotation = Matrix3x2.CreateRotationZ(orientedRectangleB.Orientation.Rotation);
  241. var circleCenterInRectangleSpace = rotation.Transform(circleA.Center - orientedRectangleB.Center);
  242. var circleInRectangleSpace = new CircleF(circleCenterInRectangleSpace, circleA.Radius);
  243. var boundingRectangle = new BoundingRectangle(new Vector2(), orientedRectangleB.Radii);
  244. var penetrationVector = PenetrationVector(circleInRectangleSpace, boundingRectangle);
  245. var inverseRotation = Matrix3x2.CreateRotationZ(-orientedRectangleB.Orientation.Rotation);
  246. var transformedPenetration = inverseRotation.Transform(penetrationVector);
  247. return transformedPenetration;
  248. }
  249. private static Vector2 PenetrationVector(RectangleF rect, CircleF circ)
  250. {
  251. return -PenetrationVector(circ, rect);
  252. }
  253. private static Vector2 PenetrationVector(RectangleF rect1, RectangleF rect2)
  254. {
  255. var intersectingRectangle = RectangleF.Intersect(rect1, rect2);
  256. Debug.Assert(!intersectingRectangle.IsEmpty,
  257. "Violation of: !intersect.IsEmpty; Rectangles must intersect to calculate a penetration vector.");
  258. Vector2 penetration;
  259. if (intersectingRectangle.Width < intersectingRectangle.Height)
  260. {
  261. var d = rect1.Center.X < rect2.Center.X
  262. ? intersectingRectangle.Width
  263. : -intersectingRectangle.Width;
  264. penetration = new Vector2(d, 0);
  265. }
  266. else
  267. {
  268. var d = rect1.Center.Y < rect2.Center.Y
  269. ? intersectingRectangle.Height
  270. : -intersectingRectangle.Height;
  271. penetration = new Vector2(0, d);
  272. }
  273. return penetration;
  274. }
  275. private static Vector2 PenetrationVector(RectangleF rectangleA, OrientedRectangle orientedRectangleB)
  276. {
  277. return PenetrationVector((OrientedRectangle)rectangleA, orientedRectangleB);
  278. }
  279. private static Vector2 PenetrationVector(OrientedRectangle orientedRectangleA, CircleF circleB)
  280. {
  281. return -PenetrationVector(circleB, orientedRectangleA);
  282. }
  283. private static Vector2 PenetrationVector(OrientedRectangle orientedRectangleA, RectangleF rectangleB)
  284. {
  285. return -PenetrationVector(rectangleB, orientedRectangleA);
  286. }
  287. private static Vector2 PenetrationVector(OrientedRectangle orientedRectangleA, OrientedRectangle orientedRectangleB)
  288. {
  289. return OrientedRectangle.Intersects(orientedRectangleA, orientedRectangleB)
  290. .MinimumTranslationVector;
  291. }
  292. #endregion
  293. }
  294. }