How to code bounce movement in pong using pygame

1.5k Views Asked by At

I'm a noob in python and I'm trying to recreate the Pong game, and I'm trying to do it by myself as much as possible.

These are the issues I currently have with my code:

  1. I basically coded every possible movement of the ball when it bounces on the edge. I spent hours working on it, and I got it to work, I was just wondering if there was a more efficient way to produce a similar output with my code (coz I know this is supposed to be a beginner project)?
  2. Every time the ball bounces past the edge, the score increments for a split second, and it goes back to 0 every time the ball respawns.
  3. How can I make the ball spawn at random directions? at the start (and restart) of every round?

Here's my code for the class and the main function of the program:

import pygame
import random
from rect_button import Button

pygame.init()

WIDTH = 800
HEIGHT = 500
window = pygame.display.set_mode((WIDTH, HEIGHT))

TITLE = "Pong"
pygame.display.set_caption(TITLE)

# COLORS
black = (0, 0, 0)
white = (255, 255, 255)
red = (255, 0, 0)
green = (0, 255, 0)

# FONTS
small_font = pygame.font.SysFont("courier", 20)
large_font = pygame.font.SysFont("courier", 60, True)


class Paddle:
    def __init__(self, x, y, width, height, vel, color):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.vel = vel
        self.color = color

    def draw(self, window):
        pygame.draw.rect(window, self.color, (self.x, self.y, self.width, self.height), 0)


class Ball:
    def __init__(self, x, y, side, vel, color):
        self.x = x
        self.y = y
        self.side = side
        self.vel = vel
        self.color = color
        self.lower_right = False
        self.lower_left = True
        self.upper_right = False
        self.upper_left = False
        self.ball_bag = []
        self.last_movement = 'ball.lower_right'

    def draw(self, window):
        pygame.draw.rect(window, self.color, (self.x, self.y, self.side, self.side), 0)

    def move_lower_right(self):
        self.x += self.vel
        self.y += self.vel

    def move_upper_right(self):
        self.x += self.vel
        self.y -= self.vel

    def move_upper_left(self):
        self.x -= self.vel
        self.y -= self.vel

    def move_lower_left(self):
        self.x -= self.vel
        self.y += self.vel

    def start(self):
        self.lower_right = True
        self.lower_left = False
        self.upper_right = False
        self.upper_left = False

        self.last_movement = 'ball.lower_left'

        # return random.choice([self.lower_right, self.lower_left, self.upper_left, self.upper_right]) is True

