Search code examples
c#xnacollision-detectiongame-physicsmonogame

XNA/Monogame Detecting collision between Circle and Rectangle not working


So I have a Circle struct, really simple, looks like this:

public struct Circle
{
    public Circle(int x, int y, int radius) : this()
    {
        Center = new Point(x, y);
        Radius = radius;
    }

    public Point Center { get; private set; }
    public int Radius { get; private set; }

}

I have a PhysicsEnity class that looks something like this:

public class PhysicsEntity
{

    public int Width { get; protected set; }
    public int Height { get; protected set; }
    public Vector2 Position { get;  set; }
    public Vector2 Velocity { get; set; }
    public float Restitution { get; protected set; }
    public float Mass { get; protected set; }

    public virtual void Update(GameTime gameTime)
    {
        float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;
        Velocity += ((Phys.Gravity * dt) * Mass);
        Position += Velocity * dt;
    }

    public virtual void Draw(SpriteBatch spriteBatch) { }

    public virtual void ApplyImpulse(Vector2 impulse)
    {
        Position += impulse;
    }

}

I have two classes that inherit from this class. CircleEntity gets a circle, and RectangleEntity gets a rectangle, but nothing else changes.

To check for collision, I have a helper function that takes in two PhysicsEntity objects, checks what type they are (either RectangleEntity or CircleEntity), and calls the function for that specific collision type. The collision detection functions simply return a boolean if they are colliding.

I then have a function called ResolveCollision, which takes two entities and looks like this:

public static void ResolveCollision(PhysicsEntity a, PhysicsEntity b)
{
    if (a.Mass + b.Mass == 0)
    {
        a.Velocity = Vector2.Zero;
        b.Velocity = Vector2.Zero;
        return;
    }
    var invMassA = a.Mass > 0 ? 1 / a.Mass : 0;
    var invMassB = b.Mass > 0 ? 1 / b.Mass : 0;
    Vector2 rv = b.Velocity - a.Velocity;
    Vector2 normal = Vector2.Normalize(b.Position - a.Position);
    float velAlongNormal = Vector2.Dot(rv, normal);
    if (velAlongNormal > 0) return;
    float e = MathHelper.Min(a.Restitution, b.Restitution);
    float j = (-(1 + e) * velAlongNormal) / (invMassA + invMassB);
    Vector2 impulse = j * normal;
    a.Velocity -= invMassA * impulse;
    b.Velocity += invMassB * impulse;
}

Circle-Circle collisions and Rectangle-Rectangle collision work just fine, but Circle-Rectangle absolutely does not. I can't even get it to detect a collision properly, it always returns false. Here's the Rectangle-Circle collision detection:

public static bool RectangleCircleCollision(CircleEntity a, RectangleEntity b)
{
    Circle c = a.Circle;
    Rectangle r = b.Rectangle;
    Vector2 v = new Vector2(MathHelper.Clamp(c.Center.X, r.Left, r.Right),
                            MathHelper.Clamp(c.Center.Y, r.Top, r.Bottom));
    Vector2 direction = c.Center.ToVector2() - v;
    float distSquare = direction.LengthSquared();
    return ((distSquare > 0) && (distSquare < c.Radius * c.Radius));
}

I am just at a complete loss at this point. I don't know what is wrong. I've studied pretty much every tutorial there is under the sun for collision detection, and I just don't know.

What am I doing wrong here?

EDIT: I was mistaken, Rectangle-Rectangle ALSO does not work. If someone can just point me in the direction of an idiot's guide to 2D collision detection, I would be so thankful.


Solution

  • Circle and rectangle overlap.

    The following image shows all the situations we can find a circle and a rectangle in.

    enter image description here

    Where the dark blue is the rectangle to be tested against. Defined by its center and its width and height

    Circles

    • A If the circle center less than half the width and height from rectangle center ( on the dark blue ) we know it is touching.
    • B The circle center has its x, or y position less than half the width or height from the rectangle center and the other distance less than half the width or height plus the circle radius. The circle center is outside the rectangle but still touching
    • C Circle is near a corner its center is above and to the left but the distance from the corner is less than the radius so it is touching.
    • D Circle is within radius distance of the top and right edges of the rectangle but further than radius distance from the corner. It is not touching.
    • E is not touching because the circle center is greater than radius from any edge.

    We can simplify the solution by considering the symmetry. If we make the x and y distance of the circle from the center positive then we are just doing one corner

    private static bool DoRectangleCircleOverlap(Circle cir, Rectangle rect) {
    
        // Get the rectangle half width and height
        float rW = (rect.Width) / 2;
        float rH = (rect.Height) / 2;
    
        // Get the positive distance. This exploits the symmetry so that we now are
        // just solving for one corner of the rectangle (memory tell me it fabs for 
        // floats but I could be wrong and its abs)
        float distX = Math.Abs(cir.Center.X - (rect.Left + rW));
        float distY = Math.Abs(cir.Center.Y - (rect.Top + rH));
    
        if (distX >= cir.Radius + rW || distY >= cir.Radius + rH) {
            // Outside see diagram circle E
            return false;
        }
        if (distX < rW || distY < rH) {
            // Inside see diagram circles A and B
            return true; // touching
        }
    
        // Now only circles C and D left to test
        // get the distance to the corner
        distX -= rW;
        distY -= rH;
    
        // Find distance to corner and compare to circle radius 
        // (squared and the sqrt root is not needed
        if (distX * distX + distY * distY < cir.Radius * cir.Radius) {
            // Touching see diagram circle C
            return true;
        }
        return false;
    }