Python Forum
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
breakout clone
#1
Hi,
I'm new to python/pygame and I'm trying to program a breakout clone.
I've got an error message: self = {Paddle} Unable to get repr for <class 'paddle.Paddle'>
I occurs in line 6 of class Paddle (super(Paddle, self).__init__()).
I'm not experienced and don't understand this error...
Please be so kind and help me...
Thanks a lot...

import pygame


class Paddle(pygame.sprite.Sprite):
    def __init__(self, playfield_rect):
        super(Paddle, self).__init__()
        self.image = pygame.image.load('images/ship_fertig.png').convert()
        self.rect = self.image.get_rect()
        self.rect.centerx = self.rect.x
        self.playfield_rect = playfield_rect
        self.rect.bottom = self.playfield_rect.bottom

    def update2(self, position):
        x, _ = position
        self.rect.centerx = x
        self.rect = self.rect.clamp(self.playfield_rect)
# !/usr/bin/env python3
import pygame, random, math
from ball import Ball
from brick import Brick
from paddle import Paddle
pygame.init()
WHITE = (255, 255, 255)
DARKBLUE = (0, 0, 139)
Screen_Width = 800
Screen_Height = 600
screen = pygame.display.set_mode((Screen_Width, Screen_Height))
pygame.display.set_caption("Breakout")
pygame.mixer.init()
pygame.mixer.music.load('images/Ketsa - Holding The Line.mp3')
pygame.mixer.music.set_volume(0.7)
pygame.mixer.music.play(-1)


def main():
    try:
        paddle = Paddle(screen.get_rect())
        ball = Ball()
        score = 0
        lives = 5
        brickgroup = pygame.sprite.Group()
        brick_coord_list = [
            32, 64, 32, 96, 32, 128, 32, 160
        ]
        i = 0
        while i < len(brick_coord_list):
            brick = Brick(brick_coord_list[i], brick_coord_list[i + 1])
            brickgroup.add(brick)
            i = i + 2

        paddlegroup = pygame.sprite.Group()
        paddlegroup.add(paddle)
        ballgroup = pygame.sprite.Group()
        ballgroup.add(ball)

        all_spritesgroup = pygame.sprite.Group()
        all_spritesgroup.add(paddlegroup, ballgroup, brickgroup)

        # to control how fast the screen updates
        clock = pygame.time.Clock()
        while True:
            # Limit to 60 frames per second
            clock.tick(60)
            # update object
            lives = ball.move(lives)
            if lives == 0:
                font = pygame.font.Font(None, 74)
                text = font.render("Game over", 1, WHITE)
                screen.blit(text, (250, 300))
                pygame.display.flip()
                pygame.time.wait(3000)
                # stop the game
                return
            # check for collisions
            # ball / paddle
            if ball.collpaddle(paddlegroup):
                ball.bounce()
            # ball / brick
            for brick in brickgroup:
                if ball.collbrick(brickgroup):
                    ball.bounce()
                    score = brick.takehit(score)
                if len(brickgroup) == 0:
                    font = pygame.font.Font(None, 74)
                    text = font.render("Level complete", 1, WHITE)
                    screen.blit(text, (200, 300))
                    pygame.display.flip()
                    pygame.time.wait(3000)
                    # stop the game
                    return
                screen.fill(DARKBLUE)
                font = pygame.font.Font(None, 34)
                text = font.render("Score: " + str(score), 1, WHITE)
                screen.blit(text, (20, 10))
                text = font.render("Lives: " + str(lives), 1, WHITE)
                screen.blit(text, (650, 10))

                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        return
                    elif event.type == pygame.MOUSEMOTION:
                        paddle.update2(event.pos)
                all_spritesgroup.draw(screen)
                pygame.display.flip()
    finally:
        pygame.quit()


if __name__ == "__main__":
    main()
import math
import random, pygame
Screen_Width = 800
Screen_Height = 600


class Ball(pygame.sprite.Sprite):
    def __init__(self):
        super(Ball, self).__init__()
        self.image = pygame.image.load('images/ballblue.png').convert()
        self.rect = self.image.get_rect()
        self.rect.x = 10
        self.rect.y = 300
        self.x = self.rect.x
        self.y = self.rect.y
        # random direction of the ball
        self.dx = random.randint(4, 6)
        self.dy = random.randint(4, 6)
        if self.dx != 0:
            pass
        else:
            self.dx = 4
        if self.dy != 0:
            pass
        else:
            self.dy = 4
        self.width = 15
        self.height = 15

    def move(self, lives):
        self.x = self.x + self.dx
        self.y = self.y + self.dy

        # Check border collision
        if self.x >= Screen_Width - self.width / 2:
            self.dx = -self.dx
        if self.x <= 0:
            self.dx = -self.dx
        if self.y <= 0:
            self.dy = -self.dy
        if self.y >= Screen_Height - self.height / 2:
            self.bounce()
            lives -= 1
        return lives

    def bounce(self):
        self.dx = self.dx
        self.dy = -self.dy

    def collpaddle(self, a):
        collide = pygame.sprite.spritecollide(self, a, False)
        return collide

    def collbrick(self, a):
        collide = pygame.sprite.spritecollide(self, a, False)
        return collide
