Python Forum
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
breakout clone
#11
Dear deanhystad,

Now I had time to deal with the new brick collision algorithm. I added it to (starting at line 64):

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

    def __init__(self, paddle, lives, bricks, speed=10):
        super().__init__(create_image(BALL_IMAGE, BALL_COLOR), self.group)
        self.bricks = bricks
        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
        # 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)
Unfortunately I'm not sure where to transfer the number of remaining bricks to ball object.
I found:
    def __len__(self):
        """Return how many bricks remaining"""
        return len(self.group)
I tried:
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, bricks)
brick.group should contain the remaining bricks...
Please be so kind and help me...
Reply
#12
You don't have to transfer anything. bricks is a group of sprites. It keeps track of how many sprites are in the group.
Reply
#13
I got the following error:
Traceback (most recent call last):
  File "D:\Daten\Breakout neu 2\main.py", line 327, in <module>
    main()
  File "D:\Daten\Breakout neu 2\main.py", line 304, in main
    ball.update()
  File "D:\Daten\Breakout neu 2\main.py", line 258, in update
    if xhits := pygame.sprite.spritecollide(self.at(x2, y1), self.bricks, False):
AttributeError: 'Ball' object has no attribute 'bricks'
(So I thought I would have to transfer bricks to the ball object...)
Reply
#14
Somewhere in Ball it needs to assign an instance variable named "bricks". In my example this happened in Ball.__init__(), and Bricks was passed to Ball.__init__() when ball was created.
Reply
#15
Dear deanhystad,

could you please be so kind and show your full code so I can better orientate myself?
Reply
#16
Post #7 in this thread. That's everything.
Reply
#17
Hello deanhystad,
unfortunately I don't get along...
I added self.bricks = bricks to Ball.__init__()
    class Ball(EnhancedSprite):
    """Ball bounces around colliding with walls, paddles and bricks"""
    group = pygame.sprite.Group()

    def __init__(self, paddle, lives, bricks, speed=10):
        super().__init__(create_image(BALL_IMAGE, BALL_COLOR), self.group)
        self.bricks = bricks
        self.paddle = paddle
In Ball.update bricks is calculated
# Did I hit some bricks?  Update the bricks and the score
        bricks = pygame.sprite.spritecollide(self, Brick.group, False)
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, bricks)
I got the error:
ball = Ball(paddle, lives, bricks)
NameError: name 'bricks' is not defined. Did you mean: 'Brick'?
If bricks is calculated in Ball.update() how to get bricks in main() when ball is created?
Reply
#18
Sorry flash77. I took a better look at the code in post 7 and it is a mixup of a couple different ideas. I tested the code posted below to verify it works. Notice that Ball uses Brick.group instead of taking bricks as an argument in the __init__() method.

It has a couple of improvements too. This one converts the image file mode to "RGB", so it works with "RGB" and "RGBA" files. Paint was saving my png files as RGBA. I fixed the life counter which used to make it appear like you had one more life than you actually do. I added difficulty levels. Just more rows of bricks and the ball moves faster, but it is something different. I randomize the number of hits a brick can take which makes the display more interesting and game play more fun.

