Issue finding side of collision for Circle-Rectangle collision

ghz 24days ago ⋅ 5 views

I have found a function that works quite well for detecting collisions between a circle and a rectangle, and also finding the point of contact. I then use that point to determine which side of the rectangle the circle hit so I can reflect the circle. However, when the center of the circle is inside the rectangle, the function gives the closest point as the center of circle, and it handles it as hitting a vertex rather than a side. Here is my code:

def collide_rect(box, ball_): 
    #convenience
    left = box.rect.left
    right = left + box.rect.width
    top = box.rect.top
    bottom = top + box.rect.height

    #find the closest point
    closest = (max(left, min(ball_.center[0], right)), max(top, min(ball_.center[1], bottom)))
    dx = ball_.center[0] - closest[0]
    dy = ball_.center[1] - closest[1]

    #handle the collsion
    if math.hypot(dx, dy) <= ball.radius:
        #Hit on the top or bottom
        if left <= closest[0] <= right and (closest[1] == top or closest[1] == bottom):
            ball_.vector = (ball_.vector[0], -1*ball_.vector[1])
        #Hit on the side
        elif top <= closest[1] <= bottom and (closest[0] == left or closest[0] == right):
            ball_.vector = (-1*ball_.vector[0], ball_.vector[1])
        #Hit a vertex
        else:
            ball_.vector = (-1*ball_.vector[0], -1*ball_.vector[1])

        return True
    else:
        return False 

Note that ball_.vector is the circle's direction vector and ball.radius is a class variable. Any help with a better way to find the side of collision would be greatly appreciated!

Answer

It looks like your current implementation is handling the reflection based on the closest point between the ball and the rectangle, but when the circle is inside the rectangle (not touching any edge), it's still treating it as a vertex collision. This is happening because you are calculating the "closest point" inside the rectangle and checking if it's the top, bottom, left, or right, which leads to incorrect results when the center is within the bounds of the rectangle.

Here's an alternative approach that addresses this issue:

Solution:

We can refine the collision detection by checking which side of the rectangle the ball is colliding with (top, bottom, left, or right), rather than simply relying on the closest point. You can break this down into steps:

  1. Determine the closest point on the rectangle to the circle's center: You already do this part using the closest point calculation.

  2. Check if the ball's center is inside the rectangle: If the ball's center is inside the rectangle, then we can return False early because the circle is not colliding with the edges.

  3. Handle edge collisions: Once we know the ball's center is outside the rectangle or touching one of the edges, we can determine which edge the ball is hitting (top, bottom, left, or right) by comparing the closest point with the ball's center.

  4. Reflect the ball's velocity accordingly.

Here's a modified version of your code that handles these cases properly:

import math

def collide_rect(box, ball_):
    # Convenience variables for the rectangle bounds
    left = box.rect.left
    right = left + box.rect.width
    top = box.rect.top
    bottom = top + box.rect.height

    # Find the closest point on the rectangle to the ball's center
    closest = (max(left, min(ball_.center[0], right)), max(top, min(ball_.center[1], bottom)))

    # Calculate the distance between the ball's center and the closest point
    dx = ball_.center[0] - closest[0]
    dy = ball_.center[1] - closest[1]

    # If the ball is not colliding, return False
    if math.hypot(dx, dy) > ball_.radius:
        return False

    # Ball is colliding with one of the rectangle's sides or vertices
    # Check if the ball is inside the rectangle (this means no collision with edges)
    if left < ball_.center[0] < right and top < ball_.center[1] < bottom:
        return False  # No collision because the ball is inside the rectangle

    # Reflect the ball based on which side it collided with
    if closest[0] == left or closest[0] == right:
        # Hit on the left or right side
        ball_.vector = (-ball_.vector[0], ball_.vector[1])
    elif closest[1] == top or closest[1] == bottom:
        # Hit on the top or bottom side
        ball_.vector = (ball_.vector[0], -ball_.vector[1])
    else:
        # Hit a corner (vertex)
        ball_.vector = (-ball_.vector[0], -ball_.vector[1])

    return True

Explanation of Changes:

  1. Inside the Rectangle Check: Before checking the closest point and handling collisions, we first check if the ball is inside the rectangle. If it's inside (i.e., the ball's center is within the rectangle's bounds), we return False since there's no collision.

    if left < ball_.center[0] < right and top < ball_.center[1] < bottom:
        return False
    

    This ensures that when the ball is fully inside the rectangle (not touching any edge), we don't mistakenly treat it as a collision.

  2. Collision Handling: After confirming that the ball's center is outside the rectangle or at a vertex, we check if the collision occurred on the left/right sides or the top/bottom sides. If the ball's center is close to either of these sides, we reflect the velocity accordingly.

  3. Corner Collisions: If the closest point lies on the corner (which happens when the dx and dy are both non-zero), we assume a corner hit and reverse both components of the ball's velocity vector.

Why This Works:

  • Handling Inside the Rectangle: The ball is considered to be inside the rectangle if its center is within the rectangle's bounds. We handle this case by returning False, ensuring that no reflection occurs when the ball is inside (but not touching edges).

  • Accurate Edge Collision: By focusing on the closest point and checking against the rectangle's edges, we accurately determine which side of the rectangle the ball collides with. This avoids treating the ball as hitting a vertex when it is inside.

Performance:

This solution reduces unnecessary checks and improves clarity. It avoids treating the ball as hitting a vertex when it’s inside the rectangle, and it handles all the edge cases cleanly.

Additional Improvements:

  • If you need further optimizations or if the rectangle size is very large, you could cache the bounds of the rectangle (left, right, top, bottom) outside of the function to reduce redundant calculations.
  • You could also extend this to handle more complex shapes, but this approach should work well for a simple rectangle.

Let me know if you'd like any further adjustments!