import pygame, random


class Brick(pygame.sprite.Sprite):
    def __init__(self, a, b):
        super(Brick, self).__init__()
        # random number for blue and green Brick
        randomnumber = random.randint(1, 2)
        if randomnumber == 1:
            self.image = pygame.image.load('images/greenbrick.png').convert()
        else:
            self.image = pygame.image.load('images/bluebrick.png').convert()
        self.rect = self.image.get_rect()
        self.rect.x = a
        self.rect.y = b
        self.hits = 3

    def takehit(self, score):
        # the ball has collided with *this* brick
        self.hits -= 1
        if self.hits == 0:
            self.kill()
            score += 1
        return score
Reply
#2
The paddle works fine for me. How are you running the code when you get this error?

Ball is not updating the rectangle, so the image of the ball does not move. I would get rid of x and y and use rect.x and rect.y.
class Ball(pygame.sprite.Sprite):
    """Ball bounces around colliding with walls, paddles and bricks"""
    def __init__(self, x=10, y=300):
        super().__init__()
        self.image = pygame.image.load('dice1.png').convert()
        self.rect = self.image.get_rect()
        self.xstart = x
        self.ystart = y
        self.start(self.xstart, self.ystart)

    def start(self, x, y):
        """Start moving the ball in a random direction"""
        self.rect.x = x
        self.rect.y = y
        self.dx = random.randint(4, 6)
        self.dy = random.randint(4, 6)

    def move(self, lives):
        """
        Update the ball position.  If "collides" with bottom of the screen
        subtract 1 life.
        """
        self.rect.x += self.dx
        self.rect.y += self.dy
        if self.rect.x < 0 or self.rect.x > Screen_Width - self.rect.width:
            self.dx = -self.dx
        if self.rect.y < 0:
            self.dy = -self.dy
        if self.rect.y >= Screen_Height - self.rect.height:
            self.start(self.xstart, self.ystart)
            lives -= 1
        return lives

    def bounce(self):
        """
        Bounce the ball because it collided with an obstacle.
        Needs some work.
        """
        self.dx = self.dx
        self.dy = -self.dy

    def collide(self, group):
        """Return list of sprites I ran into"""
        return pygame.sprite.spritecollide(self, group, False)
The two ball "collides" were identical.

Ball.collbrick() returns a list of brick sprites that the ball collided with, it does not test if the ball collided with a particular brick. You should processes the collision like this:
            bricks = ball.collide(brickgroup)
            if bricks:
                ball.bounce()  # Need to modify this to allow hitting bricks on the side or the top
                for brick in bricks:
                    score = brick.takehit(score)
In the context of the main loop:
        game_over = False
        while not game_over:
            clock.tick(60)

            lives = ball.move(lives)
            game_over = lives == 0

            if ball.collide(paddlegroup):
                ball.bounce()

            bricks = ball.collide(brickgroup)
            if bricks:
                # There was a collition with one or more bricks
                ball.bounce()
                for brick in bricks:
                    score = brick.takehit(score)
                if len(brickgroup) == 0:
                    game_over = True

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    game_over = True
                    break
                elif event.type == pygame.MOUSEMOTION:
                    paddle.move(event.pos[0])

            screen.fill(DARKBLUE)
            font = pygame.font.Font(None, 34)
            text = font.render("Score: " + str(score), 1, WHITE)
            screen.blit(text, (20, 10))
            text = font.render("Lives: " + str(lives), 1, WHITE)
            screen.blit(text, (650, 10))
            all_spritesgroup.draw(screen)
            pygame.display.flip()

        # Game over
        font = pygame.font.Font(None, 74)
        if lives == 0:
            text = font.render("Game over", 1, WHITE)
            screen.blit(text, (250, 300))
        elif len(brickgroup) == 0:
            text = font.render("Level complete", 1, WHITE)
            screen.blit(text, (200, 300))
        else:
            text = font.render("Quitter!!", 1, WHITE)
            screen.blit(text, (200, 300))
        pygame.display.flip()
        pygame.time.wait(3000)
Reply
#3
Hello deanhystad,
thanks a lot for your very good answer. I do appreciate the effort you made very much, thanks...
Because of professionaly reasons I will be able to concentrate on your answer at weekend.
I'm very happy to achieve some support in getting the game runnning...
Have a nice evening...
Hello deanhystad,

I tried as hard as I could...
What do you think of the code?
What can I do better?
Here is my attempt:

main:
# !/usr/bin/env python3
import pygame, random, math
from ball import Ball
from brick import Brick
from paddle import Paddle
pygame.init()
WHITE = (255, 255, 255)
DARKBLUE = (0, 0, 139)
Screen_Width = 800
Screen_Height = 600
screen = pygame.display.set_mode((Screen_Width, Screen_Height))
pygame.display.set_caption("Breakout")
pygame.mixer.init()
pygame.mixer.music.load('images/Ketsa - Holding The Line.mp3')
pygame.mixer.music.set_volume(0.7)
pygame.mixer.music.play(-1)


