CollisionManager.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. #region File Description
  2. //-----------------------------------------------------------------------------
  3. // CollisionManager.cs
  4. //
  5. // Microsoft XNA Community Game Platform
  6. // Copyright (C) Microsoft Corporation. All rights reserved.
  7. //-----------------------------------------------------------------------------
  8. #endregion
  9. #region Using Statements
  10. using System;
  11. using System.Collections.Generic;
  12. using Microsoft.Xna.Framework;
  13. #endregion
  14. namespace NetRumble
  15. {
  16. /// <summary>
  17. /// Manages collisions and collision events between all gameplay objects.
  18. /// </summary>
  19. public class CollisionManager : BatchRemovalCollection<GameplayObject>
  20. {
  21. #region Constants
  22. /// <summary>
  23. /// The ratio of speed to damage applied, for explosions.
  24. /// </summary>
  25. private const float speedDamageRatio = 0.5f;
  26. /// <summary>
  27. /// The number of times that the FindSpawnPoint method will try to find a point.
  28. /// </summary>
  29. private const int findSpawnPointAttempts = 25;
  30. #endregion
  31. #region Helper Types
  32. /// <summary>
  33. /// The result of a collision query.
  34. /// </summary>
  35. struct CollisionResult
  36. {
  37. /// <summary>
  38. /// How far away did the collision occur down the ray
  39. /// </summary>
  40. public float Distance;
  41. /// <summary>
  42. /// The collision "direction"
  43. /// </summary>
  44. public Vector2 Normal;
  45. /// <summary>
  46. /// What caused the collison (what the source ran into)
  47. /// </summary>
  48. public GameplayObject GameplayObject;
  49. public static int Compare(CollisionResult a, CollisionResult b)
  50. {
  51. return a.Distance.CompareTo(b.Distance);
  52. }
  53. }
  54. #endregion
  55. #region Singleton
  56. /// <summary>
  57. /// Singleton for collision management.
  58. /// </summary>
  59. private static CollisionManager collisionManager = new CollisionManager();
  60. public static BatchRemovalCollection<GameplayObject> Collection
  61. {
  62. get { return collisionManager as BatchRemovalCollection<GameplayObject>; }
  63. }
  64. #endregion
  65. #region Collision Data
  66. /// <summary>
  67. /// The dimensions of the space in which collision occurs.
  68. /// </summary>
  69. private Rectangle dimensions = new Rectangle(0, 0, 2048, 2048);
  70. public static Rectangle Dimensions
  71. {
  72. get { return (collisionManager == null ? Rectangle.Empty :
  73. collisionManager.dimensions); }
  74. set
  75. {
  76. // safety-check the singleton
  77. if (collisionManager == null)
  78. {
  79. throw new InvalidOperationException(
  80. "The collision manager has not yet been initialized.");
  81. }
  82. collisionManager.dimensions = value;
  83. }
  84. }
  85. /// <summary>
  86. /// The list of barriers in the game world.
  87. /// </summary>
  88. /// <remarks>This list is not owned by this object.</remarks>
  89. private List<Rectangle> barriers = new List<Rectangle>();
  90. public static List<Rectangle> Barriers
  91. {
  92. get { return (collisionManager == null ? null :
  93. collisionManager.barriers); }
  94. }
  95. /// <summary>
  96. /// Cached list of collision results, for more optimal collision detection.
  97. /// </summary>
  98. List<CollisionResult> collisionResults = new List<CollisionResult>();
  99. #endregion
  100. #region Initialization Methods
  101. /// <summary>
  102. /// Constructs a new collision manager.
  103. /// </summary>
  104. private CollisionManager() { }
  105. #endregion
  106. #region Updating Methods
  107. /// <summary>
  108. /// Update the collision system.
  109. /// </summary>
  110. /// <param name="elapsedTime">The amount of elapsed time, in seconds.</param>
  111. public static void Update(float elapsedTime)
  112. {
  113. // safety-check the singleton
  114. if (collisionManager == null)
  115. {
  116. throw new InvalidOperationException(
  117. "The collision manager has not yet been initialized.");
  118. }
  119. // move each object
  120. for (int i = 0; i < collisionManager.Count; ++i)
  121. {
  122. if (collisionManager[i].Active)
  123. {
  124. // determine how far they are going to move
  125. Vector2 movement = collisionManager[i].Velocity * elapsedTime;
  126. // only allow collisionManager that have not collided yet
  127. // collisionManager frame to collide
  128. // -- otherwise, objects can "double-hit" and trade their momentum
  129. if (collisionManager[i].CollidedThisFrame == false)
  130. {
  131. movement = MoveAndCollide(collisionManager[i], movement);
  132. }
  133. // determine the new position
  134. collisionManager[i].Position += movement;
  135. // collide with the barriers
  136. for (int b = 0; b < collisionManager.barriers.Count; ++b)
  137. {
  138. CollisionMath.CircleLineCollisionResult result =
  139. new CollisionMath.CircleLineCollisionResult();
  140. if (collisionManager[i] is Projectile)
  141. {
  142. CollisionMath.CircleRectangleCollide(
  143. collisionManager[i].Position - movement,
  144. collisionManager[i].Radius,
  145. collisionManager.barriers[b], ref result);
  146. if (result.Collision)
  147. {
  148. collisionManager[i].Position -= movement;
  149. collisionManager[i].Die(null, false);
  150. }
  151. }
  152. else
  153. {
  154. CollisionMath.CircleRectangleCollide(
  155. collisionManager[i].Position,
  156. collisionManager[i].Radius,
  157. collisionManager.barriers[b], ref result);
  158. if (result.Collision)
  159. {
  160. // if a non-projectile hits a barrier, bounce slightly
  161. float vn = Vector2.Dot(collisionManager[i].Velocity,
  162. result.Normal);
  163. collisionManager[i].Velocity -= (2.0f * vn) *
  164. result.Normal;
  165. collisionManager[i].Position += result.Normal *
  166. result.Distance;
  167. }
  168. }
  169. }
  170. }
  171. }
  172. CollisionManager.Collection.ApplyPendingRemovals();
  173. }
  174. /// <summary>
  175. /// Move the given gameplayObject by the given movement, colliding and adjusting
  176. /// as necessary.
  177. /// </summary>
  178. /// <param name="gameplayObject">The gameplayObject who is moving.</param>
  179. /// <param name="movement">The desired movement vector for this update.</param>
  180. /// <returns>The movement vector after considering all collisions.</returns>
  181. private static Vector2 MoveAndCollide(GameplayObject gameplayObject,
  182. Vector2 movement)
  183. {
  184. // safety-check the singleton
  185. if (collisionManager == null)
  186. {
  187. throw new InvalidOperationException(
  188. "The collision manager has not yet been initialized.");
  189. }
  190. if (gameplayObject == null)
  191. {
  192. throw new ArgumentNullException("gameplayObject");
  193. }
  194. // make sure we care about where this gameplayObject goes
  195. if (!gameplayObject.Active)
  196. {
  197. return movement;
  198. }
  199. // make sure the movement is significant
  200. if (movement.LengthSquared() <= 0f)
  201. {
  202. return movement;
  203. }
  204. // generate the list of collisions
  205. Collide(gameplayObject, movement);
  206. // determine if we had any collisions
  207. if (collisionManager.collisionResults.Count > 0)
  208. {
  209. collisionManager.collisionResults.Sort(CollisionResult.Compare);
  210. foreach (CollisionResult collision in collisionManager.collisionResults)
  211. {
  212. // let the two objects touch each other, and see what happens
  213. if (gameplayObject.Touch(collision.GameplayObject) &&
  214. collision.GameplayObject.Touch(gameplayObject))
  215. {
  216. gameplayObject.CollidedThisFrame =
  217. collision.GameplayObject.CollidedThisFrame = true;
  218. // they should react to the other, even if they just died
  219. AdjustVelocities(gameplayObject, collision.GameplayObject);
  220. return Vector2.Zero;
  221. }
  222. }
  223. }
  224. return movement;
  225. }
  226. /// <summary>
  227. /// Determine all collisions that will happen as the given gameplayObject moves.
  228. /// </summary>
  229. /// <param name="gameplayObject">The gameplayObject that is moving.</param>
  230. /// <param name="movement">The gameplayObject's movement vector.</param>
  231. /// <remarks>The results are stored in the cached list.</remarks>
  232. public static void Collide(GameplayObject gameplayObject, Vector2 movement)
  233. {
  234. // safety-check the singleton
  235. if (collisionManager == null)
  236. {
  237. throw new InvalidOperationException(
  238. "The collision manager has not yet been initialized.");
  239. }
  240. collisionManager.collisionResults.Clear();
  241. if (gameplayObject == null)
  242. {
  243. throw new ArgumentNullException("gameplayObject");
  244. }
  245. if (!gameplayObject.Active)
  246. {
  247. return;
  248. }
  249. // determine the movement direction and scalar
  250. float movementLength = movement.Length();
  251. if (movementLength <= 0f)
  252. {
  253. return;
  254. }
  255. // check each gameplayObject
  256. foreach (GameplayObject checkActor in collisionManager)
  257. {
  258. if ((gameplayObject == checkActor) || !checkActor.Active)
  259. {
  260. continue;
  261. }
  262. // calculate the target vector
  263. float combinedRadius = checkActor.Radius + gameplayObject.Radius;
  264. Vector2 checkVector = checkActor.Position - gameplayObject.Position;
  265. float checkVectorLength = checkVector.Length();
  266. if (checkVectorLength <= 0f)
  267. {
  268. continue;
  269. }
  270. float distanceBetween = MathHelper.Max(checkVectorLength -
  271. (checkActor.Radius + gameplayObject.Radius), 0);
  272. // check if they could possibly touch no matter the direction
  273. if (movementLength < distanceBetween)
  274. {
  275. continue;
  276. }
  277. // determine how much of the movement is bringing the two together
  278. float movementTowards = Vector2.Dot(movement, checkVector);
  279. // check to see if the movement is away from each other
  280. if (movementTowards < 0f)
  281. {
  282. continue;
  283. }
  284. if (movementTowards < distanceBetween)
  285. {
  286. continue;
  287. }
  288. CollisionResult result = new CollisionResult();
  289. result.Distance = distanceBetween;
  290. result.Normal = Vector2.Normalize(checkVector);
  291. result.GameplayObject = checkActor;
  292. collisionManager.collisionResults.Add(result);
  293. }
  294. }
  295. /// <summary>
  296. /// Adjust the velocities of the two collisionManager as if they have collided,
  297. /// distributing their velocities according to their masses.
  298. /// </summary>
  299. /// <param name="actor1">The first gameplayObject.</param>
  300. /// <param name="actor2">The second gameplayObject.</param>
  301. private static void AdjustVelocities(GameplayObject actor1,
  302. GameplayObject actor2)
  303. {
  304. // don't adjust velocities if at least one has negative mass
  305. if ((actor1.Mass <= 0f) || (actor2.Mass <= 0f))
  306. {
  307. return;
  308. }
  309. // determine the vectors normal and tangent to the collision
  310. Vector2 collisionNormal = actor2.Position - actor1.Position;
  311. if (collisionNormal.LengthSquared() > 0f)
  312. {
  313. collisionNormal.Normalize();
  314. }
  315. else
  316. {
  317. return;
  318. }
  319. Vector2 collisionTangent = new Vector2(
  320. -collisionNormal.Y, collisionNormal.X);
  321. // determine the velocity components along the normal and tangent vectors
  322. float velocityNormal1 = Vector2.Dot(actor1.Velocity, collisionNormal);
  323. float velocityTangent1 = Vector2.Dot(actor1.Velocity, collisionTangent);
  324. float velocityNormal2 = Vector2.Dot(actor2.Velocity, collisionNormal);
  325. float velocityTangent2 = Vector2.Dot(actor2.Velocity, collisionTangent);
  326. // determine the new velocities along the normal
  327. float velocityNormal1New = ((velocityNormal1 * (actor1.Mass - actor2.Mass))
  328. + (2f * actor2.Mass * velocityNormal2)) / (actor1.Mass + actor2.Mass);
  329. float velocityNormal2New = ((velocityNormal2 * (actor2.Mass - actor1.Mass))
  330. + (2f * actor1.Mass * velocityNormal1)) / (actor1.Mass + actor2.Mass);
  331. // determine the new total velocities
  332. actor1.Velocity = (velocityNormal1New * collisionNormal) +
  333. (velocityTangent1 * collisionTangent);
  334. actor2.Velocity = (velocityNormal2New * collisionNormal) +
  335. (velocityTangent2 * collisionTangent);
  336. }
  337. #endregion
  338. #region Interaction Methods
  339. /// <summary>
  340. /// Find a valid spawn point in the world.
  341. /// </summary>
  342. /// <param name="radius">The radius of the object to be spawned.</param>
  343. /// <param name="random">A persistent Random object.</param>
  344. /// <returns>The spawn point.</returns>
  345. public static Vector2 FindSpawnPoint(GameplayObject spawnedObject, float radius)
  346. {
  347. // safety-check the singleton
  348. if (collisionManager == null)
  349. {
  350. throw new InvalidOperationException(
  351. "The collision manager has not yet been initialized.");
  352. }
  353. // safety-check the parameters
  354. if ((radius < 0f) || (radius > Dimensions.Width / 2))
  355. {
  356. throw new ArgumentOutOfRangeException("radius");
  357. }
  358. // keep trying to find a valid point
  359. Vector2 spawnPoint = new Vector2(
  360. radius + Dimensions.X +
  361. RandomMath.Random.Next((int)Math.Floor(Dimensions.Width - radius)),
  362. radius + Dimensions.Y +
  363. RandomMath.Random.Next((int)Math.Floor(Dimensions.Height - radius)));
  364. for (int i = 0; i < findSpawnPointAttempts; i++)
  365. {
  366. bool valid = true;
  367. // check the barriers
  368. if (Barriers != null)
  369. {
  370. CollisionMath.CircleLineCollisionResult result =
  371. new CollisionMath.CircleLineCollisionResult();
  372. foreach (Rectangle rectangle in Barriers)
  373. {
  374. if (CollisionMath.CircleRectangleCollide(spawnPoint, radius,
  375. rectangle, ref result))
  376. {
  377. valid = false;
  378. break;
  379. }
  380. }
  381. }
  382. // check the other objects
  383. if (valid)
  384. {
  385. foreach (GameplayObject gameplayObject in collisionManager)
  386. {
  387. if (!gameplayObject.Active || (gameplayObject == spawnedObject))
  388. {
  389. continue;
  390. }
  391. if (CollisionMath.CircleCircleIntersect(spawnPoint, radius,
  392. gameplayObject.Position, gameplayObject.Radius))
  393. {
  394. valid = false;
  395. break;
  396. }
  397. }
  398. }
  399. if (valid)
  400. {
  401. break;
  402. }
  403. spawnPoint = new Vector2(
  404. radius + Dimensions.X + RandomMath.Random.Next(
  405. (int)Math.Floor(Dimensions.Width - radius)),
  406. radius + Dimensions.Y + RandomMath.Random.Next(
  407. (int)Math.Floor(Dimensions.Height - radius)));
  408. }
  409. return spawnPoint;
  410. }
  411. /// <summary>
  412. /// Process an explosion in the world against the objects in it.
  413. /// </summary>
  414. /// <param name="source">The source of the explosion.</param>
  415. /// <param name="target">The target of the attack.</param>
  416. /// <param name="damageAmount">The amount of explosive damage.</param>
  417. /// <param name="position">The position of the explosion.</param>
  418. /// <param name="damageRadius">The radius of the explosion.</param>
  419. /// <param name="damageOwner">If true, it will hit the source.</param>
  420. public static void Explode(GameplayObject source, GameplayObject target,
  421. float damageAmount, Vector2 position, float damageRadius, bool damageOwner)
  422. {
  423. // safety-check the singleton
  424. if (collisionManager == null)
  425. {
  426. throw new InvalidOperationException(
  427. "The collision manager has not yet been initialized.");
  428. }
  429. if (damageRadius <= 0f)
  430. {
  431. return;
  432. }
  433. float damageRadiusSquared = damageRadius * damageRadius;
  434. foreach (GameplayObject gameplayObject in collisionManager)
  435. {
  436. // don't bother if it's already dead
  437. if (!gameplayObject.Active)
  438. {
  439. continue;
  440. }
  441. // don't hurt the GameplayObject that the projectile hit, it's hurt
  442. if (gameplayObject == target)
  443. {
  444. continue;
  445. }
  446. // don't hit the owner if the damageOwner flag is off
  447. if ((gameplayObject == source) && !damageOwner)
  448. {
  449. continue;
  450. }
  451. // measure the distance to the GameplayObject and see if it's in range
  452. Vector2 direction = gameplayObject.Position - position;
  453. float distanceSquared = direction.LengthSquared();
  454. if ((distanceSquared > 0f) && (distanceSquared <= damageRadiusSquared))
  455. {
  456. float distance = (float)Math.Sqrt((float)distanceSquared);
  457. // adjust the amount of damage based on the distance
  458. // -- note that damageRadius <= 0 is accounted for earlier
  459. float adjustedDamage = damageAmount *
  460. (damageRadius - distance) / damageRadius;
  461. // if we're still damaging the GameplayObject, then apply it
  462. if (adjustedDamage > 0f)
  463. {
  464. gameplayObject.Damage(source, adjustedDamage);
  465. }
  466. // move those affected by the blast
  467. if (gameplayObject != source)
  468. {
  469. direction.Normalize();
  470. gameplayObject.Velocity += direction * adjustedDamage *
  471. speedDamageRatio;
  472. }
  473. }
  474. }
  475. }
  476. #endregion
  477. }
  478. }