7
votes

I'm making a game in which balls bounce around the inside of a much larger circle. The larger circle doesn't move.

Here's the code that I'm currently using for these collisions:

def collideCircle(circle, ball):
    """Check for collision between a ball and a circle"""
    dx = circle.x - ball.x
    dy = circle.y - ball.y

    distance = math.hypot(dx, dy)

    if distance >= circle.size + ball.size:
        # We don't need to change anything about the circle, just the ball
        tangent = math.atan2(dy, dx)
        ball.angle = 2 * tangent - ball.angle
        ball.speed *= elasticity + 0.251

        angle = 0.5 * math.pi + tangent
        ball.x -= math.sin(angle)
        ball.y += math.cos(angle)

It is based on the wonderful tutorial by Peter Collingridge over here.

The circle and ball objects are both classes, with (x,y), radius, angle and speed.

I am having two problems with this method, however:

  1. The ball bounces from (what I suspect) is its "anchor point", which seems to be in the top right corner of the circle.
  2. When colliding with the bottom 5% of the circle, is fails to bounce high enough and therefore "sinks" out of the screen. I am guessing that this is because the bounce is not high enough to move the ball above its (incorrectly placed) "anchor point"?

Having looked at possible solutions already on here, notably "Fast circle collision detection" [Link deleted due to spam link limit], which, although in Java is using the same method, these all deal with external collisions, whereas I am looking at bouncing a ball around the interior of a circle.

Here is also the class definitions of the Ball() and the Circle():

class Ball():
    def __init__(self, (x,y), size):
        """Setting up the new instance"""
        self.x = x
        self.y = y
        self.size = size
        self.colour = (0,128,255)
        self.thickness = 0
        self.speed = 0.01
        self.angle = math.pi/2

    def display(self):
        """Draw the ball"""
        pygame.draw.circle(screen, self.colour, (int(self.x), int(self.y)), self.size, self.thickness)

    def move(self):
        """Move the ball according to angle and speed"""
        self.x += math.sin(self.angle) * self.speed
        self.y -= math.cos(self.angle) * self.speed
        (self.angle, self.speed) = addVectors((self.angle, self.speed), gravity)
        self.speed *= drag

class Circle():
    def __init__(self, (x,y), size):
        """Set up the new instance of the Circle class"""
        self.x = x
        self.y = y
        self.size = size
        self.colour = (236, 236, 236)
        self.thickness = 0
        self.angle = 0 # Needed for collision...
        self.speed = 0 # detection against balls

    def display(self):
        """Draw the circle"""
        pygame.draw.circle(screen, self.colour, (int(self.x), int(self.y)), self.size, self.thickness

Thanks in advance, Nathan

3

3 Answers

14
votes

Without answering your question, I'd like to comment on your implementation strategy and recommend a new approach. You represent the velocity of the ball in polar coordinate form, as ball.angle and ball.speed.

I think that this is going to be generally inconvenient for you. For example, in your collision code you end up calling atan2 to turn the vector (dx, dy) into an angle, and then you call sin and cos to turn the angle back into a vector again. (Also, should you ever try to generalize your code to three dimensions, you will find yourself in a world of pain.) So, unless you have particular requirements that necessitate polar coordinates, I recommend that you do what everyone else does, namely represent the velocity of the ball in Cartesian coordinates as the vector (vx, vy).

I also recommend changing your physics approach from a static one ("is object A currently colliding with object B?") to a dynamic one ("will object A collide with object B during its next movement step?"). In a static physics system you often end up with objects intersecting each other at the end of a movement step, and then you have to figure out the best way to get them to separate again, which is hard to get right.

If you do both of these, it is straightforward to bounce the ball without any trigonometry.

Step 1. Transform circle/circle collision into point/circle collision using Minkowski addition:

Original problem at left shows small circle moving inside a large circle. Transformed problem at right shows point moving in circle whose radius is the difference between the radii of the circles in the original problem.

Step 2. Consider a time segment in which the ball starts at p = (px,py) and moves by v = (vx,vy). Does it intersect with the circle? You can use a standard line segment/circle test for this except that the sense of the test is reversed.

Step 3. Find the point of collision c = (cx,cy). The ball bounces off the circle in the same way as it would bounce off the line t tangent to the circle at this point. For a circle centred at the origin, the tangent vector is just (−cy,cx) and I'm sure you can work out how to compute it for other circles.

Figure described above

See this answer for how to calculate the new path of the ball based on coefficients of friction and restitution.

Step 4. Don't forget that the ball may still have some distance to move along the new vector w. If the time step is large enough or the velocity high enough it may collide again during the same time segment.

12
votes

I'm glad you liked my tutorial. I like your variation, it should actually be simpler.

First, I think you need change the test for collision to:

if distance >= circle.size - ball.size:

Because the larger the ball size, the smaller the distance between its centre and the centre of the circle can be. This should make the balls bounce at the right place (inside the circle).

Then I think you just need to swap the signs for the x and y and everything should work.

ball.x += math.sin(angle)
ball.y -= math.cos(angle)

To move the ball by the correct distance you can calculate the overlap:

overlap = math.hypot(dx, dy) - (circle.size - ball.size)

if overlap >= 0:
  tangent = math.atan2(dy, dx)
  ball.angle = 2 * tangent - ball.angle
  ball.speed *= elasticity

  angle = 0.5 * math.pi + tangent
  ball.x += math.sin(angle)*overlap
  ball.y -= math.cos(angle)*overlap

Good luck

2
votes

Most graphics packages use upper-left as start for drawing code. You most likely want 2 sets of coordinates, the one's you collide/move/etc with and the one's for drawing (x-radius, y-radius).

Also, without having thought about it too much, should the check for intersection be distance + ball.size >= circle.size? The balls distance from the center plus its radius should be less than the circle's radius, if I understood the setup correctly.