import random, math, pygame
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 = 700
SCORE_POSITION = (SCREEN_WIDTH-150, SCREEN_HEIGHT-30)

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).convert("RGB")
        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, "RGB")
    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
        return self

    # 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 I take some number of hits I die.
    Number of hits I can take is in range 1 to 3.  Hits is randomly
    selected if not specified.

    Specify brick color using (R, G, B) format.  If color not specified
    a color is selected based on the row.
    """
    group = pygame.sprite.Group()
    IMAGES = {}  # Dictionary of images.  Similar colored bricks share images

    def __init__(self, row, col, color=None, hits=None):
        self.color = color or BRICK_COLORS[row % len(BRICK_COLORS)]
        hits = hits or random.choice((1, 1, 1, 2, 2, 3))
        self.value = self.hits = max(1, min(3, hits))
        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-1]

    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.
        Return score based on being hit.
        """
        self.hits -= 1
        if self.hits > 0:
            self.image = self.get_image()
            return 0
        self.kill()
        return self.value


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

    def __init__(self, bottom):
        super().__init__(create_image(PADDLE_IMAGE, PADDLE_COLOR), self.group)
        self.bottom = bottom

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


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

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

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

    def kill(self):
        """Reduce number of lives"""
        if self.count > 1:
            self.group.sprites()[-1].kill()
        self.count = max(0, self.count-1)


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.xmax = SCREEN_WIDTH - self.rect.width
        self.ymax = self.paddle.bottom - self.rect.height
        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"""
        angle = random.random() - 0.5  # Launch angle limited to about +/-60 degrees
        self.dx = int(self.speed * math.sin(angle))
        self.dy = -int(self.speed * math.cos(angle))
        self.active = True

    def move(self):
        """Update the ball position.  Check for collisions with bricks, walls and the paddle"""
        if not self.active:
            # Sit on top of the paddle
            self.centerx = self.paddle.centerx
            self.bottom = self.paddle.y - 2
            return self

        # Did I hit some bricks?  Update the bricks and the score
        x1, y1 = self.x, self.y
        x2, y2 = x1 + self.dx, y1 + self.dy
        if (xhits := pygame.sprite.spritecollide(self.at(x2, y1), Brick.group, False)):
            self.dx = -self.dx
        if (yhits := pygame.sprite.spritecollide(self.at(x1, y2), Brick.group, False)):
            self.dy = -self.dy
        if (hits := set(xhits) or set(yhits)):
            for brick in hits:
                self.score += brick.hit(self.score)

         # Did I hit a wall?
        if x2 <= 0 or x2 >= self.xmax:
            self.dx = -self.dx
            hits = True
        if y2 <= 0:
            self.dy = abs(self.dy)
            hits = True

        # Did I hit or get past the paddle?
        if y2 >= self.ymax:
            # The got past the paddle
            self.lives.kill()
            self.active = False
        elif pygame.sprite.spritecollide(self, self.paddle.group, False):
            # I hit the paddle.  Compute angle of reflection
            bangle = math.atan2(-self.dx, self.dy)   # Ball angle of approach
            pangle = math.atan2(self.centerx - self.paddle.centerx, 30) # Paddle angle
            rangle = (pangle - bangle) / 2  # Angle of reflection
            self.dx = math.sin(rangle) * self.speed
            self.dy = -math.cos(rangle) * self.speed
            hits = True

        if hits:
            self.at(x1, y1)
        else:
            self.at(x2, y2)


def main():
    """Play game until out of lives or out of bricks"""
    def displayText(text, font, pos=None, color=TEXT_COLOR):
        text = font.render(text, 1, color)
        if pos is None:
            pos = ((SCREEN_WIDTH - text.get_width())//2, (SCREEN_HEIGHT-text.get_height())//2)
        screen.blit(text, pos)

    pygame.init()
    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    pygame.display.set_caption("Breakout")
    clock = pygame.time.Clock()
    allsprites = pygame.sprite.Group()
    score_font = pygame.font.Font(None, 34)

    try:
        level = 1
        lives = LifeCounter(10, SCREEN_HEIGHT - 30)
        paddle = Paddle(bottom = SCREEN_HEIGHT - 40)
        ball = Ball(paddle, lives)
        allsprites.add(paddle.group, lives.group, ball.group)

        while len(lives) > 0:
            for r, c in product(range(3+level), range(10)):
                Brick(r, c)
            allsprites.add(Brick.group)

            while len(lives) > 0 and len(Brick.group) > 0:
                clock.tick(60)
                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.move()
                screen.fill(BACKGROUND)
                displayText(f"Score: {ball.score}", font=score_font, pos=SCORE_POSITION)
                allsprites.draw(screen)
                pygame.display.flip()


            if len(lives) == 0:
                displayText("Game over", font=pygame.font.Font(None, 74))
            elif len(Brick.group) == 0:
                level += 1
                displayText(f"Level {level}", font=pygame.font.Font(None, 74))
                ball.speed *= 1.25
                ball.reset(ball.score)
            pygame.display.flip()
            pygame.time.wait(3000)
    finally:
        pygame.quit()


if __name__ == "__main__":
    main()
Reply
#19
Dear deanhystad,

I wish you a pleasant sunday.

Thanks a lot for your effort...

I'm trying to clamp the movement of the paddle, so it can't leave the playfield.

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

    def __init__(self,bottom):
        super().__init__(create_image(PADDLE_IMAGE, PADDLE_COLOR), self.group)
        self.bottom = bottom

    def move(self, x, a):
        """Move to follow the cursor"""
        self.centerx = x
        self = PADDLE_IMAGE.get_rect()
        self = self.clamp(a)
But PADDLE_IMAGE is a string ("paddle.png")
in a I transfer the SCREENWIDTH.
I'm not sure where to get the rect of the paddle image...
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).convert("RGB")
        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, "RGB")
    else:
        image = pygame.image.load(image_file)
    image.set_colorkey(TRANSPARENT)
    return image.convert()
Reply
#20
EnhancedSprite has a rect that is assigned in the __init__() method.
    def __init__(self, image, group = None, **kwargs):
        super().__init__(**kwargs)
        self.image = image
        self.rect = image.get_rect()  # <- This is the rectangle for the image
        if group is not None:
            group.add(self)
To clamp paddle movement so the entire paddle is always on screen.
    def __init__(self,bottom):
        super().__init__(create_image(PADDLE_IMAGE, PADDLE_COLOR), self.group)
        self.bottom = bottom
        self.xmin = self.rect.width // 2    # set paddle x range.
        self.xmax = SCREEN_WIDTH - self.xmin

    def move(self, x):
        """Move to follow the cursor.  Clamp to window bounds"""
        self.centerx = max(self.xmin, min(self.xmax, x))
I started out doing this and didn't like how it played. I wish the paddle could go even further off the edge of the screen, but it can't since it follows the cursor.
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