Search code examples
pythonmathpygamecollision-detectionellipse

Collision detection between an ellipse and a circle


I want to make a collision detection between an ellipse and circle. The way I am doing this is:

  1. Calculate angle from centre of the ellipse to the circle
  2. Calculate where the point lies in the ellipse at that angle
  3. Check for collision against that point

However, I am having one small problem. When I calculate the angle, it seems like it is 90 degrees off. I did a dirty fix by simply adding 1.5 radians to account for the 90 degrees, and it kind of works but there are inconsistencies and doesn't work properly at certain angles, particularly at around 0.7 and -2.6 radians . Here is the code (all the collision stuff is in collision method in Ellipse class)

import pygame
from math import sin, cos, atan2, radians
pygame.init()

SW = 1200
SH = 600
WIN = pygame.display
D = WIN.set_mode((SW, SH))

class Circle:
    def __init__(self, radius):
        self.x = 0
        self.y = 0
        self.radius = radius

    def update(self, pos):
        self.x = pos[0]
        self.y = pos[1]

    def draw(self, display):
        pygame.draw.circle(display, (255, 0, 0), (int(self.x), int(self.y)), self.radius, 2)

circle = Circle(30)

class Ellipse:
    def __init__(self, centre, rx, ry):
        self.centre = centre
        self.collided = False
        self.rx = rx
        self.ry = ry
        
    def draw(self, display):
        angle = 0
        while angle < 6.28:
            angle += 0.001
            x = self.centre[0] + sin(angle)* self.rx
            y = self.centre[1] + cos(angle)* self.ry
            if self.collided:
                display.set_at((int(x), int(y)), (255, 0, 0))
            else:
                display.set_at((int(x), int(y)), (0, 0, 255))
        pygame.draw.circle(D, (0, 255, 0), (int(self.centre[0]), int(self.centre[1])), 5)

    def collision(self, circle):
        #angle to the circle
        dx = circle.x - self.centre[0]
        dy = circle.y - self.centre[1]
        angle = atan2(-dy, dx)
        print(angle)

        #where the point lies in the ellipse at that angle
        x = sin(angle + 1.5)* self.rx + self.centre[0] 
        y = cos(angle + 1.5)* self.ry + self.centre[1]
        #print(x, y)

        #drawing the point just to make sure its working
        # (debugging)
        pygame.draw.circle(D, (0, 255, 0), (int(x), int(y)), 5)

        # distance between the point we just
        # calculated and the circle's centre
        distance = ((x-circle.x)**2 + (y-circle.y)**2)**0.5
        #print(distance)

        #collision condition
        if distance < circle.radius:
            self.collided = True
        else:
            self.collided = False
        
ellipse = Ellipse([600, 300], 300, 200)

while True:
    events = pygame.event.get()
    mousePos = pygame.mouse.get_pos()
    for event in events:
        if event.type == pygame.QUIT:
            pygame.quit()
    D.fill((255, 255, 255))

    circle.update(mousePos)
    circle.draw(D)
    ellipse.draw(D)
    ellipse.collision(circle)
    
    pygame.display.flip()

Solution

  • The fist mistake is, that 1.5 is not equal to pi/2:

    from math import pi
    

    x = sin(angle + 1.5)* self.rx + self.centre[0]
    y = cos(angle + 1.5)* self.ry + self.centre[1]

    x = sin(angle + pi/2)* self.rx + self.centre[0] 
    y = cos(angle + pi/2)* self.ry + self.centre[1]
    

    Second, calculating the point on an ellipse by an angle is a little more complicated. See the answers to the questions How to get a point on an ellipse's outline given an angle? and Calculating a Point that lies on an Ellipse given an Angle:

    from math import pi, sin, cos, atan2, radians, copysign, sqrt
    
    class Ellipse:
        # [...]
    
        def pointFromAngle(self, a):
            c = cos(a)
            s = sin(a)
            ta = s / c  ## tan(a)
            tt = ta * self.rx / self.ry  ## tan(t)
            d = 1. / sqrt(1. + tt * tt)
            x = self.centre[0] + copysign(self.rx * d, c)
            y = self.centre[1] - copysign(self.ry * tt * d, s)
            return x, y
    
        def collision(self, circle):
            # [...]
    
            #where the point lies in the ellipse at that angle
            x, y = self.pointFromAngle(angle)
    
            # [...]
    

    Minimal example: repl.it/@Rabbid76/PyGame-IntersectCircleEllipse

    import math
    import pygame
    
    class Circle:
        def __init__(self, center_x, center_y, radius):
            self.center = center_x, center_y
            self.radius = radius
        def update(self, center_x, center_y):
            self.center = center_x, center_y
        def draw(self, surface):
            pygame.draw.circle(surface, (255, 0, 0), (round(self.center[0]), round(self.center[1])), self.radius, 3)
    
    class Ellipse:
        def __init__(self, center, vertex):
            self.center = center
            self.collided = False
            self.vertex = vertex
        def draw(self, surface):
            bounding_rect = pygame.Rect(0, 0, self.vertex[0] * 2, self.vertex[1] * 2)
            bounding_rect.center = round(self.center[0]), round(self.center[1])
            pygame.draw.ellipse(surface, (0, 255, 0), bounding_rect, 3)
        def pointFromAngle(self, a):
            c = math.cos(a)
            s = math.sin(a)
            ta = s / c  ## tan(a)
            tt = ta * self.vertex[0] / self.vertex[1]  ## tan(t)
            d = 1. / math.sqrt(1. + tt * tt)
            x = self.center[0] + math.copysign(self.vertex[0] * d, c)
            y = self.center[1] - math.copysign(self.vertex[1] * tt * d, s)
            return x, y
    
    def intersect_circle_ellipse(circle, ellipse):
        dx = circle.center[0] - ellipse.center[0]
        dy = circle.center[1] - ellipse.center[1]
        angle = math.atan2(-dy, dx)
        x, y = ellipse.pointFromAngle(angle)
        distance = math.hypot(x - circle.center[0], y-circle.center[1])
        return distance <= circle.radius, (x, y) 
    
    pygame.init()
    window = pygame.display.set_mode((500, 300))
    circle = Circle(0, 0, 30)      
    ellipse = Ellipse(window.get_rect().center, (150, 100))
    run = True
    while run:
        events = pygame.event.get()
        mousePos = pygame.mouse.get_pos()
        for event in events:
            if event.type == pygame.QUIT:
                run = False
                
        circle.update(*mousePos)
        isect = intersect_circle_ellipse(circle, ellipse)
    
        window.fill((255, 255, 255))
        circle.draw(window)
        ellipse.draw(window) 
        color = (255, 0, 255) if isect[0] else (0, 0, 255)
        pygame.draw.circle(window, color, (round(isect[1][0]), round(isect[1][1])), 5)
        pygame.display.flip()
    
    pygame.quit()
    exit()