Search code examples
javacollision-detectionphysicscollisiongame-physics

How to fix circle and rectangle overlap in collision response?


Since in the digital world a real collision almost never happens, we will always have a situation where the "colliding" circle overlaps the rectangle.

How to put back the circle in the situation where it collides perfectly with the rectangle without overlap?

Suppose that the rectangle is stopped (null velocity) and axis-aligned.

I would solve this problem with a posteriori approach (in two dimensions).

In short I have to solve this equation for t:

enter image description here

Where:

  • t is a number that answers to the question: how many frames ago did the collision happen perfectly?

  • r is the radius of the circle.

  • (x, y) is the center of the circle

  • (v.x, v.y) is its velocity.

  • A(t) and B(t) are functions that return the x and y coordinates of the point where the circle and the rectangle collide (when the circle is at (x - t * v.x, y - t * v.y) position, that is in the position in which perfectly collide with the rectangle).

Recently I solved a similar problem for collisions between circles, but now I don't know the law of the functions A and B.


Solution

  • After years of staring at this problem, and never coming up with a perfect solution, I've finally done it!

    It's pretty much a straight forward algorithm, no need for looping and approximations.

    This is how it works at a higher level:

    1. Calculate intersection times with each side's plane IF the path from current point to future point crosses that plane.
    2. Check each side's quadrant for single-side intersection, return the intersection.
    3. Determine the corner that the circle is colliding with.
    4. Solve the triangle between the current point, the corner, and the intersecting center (radius away from the corner).
    5. Calculate time, normal, and intersection center.

    And now to the gory details!

    The input to the function is bounds (which has a left, top, right, bottom) and a current point (start) and a future point (end).

    The output is a class called Intersection which has x, y, time, nx, and ny.

    • {x, y} is the center of the circle at intersection time.
    • time is a value from 0 to 1 where 0 is at start and 1 is at end
    • {nx, ny} is the normal, used for reflecting the velocity to determine the new velocity of the circle

    We start off with caching variables we use often:

    float L = bounds.left;
    float T = bounds.top;
    float R = bounds.right;
    float B = bounds.bottom;
    float dx = end.x - start.x;
    float dy = end.y - start.y;
    

    And calculating intersection times with each side's plane (if the vector between start and end pass over that plane):

    float ltime = Float.MAX_VALUE;
    float rtime = Float.MAX_VALUE;
    float ttime = Float.MAX_VALUE;
    float btime = Float.MAX_VALUE;
    
    if (start.x - radius < L && end.x + radius > L) {
       ltime = ((L - radius) - start.x) / dx;
    }
    if (start.x + radius > R && end.x - radius < R) {
       rtime = (start.x - (R + radius)) / -dx;
    }
    if (start.y - radius < T && end.y + radius > T) {
       ttime = ((T - radius) - start.y) / dy;
    }
    if (start.y + radius > B && end.y - radius < B) {
       btime = (start.y - (B + radius)) / -dy;
    }
    

    Now we try to see if it's strictly a side intersection (and not corner). If the point of collision lies on the side then return the intersection:

    if (ltime >= 0.0f && ltime <= 1.0f) {
       float ly = dy * ltime + start.y;
       if (ly >= T && ly <= B) {
          return new Intersection( dx * ltime + start.x, ly, ltime, -1, 0 );
       }
    }
    else if (rtime >= 0.0f && rtime <= 1.0f) {
       float ry = dy * rtime + start.y;
       if (ry >= T && ry <= B) {
          return new Intersection( dx * rtime + start.x, ry, rtime, 1, 0 );
       }
    }
    
    if (ttime >= 0.0f && ttime <= 1.0f) {
       float tx = dx * ttime + start.x;
       if (tx >= L && tx <= R) {
          return new Intersection( tx, dy * ttime + start.y, ttime, 0, -1 );
       }
    }
    else if (btime >= 0.0f && btime <= 1.0f) {
       float bx = dx * btime + start.x;
       if (bx >= L && bx <= R) {
          return new Intersection( bx, dy * btime + start.y, btime, 0, 1 );
       }
    }
    

    We've gotten this far so we know either there's no intersection, or it's collided with a corner. We need to determine the corner:

    float cornerX = Float.MAX_VALUE;
    float cornerY = Float.MAX_VALUE;
    
    if (ltime != Float.MAX_VALUE) {
       cornerX = L;
    } else if (rtime != Float.MAX_VALUE) {
       cornerX = R;
    }
    
    if (ttime != Float.MAX_VALUE) {
       cornerY = T;
    } else if (btime != Float.MAX_VALUE) {
       cornerY = B;
    }
    
    // Account for the times where we don't pass over a side but we do hit it's corner
    if (cornerX != Float.MAX_VALUE && cornerY == Float.MAX_VALUE) {
       cornerY = (dy > 0.0f ? B : T);
    }
    
    if (cornerY != Float.MAX_VALUE && cornerX == Float.MAX_VALUE) {
       cornerX = (dx > 0.0f ? R : L);
    }
    

    Now we have enough information to solve for the triangle. This uses the distance formula, finding the angle between two vectors, and the law of sines (twice):

    double inverseRadius = 1.0 / radius;
    double lineLength = Math.sqrt( dx * dx + dy * dy );
    double cornerdx = cornerX - start.x;
    double cornerdy = cornerY - start.y;
    double cornerdist = Math.sqrt( cornerdx * cornerdx + cornerdy * cornerdy );
    double innerAngle = Math.acos( (cornerdx * dx + cornerdy * dy) / (lineLength * cornerdist) );
    double innerAngleSin = Math.sin( innerAngle );
    double angle1Sin = innerAngleSin * cornerdist * inverseRadius;
    
    // The angle is too large, there cannot be an intersection
    if (Math.abs( angle1Sin ) > 1.0f) {
       return null;
    }
    
    double angle1 = Math.PI - Math.asin( angle1Sin );
    double angle2 = Math.PI - innerAngle - angle1;
    double intersectionDistance = radius * Math.sin( angle2 ) / innerAngleSin;
    

    Now that we solved for all sides and angles, we can determine time and everything else:

    // Solve for time
    float time = (float)(intersectionDistance / lineLength);
    
    // If time is outside the boundaries, return null. This algorithm can 
    // return a negative time which indicates the previous intersection. 
    if (time > 1.0f || time < 0.0f) {
       return null;
    }
    
    // Solve the intersection and normal
    float ix = time * dx + start.x;
    float iy = time * dy + start.y;
    float nx = (float)((ix - cornerX) * inverseRadius);
    float ny = (float)((iy - cornerY) * inverseRadius);
    
    return new Intersection( ix, iy, time, nx, ny );
    

    Woo! That was fun... this has plenty of room for improvements as far as efficiency goes. You could reorder the side intersection checking to escape as early as possible while making as few calculations as possible.

    I was hoping there would be a way to do it without trigonometric functions, but I had to give in!

    Here's an example of me calling it and using it to calculate the new position of the circle using the normal to reflect and the intersection time to calculate the magnitude of reflection:

    Intersection inter = handleIntersection( bounds, start, end, radius );
    
    if (inter != null) 
    {
       // Project Future Position
       float remainingTime = 1.0f - inter.time;
       float dx = end.x - start.x;
       float dy = end.y - start.y;
       float dot = dx * inter.nx + dy * inter.ny;
       float ndx = dx - 2 * dot * inter.nx;
       float ndy = dy - 2 * dot * inter.ny;
       float newx = inter.x + ndx * remainingTime;
       float newy = inter.y + ndy * remainingTime;
       // new circle position = {newx, newy}
     }
    

    And I've posted the full code on pastebin with a completely interactive example where you can plot the starting and ending points and it shows you the time and resulting bounce off of the rectangle.

    Example

    If you want to get it running right away you'll have to download code from my blog, otherwise stick it in your own Java2D application.

    EDIT: I've modified the code in pastebin to also include the collision point, and also made some speed improvements.

    EDIT: You can modify this for a rotating rectangle by using that rectangle's angle to un-rotate the rectangle with the circle start and end points. You'll perform the intersection check and then rotate the resulting points and normals.

    EDIT: I modified the code on pastebin to exit early if the bounding volume of the path of the circle does not intersect with the rectangle.