def main():
    try:
        paddle = Paddle(screen.get_rect())
        ball = Ball()
        score = 0
        lives = 5
        brickgroup = pygame.sprite.Group()
        brick_coord_list = [
            32, 64, 32, 96, 32, 128, 32, 160
        ]
        i = 0
        while i < len(brick_coord_list):
            brick = Brick(brick_coord_list[i], brick_coord_list[i + 1])
            brickgroup.add(brick)
            i = i + 2

        paddlegroup = pygame.sprite.Group()
        paddlegroup.add(paddle)
        ballgroup = pygame.sprite.Group()
        ballgroup.add(ball)

        all_spritesgroup = pygame.sprite.Group()
        all_spritesgroup.add(paddlegroup, ballgroup, brickgroup)

        # to control how fast the screen updates
        clock = pygame.time.Clock()
        game_over = False
        while not game_over:
            # Limit to 60 frames per second
            clock.tick(60)
            # update object
            lives = ball.move(lives)
            if lives == 0:
                font = pygame.font.Font(None, 74)
                text = font.render("Game over", 1, WHITE)
                screen.blit(text, (250, 300))
                pygame.display.flip()
                pygame.time.wait(3000)
                # stop the game
                return
            # check for collisions
            # ball / paddle
            paddlebounce = ball.collide(paddlegroup)
            # ball / bricks
            bricks = ball.collide(brickgroup)
            ball.bounce(paddlebounce, bricks)
            if bricks:
                for brick in bricks:
                    score = brick.takehit(score)
            if len(brickgroup) == 0:
                game_over = True

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    break
                elif event.type == pygame.MOUSEMOTION:
                    paddle.move(event.pos)
            screen.fill(DARKBLUE)
            font = pygame.font.Font(None, 34)
            text = font.render("Score: " + str(score), 1, WHITE)
            screen.blit(text, (20, 10))
            text = font.render("Lives: " + str(lives), 1, WHITE)
            screen.blit(text, (650, 10))
            all_spritesgroup.draw(screen)
            pygame.display.flip()

        # Game Over
        font = pygame.font.Font(None, 74)
        if lives == 0:
            text = font.render("Game Over", 1, WHITE)
            screen.blit(text, (200, 300))
        elif len(brickgroup) == 0:
            text = font.render("Level complete!", 1, WHITE)
            screen.blit(text, (200, 300))
        else:
            text = font.render("Abbruch...", 1, WHITE)
            screen.blit(text, (200, 300))
        pygame.display.flip()
        pygame.time.wait(3000)
    finally:
        pygame.quit()


if __name__ == "__main__":
    main()
paddle:
import pygame


class Paddle(pygame.sprite.Sprite):
    def __init__(self, playfield_rect):
        super(Paddle, self).__init__()
        self.image = pygame.image.load('images/ship_fertig.png').convert()
        self.rect = self.image.get_rect()
        self.rect.centerx = self.rect.x
        self.playfield_rect = playfield_rect
        self.rect.bottom = self.playfield_rect.bottom

    def move(self, position):
        x, _ = position
        self.rect.centerx = x
        self.rect = self.rect.clamp(self.playfield_rect)
ball:
import math
import random, pygame
Screen_Width = 800
Screen_Height = 600


class Ball(pygame.sprite.Sprite):
    def __init__(self, x=10, y=300):
        super().__init__()
        self.image = pygame.image.load('images/ballblue.png').convert()
        self.rect = self.image.get_rect()
        self.rect.width = 15
        self.rect.height = 15
        self.xstart = x
        self.ystart = y
        self.start(self.xstart, self.ystart)

    def start(self, x, y):
        self.rect.x = x
        self.rect.y = y
        self.dx = random.randint(4, 6)
        self.dy = random.randint(4, 6)

    def move(self, lives):
        self.rect.x += self.dx
        self.rect.y += self.dy

        # Check border collision
        if self.rect.x < 0 or self.rect.x > Screen_Width - self.rect.width:
            self.dx = -self.dx
        if self.rect.y < 0:
            self.dy = -self.dy
        if self.rect.y > Screen_Height - self.rect.height:
            lives -=1
            self.start(self.xstart, self.ystart)
        return lives

    def bounce(self, paddlebounce, bricks):
        if paddlebounce:
            self.dy = -self.dy
        if bricks:
            for brick in bricks:
                if self.rect.x > brick.rect.right or self.rect.x < brick.rect.left:
                    self.dx = -self.dx
                else:
                    self.dy = self.dy

    def collide(self, group):
        return pygame.sprite.spritecollide(self, group, False)
brick:
import pygame, random


