Posts: 130
Threads: 30
Joined: May 2020
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...
Posts: 6,221
Threads: 16
Joined: Feb 2020
You don't have to transfer anything. bricks is a group of sprites. It keeps track of how many sprites are in the group.
Posts: 130
Threads: 30
Joined: May 2020
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...)
Posts: 6,221
Threads: 16
Joined: Feb 2020
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.
Posts: 130
Threads: 30
Joined: May 2020
Dear deanhystad,
could you please be so kind and show your full code so I can better orientate myself?
Posts: 6,221
Threads: 16
Joined: Feb 2020
Post #7 in this thread. That's everything.
Posts: 130
Threads: 30
Joined: May 2020
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?
Posts: 6,221
Threads: 16
Joined: Feb 2020
Feb-27-2022, 05:05 AM
(This post was last modified: Feb-27-2022, 04:06 PM by deanhystad.)
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()
Posts: 130
Threads: 30
Joined: May 2020
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()
Posts: 6,221
Threads: 16
Joined: Feb 2020
Feb-27-2022, 02:41 PM
(This post was last modified: Feb-27-2022, 06:33 PM by deanhystad.)
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.
|