def main():
    run = True
    fps = 60
    clock = pygame.time.Clock()

    # Initializing Paddles

    left_paddle = Paddle(20, 100, 10, 50, 5, white)
    right_paddle = Paddle(770, 350, 10, 50, 5, white)

    balls = []

    def redraw_window():
        window.fill(black)

        left_paddle.draw(window)
        right_paddle.draw(window)
        for ball in balls:
            ball.draw(window)

        player_A_text = small_font.render("Player A: " + str(score_A), 1, white)
        player_B_text = small_font.render("Player B: " + str(score_B), 1, white)
        window.blit(player_A_text, (320 - int(player_A_text.get_width() / 2), 10))
        window.blit(player_B_text, (480 - int(player_B_text.get_width() / 2), 10))

        pygame.draw.rect(window, white, (20, 450, 760, 1), 0)
        pygame.draw.rect(window, white, (20, 49, 760, 1), 0)
        pygame.draw.rect(window, white, (19, 50, 1, 400), 0)
        pygame.draw.rect(window, white, (780, 50, 1, 400), 0)

        pygame.display.update()

    while run:
        score_A = 0
        score_B = 0
        clock.tick(fps)

        if len(balls) == 0:
            ball = Ball(random.randrange(320, 465), random.randrange(200, 285), 15, 3, white)
            balls.append(ball)
        if ball.lower_left:
            ball.move_lower_left()

            if ball.last_movement == 'ball.lower_right':
                if ball.y + ball.side > HEIGHT - 50:
                    ball.lower_left = False
                    ball.last_movement = 'ball.lower_left'
                    ball.upper_left = True

            if ball.last_movement == 'ball.upper_left':
                if ball.x < 30:
                    if left_paddle.x < ball.x < left_paddle.x + left_paddle.width:
                        if left_paddle.y < ball.y + ball.side < left_paddle.y + left_paddle.height:
                            ball.lower_left = False
                            ball.last_movement = 'ball.lower_left'
                            ball.lower_right = True
                    else:
                        score_B += 1
                        balls.remove(ball)
                        #ball.start()

                if ball.y + ball.side > HEIGHT - 50:
                    ball.lower_left = False
                    ball.last_movement = 'ball.lower_left'
                    ball.upper_left = True

        if ball.upper_left:
            ball.move_upper_left()

            if ball.last_movement == 'ball.lower_left':
                if ball.x < 30:
                    if left_paddle.x < ball.x < left_paddle.x + left_paddle.width:
                        if left_paddle.y < ball.y + ball.side < left_paddle.y + left_paddle.height:
                            ball.upper_left = False
                            ball.last_movement = 'ball.upper_left'
                            ball.upper_right = True
                    else:
                        score_B += 1
                        balls.remove(ball)
                        #ball.start()

                if ball.y < 50:
                    ball.upper_left = False
                    ball.last_movement = 'ball.upper_left'
                    ball.lower_left = True

            if ball.last_movement == 'ball.upper_right':
                if ball.y < 50:
                    ball.upper_left = False
                    ball.last_movement = 'ball.upper_left'
                    ball.lower_left = True

        if ball.upper_right:
            ball.move_upper_right()

            if ball.last_movement == 'ball.upper_left':
                if ball.y < 50:
                    ball.upper_right = False
                    ball.last_movement = 'ball.upper_right'
                    ball.lower_right = True

            if ball.last_movement == 'ball.lower_right':
                if ball.x + ball.side > WIDTH - 30:
                    if right_paddle.x + right_paddle.width > ball.x + ball.side > right_paddle.x:
                        if right_paddle.y < ball.y + ball.side < right_paddle.y + right_paddle.height:
                            ball.upper_right = False
                            ball.last_movement = 'ball.upper_right'
                            ball.upper_left = True
                    else:
                        score_A += 1
                        balls.remove(ball)
                        #ball.start()

                if ball.y < 50:
                    ball.upper_right = False
                    ball.last_movement = 'ball.upper_right'
                    ball.lower_right = True

        if ball.lower_right:
            ball.move_lower_right()

            if ball.last_movement == 'ball.upper_right':
                if ball.y + ball.side > HEIGHT - 50:
                    ball.lower_right = False
                    ball.last_movement = 'ball.lower_right'
                    ball.upper_right = True

                if ball.x + ball.side > WIDTH - 30:
                    if right_paddle.x + right_paddle.width > ball.x + ball.side > right_paddle.x:
                        if right_paddle.y < ball.y + ball.side < right_paddle.y + right_paddle.height:
                            ball.lower_right = False
                            ball.last_movement = 'ball.lower_right'
                            ball.lower_left = True
                    else:
                        score_A += 1
                        balls.remove(ball)
                        #ball.start()

            if ball.last_movement == 'ball.lower_left':
                if ball.y + ball.side > HEIGHT - 50:
                    ball.lower_right = False
                    ball.last_movement = 'ball.lower_right'
                    ball.upper_right = True

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

        keys = pygame.key.get_pressed()

        if keys[pygame.K_UP] and right_paddle.y > 50:
            right_paddle.y -= right_paddle.vel
        if keys[pygame.K_w] and left_paddle.y > 50:
            left_paddle.y -= left_paddle.vel
        if keys[pygame.K_DOWN] and right_paddle.y + right_paddle.height < HEIGHT - 50:
            right_paddle.y += right_paddle.vel
        if keys[pygame.K_s] and left_paddle.y + left_paddle.height < HEIGHT - 50:
            left_paddle.y += left_paddle.vel
        if keys[pygame.K_SPACE]:
            pass

        redraw_window()

    quit()


def main_menu():
    run = True

    play_button = Button(green, 100, 350, 150, 75, "Play Pong")
    quit_button = Button(red, 550, 350, 150, 75, "Quit")

    pong_text = large_font.render("Let's Play Pong!!!", 1, black)

    while run:
        window.fill(white)

        play_button.draw(window, black)
        quit_button.draw(window, black)

        window.blit(pong_text, (int(WIDTH / 2 - pong_text.get_width() / 2), 100))

        pygame.display.update()

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

            if event.type == pygame.MOUSEMOTION:
                if play_button.hover(pygame.mouse.get_pos()):
                    play_button.color = (0, 200, 0)
                else:
                    play_button.color = green
                if quit_button.hover(pygame.mouse.get_pos()):
                    quit_button.color = (200, 0, 0)
                else:
                    quit_button.color = red

            if event.type == pygame.MOUSEBUTTONDOWN:
                if play_button.hover(pygame.mouse.get_pos()):
                    main()
                if quit_button.hover(pygame.mouse.get_pos()):
                    run = False
                    quit()