class Brick(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        # random number for blue and green Brick
        randomnumber = random.randint(1, 2)
        if randomnumber == 1:
            self.image = pygame.image.load('images/greenbrick.png').convert()
        else:
            self.image = pygame.image.load('images/bluebrick.png').convert()
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.hits = 3

    def takehit(self, score):
        # the ball has collided with *this* brick
        self.hits -= 1
        if self.hits == 0:
            self.kill()
            score += 1
        return score
My Questions:
for Ball class:
Should I use self.rect.x or self.rect.centerx ?

I'm trying to get every brick vanishing after 3 hits, because I would like to give the game to some persons
(I draw the name of each person using the bricks). And some time should pass by so the person can read its name...

How to get every brick gets vanishing after 3 hits?
This doesn't work proberly...

I politely ask for help...
Reply
#4
"What can I do better?" is a lazy question. Please ask specific questions.

The first thing you should do is put all the code in one module. I and others would be more inclined to help if we could copy your posted code directly into our editor and run it. This is not a long enough program to need multiple modules.

I think bounce is not quite right. You can put all the bounces in one function (I would add a wall bounce), but I think it clearer if you have wall_bounce(), paddle_bounce() and brick_bounce().

I think paddle bounce "self.dy = -self.dy" makes for a dull game. If the ball hits in the center of the panel then dy = -dy, but if the ball hits off center you should adjust the relative lateral and vertical velocities. This would give the player some ability to aim the ball instead of just blocking the ball.

The bricks bounce looks wrong. You are going to hit a brick on the top, on the bottom, on the left side, or on the right side. You need a test for each. I would start by trying these (pseudo code):
if brick.left <= ball.center_x <= brick.right:
    # Hit top or bottom
    ball.dy = -ball.dy
else:
    # Hit side
    ball.dx = -ball.dx
I also question if it makes sense to do all the bricks. I would run your code to find out if that was easy for me to do.

I don't understand this: "How to get every brick gets vanishing after 3 hits?' Should a brick vanish after it is hit 3 times? Should all the bricks vanish after you hit a particular brick three times? Should all the bricks vanish after you hit any brick three times? Should all the bricks vanish after 3 brick hits? I suspect the first (hit a brick 3 times and it vanishes), but I also expect your code to do that.

This code:
            if bricks:
                for brick in bricks:
                    score = brick.takehit(score)
In conjunction with this code:
    def takehit(self, score):
        # the ball has collided with *this* brick
        self.hits -= 1
        if self.hits == 0:
            self.kill()
            score += 1
        return score
Should result in a brick disappearing after it "takehit()" 3 times
Reply
#5
Hello,
I put the code in one module and modified the code.
I want each brick to vanish after it is hit 3 times.
Actually the bricks vanish after 1 hit.
Is it correct to define self.hits = 3 in def __init__(self, x, y): ?
I want that each brick starts with a "strength" of 3 (can take 3 hits). When a brick is being hit the amount of strength is decreased by 1 until the strength == 0 (the brick is killed).

Could you give me a hint why the bricks don't vanish after 3 hits?

Thanks a lot...


# !/usr/bin/env python3
, brick):import pygame, random, math

pygame.init()
WHITE = (255, 255, 255)
DARKBLUE = (0, 0, 139)
Screen_Width = 800
Screen_Height = 600
screen = pygame.display.set_mode((Screen_Width, Screen_Height))
pygame.display.set_caption("Breakout")
pygame.mixer.init()
pygame.mixer.music.load('images/Ketsa - Holding The Line.mp3')
pygame.mixer.music.set_volume(0.7)
pygame.mixer.music.play(-1)


