2
votes

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!

1

1 Answers

3
votes

You can find the side of the circle, by finding the on the rectangle, which is on the straight line, which is given by the center of the circle and the center of the rectangle.

The point on the rectangle and the circle can be computed by the minimum relation of the offset between the center points and the size of the rectangle.
In the following algorithm, the rectangle is defined by the center point (r_cpt) and the size (r_size) and the circle is defined by the center point (c_cpt) and the radius (c_rad):

def intersectRectangleCircle(r_cpt, r_size, c_cpt, c_rad):

    v2_c_cpt = pygame.math.Vector2(c_cpt)
    v2_r_cpt = pygame.math.Vector2(r_cpt)

    offset = v2_c_cpt - v2_r_cpt
    if offset.x == 0 and offset.y == 0:
        return [v2_c_cpt, v2_r_cpt]

    if offset.x == 0:   
        ratio = r_size[1] / abs(offset.y)
    elif offset.y == 0: 
        ratio = r_size[0] / abs(offset.x)
    else:
        ratio  = min(r_size[0] / abs(offset.x), r_size[1] / abs(offset.y))
    ratio *= 0.5

    p1 = v2_r_cpt + (offset * ratio)
    offset.scale_to_length(c_rad)
    p2 = v2_c_cpt - offset

    return [p1, p2]

The direction to the circle is the given by the vector from the center point of the rectangle to the point on the rectangle contour:

isect_pts = intersectRectangleCircle(rect_center, rect_size, circle_center, circle_diameter/2)
dx, dy = isect_pts[0].x - rect_center[0], isect_pts[1].y - rect_center[1] 

See the example, (dx, dy) is represented by the magenta colored line:

repl.it/@Rabbid76/PyGame-NearestPointOnRectangle

import pygame
import math

pygame.init()
screen = pygame.display.set_mode((500, 500))

def intersectRectangleCircle(r_cpt, r_size, c_cpt, c_rad):

    v2_c_cpt = pygame.math.Vector2(c_cpt)
    v2_r_cpt = pygame.math.Vector2(r_cpt)

    offset = v2_c_cpt - v2_r_cpt
    if offset.x == 0 and offset.y == 0:
        return [v2_c_cpt, v2_r_cpt]

    if offset.x == 0:   
        ratio = r_size[1] / abs(offset.y)
    elif offset.y == 0: 
        ratio = r_size[0] / abs(offset.x)
    else:
        ratio  = min(r_size[0] / abs(offset.x), r_size[1] / abs(offset.y))
    ratio *= 0.5

    p1 = v2_r_cpt + (offset * ratio)
    offset.scale_to_length(c_rad)
    p2 = v2_c_cpt - offset

    return [p1, p2]


def inBetween(p1, p2, px):

    v = pygame.math.Vector2(p2) - pygame.math.Vector2(p1)
    d = v.length()
    if d == 0:
        return False
    v.normalize_ip()

    vx = pygame.math.Vector2(px) - pygame.math.Vector2(p1)
    dx = v.dot(vx)
    
    return dx >= 0 and dx <= d

done = False
while not done:

    for event in pygame.event.get(): 
        if event.type == pygame.QUIT:
            done = True


    rect_center     = screen.get_rect().center
    rect_size       = screen.get_width() // 5, screen.get_height() // 10
    rect            = pygame.Rect(rect_center[0] - rect_size[0] // 2, rect_center[1] - rect_size[1] // 2, *rect_size)
    circle_center   = pygame.mouse.get_pos()
    circle_diameter = min(*screen.get_size()) // 5

    isect_pts = intersectRectangleCircle(rect_center, rect_size, circle_center, circle_diameter/2)
    dx, dy = isect_pts[0].x - rect_center[0], isect_pts[1].y - rect_center[1] 

    screen.fill((255,255,255))
    pygame.draw.rect(screen, (0, 0, 0), rect, 3)
    pygame.draw.circle(screen, (0, 0, 0), circle_center, circle_diameter // 2, 3)

    pygame.draw.line(screen, (0, 0, 255), rect_center, circle_center, 1)
    pygame.draw.line(screen, (255, 0, 255), rect_center, (round(isect_pts[0].x), round(isect_pts[0].y)), 3)
    for i in range(2):
        px, py = round(isect_pts[i].x), round(isect_pts[i].y)
        col = (255, 0, 0) if inBetween(rect_center, circle_center, (px, py)) else (0, 255, 0)
        pygame.draw.line(screen, col, (px-5, py), (px+5, py), 3)
        pygame.draw.line(screen, col, (px, py-5), (px, py+5), 3)

    pygame.display.flip()

pygame.quit()
quit()