#region File Description
//-----------------------------------------------------------------------------
// CollisionManager.cs
//
// Microsoft XNA Community Game Platform
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
#endregion
#region Using Statements
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
#endregion
namespace NetRumble
{
///
/// Manages collisions and collision events between all gameplay objects.
///
public class CollisionManager : BatchRemovalCollection
{
#region Constants
///
/// The ratio of speed to damage applied, for explosions.
///
private const float speedDamageRatio = 0.5f;
///
/// The number of times that the FindSpawnPoint method will try to find a point.
///
private const int findSpawnPointAttempts = 25;
#endregion
#region Helper Types
///
/// The result of a collision query.
///
struct CollisionResult
{
///
/// How far away did the collision occur down the ray
///
public float Distance;
///
/// The collision "direction"
///
public Vector2 Normal;
///
/// What caused the collison (what the source ran into)
///
public GameplayObject GameplayObject;
public static int Compare(CollisionResult a, CollisionResult b)
{
return a.Distance.CompareTo(b.Distance);
}
}
#endregion
#region Singleton
///
/// Singleton for collision management.
///
private static CollisionManager collisionManager = new CollisionManager();
public static BatchRemovalCollection Collection
{
get { return collisionManager as BatchRemovalCollection; }
}
#endregion
#region Collision Data
///
/// The dimensions of the space in which collision occurs.
///
private Rectangle dimensions = new Rectangle(0, 0, 2048, 2048);
public static Rectangle Dimensions
{
get { return (collisionManager == null ? Rectangle.Empty :
collisionManager.dimensions); }
set
{
// safety-check the singleton
if (collisionManager == null)
{
throw new InvalidOperationException(
"The collision manager has not yet been initialized.");
}
collisionManager.dimensions = value;
}
}
///
/// The list of barriers in the game world.
///
/// This list is not owned by this object.
private List barriers = new List();
public static List Barriers
{
get { return (collisionManager == null ? null :
collisionManager.barriers); }
}
///
/// Cached list of collision results, for more optimal collision detection.
///
List collisionResults = new List();
#endregion
#region Initialization Methods
///
/// Constructs a new collision manager.
///
private CollisionManager() { }
#endregion
#region Updating Methods
///
/// Update the collision system.
///
/// The amount of elapsed time, in seconds.
public static void Update(float elapsedTime)
{
// safety-check the singleton
if (collisionManager == null)
{
throw new InvalidOperationException(
"The collision manager has not yet been initialized.");
}
// move each object
for (int i = 0; i < collisionManager.Count; ++i)
{
if (collisionManager[i].Active)
{
// determine how far they are going to move
Vector2 movement = collisionManager[i].Velocity * elapsedTime;
// only allow collisionManager that have not collided yet
// collisionManager frame to collide
// -- otherwise, objects can "double-hit" and trade their momentum
if (collisionManager[i].CollidedThisFrame == false)
{
movement = MoveAndCollide(collisionManager[i], movement);
}
// determine the new position
collisionManager[i].Position += movement;
// collide with the barriers
for (int b = 0; b < collisionManager.barriers.Count; ++b)
{
CollisionMath.CircleLineCollisionResult result =
new CollisionMath.CircleLineCollisionResult();
if (collisionManager[i] is Projectile)
{
CollisionMath.CircleRectangleCollide(
collisionManager[i].Position - movement,
collisionManager[i].Radius,
collisionManager.barriers[b], ref result);
if (result.Collision)
{
collisionManager[i].Position -= movement;
collisionManager[i].Die(null, false);
}
}
else
{
CollisionMath.CircleRectangleCollide(
collisionManager[i].Position,
collisionManager[i].Radius,
collisionManager.barriers[b], ref result);
if (result.Collision)
{
// if a non-projectile hits a barrier, bounce slightly
float vn = Vector2.Dot(collisionManager[i].Velocity,
result.Normal);
collisionManager[i].Velocity -= (2.0f * vn) *
result.Normal;
collisionManager[i].Position += result.Normal *
result.Distance;
}
}
}
}
}
CollisionManager.Collection.ApplyPendingRemovals();
}
///
/// Move the given gameplayObject by the given movement, colliding and adjusting
/// as necessary.
///
/// The gameplayObject who is moving.
/// The desired movement vector for this update.
/// The movement vector after considering all collisions.
private static Vector2 MoveAndCollide(GameplayObject gameplayObject,
Vector2 movement)
{
// safety-check the singleton
if (collisionManager == null)
{
throw new InvalidOperationException(
"The collision manager has not yet been initialized.");
}
if (gameplayObject == null)
{
throw new ArgumentNullException("gameplayObject");
}
// make sure we care about where this gameplayObject goes
if (!gameplayObject.Active)
{
return movement;
}
// make sure the movement is significant
if (movement.LengthSquared() <= 0f)
{
return movement;
}
// generate the list of collisions
Collide(gameplayObject, movement);
// determine if we had any collisions
if (collisionManager.collisionResults.Count > 0)
{
collisionManager.collisionResults.Sort(CollisionResult.Compare);
foreach (CollisionResult collision in collisionManager.collisionResults)
{
// let the two objects touch each other, and see what happens
if (gameplayObject.Touch(collision.GameplayObject) &&
collision.GameplayObject.Touch(gameplayObject))
{
gameplayObject.CollidedThisFrame =
collision.GameplayObject.CollidedThisFrame = true;
// they should react to the other, even if they just died
AdjustVelocities(gameplayObject, collision.GameplayObject);
return Vector2.Zero;
}
}
}
return movement;
}
///
/// Determine all collisions that will happen as the given gameplayObject moves.
///
/// The gameplayObject that is moving.
/// The gameplayObject's movement vector.
/// The results are stored in the cached list.
public static void Collide(GameplayObject gameplayObject, Vector2 movement)
{
// safety-check the singleton
if (collisionManager == null)
{
throw new InvalidOperationException(
"The collision manager has not yet been initialized.");
}
collisionManager.collisionResults.Clear();
if (gameplayObject == null)
{
throw new ArgumentNullException("gameplayObject");
}
if (!gameplayObject.Active)
{
return;
}
// determine the movement direction and scalar
float movementLength = movement.Length();
if (movementLength <= 0f)
{
return;
}
// check each gameplayObject
foreach (GameplayObject checkActor in collisionManager)
{
if ((gameplayObject == checkActor) || !checkActor.Active)
{
continue;
}
// calculate the target vector
float combinedRadius = checkActor.Radius + gameplayObject.Radius;
Vector2 checkVector = checkActor.Position - gameplayObject.Position;
float checkVectorLength = checkVector.Length();
if (checkVectorLength <= 0f)
{
continue;
}
float distanceBetween = MathHelper.Max(checkVectorLength -
(checkActor.Radius + gameplayObject.Radius), 0);
// check if they could possibly touch no matter the direction
if (movementLength < distanceBetween)
{
continue;
}
// determine how much of the movement is bringing the two together
float movementTowards = Vector2.Dot(movement, checkVector);
// check to see if the movement is away from each other
if (movementTowards < 0f)
{
continue;
}
if (movementTowards < distanceBetween)
{
continue;
}
CollisionResult result = new CollisionResult();
result.Distance = distanceBetween;
result.Normal = Vector2.Normalize(checkVector);
result.GameplayObject = checkActor;
collisionManager.collisionResults.Add(result);
}
}
///
/// Adjust the velocities of the two collisionManager as if they have collided,
/// distributing their velocities according to their masses.
///
/// The first gameplayObject.
/// The second gameplayObject.
private static void AdjustVelocities(GameplayObject actor1,
GameplayObject actor2)
{
// don't adjust velocities if at least one has negative mass
if ((actor1.Mass <= 0f) || (actor2.Mass <= 0f))
{
return;
}
// determine the vectors normal and tangent to the collision
Vector2 collisionNormal = actor2.Position - actor1.Position;
if (collisionNormal.LengthSquared() > 0f)
{
collisionNormal.Normalize();
}
else
{
return;
}
Vector2 collisionTangent = new Vector2(
-collisionNormal.Y, collisionNormal.X);
// determine the velocity components along the normal and tangent vectors
float velocityNormal1 = Vector2.Dot(actor1.Velocity, collisionNormal);
float velocityTangent1 = Vector2.Dot(actor1.Velocity, collisionTangent);
float velocityNormal2 = Vector2.Dot(actor2.Velocity, collisionNormal);
float velocityTangent2 = Vector2.Dot(actor2.Velocity, collisionTangent);
// determine the new velocities along the normal
float velocityNormal1New = ((velocityNormal1 * (actor1.Mass - actor2.Mass))
+ (2f * actor2.Mass * velocityNormal2)) / (actor1.Mass + actor2.Mass);
float velocityNormal2New = ((velocityNormal2 * (actor2.Mass - actor1.Mass))
+ (2f * actor1.Mass * velocityNormal1)) / (actor1.Mass + actor2.Mass);
// determine the new total velocities
actor1.Velocity = (velocityNormal1New * collisionNormal) +
(velocityTangent1 * collisionTangent);
actor2.Velocity = (velocityNormal2New * collisionNormal) +
(velocityTangent2 * collisionTangent);
}
#endregion
#region Interaction Methods
///
/// Find a valid spawn point in the world.
///
/// The radius of the object to be spawned.
/// A persistent Random object.
/// The spawn point.
public static Vector2 FindSpawnPoint(GameplayObject spawnedObject, float radius)
{
// safety-check the singleton
if (collisionManager == null)
{
throw new InvalidOperationException(
"The collision manager has not yet been initialized.");
}
// safety-check the parameters
if ((radius < 0f) || (radius > Dimensions.Width / 2))
{
throw new ArgumentOutOfRangeException("radius");
}
// keep trying to find a valid point
Vector2 spawnPoint = new Vector2(
radius + Dimensions.X +
RandomMath.Random.Next((int)Math.Floor(Dimensions.Width - radius)),
radius + Dimensions.Y +
RandomMath.Random.Next((int)Math.Floor(Dimensions.Height - radius)));
for (int i = 0; i < findSpawnPointAttempts; i++)
{
bool valid = true;
// check the barriers
if (Barriers != null)
{
CollisionMath.CircleLineCollisionResult result =
new CollisionMath.CircleLineCollisionResult();
foreach (Rectangle rectangle in Barriers)
{
if (CollisionMath.CircleRectangleCollide(spawnPoint, radius,
rectangle, ref result))
{
valid = false;
break;
}
}
}
// check the other objects
if (valid)
{
foreach (GameplayObject gameplayObject in collisionManager)
{
if (!gameplayObject.Active || (gameplayObject == spawnedObject))
{
continue;
}
if (CollisionMath.CircleCircleIntersect(spawnPoint, radius,
gameplayObject.Position, gameplayObject.Radius))
{
valid = false;
break;
}
}
}
if (valid)
{
break;
}
spawnPoint = new Vector2(
radius + Dimensions.X + RandomMath.Random.Next(
(int)Math.Floor(Dimensions.Width - radius)),
radius + Dimensions.Y + RandomMath.Random.Next(
(int)Math.Floor(Dimensions.Height - radius)));
}
return spawnPoint;
}
///
/// Process an explosion in the world against the objects in it.
///
/// The source of the explosion.
/// The target of the attack.
/// The amount of explosive damage.
/// The position of the explosion.
/// The radius of the explosion.
/// If true, it will hit the source.
public static void Explode(GameplayObject source, GameplayObject target,
float damageAmount, Vector2 position, float damageRadius, bool damageOwner)
{
// safety-check the singleton
if (collisionManager == null)
{
throw new InvalidOperationException(
"The collision manager has not yet been initialized.");
}
if (damageRadius <= 0f)
{
return;
}
float damageRadiusSquared = damageRadius * damageRadius;
foreach (GameplayObject gameplayObject in collisionManager)
{
// don't bother if it's already dead
if (!gameplayObject.Active)
{
continue;
}
// don't hurt the GameplayObject that the projectile hit, it's hurt
if (gameplayObject == target)
{
continue;
}
// don't hit the owner if the damageOwner flag is off
if ((gameplayObject == source) && !damageOwner)
{
continue;
}
// measure the distance to the GameplayObject and see if it's in range
Vector2 direction = gameplayObject.Position - position;
float distanceSquared = direction.LengthSquared();
if ((distanceSquared > 0f) && (distanceSquared <= damageRadiusSquared))
{
float distance = (float)Math.Sqrt((float)distanceSquared);
// adjust the amount of damage based on the distance
// -- note that damageRadius <= 0 is accounted for earlier
float adjustedDamage = damageAmount *
(damageRadius - distance) / damageRadius;
// if we're still damaging the GameplayObject, then apply it
if (adjustedDamage > 0f)
{
gameplayObject.Damage(source, adjustedDamage);
}
// move those affected by the blast
if (gameplayObject != source)
{
direction.Normalize();
gameplayObject.Velocity += direction * adjustedDamage *
speedDamageRatio;
}
}
}
}
#endregion
}
}