def main():
    try:

        class Brick(pygame.sprite.Sprite):
            def __init__(self, x, y):
                super().__init__()
                # random number for blue and green Brick
                randomnumber = random.randint(1, 2)
                if randomnumber == 1:
                    self.image = pygame.image.load('images/greenbrick.png').convert()
                else:
                    self.image = pygame.image.load('images/bluebrick.png').convert()
                self.rect = self.image.get_rect()
                self.rect.x = x
                self.rect.y = y
                self.hits = 3

            def takehit(self, score):
                # the ball has collided with *this* brick
                self.hits -= 1
                if self.hits == 0:
                    self.kill()
                    score += 1
                return score

        class Paddle(pygame.sprite.Sprite):
            def __init__(self, playfield_rect):
                super(Paddle, self).__init__()
                self.image = pygame.image.load('images/ship_fertig.png').convert()
                self.rect = self.image.get_rect()
                self.rect.centerx = self.rect.x
                self.playfield_rect = playfield_rect
                self.rect.bottom = self.playfield_rect.bottom

            def move(self, position):
                x, _ = position
                self.rect.centerx = x
                self.rect = self.rect.clamp(self.playfield_rect)

        class Ball(pygame.sprite.Sprite):
            def __init__(self, x=10, y=300):
                super().__init__()
                self.image = pygame.image.load('images/ballblue.png').convert()
                self.rect = self.image.get_rect()
                self.rect.width = 15
                self.rect.height = 15
                self.xstart = x
                self.ystart = y
                self.start(self.xstart, self.ystart)

            def start(self, x, y):
                self.rect.x = x
                self.rect.y = y
                self.dx = random.randint(4, 6)
                self.dy = random.randint(4, 6)

            def move(self, lives):
                self.rect.x += self.dx
                self.rect.y += self.dy

                # Check border collision
                if self.rect.x < 0 or self.rect.x > Screen_Width - self.rect.width:
                    self.dx = -self.dx
                if self.rect.y < 0:
                    self.dy = -self.dy
                if self.rect.y > Screen_Height - self.rect.height:
                    lives -= 1
                    self.start(self.xstart, self.ystart)
                return lives

            def paddlebounce(self):
                self.dy = -self.dy

            def bricksbounce(self, brick):
                # collision left
                if self.rect.centerx <= brick.rect.left and self.rect.centery <= brick.rect.top and self.rect.centery >= brick.rect.bottom:
                    self.dx = -self.dx
                # collision right
                if self.rect.centerx >= brick.rect.right and self.rect.centery <= brick.rect.top and self.rect.centery >= brick.rect.bottom:
                    self.dx = -self.dx
                # collision top
                if self.rect.centery >= brick.rect.top and self.rect.centerx >= brick.rect.left and self.rect.centerx <= brick.rect.right:
                    self.dy = -self.dy
                # collision bottom
                if self.rect.centery <= brick.rect.bottom and self.rect.centerx >= brick.rect.left and self.rect.centerx <= brick.rect.right:
                    self.dy = -self.dy

            def collide(self, group):
                return pygame.sprite.spritecollide(self, group, False)

        paddle = Paddle(screen.get_rect())
        ball = Ball()
        score = 0
        lives = 5
        brickgroup = pygame.sprite.Group()
        brick_coord_list = [
            32, 64, 32, 96, 32, 128, 32, 160
        ]
        i = 0
        while i < len(brick_coord_list):
            brick = Brick(brick_coord_list[i], brick_coord_list[i + 1])
            brickgroup.add(brick)
            i = i + 2

        paddlegroup = pygame.sprite.Group()
        paddlegroup.add(paddle)
        ballgroup = pygame.sprite.Group()
        ballgroup.add(ball)

        all_spritesgroup = pygame.sprite.Group()
        all_spritesgroup.add(paddlegroup, ballgroup, brickgroup)

        # to control how fast the screen updates
        clock = pygame.time.Clock()
        game_over = False
        while not game_over:
            all_spritesgroup.update()
            # Limit to 60 frames per second
            clock.tick(60)
            lives = ball.move(lives)
            if lives == 0 or len(brickgroup) == 0:
                game_over = True
                break
            # check for collisions
            # ball / paddle
            paddlebounce = ball.collide(paddlegroup)
            if paddlebounce:
                ball.paddlebounce()
            # ball / bricks
            bricksbounce = ball.collide(brickgroup)
            if bricksbounce:
                for brick in bricksbounce:
                    ball.bricksbounce(brick)
                    score = brick.takehit(score)

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    break
                elif event.type == pygame.MOUSEMOTION:
                    paddle.move(event.pos)
            screen.fill(DARKBLUE)
            font = pygame.font.Font(None, 34)
            text = font.render("Score: " + str(score), 1, WHITE)
            screen.blit(text, (20, 10))
            text = font.render("Lives: " + str(lives), 1, WHITE)
            screen.blit(text, (650, 10))
            all_spritesgroup.draw(screen)
            pygame.display.flip()

        # Game Over
        font = pygame.font.Font(None, 74)
        if lives == 0:
            text = font.render("Game Over", 1, WHITE)
            screen.blit(text, (200, 300))
        elif len(brickgroup) == 0:
            text = font.render("Level complete!", 1, WHITE)
            screen.blit(text, (200, 300))
        else:
            text = font.render("Abbruch...", 1, WHITE)
            screen.blit(text, (200, 300))
        pygame.display.flip()
        pygame.time.wait(3000)

    finally:
        pygame.quit()


if __name__ == "__main__":
    main()
Reply
#6
If the ball strikes multiple blocks you change the ball direction multiple times. Lets say you hit bricks 1 and 2 on the bottom. This code is executed twice:
                if self.rect.centery <= brick.rect.bottom and self.rect.centerx >= brick.rect.left and self.rect.centerx <= brick.rect.right:
                    self.dy = -self.dy
self.dy ends up with the same value as before , so the ball continues to drive into the bricks. The next time through the main loop the ball is even further into the bricks, so we get two more collisions and the ball direction is again reversed, then reversed. After a third time through the main loop the bricks are killed and the ball can continue on.

