Ball doesn't bounce of screen edges in PyGame

Viewed 189

I'm building a Pong game in Python with PyGame. For some reason, the ball in the game doesn't bounce off the walls / screen edges sometimes. It bounces off the walls once or twice and stops.

I don't know why that happens.

Could someone check my code to see if there are any errors?

import os
import pygame

WIDTH = 600
HEIGHT = 500

class Slider:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.height = 120
        self.width = 8

    def draw(self, win):
        pygame.draw.rect(win, (0, 255, 64), (self.x, self.y, self.width, self.height))

    def move(self, dir, dis):
        if dir == 'top' and self.y >= 0:
            self.y -= dis
        elif dir == 'bottom' and self.y <= 379:
            self.y += dis 
        else: 
            self.y = self.y

class Ball:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.radius = 15

        self.speedY = 0.2
        self.speedX = 0.2

        self.top = y
        self.bottom = y + 30
        self.left = x
        self.right = x + 30

    def draw(self, win):
        ball = pygame.draw.circle(win, (255, 255, 0), (self.x, self.y), self.radius)
        self.top = ball.top
        self.bottom = ball.bottom
        self.left = ball.left
        self.right = ball.right

    def move(self):

        if self.top <= 0:
            self.speedY *= -1
        else:
            self.y += self.speedY
            self.x += self.speedX

        if self.bottom >= HEIGHT:
            self.speedY *= -1
        else:
            self.y += self.speedY
            self.x += self.speedX
            
        if self.left <= 0:
            self.speedX *= -1
        else:
            self.y += self.speedY
            self.x += self.speedX

        if self.right >= WIDTH:
            self.speedX *= -1
        else:
            self.y += self.speedY
            self.x += self.speedX


def draw_window(win, *sprites):
    pygame.display.update()
    win.fill((0, 0, 0, 0))
    for sprite in sprites:
        sprite.draw(win)

def main():
    win = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption('Pong Game')

    slider1 = Slider(100, 250)
    slider2 = Slider(500, 250)
    ball = Ball(300, 250)

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

        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_DOWN:
                slider1.move('bottom', 0.3)
            elif event.key == pygame.K_UP:
                slider1.move('top', 0.3)
            else:
                print(pygame.key.name(event.key))

        ball.move()

        key = pygame.key.get_pressed()

        if key[pygame.K_s]:
            slider2.move('bottom', 0.3)
        elif key[pygame.K_w]:
            slider2.move('top', 0.3)

        draw_window(win, slider1, slider2, ball)


    pygame.quit()
    quit()

main()
3 Answers

You need to change your move method a bit, you need to remove the else blocks as they mess up ball's movement, just always move the ball once when calling move. You can also combine checking whether ball is on edge for an axis in one line (using or):

def move(self):

    if self.top <= 0 or self.bottom >= HEIGHT:
        self.speedY *= -1

    if self.left <= 0 or self.right >= WIDTH:
        self.speedX *= -1

    self.y += self.speedY
    self.x += self.speedX

Move the ball after changing the direction, in any case:

class Ball:
   # [...]

   def move(self):

        if self.top <= 0 or self.bottom >= HEIGHT:
            self.speedY *= -1
        if self.left <= 0 or self.right >= WIDTH:
            self.speedX *= -1

        self.y += self.speedY
        self.x += self.speedX

Note, you do not need the top, bottom, left and right attributes at all. Use a pygame.Rect object:

class Ball:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.radius = 15
        self.rect = pygame.Rect(0, 0, self.radius*2, self.radius*2)  
        self.speedY = 0.2
        self.speedX = 0.2

    def draw(self, win):
         pygame.draw.circle(win, (255, 255, 0), (self.x, self.y), self.radius)

    def move(self):
        self.y += self.speedY
        self.x += self.speedX
        self.rect.center = round(self.x), round(self.y)

        if self.rect.top <= 0 or self.rect.bottom >= HEIGHT:
            self.speedY *= -1
        if self.rect.left <= 0 or self.rect.right >= WIDTH:
            self.speedX *= -1

What Rabbid76 suggests is pretty clever:

You could either use composition (like below with a self.boundary component) or inherit Ball from Rect.

Then you can reuse methods from that boundary like move_ip(x,y). Or test inside of the screen by screen.contains(boundary) or some collision.

Non-integer movements

Even if the Balls position (x,y) might be non-integer (due to speed of e.g. 0.2), the location on screen can only be represented as and rendered as integer pixel coordinates. Thus I deliberately use round and pygame.Rect for screen positions.

pygame.Rect represents objects in pixel granularity hence its attributes are stored as integers:

The coordinates for Rect objects are all integers.

Test collision using a boundary of pygame.Rect

class Ball:
    def __init__(self, x, y):
        # use center to get (x,y) equivalent to former (self.x,self.y)
        self.center = (x,y)
        self.radius = 15
        # boundary as Rect (actually square) around the circle of the ball
        self.boundary = pygame.Rect(round(x)-self.radius, round(y)-self.radius, self.radius*2, self.radius*2)  
        self.speedY = 0.2
        self.speedX = 0.2

    def draw(self, win):
         pygame.draw.circle(win, (255, 255, 0), self.center, self.radius)

    def move(self):
        # move center to new position
        self.center[0] += self.speedX
        self.center[1] += self.speedY 
        # move boundary rectangle (screen position by integers only)
        self.boundary.move_ip(round(self.center[0]), round(self.center[1]))
        
        # test if inside the box using integers
        # if not pygame.Rect(0,0, WIDTH,HEIGHT).contains(self.boundary):
        if self.boundary.top <= 0 or self.boundary.bottom >= HEIGHT:
            self.speedY *= -1
        if self.boundary.left <= 0 or self.boundary.right >= WIDTH:
            self.speedX *= -1

See also this interesting post explaining the difference between move_ip (move in place) and move (which does not move the instance but returns the moved): Difference between rect.move() and rect.move_ip in pygame

Related