Feb-18-2022, 11:04 PM
I've had fun playing around with your code and came up with this:
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.
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.