I rolled your collide code together with the bounce code and put it all inside the Ball class.
    def brick_collide(self, score):
        bricks = pygame.sprite.spritecollide(self, Brick.group, False)
        dx = dy = 1
        for brick in bricks:
            # Is the outside of the ball closer to the top/bottom or the left/right edge of the brick?
            x = min(abs(self.rect.right - brick.rect.left), abs(self.rect.left - brick.rect.right))
            y = min(abs(self.rect.top - brick.rect.bottom), abs(self.rect.bottom - brick.rect.top))
            # Only reverse x or y direction once.  Striking two bricks on the bottom must not do a double reversal
            if x < y:
                dx = -1
            else:
                dy = -1
            score = brick.take_hit(score)
        self.dx *= dx
        self.dy *= dy
        return score
Another thing I tried is setting dx and dy based on what side of the brick the ball it. If striking the bottom of the brick dy is made positive. If striking the left side of the brick dx is made negative. Both worked pretty well.
    def brick_collide(self):
        bricks = pygame.sprite.spritecollide(self, self.bricks, False)
        x, y = self.centerx, self.centery
        for brick in bricks:
            if y < brick.top:
                self.dy = -abs(self.dy)
            elif y > brick.bottom:
                self.dy = abs(self.dy)
            elif x > brick.centerx:
                self.dx = abs(self.dx)
            else:
                self.dx = -abs(self.dx)
            brick.hit(1)
I also rolled the paddle collide and bounce into Ball.
    def paddle_collide(self, paddle):
        if pygame.sprite.spritecollide(self, paddle.group, False):
            if self.dy > 0:
                self.dy = -self.dy
Paddle collide needs work. This game is really boring when you can't influence the direction of the ball.

You're probably wondering about Brick.group and paddle.group. I moved the sprite group out of the main program into the Ball, Brick and Paddle classes. For example. Brick now looks like this:
class Brick(pygame.sprite.Sprite):
    group = pygame.sprite.Group()  # Making the brick group here

    def __init__(self, x, y):
        super().__init__()
        self.image = pygame.image.load('brick.png').convert()
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.hits = 3
        self.group.add(self)  # Adding bricks to the group here

    def take_hit(self, score):
        # the ball has collided with *this* brick
        self.hits -= 1
        if self.hits == 0:
            self.kill()
            score += 1
        return score
This cleans up the setup code before the main loop.
        score = 0
        lives = 5
        paddle = Paddle()
        ball = Ball(paddle)
        for x in range(0, Screen_Width, 100):  # My bricks are a 100 pixels wide
            Brick(x, 0)
        all_sprites = pygame.sprite.Group()
        all_sprites.add(paddle.group, ball.group, Brick.group)
In addition to changing the paddle bounce so you can influence the angle of bounce, I changed the ball to release from the paddle and go up (paddle serves on start instead of receiving). This helps get that last pesky brick and makes the player feel like they are in control of the game instead of always reacting.
Reply
#7
I've had fun playing around with your code and came up with this:
import pygame, random, math
from PIL import Image
from itertools import product

# Define colors used by the game.
TEXT_COLOR = (255, 255, 255)
BACKGROUND = (0, 0, 200)
FOREGROUND = (0, 0, 0)         # Recolor image pixels that are this color
TRANSPARENT = (255, 255, 255)  # Make image pixels this color transparent
BALL_COLOR = (220, 220, 220)
PADDLE_COLOR = (255, 255, 0)
BRICK_COLORS = ((255, 0, 0), (255, 50, 0), (255, 100, 0), (255, 150, 0), (255, 200, 0), (255, 255, 0))

# Define some image files
BALL_IMAGE = "ball.png"
PADDLE_IMAGE = "paddle.png"
BRICK_FILES = ("brick0.png", "brick1.png", "brick2.png")

Screen_Width = 600
Screen_Height = 800

def create_image(image_file, color=None):
    """
    Create image from a file.  If color is specified, replace all FOREGROUND
    pixels with color pixels.  Modify image so TRANSPARENT colored pixels are
    transparent.
    """
    if color:
        # Recolor the image
        image = Image.open(image_file)
        for xy in product(range(image.width), range(image.height)):
            if image.getpixel(xy) == FOREGROUND:
                image.putpixel(xy, color)
        image = pygame.image.fromstring(image.tobytes(), image.size, image.mode)
    else:
        image = pygame.image.load(image_file)
    image.set_colorkey(TRANSPARENT)
    return image.convert()

class EnhancedSprite(pygame.sprite.Sprite):
    """
    Sprite with image and rectangle.  I expose some of my rectangle's
    properties.
    """
    def __init__(self, image, group = None, **kwargs):
        super().__init__(**kwargs)
        self.image = image
        self.rect = image.get_rect()
        if group is not None:
            group.add(self)

    def at(self, x, y):
        """Convenience method for setting my position"""
        self.x = x
        self.y = y

    # Properties below expose properties of my rectangle so you can use
    # self.x = 10 or self.centery = 30 instead of self.rect.x = 10
    @property
    def x(self):
        return self.rect.x

    @x.setter
    def x(self, value):
        self.rect.x = value

    @property
    def y(self):
        return self.rect.y

    @y.setter
    def y(self, value):
        self.rect.y = value

    @property
    def centerx(self):
        return self.rect.centerx

    @centerx.setter
    def centerx(self, value):
        self.rect.centerx = value

    @property
    def centery(self):
        return self.rect.centery

    @centery.setter
    def centery(self, value):
        self.rect.centery = value

    @property
    def right(self):
        return self.rect.right

    @right.setter
    def right(self, value):
        self.rect.right = value

    @property
    def bottom(self):
        return self.rect.bottom

    @bottom.setter
    def bottom(self, value):
        self.rect.bottom = value


