CollisionManager.cs 20 KB

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