I am working on making my own simple collision resolution class so I can learn a bit about how it works, while also improving my entity-component model framework. For those of you unfamiliar with a entity-component model, read this, it's a super fascinating way to build games very quickly and focus on code reuseability. I have a algorithm which I got from this tutorial that worked pretty well in my stand-alone tests but, when I implemented them into my framework, it can do some wonky things.
The resolution will work 9/10 but, there are cases where it does bugs, like when the objects "swap" places. You can see the bug in my video here -> http://www.youtube.com/watch?v=7tGDylY52Wc
Here is the `CollisionHandler class which deals with the pairing and resolution. This is a service, which is a non-entity way of managing entities, components, or anything you need in a state.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using EntityEngineV4.Collision.Shapes;
using EntityEngineV4.Engine;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace EntityEngineV4.Collision
{
public class CollisionHandler : Service
{
private List<Collision> _collideables;
private HashSet<Pair> _pairs;
private HashSet<Manifold> _manifolds;
public CollisionHandler(EntityState stateref) : base(stateref)
{
_collideables = new List<Collision>();
_pairs = new HashSet<Pair>();
_manifolds = new HashSet<Manifold>();
}
public override void Update(GameTime gt)
{
BroadPhase();
foreach (var manifold in _manifolds)
{
manifold.A.OnCollision(manifold.B);
manifold.B.OnCollision(manifold.A);
//Attempt to resolve collisions
if (CanObjectsResolve(manifold.A, manifold.B) || CanObjectsResolve(manifold.B, manifold.A))
{
ResolveCollision(manifold);
PositionalCorrection(manifold);
}
}
}
public override void Draw(SpriteBatch sb)
{
_manifolds.Clear();
}
public void AddCollision(Collision c)
{
//Check if the Collision is already in the list.
if (Enumerable.Contains(_collideables, c)) return;
_collideables.Add(c);
//Generate our pairs
GeneratePairs();
}
public void GeneratePairs()
{
if (_collideables.Count() <= 1) return;
_pairs.Clear();
foreach (var a in _collideables)
{
foreach (var b in _collideables)
{
if (a.Equals(b)) continue;
if (CanObjectsPair(a, b))
{
var p = new Pair(a,b);
_pairs.Add(p);
}
}
}
}
public void BroadPhase()
{
//Do a basic SAT test
foreach (var pair in _pairs)
{
Vector2 normal = pair.A.Position - pair.B.Position;
//Calculate half widths
float aExtent = pair.A.BoundingRect.Width / 2f;
float bExtent = pair.B.BoundingRect.Width / 2f;
//Calculate the overlap.
float xExtent = aExtent + bExtent - Math.Abs(normal.X);
//If the overlap is greater than 0
if (xExtent > 0)
{
//Calculate half widths
aExtent = pair.A.BoundingRect.Height / 2f;
bExtent = pair.B.BoundingRect.Height / 2f;
//Calculate overlap
float yExtent = aExtent + bExtent - Math.Abs(normal.Y);
if (yExtent > 0)
{
//Do our real test now.
Manifold m = CheckCollision(pair.A.Shape, pair.B.Shape);
if (m.AreColliding)
_manifolds.Add(m);
}
}
}
}
//Static methods
/// <summary>
/// Compares the masks and checks to see if they should be allowed to form a pair.
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns>Whether or not the the two objects should be paired</returns>
public static bool CanObjectsPair(Collision a, Collision b)
{
return a.GroupMask.HasMatchingBit(b.GroupMask) || //Compare the group masks.
a.GroupMask.HasMatchingBit(b.PairMask) || //Compare the pair masks to the group masks.
a.PairMask.HasMatchingBit(b.GroupMask);
}
public static bool CanObjectsResolve(Collision resolver, Collision other)
{
return resolver.ResolutionGroupMask.HasMatchingBit(other.ResolutionGroupMask) || //Compare the group masks.
resolver.ResolutionPairMask.HasMatchingBit(other.ResolutionGroupMask);
}
public static void ResolveCollision(Manifold m)
{
Vector2 relVelocity = m.B.Velocity - m.A.Velocity;
//Finds out if the objects are moving towards each other.
//We only need to resolve collisions that are moving towards, not away.
float velAlongNormal = PhysicsMath.DotProduct(relVelocity, m.Normal);
if (velAlongNormal > 0)
return;
float e = Math.Min(m.A.Restitution, m.B.Restitution);
float j = -(1 + e) * velAlongNormal;
j /= m.A.InvertedMass + m.B.InvertedMass;
Vector2 impulse = j * m.Normal;
if (CanObjectsResolve(m.A, m.B))
m.A.Velocity -= m.A.InvertedMass * impulse;
if(CanObjectsResolve(m.B, m.A))
m.B.Velocity += m.B.InvertedMass * impulse;
}
public static void PositionalCorrection(Manifold m)
{
const float percent = 0.2f;
const float slop = 0.01f;
Vector2 correction = Math.Max(m.PenetrationDepth - slop, 0.0f) / (m.A.InvertedMass + m.B.InvertedMass) * percent * m.Normal;
m.A.Position -= m.A.InvertedMass * correction;
m.B.Position += m.B.InvertedMass * correction;
}
/// <summary>
/// Compares bounding boxes using Seperating Axis Thereom.
/// </summary>
public static Manifold AABBvsAABB(AABB a, AABB b)
{
//Start packing the manifold
Manifold m = new Manifold(a.Collision, b.Collision);
m.Normal = a.Position - b.Position;
//Calculate half widths
float aExtent = a.Width / 2f;
float bExtent = b.Width / 2f;
//Calculate the overlap.
float xExtent = aExtent + bExtent - Math.Abs(m.Normal.X);
//If the overlap is greater than 0
if (xExtent > 0)
{
//Calculate half widths
aExtent = a.Height / 2f;
bExtent = b.Height / 2f;
//Calculate overlap
float yExtent = aExtent + bExtent - Math.Abs(m.Normal.Y);
if (yExtent > 0)
{
//Find which axis has the biggest penetration ;D
Vector2 fixnormal;
if (xExtent > yExtent){
if(m.Normal.X < 0)
fixnormal = -Vector2.UnitX;
else
fixnormal = Vector2.UnitX;
m.Normal = PhysicsMath.GetNormal(a.Position, b.Position) * fixnormal.X;
m.PenetrationDepth = xExtent;
}
else {
if(m.Normal.Y < 0)
fixnormal = -Vector2.UnitY;
else
fixnormal= Vector2.UnitY;
m.Normal = PhysicsMath.GetNormal(a.Position, b.Position) * fixnormal.Y;
m.PenetrationDepth = yExtent;
}
m.AreColliding = true;
return m;
}
}
m.AreColliding = false;
return m;
}
//Collision resolver methods
public static Manifold CheckCollision(Shape a, Shape b)
{
return collide((dynamic) a, (dynamic) b);
}
private static Manifold collide(AABB a, AABB b)
{
return AABBvsAABB(a, b);
}
}
}
Next is the Collision component. This holds some bitmasks which tell it what groups it is a part of(GroupMask), what groups it is allowed to pair with with out being a part of that group (PairMask), and who it should resolve collisions with, (ResolutionGroupMask) and finally, who it should resolve collisions with one-way (ResolutionPairMask). It also holds values related to detection and resolution, such as Shape, mass, and restitution.
Just as a quick side note, under the comment "Dependencies" there is a field for a Body and a Physics, both of these objects are components which are supplied to track the location of the collideable.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using EntityEngineV4.Collision.Shapes;
using EntityEngineV4.Components;
using EntityEngineV4.Engine;
using Microsoft.Xna.Framework;
namespace EntityEngineV4.Collision
{
public class Collision : Component
{
//Delegates and events
public delegate void EventHandler(Collision c);
public event EventHandler CollideEvent;
/// <summary>
/// The group mask is the bit mask used to determine which groups the component is a part of.
/// The CollisionHandler will pair all components with the same mask.
/// </summary>
/// <value>
/// The group mask.
/// </value>
public Bitmask GroupMask { get; protected set; }
/// <summary>
/// The pair mask is the bit mask used to determine which groups the component will pair with.
/// The CollisionHandler will only pair components whose group mask matches the pair mask.
/// </summary>
/// <value>
/// The pair mask.
/// </value>
public Bitmask PairMask { get; protected set; }
/// <summary>
/// The resolution mask is the bit mask which will determine which groups will physically collide with each other
/// </summary>
public Bitmask ResolutionGroupMask { get; protected set; }
/// <summary>
/// The resolution mask is the bit mask which will determine which pairs will physically collide with each other
/// </summary>
public Bitmask ResolutionPairMask { get; protected set; }
//Collision Related Values
//// <summary>
/// Backing field for Mass.
/// </summary>
private float _mass = 1f;
/// <summary>
/// The mass of the object.
/// </summary>
/// <value>
/// The mass.
/// </value>
public float Mass
{
get { return _mass; }
set
{
if (value < 0) throw new Exception("Mass cannot be less than zero!");
_mass = value;
if (Math.Abs(value - 0) < .00001f)
InvertedMass = 0;
else
InvertedMass = 1 / _mass;
}
}
/// <summary>
/// Gets one divided by mass (1/mass).
/// </summary>
/// <value>
/// The inverted mass.
/// </value>
public float InvertedMass { get; private set; }
/// <summary>
/// Bounciness of this object
/// </summary>
public float Restitution = 0f;
public Shape Shape;
//Dependencies
private CollisionHandler _collisionHandler;
private Body _collisionBody;
private Physics _collisionPhysics;
//Properties
public Rectangle BoundingRect
{
get { return _collisionBody.BoundingRect; }
set { _collisionBody.BoundingRect = value; }
}
public Vector2 Position
{
get { return _collisionBody.Position; }
set { _collisionBody.Position = value; }
}
public Vector2 Bounds
{
get { return _collisionBody.Bounds; }
set { _collisionBody.Bounds = value; }
}
public Vector2 Velocity
{
get { return _collisionPhysics.Velocity; }
set { _collisionPhysics.Velocity = value; }
}
public Collision(Entity parent, string name, Shape shape, Body collisionBody) : base(parent, name)
{
_collisionBody = collisionBody;
_collisionHandler = parent.StateRef.GetService<CollisionHandler>();
_collisionPhysics = new Physics(Parent, name + ".Physics", _collisionBody);
Shape = shape;
Shape.Collision = this;
GroupMask = new Bitmask();
PairMask = new Bitmask();
ResolutionGroupMask = new Bitmask();
ResolutionPairMask = new Bitmask();
}
public Collision(Entity parent, string name, Shape shape, Body collisionBody, Physics collisionPhysics)
: base(parent, name)
{
_collisionBody = collisionBody;
_collisionHandler = parent.StateRef.GetService<CollisionHandler>();
_collisionPhysics = collisionPhysics;
Shape = shape;
Shape.Collision = this;
GroupMask = new Bitmask();
PairMask = new Bitmask();
ResolutionGroupMask = new Bitmask();
ResolutionPairMask = new Bitmask();
}
public void OnCollision(Collision c)
{
if (CollideEvent != null)
CollideEvent(c);
}
public void AddToHandler()
{
_collisionHandler.AddCollision(this);
}
}
}
Lastly, I felt this was pretty self-explanatory but, I feel I should include it in case of an error. This is the PhysicsMath class which just holds some simple methods for calculating dot products, normals, and distance.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
namespace EntityEngineV4.Engine
{
public static class PhysicsMath
{
public static float DotProduct(Vector2 a, Vector2 b)
{
return a.X * b.X + a.Y * b.Y;
}
public static float Distance(Vector2 a, Vector2 b)
{
return (float)Math.Sqrt(((int)(a.X - b.X) ^ 2 + (int)(a.Y - b.Y) ^ 2));
}
public static Vector2 GetNormal(Vector2 a, Vector2 b)
{
Vector2 ret = b - a;
ret.Normalize();
return ret;
}
}
}
Any help is greatly appreciated.