class Brick(EnhancedSprite):
    """
    A target for the ball.  After 3 hits I die.
    I change my image to reflect how many hits I've taken.
    """
    group = pygame.sprite.Group()
    IMAGES = {}  # Dictionary of images.  Similar colored bricks share images

    def __init__(self, row, col):
        # Set brick color
        self.color = BRICK_COLORS[row % len(BRICK_COLORS)]  # based on row
        # self.color = BRICK_COLORS[col % len(BRICK_COLORS)] # based on column
        # self.color = random.choice(BRICK_COLORS) # random choice
        self.hits = 0
        super().__init__(self.get_image(), self.group)
        self.at(col * self.rect.width, row * self.rect.height * 2 + 60)

    def get_image(self):
        """Return an image based on my color and number of hits."""
        images = self.IMAGES.get(self.color, None)
        if images is None:
            # Make brick images for this color
            images = [create_image(image_file, self.color) for image_file in BRICK_FILES]
            self.IMAGES[self.color] = images
        return images[self.hits]

    def __len__(self):
        """Return how many bricks remaining"""
        return len(self.group)

    def hit(self, score):
        """
        I was hit!  Update my appearance or die based on my hit total.
        Increment the score if I died.
        """
        self.hits += 1
        if self.hits > 2:
            self.kill()
            score += 1
        else:
            self.image = self.get_image()
        return score


class Paddle(EnhancedSprite):
    """The sprite the player moves around to redirect the ball"""
    group = pygame.sprite.Group()

    def __init__(self):
        super().__init__(create_image(PADDLE_IMAGE, PADDLE_COLOR), self.group)
        self.centerx = Screen_Width // 2
        self.bottom = Screen_Height - 40

    def move(self, x):
        """Move to follow the cursor"""
        self.centerx = x


class LifeCounter():
    """Keep track of lives count.  Display lives remaining"""
    def __init__(self, x, y, count=5):
        self.image = create_image(BALL_IMAGE, BALL_COLOR)
        self.x = x
        self.y = y
        self.group = pygame.sprite.Group()
        self.reset(count)

    def reset(self, count):
        """Reset number of lives"""
        for c in range(count):
            EnhancedSprite(self.image, self.group).at(self.x + c * (self.image.get_width() + 5), self.y)

    def __len__(self):
        """Return number of lives remaining"""
        return len(self.group)

    def kill(self):
        """Reduce number of lives"""
        self.group.sprites()[-1].kill()


class Ball(EnhancedSprite):
    """Ball bounces around colliding with walls, paddles and bricks"""
    group = pygame.sprite.Group()

    def __init__(self, paddle, lives, speed=10):
        super().__init__(create_image(BALL_IMAGE, BALL_COLOR), self.group)
        self.paddle = paddle
        self.lives = lives
        self.speed = speed
        self.dx = self.dy = 0
        self.reset(0)

    def reset(self, score=None):
        """Reset for a new game"""
        self.active = False
        if score is not None:
            self.score = score

    def start(self):
        """Start moving the ball in a random direction"""
        self.centerx = self.paddle.centerx
        self.bottom = self.paddle.y - 2
        angle = (random.random() - 0.5) * math.pi / 2
        self.dx = int(self.speed * math.sin(angle))
        self.dy = -int(self.speed * math.cos(angle))
        self.active = True

    def update(self):
        """Update the ball position"""
        if not self.active:
            # Sit on top of the paddle
            self.centerx = self.paddle.centerx
            self.bottom = self.paddle.y - 5

        # Ball is active.  Move the ball
        self.x += self.dx
        self.y += self.dy

        # Did I hit a wall?
        if self.x <= 0:
            self.dx = abs(self.dx)
        if self.right >= Screen_Width:
            self.dx = -abs(self.dx)
        if self.y < 0:
            self.dy = abs(self.dy)

        # Did I get past the paddle?
        if self.centery > self.paddle.centery:
            self.lives.kill()
            self.active = False


        # Did I hit the paddle?  Change angle of reflection based on where
        # I hit the paddle.
        if pygame.sprite.spritecollide(self, self.paddle.group, False) and self.dy > 0:
            bangle = math.atan2(-self.dx, self.dy)# Angle of ball
            pangle = math.atan2(self.centerx - self.paddle.centerx, 50) # Angle fo paddle
            angle = (pangle - bangle) / 2  # Angle of reflection rotated 90 degrees CW
            self.dx = int(math.sin(angle) * self.speed)
            self.dy = -int(math.cos(angle) * self.speed)


        # Did I hit some bricks?  Update the bricks and the score
        bricks = pygame.sprite.spritecollide(self, Brick.group, False)
        for brick in bricks:
            self.score = brick.hit(self.score)
            # Where I hit the brick determines how I bounce
            if brick.y < self.centery < brick.bottom:
                # Ball hit left or right side.  Bounce in x direction
                self.dx = abs(self.dx) if self.centerx > brick.centerx else -abs(self.dx)
            else:
                # Ball hit top or bottom.  Bounce in y direction
                self.dy = abs(self.dy) if self.centery > brick.centery else -abs(self.dy)


