0
\$\begingroup\$

I've been working on this golf game for a couple of weeks and have been trying to create a reliable collision system for whenever a golf ball (mini golf) hits a block. I'm using regular rectangular box collision to help me with, but I have constantly been running into glitching issues, eg, the golf ball can clip through blocks. I've tried many methods to solve this but none of them work well.

The way I handle moving the ball is quite simple, is that the player clicks on the ball, drags and releases, which provides an initial velocity and angle, I then get x & y components and decrease or increase the position of the golf ball, I also have variables which can make the components negative. Code for this is beneath.

    def shooting(self):

    self.x_vel = self.initial_vel * (math.cos(math.radians(self.angle)))
    self.y_vel = self.initial_vel * (math.sin(math.radians(self.angle)))
    if self.x_inverse:
        self.x_vel *= -1
    if self.y_inverse:
        self.y_vel *= -1
    self.rect.centerx -= self.x_vel*self.dt
    self.rect.centery -= self.y_vel*self.dt
    self.initial_vel -=  self.resistance * self.dt
    if self.initial_vel <= 10:
        self.shoot = False

My method for calculating collisions with a block is just regular bounding box/rectangular box collision, code is beneath.

def bounced(self, golf_ball):

    if golf_ball.rect.colliderect(self.rect):
        if abs(self.rect.left - golf_ball.rect.right) <= 5:

            if golf_ball.rect.right > self.rect.left:
                golf_ball.rect.right = self.rect.left
                golf_ball.rect.right -= 1
            if golf_ball.x_inverse:
                golf_ball.x_inverse = False
            else:
                golf_ball.x_inverse = True  # Left

        if abs(self.rect.right - golf_ball.rect.left) <= 5:

            if golf_ball.rect.left < self.rect.right:
                golf_ball.rect.left = self.rect.right
                golf_ball.rect.left += 1

            if golf_ball.x_inverse:
                golf_ball.x_inverse = False
            else:
                golf_ball.x_inverse = True  

        if abs(self.rect.bottom - golf_ball.rect.top) <= 5:  # Bottom

            if golf_ball.rect.top < self.rect.bottom:
                golf_ball.rect.top = self.rect.bottom
                golf_ball.rect.top += 1
            if golf_ball.y_inverse:
                golf_ball.y_inverse = False
            else:
                golf_ball.y_inverse = True

        if abs(self.rect.top - golf_ball.rect.bottom) <= 5:
            if golf_ball.rect.bottom > self.rect.top:
                golf_ball.rect.bottom = self.rect.top
                golf_ball.rect.bottom -= 1

            if golf_ball.y_inverse:
                golf_ball.y_inverse = False
            else:
                golf_ball.y_inverse = True`

My issue arises when the ball is launched at high speeds near the block, the golf ball can glitch through the block, which is obviously undesirable. Golf Ball glitching in block

Below is my code and image for golf ball and box/block, the specific parts of code worth looking at is the shooting method and bounce_detection in the golf ball class, and the bounced method in the block class, the only reason I added all other code, is so that you can test out the code, and attempt to solve the issue.

import pygame
import math

pygame.init()
screen = pygame.display.set_mode((640, 480))
clock = pygame.time.Clock()
mouse_pos = pygame.mouse.get_pos()
dt = clock.tick(70) / 1000


class Golf_Ball:
 def __init__(self, x_pos, y_pos, mass, w, h, mouse_pos, screen, dt):
     self.screen = screen
     self.resisting = False

     # self.dt = dt
     self.mass = mass
     self.w = w
     self.h = h
     self.visible = True
     self.initial_vel = 0
     self.x_vel, self.y_vel = 0, 0
     self.resultant = 0
     self.mouse_pos = mouse_pos
     self.fired = True
     self.hit_time = 0
     self.held_down = False
     self.line = False
     self.fired_resultant = 0
     self.x_pos = x_pos
     self.y_pos = y_pos
     self.angle = 0
     self.shoot = False
     self.resistance = 100
     self.velocity_constant = 0.3
     self.spin_angel = 0
     self.image = pygame.image.load('Assets/golf.png').convert_alpha()
     self.image_copy = pygame.transform.rotate(self.image, 0)
     self.rect = self.image.get_rect(center=(self.x_pos, self.y_pos))
     self.x_inverse, self.y_inverse = False, False

 def mouse_click_manager(self):
     if pygame.mouse.get_pressed()[0] == True and self.shoot == False:

         if self.rect.collidepoint(self.mouse_pos):

             if self.line == False and self.held_down == False:
                 self.held_down = True
                 self.line = True
         if self.line and self.held_down == False:
             self.held_down = True
             self.line = False

             self.fired_resultant = self.resultant * self.velocity_constant
             self.initial_vel = int(round(((self.fired_resultant / self.mass) * 1), 0))
             print("Hit", self.initial_vel)
             self.shoot = True
             self.x_inverse, self.y_inverse = False, False

     elif not pygame.mouse.get_pressed()[0]:
         self.held_down = False

 def line_manager(self):
     if self.line:

         if abs(self.rect.centerx - self.mouse_pos[0]) != 0:
             self.angle = math.degrees(
                 math.atan(abs(self.rect.centery - self.mouse_pos[1]) / abs(self.rect.centerx - self.mouse_pos[0])))
             if self.mouse_pos[1] < self.rect.centery:
                 self.angle = self.angle * -1
             if self.mouse_pos[0] < self.rect.centerx:
                 self.angle = abs(self.angle - 90) + 90
         elif abs(self.rect.centerx - self.mouse_pos[0]) == 0:
             if self.mouse_pos[1] > self.rect.centery:
                 self.angle = 90
             else:
                 self.angle = -90

         self.resultant = math.sqrt(
             abs(self.rect.centerx - self.mouse_pos[0]) ** 2 + abs(self.rect.centery - self.mouse_pos[1]) ** 2)

         pygame.draw.line(self.screen, "Black", (self.rect.centerx, self.rect.centery), self.mouse_pos, 3)

 def shooting(self):

     self.x_vel = self.initial_vel * (math.cos(math.radians(self.angle)))
     self.y_vel = self.initial_vel * (math.sin(math.radians(self.angle)))
     if self.x_inverse:
         self.x_vel *= -1
     if self.y_inverse:
         self.y_vel *= -1
     self.rect.centerx -= self.x_vel * self.dt
     self.rect.centery -= self.y_vel * self.dt
     self.initial_vel -= self.resistance * self.dt
     if self.initial_vel <= 10:
         self.shoot = False

 def bounce_detection(self):
     if self.rect.left <= 0:  # Left
         if self.rect.left < 0:
             self.rect.left = 1

         if self.x_inverse:
             self.x_inverse = False
         else:
             self.x_inverse = True

     if self.rect.right >= 640:  # Right
         if self.rect.right > 640:
             self.rect.right = 639
         if self.x_inverse:
             self.x_inverse = False
         else:
             self.x_inverse = True

     if self.rect.top <= 0:  # Top
         if self.rect.top < 0:
             self.rect.top = 1
         if self.y_inverse:
             self.y_inverse = False
         else:
             self.y_inverse = True

     if self.rect.bottom >= 480:  # Bottom
         if self.rect.bottom > 480:
             self.rect.bottom = 479
         if self.y_inverse:
             self.y_inverse = False
         else:
             self.y_inverse = True

 def update(self, mouse_pos, dt):
     self.mouse_pos = mouse_pos
     self.dt = dt
     if self.visible:
         if self.shoot:
             self.shooting()
             self.bounce_detection()
             self.spin_angel += self.initial_vel * 10 * self.dt
             self.image_copy = pygame.transform.rotate(self.image, self.spin_angel)

         else:
             self.mouse_click_manager()
             self.line_manager()
         self.resistance = 300
         self.screen.blit(self.image_copy, (self.rect.centerx - int(self.image_copy.get_width() / 2),
                                            self.rect.centery - int(self.image_copy.get_height() / 2)))
         pygame.draw.rect(self.screen, 'White', self.rect, 1)


class Block:

 def __init__(self, x_pos, y_pos, width, height, screen, disabled, dt):
     self.x_pos = x_pos
     self.y_pos = y_pos
     self.screen = screen
     self.disabled = disabled
     self.width = width
     self.height = height
     self.image = pygame.transform.smoothscale(pygame.image.load('Assets\Wooden_Block.png'),
                                               (self.width, self.height))
     self.rect = self.image.get_rect(topleft=(self.x_pos, self.y_pos))
     self.mask = pygame.mask.from_surface(self.image)
     self.dt = dt

     self.clock = 0
     self.counter = 0
     self.dummy_rect = pygame.Rect((self.x_pos - 5, self.y_pos - 5), (self.rect.width + 10, self.rect.height + 10))
     self.direction = True
     self.in_block = False

 def bounced(self, golf_ball):

     if golf_ball.rect.colliderect(self.rect):
         if abs(self.rect.left - golf_ball.rect.right) <= 5:

             if golf_ball.rect.right > self.rect.left:
                 golf_ball.rect.right = self.rect.left
                 golf_ball.rect.right -= 1
             if golf_ball.x_inverse:
                 golf_ball.x_inverse = False
             else:
                 golf_ball.x_inverse = True  # Left

         if abs(self.rect.right - golf_ball.rect.left) <= 5:

             if golf_ball.rect.left < self.rect.right:
                 golf_ball.rect.left = self.rect.right
                 golf_ball.rect.left += 1

             if golf_ball.x_inverse:
                 golf_ball.x_inverse = False
             else:
                 golf_ball.x_inverse = True

         if abs(self.rect.bottom - golf_ball.rect.top) <= 5:  # Bottom

             if golf_ball.rect.top < self.rect.bottom:
                 golf_ball.rect.top = self.rect.bottom
                 golf_ball.rect.top += 1
             if golf_ball.y_inverse:
                 golf_ball.y_inverse = False
             else:
                 golf_ball.y_inverse = True

         if abs(self.rect.top - golf_ball.rect.bottom) <= 5:
             if golf_ball.rect.bottom > self.rect.top:
                 golf_ball.rect.bottom = self.rect.top
                 golf_ball.rect.bottom -= 1

             if golf_ball.y_inverse:
                 golf_ball.y_inverse = False
             else:
                 golf_ball.y_inverse = True

 def update(self, golf_ball):
     self.bounced(golf_ball)

     self.screen.blit(self.image, self.rect)
     pygame.draw.rect(self.screen, 'Black', self.rect, 1)


golf_ball = Golf_Ball(200, 200, 0.04593, 20, 20, mouse_pos, screen, dt)
block_1 = Block(100, 300, 50, 20, screen, False, dt)

block = pygame.transform.rotate(
 pygame.transform.smoothscale(pygame.image.load('Assets\Wooden_Block.png').convert_alpha(), (100, 50)), 45)
block_rect = block.get_rect(topleft=(300, 200))

running = True
while running:
 for event in pygame.event.get():
     if event.type == pygame.QUIT:
         running = False
     elif event.type == pygame.KEYDOWN:

         key = event.key
         if event.key == pygame.K_ESCAPE:
             running = False

 mouse_pos = pygame.mouse.get_pos()
 screen.fill('Green')
 golf_ball.update(mouse_pos, dt)
 block_1.update(golf_ball)

 # screen.blit(block, block_rect)
 clock.tick(70)
 dt = clock.tick(70) / 1000
 pygame.display.flip()

pygame.quit()

golf ball imageBlock image

\$\endgroup\$
2
  • \$\begingroup\$ Please lookup continuous collision detection. \$\endgroup\$
    – agone
    Commented Dec 8, 2023 at 23:47
  • \$\begingroup\$ Thanks, will do. \$\endgroup\$
    – Jet
    Commented Dec 8, 2023 at 23:55

1 Answer 1

0
\$\begingroup\$

swept sphere(or circle) collision. Simply put, instead of checking for two overlapping shapes you check for intersections between a line and the object. The ball starts at A and after movement vector is applied it should be at B. AB is your line segment to check against the boxes. Let's say your box is defined as DEFG, representing the 4 points cockwise. (box can be rotated as well, it does not matter) Check for the intersection of AB with DE, EF, FG, and GD. There's a ton of info out there on line-line intersection tests, I don't recall the equations off the top of my head exactly.

From there you can determine the exact point of intersection and move the golfball accordingly. For instance, if the ball moves 10 units but hits a wall after 7, then the intersection point can be calculated with linear interpolation:

I = A + (B-A) * 0.7

The 0.7 will come from the intersections tests. As the line tests work on infinite lines, the resulting values should fall between 0 and 1 to ensure its within the length of the segments. So that's a boundary check to consider.

From the intersection point you want to subtract the ball's radius for the final position otherwise it'll appear to be half inside the box. Similarly, the movement vector AB should include the ball's radius as well. So technically the line is A to B + r. Otherwise, the golfball could land right next to the square without intersecting it but appear to be partially inside.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .