pythonpython-3.xpygamebreakout

How to remove a rect in pygame using "colliderect" only if the rect has been "hit" x times


I'm making a version of breakout and although the base game is working, I want to make levels and such. So what I want to do is make it so that the brick isn't removed until the ball has hit it x times.

I've tried adding a counter:

 for brick in self.bricks:
        if self.ball.colliderect(brick):
            self.brick_counter += 1
            self.score += 3
            self.ball_vel[1] = -self.ball_vel[1]
            if self.brick_counter == 2:
            self.bricks.remove(brick)
            break

But the result is that the ball just passes through the brick rects.

I also thought about creating two (or more) layers of bricks, but I'm not sure how I would implement that.

Here is the complete code:

import sys
import pygame

# Global variables is usually in the top of the document
SCREEN_SIZE = 750, 550

#OBJECT ATTRIBUTES
#PADDLE ATTRIBUTES
PADDLE_WIDTH = 54
PADDLE_HEIGHT = 10

MAX_PADDLE_X = SCREEN_SIZE[0] - PADDLE_WIDTH
PADDLE_Y = SCREEN_SIZE[1] - PADDLE_HEIGHT - 10

#BRICK ATTRIBUTES
BRICK_WIDTH = 75
BRICK_HEIGHT = 15

#BALL ATTRIBUTES
BALL_DIAMETER = 16
BALL_RADIUS = BALL_DIAMETER // 2

MAX_BALL_X = SCREEN_SIZE[0] - BALL_DIAMETER
MAX_BALL_Y = SCREEN_SIZE[1] - BALL_DIAMETER

#COLORS
BLACK       = (0,   0,     0)
WHITE       = (255, 255, 255)
BLUE        = (0,   0,   255)
CYAN        = (0,   255, 255)
PINK        = (255, 102, 255)
WHITE       = (255, 255, 255)
NAVYBLUE    = ( 60,  60, 100)
RED         = (200,   0,   0)
DARKRED     = (100,   0,   0)
ORANGE      = (200, 100,   0)
DARKORANGE  = (100,  50,   0)
YELLOW      = (200, 200,   0)
DARKYELLOW  = (100, 100,   0)
GREEN       = (  0, 200,   0)
DARKGREEN   = (  0, 100,   0)
BLUE        = (  0,   0, 200)
DARKBLUE    = (  0,   0, 100)
PURPLE      = (200,   0, 200)
DARKPURPLE  = (100,   0, 100)

RAINBOW = [(RED, DARKRED),(ORANGE,DARKORANGE),(YELLOW,DARKYELLOW),
           (GREEN,DARKGREEN),(BLUE,DARKBLUE),(PURPLE,DARKPURPLE)]
BRICK_COLOR =(0,   255, 255)

# State constants
STATE_BALL_IN_PADDLE = 0
STATE_PLAYING = 1
STATE_GAME_WON = 2
STATE_GAME_OVER = 3
STATE_LEVEL_ONE = 4
STATE_LEVEL_TWO = 5
STATE_LEVEL_THREE = 6
STATE_LEVEL_ONE_WON = 7
STATE_LEVEL_TWO_WON = 8
STATE_LEVEL_THREE_WON = 9

# Initialising pygame
class Breakout:
    def __init__(self):
        pygame.init()

        self.screen = pygame.display.set_mode(SCREEN_SIZE)
        pygame.display.set_caption("Breakout")

        self.clock = pygame.time.Clock()

        if pygame.font:
            self.font = pygame.font.SysFont("impact", 20)
        else:
            self.font = None

        self.init_game()

    def init_game(self):
        self.lives = 3
        self.score = 0
        self.state = STATE_BALL_IN_PADDLE
        self.level_state = STATE_LEVEL_ONE

        self.paddle   = pygame.Rect(300,PADDLE_Y,PADDLE_WIDTH,PADDLE_HEIGHT)
        self.ball     = pygame.Rect(300,PADDLE_Y - BALL_DIAMETER,BALL_DIAMETER,BALL_DIAMETER)

        self.ball_vel = [5,-5]

        self.create_bricks()


    def draw_level_one(self):
        self.lives = 3
        self.score = 0
        self.level_state = STATE_LEVEL_ONE
        self.state = STATE_BALL_IN_PADDLE

        self.paddle = pygame.Rect(300, PADDLE_Y,PADDLE_WIDTH,PADDLE_HEIGHT)
        self.ball   = pygame.Rect(300,PADDLE_Y - BALL_DIAMETER,BALL_DIAMETER,BALL_DIAMETER)

        self.ball_vel = [5,-5]

        self.create_bricks()

    def draw_level_two(self):
        self.lives = 3
        self.score = 0
        self.level_state = STATE_LEVEL_TWO
        self.state = STATE_BALL_IN_PADDLE

        self.paddle   = pygame.Rect(300,PADDLE_Y,PADDLE_WIDTH,PADDLE_HEIGHT)
        self.ball     = pygame.Rect(300,PADDLE_Y - BALL_DIAMETER,BALL_DIAMETER,BALL_DIAMETER)

        self.ball_vel = [5,-5]

        self.create_bricks()

    def draw_level_three(self):
        self.lives = 3
        self.score = 0
        self.level_state = STATE_LEVEL_THREE
        self.state = STATE_BALL_IN_PADDLE

        self.paddle   = pygame.Rect(300,PADDLE_Y,PADDLE_WIDTH,PADDLE_HEIGHT)
        self.ball     = pygame.Rect(300,PADDLE_Y - BALL_DIAMETER,BALL_DIAMETER,BALL_DIAMETER)

        self.ball_vel = [5,-5]

        self.create_bricks()

    def create_bricks(self):
        y_ofs = 50
        self.bricks = []
        for i in range(7):
            x_ofs = 50
            for j in range(8):
                self.bricks.append(pygame.Rect(x_ofs,y_ofs,BRICK_WIDTH,BRICK_HEIGHT))
                x_ofs += BRICK_WIDTH + 10
            y_ofs += BRICK_HEIGHT + 5

    def draw_bricks(self):
        if self.level_state == STATE_LEVEL_ONE:
            for brick in self.bricks:
                pygame.draw.rect(self.screen, CYAN, brick)

        elif self.level_state == STATE_LEVEL_TWO:
            for brick in self.bricks:
                pygame.draw.rect(self.screen, GREEN, brick)

        elif self.level_state == STATE_LEVEL_THREE:
            for brick in self.bricks:
                pygame.draw.rect(self.screen, DARKRED, brick)

    def check_input(self):
        keys = pygame.key.get_pressed()

        if keys[pygame.K_LEFT]:
            self.paddle.left -= 7
            if self.paddle.left < 0:
                self.paddle.left = 0

        if keys[pygame.K_RIGHT]:
            self.paddle.left += 7
            if self.paddle.left > MAX_PADDLE_X:
                self.paddle.left = MAX_PADDLE_X

        if keys[pygame.K_SPACE] and self.state == STATE_BALL_IN_PADDLE:
            self.ball_vel = [5,-5]
            self.state = STATE_PLAYING
        elif keys[pygame.K_RETURN] and (self.state == STATE_GAME_OVER
                                            or self.state == STATE_GAME_WON):
                                            self.draw_level_one()
        if keys[pygame.K_w]:
            self.state = STATE_GAME_WON
            self.ball.left = self.paddle.left + self.paddle.width / 2
            self.ball.top  = self.paddle.top - self.ball.height

        if keys[pygame.K_l]:
            self.state = STATE_GAME_OVER
            self.ball.left = self.paddle.left + self.paddle.width / 2
            self.ball.top  = self.paddle.top - self.ball.height

        if keys[pygame.K_1]:
            self.draw_level_one()
            self.ball.left = self.paddle.left + self.paddle.width / 2
            self.ball.top  = self.paddle.top - self.ball.height

        if keys[pygame.K_2]:
            self.draw_level_two()
            self.ball.left = self.paddle.left + self.paddle.width / 2
            self.ball.top  = self.paddle.top - self.ball.height

        if keys[pygame.K_3]:
            self.draw_level_three()
            self.ball.left = self.paddle.left + self.paddle.width / 2
            self.ball.top  = self.paddle.top - self.ball.height

        if self.level_state == STATE_LEVEL_ONE_WON:
            if keys[pygame.K_KP_ENTER]:
                self.draw_level_two()
                self.ball.left = self.paddle.left + self.paddle.width / 2
                self.ball.top  = self.paddle.top - self.ball.height
        if self.level_state == STATE_LEVEL_TWO_WON:
            if keys[pygame.K_KP_ENTER]:
                self.draw_level_three()
                self.ball.left = self.paddle.left + self.paddle.width / 2
                self.ball.top  = self.paddle.top - self.ball.height
        if self.level_state == STATE_LEVEL_THREE_WON:
            if keys[pygame.K_KP_ENTER]:
                self.draw_level_one()
                self.ball.left = self.paddle.left + self.paddle.width / 2
                self.ball.top  = self.paddle.top - self.ball.height

        if keys[pygame.K_ESCAPE]:
            sys.exit()

    def move_ball (self):
        self.ball.left += self.ball_vel[0]
        self.ball.top += self.ball_vel[1]

        #bounds check
        if self.ball.left <= 0:
            self.ball.left = 0
            self.ball_vel[0] = -self.ball_vel[0]
        elif self.ball.left >= MAX_BALL_X:
            self.ball.left = MAX_BALL_X
            self.ball_vel[0] = -self.ball_vel[0]

        if self.ball.top < 0:
            self.ball.top = 0
            self.ball_vel[1] = -self.ball_vel[1]
        elif self.ball.top >= MAX_BALL_Y:
            self.ball.top = MAX_BALL_Y
            self.ball_vel[1] = -self.ball_vel[1]


    def handle_collisions(self):

        self.brick_counter = 0

        for brick in self.bricks:
            if self.ball.colliderect(brick):
                #self.brick_counter += 1
                self.score += 3
                self.ball_vel[1] = -self.ball_vel[1]
                #if self.brick_counter == 2:
                self.bricks.remove(brick)
                break

        if len(self.bricks) == 0 or self.state == STATE_GAME_WON:
            if self.level_state == STATE_LEVEL_ONE:
                self.level_state == STATE_LEVEL_ONE_WON
            elif self.level_state == STATE_LEVEL_TWO:
                self.level_state == STATE_LEVEL_TWO_WON
            elif self.level_state == STATE_LEVEL_THREE:
                self.level_state == STATE_LEVEL_THREE_WON

        if self.ball.colliderect(self.paddle):
            self.ball.top = PADDLE_Y - BALL_DIAMETER
            self.ball_vel[1] = -self.ball_vel[1]
        elif self.ball.top > self.paddle.top:
            self.lives -= 1
            if self.lives > 0:
                self.state = STATE_BALL_IN_PADDLE
            else:
                self.state = STATE_GAME_OVER

    def show_stats(self):
        if self.font:
            font_surface = self.font.render("SCORE: " + str(self.score)
                                             + " LIVES: " + str(self.lives), False, WHITE)
            self.screen.blit(font_surface, (205, 5))

    def show_message(self, message):
        if self.font:
            size = self.font.size(message)
            font_surface = self.font.render(message, False, WHITE)
            x = (SCREEN_SIZE[0] - size[0]) /2
            y = (SCREEN_SIZE[1] - size[1]) /2
            self.screen.blit(font_surface, (x,y))

    def run(self):
        while 1:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    sys.exit()

            self.clock.tick(60)
            self.screen.fill(BLACK)
            self.check_input()
            self.handle_collisions()

            if self.state == STATE_PLAYING:
                self.move_ball()
                self.handle_collisions()
            elif self.state == STATE_BALL_IN_PADDLE:
                self.ball.left = self.paddle.left + self.paddle.width / 2
                self.ball.top = self.paddle.top - self.ball.height
                self.show_message("PRESS SPACE TO LAUNCH THE BALL")
            elif self.state == STATE_GAME_OVER:
                self.show_message("GAME OVER. PRESS ENTER TO PLAY AGAIN")
            elif self.state == STATE_GAME_WON:
                self.show_message("YOU WON! PRESS ENTER TO GO TO THE NEXT LEVEL")

            self.draw_bricks()

            #Draw paddle
            pygame.draw.rect(self.screen, PINK, self.paddle)

            #DraW ball
            pygame.draw.circle(self.screen, WHITE, (self.ball.left + BALL_RADIUS,
                               self.ball.top + BALL_RADIUS), BALL_RADIUS)

            self.show_stats()

            pygame.display.flip()

if __name__ == "__main__":
    Breakout().run()

Solution

  • It looks like you're using a pygame.Rect as your brick objects and a single global counter for how many times a brick has been hit. Once any brick gets hit twice, then the counter no longer serves its purpose properly. What you really should do is instead create a Brick class that has both a rectangle and a counter, and update the counter for the brick.

    class Brick:
      def __init__(self, rect):
        self.rect = rect
        self.hit_counter = 2
    

    And then self.bricks would be a list of these objects instead.

    Also, it looks like you're assuming the brick will collide from the bottom or top and cause the y component of the velocity to invert. If it collides from the side, the bounce will look kind of weird and may be contributing to the sensation that the ball is "just passing through". At the time of the collision, you'll have to look at where the ball is relative to the brick and make a decision to either flip the x component of the velocity or the y component.

    Something like this (sorry, I'm probably not matching your fields exactly, but you get the idea):

    dx = brick.centerx - ball.centerx
    dy = brick.centery - ball.centery
    if abs(dx) > abs(dy):
      self.ball_vel[0] *= -1
    else:
      self.ball_vel[1] *= -1