pygame.init()
screen = pygame.display.set_mode((Screen_Width, Screen_Height))
pygame.display.set_caption("Breakout")

def main():
    """Play game until out of lives or out of bricks"""
    try:
        lives = LifeCounter(10, Screen_Height - 30)
        paddle = Paddle()
        ball = Ball(paddle, lives)
        for r in range(6):
            for c in range(10):
                Brick(r, c)
        all_spritesgroup = pygame.sprite.Group()
        all_spritesgroup.add(paddle.group, lives.group, ball.group, Brick.group)

        clock = pygame.time.Clock()
        while len(lives) > 0 and len(Brick.group) > 0:
            clock.tick(40)
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    raise SystemExit
                elif event.type == pygame.MOUSEMOTION:
                    paddle.move(event.pos[0])
                elif event.type == pygame.MOUSEBUTTONUP:
                    if not ball.active:
                        ball.start()

            ball.update()
            screen.fill(BACKGROUND)
            font = pygame.font.Font(None, 34)
            text = font.render(f"Score: {ball.score}", 1, TEXT_COLOR)
            screen.blit(text, (Screen_Width-150, Screen_Height-30))
            all_spritesgroup.draw(screen)
            pygame.display.flip()

        # Game over
        font = pygame.font.Font(None, 74)
        if len(lives) == 0:
            text = font.render("Game over", 1, TEXT_COLOR)
            screen.blit(text, (250, 300))
        elif len(Brick.group) == 0:
            text = font.render("Level complete", 1, TEXT_COLOR)
            screen.blit(text, (200, 300))
        pygame.display.flip()
        pygame.time.wait(3000)
    finally:
        pygame.quit()


if __name__ == "__main__":
    main()
This being my first real pygame I am sure it does a lot of things wrong, but it plays pretty well. I haven't got the ball bouncing of the bricks right all the time. The ball moves too much between frames to accurately determine where it hits the brick. I should retain the previous ball position and check the intersection of the "ball travel" segment and the "brick surface" segments, but that just feels like overkill. Maybe there is a better way.

To run you will need to make some image files for the ball, paddle and bricks. I use 3 brick image files that show brick taking more damage with each hit (pristine, cracks forming, pieces missing). I replaced your lives counter text with a ball image for each life. Maybe the biggest departure is I start with the ball sitting atop the paddle until the player presses a mouse button. This launches the ball upward into the bricks.
Reply
#8
Dear deanhystad,

thanks a lot for your great effort in making the game run (and solving the problem that each brick is killed after 3 hits)!
I tested it with pngs for the paddle, ball and the bricks - especially for the brick I used 3 pngs to realize the different state of damage.

I appreciate it very much that I have the opportunity to learn from your code...

Now I can try to modify it a bit...

Thank you very much!

Have a nice evenning...
Reply
#9
I found a brick collision algorithm I like better. Beak the move into x and y components. Do the x move first. Check for collisions. Undo the x move and do the y move. Check for collisions. If you have x collisions, reverse x direction. If you have y collisions reverse y direction. If you have any collisions, revert to the previous position.
        # Check for collisions with bricks.
        x1, y1 = self.x, self.y  # Current position
        x2, y2 = x1 + self.dx, y1 + self.dy  # Next position
        # Is there a collision if we only move in x?
        if (xhits := pygame.sprite.spritecollide(self.at(x2, y1), self.bricks, False)):
            self.dx = -self.dx
        # Is there a collision if we only move in y?
        if (yhits := pygame.sprite.spritecollide(self.at(x1, y2), self.bricks, False)):
            self.dy = -self.dy

        hits = set(xhits) or set(yhits)  # Using sets to prevent a brick from being hit twice
        if hits:
            # If there were collisions undo the move and update the bricks.
            self.at(x1, y1)
            for brick in hits:
                self.score += brick.hit()
        else:
            # Move ball
            self.at(x2, y2)
Been playing with this for a while and I'm not experiencing any odd bounces or unexpected multiple hits.
Reply
#10
Dear deanhystad,
Thanks a lot for your effort! Because of professional reasons I will be able to try this out at the weekend.
I'm looking forward to it very happily.
Have a nice evening...
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  breakout clone pygame flash77 2 1,729 Feb-06-2022, 06:36 PM
Last Post: flash77
  [PyGame] arkanoid / breakout clone flash77 2 4,036 Feb-04-2022, 05:42 PM
Last Post: flash77

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020