main_menu()

Thanks!!!

4

There are 4 best solutions below

0
On BEST ANSWER

Answer to

  1. Every time the ball bounces past the edge, the score increments for a split second, and it goes back to 0 every time the ball respawns.

The score is continuously initialized in the main loop. You have to initialize the score before the loop:

def main():
    # [...]

    score_A = 0 # <--- INSERT
    score_B = 0
    
    while run:
        # score_A = 0 <--- DELETE
        # score_B = 0
0
On

I basically coded every possible movement of the ball when it bounces on the edge. I spent hours working on it, and I got it to work, I was just wondering if there was a more efficient way to produce a similar output with my code (coz I know this is supposed to be a beginner project)?

and

How can I make the ball spawn at random directions? at the start (and restart) of every round?

Rather than implementing "every" direction of the ball you can use float coordinates. these variables are usualy called dx and dy. This way getting a random or reversed direction for your ball is simple, just use random or inverse values for dx and dy.

Th update for your ball should then look like:

def update(self. dt):
    self.x += self.dx * self.speed * time_elapsed
    self.y += self.dy * self.speed * time_elapsed # Time elasped is often called dt.

Every time the ball bounces past the edge, the score increments for a split second, and it goes back to 0 every time the ball respawns.

See Rabid76's answer. Ideally you should have a GameState object with scores, lives and other stuff as attributes.

0
On

Answer to question 1: Yes. There is definitely a lot more effective way to do things. There is no need to make so many variables for direction. Simply say direction = [True, False], where direction[0] represents left and not direction[0] represents right in x-axis. Similarly, direction[1] represents y axis. This also solves your problem of randomizing the direction at start. You can simply do direction = [random.randint(0, 1), random.randint(0, 1)] in your init method to randomize the direction. The same way, make a list for speed as well. self.speed = [0.5, random.uniform(0.1, 1)]. This way, speed in left and right will always be same, but y will vary depending on the random number chosen, so there is proper randomness and you also don't have to hardcode random directions in. With this, movement becomes really simple.

class Ball:
def __init__(self, x, y, color, size):
    self.x = x
    self.y = y
    self.color = color
    self.size = size
    self.direction = [random.randint(0, 1), random.randint(0, 1)]
    self.speed = [0.5, random.uniform(0.1, 1)]
    
def draw(self, display):
    pygame.draw.rect(display, self.color, (self.x, self.y, self.size, self.size))

def move(self):
    if self.direction[0]:
        self.x += self.speed[0]
    else:
        self.x -= self.speed[0]
    if self.direction[1]:
        self.y += self.speed[0]
    else:
        self.y -= self.speed[0]

Because we have defined direction directions are Booleans, changing the state also becomes really easy. If the ball hits the paddle, you can simply toggle the bool direction[0] = not direction[0] in x and pick a new random number for y, instead of manually assigning bools.

def switchDirection(self):
    self.direction = not self.direction
    self.speed[1] = random.uniform(0.1, 1)

Paddle can be improved slightly as well, by giving Paddle class a move function instead of moving in the main loop. It just means you have to write less code.

def move(self, vel, up=pygame.K_UP, down=pygame.K_DOWN):
    keys = pygame.key.get_perssed()
    if keys[up]:
        self.y -= vel
    if keys[down]:
        self.y += vel

For collisions, I recommend using pygame.Rect() and colliderect since it's a lot more robust and probably more efficient as well.

Example:

import random
import pygame

WIN = pygame.display
D = WIN.set_mode((800, 500))

class Paddle:
    def __init__(self, x, y, width, height, vel, color):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.vel = vel
        self.color = color

    def move(self, vel, up=pygame.K_UP, down=pygame.K_DOWN):
        keys = pygame.key.get_pressed()
        if keys[up]:
            self.y -= vel
        if keys[down]:
            self.y += vel
        
    def draw(self, window):
        pygame.draw.rect(window, self.color, (self.x, self.y, self.width, self.height), 0)

    def getRect(self):
        return pygame.Rect(self.x, self.y, self.width, self.height)

left_paddle = Paddle(20, 100, 10, 50, 5, (0, 0, 0))
right_paddle = Paddle(770, 350, 10, 50, 5, (0, 0, 0))

