Search code examples
pythonpython-3.xpygamecollision-detection

PyGame collision system working only every other time


I was trying to make a working collision system for my simple game, when i noticed a weird bug. There is a rectangle with random postion, travelling up with a constant velocity. Then there is a ball also with random position, but it can be moved using arrows. When the ball hits the rectangle it is supposed to go up with the rectangle as if the rectangle had "caught the ball" and when the ball leaves the rectangle it is supossed to fall again.

This is my code:

import time
import random
import pygame
from pygame.locals import *

pygame.init()

# Window size.
screen_width = 800
screen_height = 400

# Rectangle size and position.
position_x = random.randint(0, screen_width-150)
position_y = random.randint(0, screen_height-50)
r_width = random.randint(150, (screen_width - position_x))
r_height = 10
rect_gap = 50
velocity_r = 1

# Ball properties.
radius = 10
xcoor = random.randint((radius + 2), screen_width-r_width)
ycoor = 20
gravity = 1
velocity_b = 1


# colors
white = (255, 255, 255,)
black = (0, 0, 0)
red = (255, 0, 0)
green = (0, 255, 0)
blue = (0, 0, 255)
light_blue = (78, 231, 245)
dark_green = (37, 125, 0)

# Window.
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("Keep Falling")
screen.fill(light_blue)

# Class for generating the Ball.
class Ball:
    def __init__(self, screen, color, xcoor, ycoor, radius, ):
        self.screen = screen
        self.color = color
        self.xcoor = xcoor
        self.ycoor = ycoor
        self.radius = radius

    def draw(self, color, xcoor, ycoor, radius):
        pygame.draw.circle(screen, color, (xcoor, ycoor), radius)
    
# Class for generating the 
class Rectangle:
    def __init__(self, screen, color, position_x, position_y, r_width, r_height):
        self.screen = screen
        self.color = color
        self.position_x = position_x
        self.position_y = position_y
        self.r_width = r_width
        self.r_height = r_height

    def draw(self, color, position_x, position_y, r_width, r_height):
        pygame.draw.rect(screen, dark_green, (position_x,
                                              position_y, r_width, r_height))


# game loop
run = True
while run:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False

    screen.fill(light_blue)
    time.sleep(0.005)

    # Upward movement of the rectangle.
    position_y -= velocity_r

    # Teleportation of the triangle to the bottom.
    if position_y < 0 - r_height:
        position_y = screen_height+radius

    key = pygame.key.get_pressed()

    # Ball controls and collisions with the rectangle from the sides.
    if key[pygame.K_LEFT]:
        xcoor -= velocity_b
        if xcoor < 0:
            xcoor = screen_width
        if ycoor > position_y and ycoor < (position_y + r_height) and xcoor == (position_x + r_width + radius):
            xcoor += velocity_b
        
    if key[pygame.K_RIGHT]:
        xcoor += velocity_b
        if xcoor > screen_width:
            xcoor = 0
        if ycoor > position_y and ycoor < (position_y + r_height) and xcoor == (position_x - radius):
            xcoor -= velocity_b

    # Teleporation of the ball to the top.
    if ycoor > screen_height:
        ycoor = 0

    # Collision system.
    if ycoor == (position_y+r_height+radius) and xcoor >= position_x and xcoor <= (position_x + r_width):
        ycoor += velocity_b

    if ycoor == (position_y - radius) and xcoor >= position_x and xcoor <= (position_x + r_width):
        gravity = 0
        ycoor -= velocity_r
        if ycoor < 0:
            ycoor = screen_height

    if ycoor == (position_y - radius) and xcoor <= position_x or xcoor >= (position_x + r_width):
        gravity = 1
    
    # Falling of the ball.
    ycoor += gravity

    # Ball and rectangle display.
    Rectangle.draw(screen, dark_green, position_x, position_y, r_width, r_height)
    Ball.draw(screen, white, xcoor, ycoor, radius)

    pygame.display.update()
pygame.quit()

My collision system is based on coordinates of the ball and rectangle, as shown here:

    # Collision system.
    if ycoor == (position_y+r_height+radius) and xcoor >= position_x and xcoor <= (position_x + r_width):
        ycoor += velocity_b

    if ycoor == (position_y - radius) and xcoor >= position_x and xcoor <= (position_x + r_width):
        gravity = 0
        ycoor -= velocity_r
        if ycoor < 0:
            ycoor = screen_height

    if ycoor == (position_y - radius) and xcoor <= position_x or xcoor >= (position_x + r_width):
        gravity = 1
    

For a unknown reason, in this case the ball only gets "caught" only every other time, otherwise it falls trough the triangle.

I came up with an alternative which is also based on coordinates

while ycoor == (position_y - radius) and xcoor >= position_x and xcoor <= (position_x + r_width):
        gravity = 0
        ycoor -= velocity_r
        if ycoor < 0:
            ycoor = screen_height

Using this method also causes bugs.

  1. The ball falls through on first collision with the triangle, from second rectangle onwards, collision works properly.
  2. When the ball leaves the rectangle, it does not start falling again. Adding gravity = 1 after the loop causes the ball to not collide with the rectangles at all.

Is there a bug or is my code logically flawed and i should redo the whole collision detection system?
Thank you for any suggestions.


Solution

  • The issue is, that rectangle moves by on and the ball moves by one in every frame. As a result, the bottom of the ball doesn't always hit the top of the rectangle exactly. Sometimes the condition ycoor == (position_y - radius) is not fulfilled.
    You have to evaluate if the bottom of the ball is "in the range" of the top of the rectangle:

    if ycoor == (position_y - radius) and ...

    if (position_y - radius - 1) <= ycoor <= (position_y - radius + 1) and ...
    

    For instnace:

    while run:
        # [...]
    
        if  (position_y - radius - 1) <= ycoor <= (position_y - radius + 1) and xcoor >= position_x and xcoor <= (position_x + r_width):
            gravity = 0
            ycoor -= velocity_r
            if ycoor < 0:
                ycoor = screen_height
    
        if (position_y - radius - 1) <= ycoor <= (position_y - radius + 1) and xcoor <= position_x or xcoor >= (position_x + r_width):
            gravity = 1
    

    Anyway, I recommend to use a pygame.Rect object and either .collidepoint() or colliderect() to find a collision between a rectangle and an object.

    rect1 = pygame.Rect(x1, y1, w1, h1)
    rect2 = pygame.Rect(x2, y2, w2, h2)
    if rect1.colliderect(rect2):
        # [...]
    

    For instance:

    run = True
    while run:
        # [...]
    
        # Teleporation of the ball to the top.
        if ycoor > screen_height:
            ycoor = 0
    
        rect_rect = pygame.Rect(position_x, position_y, r_width, r_height)
        ball_rect = pygame.Rect(xcoor-radius, ycoor-radius, radius*2, radius*2)
        if rect_rect.colliderect(ball_rect):
            ycoor = position_y - radius
        else:
            # Falling of the ball.
            ycoor += gravity
    
        # Ball and rectangle display.
        # [...]