class Ball:
    def __init__(self, x, y, color, size):
        self.x = x
        self.y = y
        self.color = color
        self.size = size
        self.direction = [random.randint(0, 1), random.randint(0, 1)]
        self.speed = [0.3, random.uniform(0.2, 0.2)]
        
    def draw(self, window):
        pygame.draw.rect(window, self.color, (self.x, self.y, self.size, self.size))

    def switchDirection(self):
        self.direction[0] = not self.direction[0]
        self.direction[1] = not self.direction[1]
        self.speed = [0.2, random.uniform(0.1, 0.5)]

    def bounce(self):
        self.direction[1] = not self.direction[1]
        self.speed = [0.2, random.uniform(0.01, 0.2)]
        
    def move(self):
        if self.direction[0]:
            self.x += self.speed[0]
        else:
            self.x -= self.speed[0]
        if self.direction[1]:
            self.y += self.speed[1]
        else:
            self.y -= self.speed[1]

    def getRect(self):
        return pygame.Rect(self.x, self.y, self.size, self.size)

    def boundaries(self):
        if ball.x <= 10:
            self.switchDirection()
        if ball.x + self.size >= 800:
            self.switchDirection()
        if ball.y + self.size >= 490:
            self.bounce()
        if ball.y <= 0:
            self.bounce()


ball = Ball(400, 250, (255, 0, 0), 20)
while True:
    pygame.event.get()
    D.fill((255, 255, 255))

    ball.draw(D)
    ball.boundaries()
    ball.move()
    #print(ball.x, ball.y)
    
    left_paddle.draw(D)
    right_paddle.draw(D)

    right_paddle.move(0.4)
    left_paddle.move(0.4, down=pygame.K_s, up=pygame.K_w)

    if left_paddle.getRect().colliderect(ball.getRect()):
        ball.switchDirection()
    if right_paddle.getRect().colliderect(ball.getRect()):
        ball.switchDirection()
    
    WIN.flip()
0
On

Some food for thought on how to reduce all that ball movement/collision code, and make things a bit more reusable:

import pygame


class Ball:

    def __init__(self, bounds, color):
        from random import randint, choice
        self.bounds = bounds
        self.position = pygame.math.Vector2(
            randint(self.bounds.left, self.bounds.left+self.bounds.width),
            randint(self.bounds.top, self.bounds.top+self.bounds.height)
        )
        self.velocity = pygame.math.Vector2(choice((-1, 1)), choice((-1, 1)))
        self.color = color
        self.size = 8

    def draw(self, window):
        pygame.draw.rect(
            window,
            self.color,
            (
                self.position.x-self.size,
                self.position.y-self.size,
                self.size*2,
                self.size*2
            ),
            0
        )

    def update(self):
        self.position.x += self.velocity.x
        self.position.y += self.velocity.y
        if not self.bounds.left+self.size < self.position.x < self.bounds.left+self.bounds.width-self.size:
            self.velocity.x *= -1
        if not self.bounds.top+self.size < self.position.y < self.bounds.top+self.bounds.height-self.size:
            self.velocity.y *= -1


def main():

    from random import randint

    window_width, window_height = 800, 500

    window = pygame.display.set_mode((window_width, window_height))
    pygame.display.set_caption("Pong")

    clock = pygame.time.Clock()

    black = (0, 0, 0)
    white = (255, 255, 255)

    padding = 20

    bounds = pygame.Rect(padding, padding, window_width-(padding*2), window_height-(padding*2))

    ball = Ball(bounds, white)

    def redraw_window():
        window.fill(black)
        pygame.draw.rect(
            window,
            white,
            (
                padding,
                padding,
                window_width-(padding*2),
                window_height-(padding*2)
            ),
            1
        )

        ball.draw(window)

        pygame.display.update()

    while True:
        clock.tick(60)
        ball.update()
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                break
        else:
            redraw_window()
            continue
        break
    pygame.quit()
    return 0
        

if __name__ == "__main__":
    import sys
    sys.exit(main())

This isn't a complete drop-in replacement, I've just reimplemented the Ball class. There are no paddles. Essentially, when instantiating a ball, you pass in a pygame.Rect, which describes the bounds in which the ball is allowed to bounce around in. You also pass in a color tuple. The ball then picks a random position within the bounds (the position is a pygame.math.Vector2, as opposed to storing x and y as separate instance variables). A ball also has a velocity, which is also a pygame.math.Vector2, so that you may have independent velocity components - one for x (horizontal velocity) and one for y (vertical velocity). The size of a ball simply describes the dimensions of the ball. If the size is set to 8, for example, then a ball will be 16x16 pixels.

The Ball class also has an update method, which is invoked once per game-loop iteration. It moves the ball to the next position dictated by the velocity, and checks to see if the ball is colliding with the bounds. If it is, reverse the corresponding velocity component.